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