001package com.intentsoftware.addapptr.consent;
002
003import android.app.Activity;
004import android.content.Context;
005import android.preference.PreferenceManager;
006import android.util.Log;
007
008import com.google.android.ump.ConsentForm;
009import com.google.android.ump.ConsentInformation;
010import com.google.android.ump.ConsentRequestParameters;
011import com.google.android.ump.FormError;
012import com.google.android.ump.UserMessagingPlatform;
013import com.intentsoftware.addapptr.AdNetwork;
014import com.intentsoftware.addapptr.CMPImplementation;
015import com.intentsoftware.addapptr.ManagedConsent;
016import com.intentsoftware.addapptr.NonIABConsent;
017import com.intentsoftware.addapptr.module.Logger;
018
019import java.util.Arrays;
020import java.util.List;
021
022import androidx.annotation.Nullable;
023
024@SuppressWarnings("unused")
025public class CMPGoogle extends CMPImplementation {
026
027    private enum MissedCallback {
028        FAILED_TO_LOAD,
029        FAILED_TO_SHOW,
030        NEEDS_UI,
031        CONSENT_UPDATED
032    }
033
034    private static final String MORE_PARTNERS_STRING_KEY = "IABTCF_AddtlConsent";
035
036    private CMPDelegate delegate;
037
038    private ConsentInformation consentInformation;
039    private ConsentForm consentForm;
040    private boolean needsUI;
041    private List<String> enabledAdditionalPartners;
042
043    private boolean consentInfoLoading;
044    private boolean consentFormLoading;
045    private MissedCallback missedCallback; //to handle Google notifications sent before delegate is set
046    private String missedCallbackErrorDescription;
047
048    /**
049     * Constructor.
050     *
051     * @param activity your Activity.
052     */
053    public CMPGoogle(final Activity activity) {
054        if (checkRequiredClasses("com.google.android.ump.ConsentForm", "com.google.android.ump.ConsentInformation",
055                "com.google.android.ump.ConsentRequestParameters", "com.google.android.ump.FormError", "com.google.android.ump.UserMessagingPlatform")) {
056            updateMorePartnersData(activity);
057            loadConsentInfo(activity);
058            onSuccessfulInitialization();
059        }
060    }
061
062    protected void setDelegate(CMPDelegate delegate) {
063        this.delegate = delegate;
064        if (missedCallback != null) {
065            handleMissedCallback();
066        }
067    }
068
069    private void handleMissedCallback() {
070        switch (missedCallback) {
071            case FAILED_TO_LOAD:
072                delegate.onCMPFailedToLoad(missedCallbackErrorDescription);
073                break;
074            case FAILED_TO_SHOW:
075                delegate.onCMPFailedToShow(missedCallbackErrorDescription);
076                break;
077            case NEEDS_UI:
078                delegate.CMPNeedsUI();
079                break;
080            case CONSENT_UPDATED:
081                delegate.onConsentUpdated(ManagedConsent.ManagedConsentState.UNKNOWN); //this should actually never happen, so we are good just leaving it as UNKNOWN
082                break;
083        }
084        missedCallbackErrorDescription = null;
085        missedCallback = null;
086    }
087
088    private void loadConsentInfo(final Activity activity) {
089        if (consentInfoLoading) {
090            if (Logger.isLoggable(Log.VERBOSE)) {
091                Logger.v(this, "Consent info already loading, reload request ignored");
092            }
093            return;
094        }
095        if (Logger.isLoggable(Log.VERBOSE)) {
096            Logger.v(this, "Loading consent info");
097        }
098
099        ConsentRequestParameters params = new ConsentRequestParameters.Builder().build();
100        consentInfoLoading = true;
101
102        final ConsentInformation consentInformation = UserMessagingPlatform.getConsentInformation(activity);
103        consentInformation.requestConsentInfoUpdate(activity, params,
104                new ConsentInformation.OnConsentInfoUpdateSuccessListener() {
105                    @Override
106                    public void onConsentInfoUpdateSuccess() {
107                        if (Logger.isLoggable(Log.VERBOSE)) {
108                            Logger.v(CMPGoogle.this, "Consent info update successful");
109                        }
110                        consentInfoLoading = false;
111                        if (consentInformation.isConsentFormAvailable()) {
112                            CMPGoogle.this.consentInformation = consentInformation;
113                            loadForm(activity);
114                        } else {
115                            if (Logger.isLoggable(Log.WARN)) {
116                                Logger.w(CMPGoogle.this, "Consent form not available");
117                            }
118                            String description = "Consent form not available";
119                            if (delegate != null) {
120                                delegate.onCMPFailedToLoad(description);
121                            } else {
122                                missedCallback = MissedCallback.FAILED_TO_LOAD;
123                                missedCallbackErrorDescription = description;
124                            }
125                        }
126                    }
127                },
128                new ConsentInformation.OnConsentInfoUpdateFailureListener() {
129                    @Override
130                    public void onConsentInfoUpdateFailure(FormError formError) {
131                        consentInfoLoading = false;
132                        String description = "Failed to update consent info: " + formError.getMessage();
133                        if (delegate != null) {
134                            delegate.onCMPFailedToLoad(description);
135                        } else {
136                            missedCallback = MissedCallback.FAILED_TO_LOAD;
137                            missedCallbackErrorDescription = description;
138                        }
139                    }
140                });
141    }
142
143    private void loadForm(Context context) {
144        if (consentFormLoading) {
145            if (Logger.isLoggable(Log.VERBOSE)) {
146                Logger.v(this, "Consent form already loading, reload request ignored");
147            }
148            return;
149        }
150        if (Logger.isLoggable(Log.VERBOSE)) {
151            Logger.v(this, "Loading consent form");
152        }
153        consentFormLoading = true;
154
155        UserMessagingPlatform.loadConsentForm(context,
156                new UserMessagingPlatform.OnConsentFormLoadSuccessListener() {
157                    @Override
158                    public void onConsentFormLoadSuccess(ConsentForm consentForm) {
159                        if (Logger.isLoggable(Log.VERBOSE)) {
160                            Logger.v(CMPGoogle.this, "Consent form loaded");
161                        }
162                        consentFormLoading = false;
163                        CMPGoogle.this.consentForm = consentForm;
164                        if (consentInformation.getConsentStatus() == ConsentInformation.ConsentStatus.REQUIRED) {
165                            needsUI = true;
166                            if (delegate != null) {
167                                delegate.CMPNeedsUI();
168                            } else {
169                                missedCallback = MissedCallback.NEEDS_UI;
170                            }
171                        }
172                    }
173                },
174                new UserMessagingPlatform.OnConsentFormLoadFailureListener() {
175                    @Override
176                    public void onConsentFormLoadFailure(FormError formError) {
177                        consentFormLoading = false;
178                        String description = "Failed to load consent form: " + formError.getMessage();
179                        if (delegate != null) {
180                            delegate.onCMPFailedToLoad(description);
181                        } else {
182                            missedCallback = MissedCallback.FAILED_TO_LOAD;
183                            missedCallbackErrorDescription = description;
184                        }
185                    }
186                }
187        );
188    }
189
190    @Override
191    protected void reload(Activity activity) {
192        if (this.consentInformation == null) {
193            loadConsentInfo(activity);
194        } else if (this.consentForm == null) {
195            loadForm(activity);
196        }
197    }
198
199    @Override
200    protected void showIfNeeded(Activity activity) {
201        if (needsUI) {
202            editConsent(activity);
203        }
204    }
205
206    @Override
207    protected void editConsent(final Activity activity) {
208        if (consentForm != null) {
209            needsUI = false;
210            consentForm.show(activity,
211                    new ConsentForm.OnConsentFormDismissedListener() {
212                        @Override
213                        public void onConsentFormDismissed(@Nullable FormError formError) {
214                            // Handle dismissal by reloading form.
215                            updateMorePartnersData(activity);
216                            CMPGoogle.this.consentForm = null;
217                            loadForm(activity);
218                            if (delegate != null) {
219                                ManagedConsent.ManagedConsentState state;
220                                String vendorsConsentString = PreferenceManager.getDefaultSharedPreferences(activity).getString("IABTCF_VendorConsents", null);
221                                String purposesConsentString = PreferenceManager.getDefaultSharedPreferences(activity).getString("IABTCF_PurposeConsents", null);
222                                if (vendorsConsentString == null || purposesConsentString == null) {
223                                    if (Logger.isLoggable(Log.ERROR)) {
224                                        Logger.e(this, "At least one of the required consent strings was not saved!" +
225                                                "\nvendor consents: " + vendorsConsentString +
226                                                "\npurpose consents:" + purposesConsentString);
227                                    }
228                                    state = ManagedConsent.ManagedConsentState.UNKNOWN;
229                                } else if (vendorsConsentString.contains("1") && purposesConsentString.contains("1")) { //at least one vendor and one purpose enabled
230                                    state = ManagedConsent.ManagedConsentState.CUSTOM;
231                                } else {
232                                    state = ManagedConsent.ManagedConsentState.WITHHELD;
233                                }
234
235                                delegate.onConsentUpdated(state);
236                            } else {
237                                missedCallback = MissedCallback.CONSENT_UPDATED;
238                            }
239                        }
240                    });
241        } else {
242            String description = "Consent form not ready";
243            if (delegate != null) {
244                delegate.onCMPFailedToShow(description);
245            } else {
246                missedCallback = MissedCallback.FAILED_TO_SHOW;
247                missedCallbackErrorDescription = description;
248            }
249        }
250    }
251
252    @Override
253    protected NonIABConsent getConsentForNetwork(AdNetwork network) {
254        switch (network) {
255            case APPLOVIN:
256                return readConsentForProviderId(1301);
257            case FACEBOOK:
258                return readConsentForProviderId(89);
259            case ADMOB:
260            case ADX:
261            case DFP:
262                return readConsentForProviderId(229);
263            case PUBNATIVE:
264                return readConsentForProviderId(2977);
265            case YANDEX:
266                return readConsentForProviderId(1033);
267            case MOPUB:
268                return readConsentForProviderId(1031);
269            default:
270                //FIXME: missing mappings for Huawei, InMobi, UnityAds
271                if (Logger.isLoggable(Log.WARN)) {
272                    Logger.w(this, "No mapping for network " + network + " available, treating consent as withheld");
273                }
274                return NonIABConsent.WITHHELD;
275        }
276    }
277
278    private void updateMorePartnersData(Context context) {
279        String morePartnersConsentString = PreferenceManager.getDefaultSharedPreferences(context).getString(MORE_PARTNERS_STRING_KEY, null);
280        enabledAdditionalPartners = null;
281        if (morePartnersConsentString != null) {
282            morePartnersConsentString = morePartnersConsentString.replaceFirst(".*~", ""); //remove "1~" or similar
283            String[] partnersArray = morePartnersConsentString.split("\\.");
284            enabledAdditionalPartners = Arrays.asList(partnersArray);
285        } else {
286            if (Logger.isLoggable(Log.VERBOSE)) {
287                Logger.v(this, "No more partners string found");
288            }
289        }
290    }
291
292    private NonIABConsent readConsentForProviderId(int id) {
293        if (enabledAdditionalPartners != null) {
294            if (enabledAdditionalPartners.contains(String.valueOf(id))) {
295                return NonIABConsent.OBTAINED;
296            } else {
297                return NonIABConsent.WITHHELD;
298            }
299        } else {
300            if (Logger.isLoggable(Log.WARN)) {
301                Logger.w(this, "Enabled additional partners list is not available");
302            }
303            return NonIABConsent.WITHHELD;
304        }
305    }
306}