1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.adservices.ui.notifications; 18 19 import static com.android.adservices.service.FlagsConstants.KEY_GA_UX_FEATURE_ENABLED; 20 import static com.android.adservices.service.FlagsConstants.KEY_PAS_UX_ENABLED; 21 import static com.android.adservices.service.FlagsConstants.KEY_RECORD_MANUAL_INTERACTION_ENABLED; 22 import static com.android.adservices.service.FlagsConstants.KEY_UI_OTA_RESOURCES_FEATURE_ENABLED; 23 import static com.android.adservices.service.FlagsConstants.KEY_UI_OTA_STRINGS_FEATURE_ENABLED; 24 import static com.android.adservices.service.consent.ConsentManager.MANUAL_INTERACTIONS_RECORDED; 25 import static com.android.adservices.service.consent.ConsentManager.NO_MANUAL_INTERACTIONS_RECORDED; 26 import static com.android.adservices.ui.UxUtil.isUxStatesReady; 27 import static com.android.adservices.ui.ganotifications.ConsentNotificationPasFragment.IS_RENOTIFY_KEY; 28 import static com.android.adservices.ui.ganotifications.ConsentNotificationPasFragment.IS_STRICT_CONSENT_BEHAVIOR; 29 30 import android.app.Notification; 31 import android.app.NotificationChannel; 32 import android.app.NotificationManager; 33 import android.app.PendingIntent; 34 import android.content.Context; 35 import android.content.Intent; 36 import android.os.Build; 37 38 import androidx.annotation.NonNull; 39 import androidx.annotation.RequiresApi; 40 import androidx.core.app.NotificationCompat; 41 import androidx.core.app.NotificationManagerCompat; 42 43 import com.android.adservices.api.R; 44 import com.android.adservices.service.consent.AdServicesApiType; 45 import com.android.adservices.service.consent.ConsentManager; 46 import com.android.adservices.service.stats.UiStatsLogger; 47 import com.android.adservices.service.ui.data.UxStatesManager; 48 import com.android.adservices.ui.OTAResourcesManager; 49 import com.android.adservices.ui.UxUtil; 50 51 /** Provides methods which can be used to display Privacy Sandbox consent notification. */ 52 // TODO(b/269798827): Enable for R. 53 @RequiresApi(Build.VERSION_CODES.S) 54 public class ConsentNotificationTrigger { 55 /* Random integer for NotificationCompat purposes. */ 56 public static final int NOTIFICATION_ID = 67920; 57 private static final String CHANNEL_ID = "PRIVACY_SANDBOX_CHANNEL"; 58 private static final int NOTIFICATION_PRIORITY = NotificationCompat.PRIORITY_MAX; 59 // Request codes should be different for notifications that are not mutually exclusive per user. 60 // When in doubt, always use a different request code. 61 private static final int BETA_REQUEST_CODE = 1; 62 private static final int GA_REQUEST_CODE = 2; 63 private static final int U18_REQUEST_CODE = 3; 64 private static final int GA_WITH_PAS_REQUEST_CODE = 4; 65 66 /** 67 * Shows consent notification as the highest priority notification to the user. 68 * 69 * @param context Context which is used to display {@link NotificationCompat} 70 */ showConsentNotification(@onNull Context context, boolean isEuDevice)71 public static void showConsentNotification(@NonNull Context context, boolean isEuDevice) { 72 UiStatsLogger.logRequestedNotification(); 73 74 boolean gaUxFeatureEnabled = 75 UxStatesManager.getInstance().getFlag(KEY_GA_UX_FEATURE_ENABLED); 76 77 NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); 78 ConsentManager consentManager = ConsentManager.getInstance(); 79 if (!notificationManager.areNotificationsEnabled()) { 80 recordNotificationDisplayed(context, gaUxFeatureEnabled, consentManager); 81 UiStatsLogger.logNotificationDisabled(); 82 return; 83 } 84 85 // Set OTA resources if it exists. 86 if (UxStatesManager.getInstance().getFlag(KEY_UI_OTA_STRINGS_FEATURE_ENABLED) 87 || UxStatesManager.getInstance().getFlag(KEY_UI_OTA_RESOURCES_FEATURE_ENABLED)) { 88 OTAResourcesManager.applyOTAResources(context.getApplicationContext(), true); 89 } 90 91 createNotificationChannel(context); 92 Notification notification = 93 getNotification(context, isEuDevice, gaUxFeatureEnabled, consentManager); 94 95 notificationManager.notify(NOTIFICATION_ID, notification); 96 97 setupConsents(context, isEuDevice, gaUxFeatureEnabled, consentManager); 98 99 UiStatsLogger.logNotificationDisplayed(); 100 recordNotificationDisplayed(context, gaUxFeatureEnabled, consentManager); 101 } 102 recordNotificationDisplayed( @onNull Context context, boolean gaUxFeatureEnabled, ConsentManager consentManager)103 private static void recordNotificationDisplayed( 104 @NonNull Context context, boolean gaUxFeatureEnabled, ConsentManager consentManager) { 105 if (UxStatesManager.getInstance().getFlag(KEY_RECORD_MANUAL_INTERACTION_ENABLED) 106 && consentManager.getUserManualInteractionWithConsent() 107 != MANUAL_INTERACTIONS_RECORDED) { 108 consentManager.recordUserManualInteractionWithConsent(NO_MANUAL_INTERACTIONS_RECORDED); 109 } 110 111 if (isUxStatesReady(context)) { 112 switch (UxUtil.getUx(context)) { 113 case GA_UX: 114 if (UxStatesManager.getInstance().getFlag(KEY_PAS_UX_ENABLED)) { 115 consentManager.recordPasNotificationDisplayed(true); 116 break; 117 } 118 consentManager.recordGaUxNotificationDisplayed(true); 119 break; 120 // Both U18_UX and RVC_UX are showing U18 Notification 121 case U18_UX: 122 case RVC_UX: 123 consentManager.setU18NotificationDisplayed(true); 124 break; 125 case BETA_UX: 126 consentManager.recordNotificationDisplayed(true); 127 break; 128 default: 129 break; 130 } 131 } else { 132 if (gaUxFeatureEnabled) { 133 consentManager.recordGaUxNotificationDisplayed(true); 134 } 135 consentManager.recordNotificationDisplayed(true); 136 } 137 } 138 139 @NonNull getNotification( @onNull Context context, boolean isEuDevice, boolean gaUxFeatureEnabled, ConsentManager consentManager)140 private static Notification getNotification( 141 @NonNull Context context, 142 boolean isEuDevice, 143 boolean gaUxFeatureEnabled, 144 ConsentManager consentManager) { 145 Notification notification; 146 if (isUxStatesReady(context)) { 147 switch (UxUtil.getUx(context)) { 148 case GA_UX: 149 if (UxStatesManager.getInstance().getFlag(KEY_PAS_UX_ENABLED)) { 150 notification = 151 getPasConsentNotification(context, consentManager, isEuDevice); 152 break; 153 } 154 notification = getGaV2ConsentNotification(context, isEuDevice); 155 break; 156 // Both U18_UX and RVC_UX are showing U18 Notification 157 case U18_UX: 158 case RVC_UX: 159 notification = getU18ConsentNotification(context); 160 break; 161 case BETA_UX: 162 notification = getConsentNotification(context, isEuDevice); 163 break; 164 default: 165 notification = getGaV2ConsentNotification(context, isEuDevice); 166 } 167 } else { 168 if (gaUxFeatureEnabled) { 169 notification = getGaV2ConsentNotification(context, isEuDevice); 170 } else { 171 notification = getConsentNotification(context, isEuDevice); 172 } 173 } 174 175 // make notification sticky (non-dismissible) for EuDevices when the GA UX feature is on 176 if (gaUxFeatureEnabled && isEuDevice) { 177 notification.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR; 178 } 179 return notification; 180 } 181 182 // setup default consents based on information whether the device is EU or non-EU device and 183 // GA UX feature flag is enabled. setupConsents( @onNull Context context, boolean isEuDevice, boolean gaUxFeatureEnabled, ConsentManager consentManager)184 private static void setupConsents( 185 @NonNull Context context, 186 boolean isEuDevice, 187 boolean gaUxFeatureEnabled, 188 ConsentManager consentManager) { 189 if (isUxStatesReady(context)) { 190 switch (UxUtil.getUx(context)) { 191 case GA_UX: 192 if (isPasRenotifyUser(consentManager) 193 && !isOtaRvcMsmtEnabledUser(consentManager)) { 194 // Is PAS renotify user AND Not adult user from Rvc with measurement 195 // enabled, respect previous consents. 196 break; 197 } 198 setUpGaConsent(context, isEuDevice, consentManager); 199 break; 200 case U18_UX: 201 consentManager.recordMeasurementDefaultConsent(true); 202 consentManager.enable(context, AdServicesApiType.MEASUREMENTS); 203 break; 204 case RVC_UX: 205 if (isEuDevice) { 206 consentManager.recordMeasurementDefaultConsent(false); 207 consentManager.disable(context, AdServicesApiType.MEASUREMENTS); 208 } else { 209 consentManager.recordMeasurementDefaultConsent(true); 210 consentManager.enable(context, AdServicesApiType.MEASUREMENTS); 211 } 212 break; 213 case BETA_UX: 214 if (!isEuDevice) { 215 consentManager.enable(context); 216 } else { 217 consentManager.disable(context); 218 } 219 break; 220 default: 221 // Default behavior is GA UX. 222 setUpGaConsent(context, isEuDevice, consentManager); 223 break; 224 } 225 } else { 226 // Keep the feature flag at the upper level to make it easier to cleanup the code once 227 // the beta functionality is fully deprecated and abandoned. 228 if (gaUxFeatureEnabled) { 229 setUpGaConsent(context, isEuDevice, consentManager); 230 } else { 231 // For the ROW devices, set the consent to GIVEN (enabled). 232 // For the EU devices, set the consent to REVOKED (disabled) 233 if (!isEuDevice) { 234 consentManager.enable(context); 235 } else { 236 consentManager.disable(context); 237 } 238 } 239 } 240 } 241 isPasRenotifyUser(ConsentManager consentManager)242 private static boolean isPasRenotifyUser(ConsentManager consentManager) { 243 return UxStatesManager.getInstance().getFlag(KEY_PAS_UX_ENABLED) 244 && (isFledgeOrMsmtEnabled(consentManager) 245 || consentManager.getUserManualInteractionWithConsent() 246 == MANUAL_INTERACTIONS_RECORDED); 247 } 248 isOtaRvcMsmtEnabledUser(ConsentManager consentManager)249 private static boolean isOtaRvcMsmtEnabledUser(ConsentManager consentManager) { 250 return consentManager.isOtaAdultUserFromRvc() 251 && consentManager.getConsent(AdServicesApiType.MEASUREMENTS).isGiven(); 252 } 253 getGaV2ConsentNotification( @onNull Context context, boolean isEuDevice)254 private static Notification getGaV2ConsentNotification( 255 @NonNull Context context, boolean isEuDevice) { 256 Intent intent = new Intent(context, ConsentNotificationActivity.class); 257 258 PendingIntent pendingIntent = 259 PendingIntent.getActivity( 260 context, GA_REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE); 261 262 String bigText = 263 isEuDevice 264 ? context.getString(R.string.notificationUI_notification_ga_content_eu_v2) 265 : context.getString(R.string.notificationUI_notification_ga_content_v2); 266 267 NotificationCompat.BigTextStyle textStyle = 268 new NotificationCompat.BigTextStyle().bigText(bigText); 269 270 String contentTitle = 271 context.getString( 272 isEuDevice 273 ? R.string.notificationUI_notification_ga_title_eu_v2 274 : R.string.notificationUI_notification_ga_title_v2); 275 String contentText = 276 context.getString( 277 isEuDevice 278 ? R.string.notificationUI_notification_ga_content_eu_v2 279 : R.string.notificationUI_notification_ga_content_v2); 280 281 NotificationCompat.Builder notification = 282 new NotificationCompat.Builder(context, CHANNEL_ID) 283 .setSmallIcon(R.drawable.ic_info_icon) 284 .setContentTitle(contentTitle) 285 .setContentText(contentText) 286 .setStyle(textStyle) 287 .setPriority(NOTIFICATION_PRIORITY) 288 .setAutoCancel(true) 289 .setContentIntent(pendingIntent); 290 return notification.build(); 291 } 292 293 /** 294 * Returns a {@link NotificationCompat.Builder} which can be used to display consent 295 * notification to the user. 296 * 297 * @param context {@link Context} which is used to prepare a {@link NotificationCompat}. 298 */ getConsentNotification( @onNull Context context, boolean isEuDevice)299 private static Notification getConsentNotification( 300 @NonNull Context context, boolean isEuDevice) { 301 Intent intent = new Intent(context, ConsentNotificationActivity.class); 302 303 PendingIntent pendingIntent = 304 PendingIntent.getActivity( 305 context, BETA_REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE); 306 NotificationCompat.BigTextStyle textStyle = 307 new NotificationCompat.BigTextStyle() 308 .bigText( 309 isEuDevice 310 ? context.getString( 311 R.string.notificationUI_notification_content_eu) 312 : context.getString( 313 R.string.notificationUI_notification_content)); 314 return new NotificationCompat.Builder(context, CHANNEL_ID) 315 .setSmallIcon(R.drawable.ic_info_icon) 316 .setContentTitle( 317 context.getString( 318 isEuDevice 319 ? R.string.notificationUI_notification_title_eu 320 : R.string.notificationUI_notification_title)) 321 .setContentText( 322 context.getString( 323 isEuDevice 324 ? R.string.notificationUI_notification_content_eu 325 : R.string.notificationUI_notification_content)) 326 .setStyle(textStyle) 327 .setPriority(NOTIFICATION_PRIORITY) 328 .setAutoCancel(true) 329 .setContentIntent(pendingIntent) 330 .build(); 331 } 332 333 /** 334 * Returns a {@link NotificationCompat.Builder} which can be used to display consent 335 * notification to U18 users. 336 * 337 * @param context {@link Context} which is used to prepare a {@link NotificationCompat}. 338 */ getU18ConsentNotification(@onNull Context context)339 private static Notification getU18ConsentNotification(@NonNull Context context) { 340 Intent intent = new Intent(context, ConsentNotificationActivity.class); 341 342 PendingIntent pendingIntent = 343 PendingIntent.getActivity( 344 context, U18_REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE); 345 NotificationCompat.BigTextStyle textStyle = 346 new NotificationCompat.BigTextStyle() 347 .bigText( 348 context.getString( 349 R.string.notificationUI_u18_notification_content)); 350 return new NotificationCompat.Builder(context, CHANNEL_ID) 351 .setSmallIcon(R.drawable.ic_info_icon) 352 .setContentTitle(context.getString(R.string.notificationUI_u18_notification_title)) 353 .setContentText(context.getString(R.string.notificationUI_u18_notification_content)) 354 .setStyle(textStyle) 355 .setPriority(NOTIFICATION_PRIORITY) 356 .setAutoCancel(true) 357 .setContentIntent(pendingIntent) 358 .build(); 359 } 360 getPasConsentNotification( @onNull Context context, ConsentManager consentManager, boolean isEuDevice)361 private static Notification getPasConsentNotification( 362 @NonNull Context context, ConsentManager consentManager, boolean isEuDevice) { 363 boolean isRenotify = isFledgeOrMsmtEnabled(consentManager); 364 Intent intent = new Intent(context, ConsentNotificationActivity.class); 365 intent.putExtra(IS_RENOTIFY_KEY, isRenotify); 366 // isEuDevice argument here includes AdId disabled ROW users, which cannot be obtained 367 // within the notification activity from DeviceRegionProvider. 368 intent.putExtra(IS_STRICT_CONSENT_BEHAVIOR, isEuDevice); 369 370 PendingIntent pendingIntent = 371 PendingIntent.getActivity( 372 context, GA_WITH_PAS_REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE); 373 374 String bigText = 375 isRenotify 376 ? context.getString(R.string.notificationUI_pas_re_notification_content) 377 : context.getString(R.string.notificationUI_pas_notification_content); 378 379 NotificationCompat.BigTextStyle textStyle = 380 new NotificationCompat.BigTextStyle().bigText(bigText); 381 382 String contentTitle = 383 context.getString( 384 isRenotify 385 ? R.string.notificationUI_pas_re_notification_title 386 : R.string.notificationUI_pas_notification_title); 387 String contentText = 388 context.getString( 389 isRenotify 390 ? R.string.notificationUI_pas_re_notification_content 391 : R.string.notificationUI_pas_notification_content); 392 393 NotificationCompat.Builder notification = 394 new NotificationCompat.Builder(context, CHANNEL_ID) 395 .setSmallIcon(R.drawable.ic_info_icon) 396 .setContentTitle(contentTitle) 397 .setContentText(contentText) 398 .setStyle(textStyle) 399 .setPriority(NOTIFICATION_PRIORITY) 400 .setAutoCancel(true) 401 .setContentIntent(pendingIntent); 402 return notification.build(); 403 } 404 isFledgeOrMsmtEnabled(ConsentManager consentManager)405 private static boolean isFledgeOrMsmtEnabled(ConsentManager consentManager) { 406 return consentManager.getConsent(AdServicesApiType.FLEDGE).isGiven() 407 || consentManager.getConsent(AdServicesApiType.MEASUREMENTS).isGiven(); 408 } 409 createNotificationChannel(@onNull Context context)410 private static void createNotificationChannel(@NonNull Context context) { 411 // TODO (b/230372892): styling -> adjust channels to use Android System labels. 412 int importance = NotificationManager.IMPORTANCE_HIGH; 413 NotificationChannel channel = 414 new NotificationChannel( 415 CHANNEL_ID, 416 context.getString(R.string.settingsUI_main_view_title), 417 importance); 418 channel.setDescription(context.getString(R.string.settingsUI_main_view_title)); 419 NotificationManager notificationManager = 420 context.getSystemService(NotificationManager.class); 421 notificationManager.createNotificationChannel(channel); 422 } 423 setUpGaConsent( @onNull Context context, boolean isEuDevice, ConsentManager consentManager)424 private static void setUpGaConsent( 425 @NonNull Context context, boolean isEuDevice, ConsentManager consentManager) { 426 if (isEuDevice) { 427 consentManager.recordTopicsDefaultConsent(false); 428 consentManager.recordFledgeDefaultConsent(false); 429 consentManager.recordMeasurementDefaultConsent(false); 430 431 consentManager.disable(context, AdServicesApiType.TOPICS); 432 consentManager.disable(context, AdServicesApiType.FLEDGE); 433 consentManager.disable(context, AdServicesApiType.MEASUREMENTS); 434 } else { 435 consentManager.recordTopicsDefaultConsent(true); 436 consentManager.recordFledgeDefaultConsent(true); 437 consentManager.recordMeasurementDefaultConsent(true); 438 439 consentManager.enable(context, AdServicesApiType.TOPICS); 440 consentManager.enable(context, AdServicesApiType.FLEDGE); 441 consentManager.enable(context, AdServicesApiType.MEASUREMENTS); 442 } 443 } 444 } 445