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 final 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}