1 /*
2  * Copyright (C) 2023 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.devicelockcontroller.activities;
18 
19 import static com.android.devicelockcontroller.activities.ProvisioningActivity.EXTRA_SHOW_PROVISION_FAILED_UI_ON_START;
20 
21 import static com.google.common.base.Preconditions.checkNotNull;
22 
23 import android.annotation.SuppressLint;
24 import android.app.Notification;
25 import android.app.NotificationChannel;
26 import android.app.NotificationManager;
27 import android.app.PendingIntent;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.os.SystemClock;
31 import android.widget.RemoteViews;
32 
33 import androidx.core.app.NotificationCompat;
34 import androidx.core.app.NotificationManagerCompat;
35 
36 import com.android.devicelockcontroller.DeviceLockControllerApplication;
37 import com.android.devicelockcontroller.R;
38 import com.android.devicelockcontroller.storage.SetupParametersClient;
39 import com.android.devicelockcontroller.storage.UserParameters;
40 import com.android.devicelockcontroller.util.LogUtil;
41 import com.android.devicelockcontroller.util.StringUtil;
42 import com.android.internal.annotations.GuardedBy;
43 import com.android.internal.annotations.VisibleForTesting;
44 
45 import com.google.common.util.concurrent.FutureCallback;
46 import com.google.common.util.concurrent.Futures;
47 import com.google.common.util.concurrent.ListenableFuture;
48 import com.google.common.util.concurrent.ListeningExecutorService;
49 import com.google.common.util.concurrent.MoreExecutors;
50 
51 import java.time.LocalDateTime;
52 import java.time.format.DateTimeFormatter;
53 import java.time.format.FormatStyle;
54 import java.util.UUID;
55 import java.util.concurrent.Executors;
56 
57 /**
58  * A singleton class used to send notifications.
59  */
60 public final class DeviceLockNotificationManager {
61 
62     private static final String TAG = "DeviceLockNotificationManager";
63 
64     private static final String PROVISION_NOTIFICATION_CHANNEL_ID_BASE = "devicelock-provision";
65     @VisibleForTesting
66     public static final String DEVICE_RESET_NOTIFICATION_TAG = "devicelock-device-reset";
67     @VisibleForTesting
68     public static final int DEVICE_RESET_NOTIFICATION_ID = 0;
69     private static final int DEFER_PROVISIONING_NOTIFICATION_ID = 1;
70     @GuardedBy("DeviceLockNotificationManager.class")
71     private static DeviceLockNotificationManager sInstance;
72     private final ListeningExecutorService mListeningExecutorService;
73 
74 
75     /**
76      * Get instance of {@link DeviceLockNotificationManager}.
77      */
getInstance()78     public static DeviceLockNotificationManager getInstance() {
79         synchronized (DeviceLockNotificationManager.class) {
80             if (sInstance == null) {
81                 createAndSetDeviceLockNotificationManager(
82                         MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()));
83             }
84             return sInstance;
85         }
86     }
87 
88     /**
89      * Create and set instance of {@link DeviceLockNotificationManager}.
90      */
91     @VisibleForTesting
createAndSetDeviceLockNotificationManager( ListeningExecutorService executor)92     public static void createAndSetDeviceLockNotificationManager(
93             ListeningExecutorService executor) {
94         synchronized (DeviceLockNotificationManager.class) {
95             sInstance = new DeviceLockNotificationManager(executor);
96         }
97     }
98 
DeviceLockNotificationManager(ListeningExecutorService listeningExecutorService)99     private DeviceLockNotificationManager(ListeningExecutorService listeningExecutorService) {
100         mListeningExecutorService = listeningExecutorService;
101     }
102 
103     /**
104      * Similar to {@link #sendDeviceResetNotification(Context, int)}, except that:
105      * 1. The number of days to reset is always one.
106      * 2. The notification is ongoing.
107      */
sendDeviceResetInOneDayOngoingNotification(Context context)108     public void sendDeviceResetInOneDayOngoingNotification(Context context) {
109         sendDeviceResetNotification(context, /* days= */ 1, /* ongoing= */ true);
110     }
111 
112     /**
113      * Send the device reset notification. The call is thread safe and can be called from any
114      * thread.
115      *
116      * @param context the context where the notification will be sent out
117      * @param days    the number of days the reset will happen
118      */
sendDeviceResetNotification(Context context, int days)119     public void sendDeviceResetNotification(Context context, int days) {
120         sendDeviceResetNotification(context, days, /* ongoing= */ false);
121     }
122 
123     /**
124      * Send the device reset timer notification.
125      *
126      * @param context       the context where the notification will be sent out
127      * @param countDownBase the time when device will be reset in
128      *                      {@link SystemClock#elapsedRealtime()}.
129      */
sendDeviceResetTimerNotification(Context context, long countDownBase)130     public void sendDeviceResetTimerNotification(Context context, long countDownBase) {
131         ListenableFuture<String> channelIdFuture = createNotificationChannel(context);
132         ListenableFuture<String> kioskAppProviderNameFuture =
133                 SetupParametersClient.getInstance().getKioskAppProviderName();
134         ListenableFuture<Void> result =
135                 Futures.whenAllSucceed(channelIdFuture, kioskAppProviderNameFuture).call(() -> {
136                     String channelId = Futures.getDone(channelIdFuture);
137                     String providerName = Futures.getDone(kioskAppProviderNameFuture);
138                     Notification notification =
139                             new NotificationCompat.Builder(context,
140                                     channelId)
141                                     .setSmallIcon(R.drawable.ic_action_lock)
142                                     .setColor(context.getResources()
143                                             .getColor(R.color.notification_background_color,
144                                                     context.getTheme()))
145                                     .setOngoing(true)
146                                     .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
147                                     .setCustomContentView(
148                                             buildResetTimerNotif(countDownBase,
149                                                     providerName,
150                                                     false))
151                                     .setCustomBigContentView(
152                                             buildResetTimerNotif(countDownBase, providerName,
153                                                     true))
154                                     .build();
155                     NotificationManagerCompat notificationManager =
156                             NotificationManagerCompat.from(context);
157                     notificationManager.notify(DEVICE_RESET_NOTIFICATION_TAG,
158                             DEVICE_RESET_NOTIFICATION_ID, notification);
159                     return null;
160                 }, mListeningExecutorService);
161 
162         Futures.addCallback(result, new FutureCallback<>() {
163             @Override
164             public void onSuccess(Void result) {
165                 LogUtil.d(TAG, "send device reset notification");
166             }
167 
168             @Override
169             public void onFailure(Throwable t) {
170                 LogUtil.e(TAG, "Failed to create device reset notification", t);
171             }
172         }, mListeningExecutorService);
173     }
174 
buildResetTimerNotif(long countDownBase, String providerName, boolean isExpanded)175     private static RemoteViews buildResetTimerNotif(long countDownBase, String providerName,
176             boolean isExpanded) {
177         Context appContext = DeviceLockControllerApplication.getAppContext();
178         RemoteViews content = new RemoteViews(appContext.getPackageName(),
179                 isExpanded ? R.layout.reset_timer_notif_large : R.layout.reset_timer_notif_small);
180         content.setChronometer(R.id.reset_timer_title, countDownBase,
181                 appContext.getString(R.string.device_reset_timer_notification_title),
182                 /* started= */ true);
183         if (isExpanded) {
184             content.setCharSequence(R.id.reset_content, "setText",
185                     appContext.getString(R.string.device_reset_notification_content, providerName));
186         }
187         return content;
188     }
189 
sendDeviceResetNotification(Context context, int days, boolean ongoing)190     private void sendDeviceResetNotification(Context context, int days, boolean ongoing) {
191         // TODO: check/request permission first
192 
193         // re-creating the same notification channel is essentially no-op
194         ListenableFuture<String> channelIdFuture = createNotificationChannel(context);
195         ListenableFuture<Notification> notificationFuture = Futures.transformAsync(channelIdFuture,
196                 channelId -> createDeviceResetNotification(context, days, ongoing, channelId),
197                 mListeningExecutorService);
198         Futures.addCallback(notificationFuture,
199                 new FutureCallback<>() {
200                     @Override
201                     public void onSuccess(Notification notification) {
202                         LogUtil.d(TAG, "send device reset notification");
203                         NotificationManagerCompat notificationManager =
204                                 NotificationManagerCompat.from(context);
205                         notificationManager.notify(DEVICE_RESET_NOTIFICATION_TAG,
206                                 DEVICE_RESET_NOTIFICATION_ID, notification);
207                     }
208 
209                     @Override
210                     public void onFailure(Throwable t) {
211                         LogUtil.e(TAG, "Failed to create device reset notification", t);
212                     }
213                 }, mListeningExecutorService);
214     }
215 
216     /**
217      * Send the device deferred provisioning notification.
218      * (POST_NOTIFICATION already requested permission in ProvisionInfoFragment).
219      *
220      * @param context        the context where the notification will be sent out
221      * @param resumeDateTime the time when device will show the notification
222      * @Param pendingIntent  pending intent for the notification
223      */
224     @SuppressLint("MissingPermission")
sendDeferredProvisioningNotification(Context context, LocalDateTime resumeDateTime, PendingIntent pendingIntent)225     public void sendDeferredProvisioningNotification(Context context,
226             LocalDateTime resumeDateTime, PendingIntent pendingIntent) {
227         ListenableFuture<String> channelIdFuture = createNotificationChannel(context);
228 
229         Futures.addCallback(channelIdFuture, new FutureCallback<String>() {
230             @Override
231             public void onSuccess(String channelId) {
232                 DateTimeFormatter timeFormatter =
233                         DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
234                 String enrollmentResumeTime = timeFormatter.format(resumeDateTime);
235                 NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
236                         context, channelId)
237                         .setContentTitle(context.getString(R.string.device_enrollment_header_text))
238                         .setContentText(context
239                                 .getString(R.string.device_enrollment_notification_body_text,
240                                         enrollmentResumeTime))
241                         .setSmallIcon(R.drawable.ic_action_lock)
242                         .setColor(context.getResources()
243                                 .getColor(R.color.notification_background_color,
244                                         context.getTheme()))
245                         .setContentIntent(pendingIntent)
246                         .setAutoCancel(true)
247                         .setOngoing(true);
248                 NotificationManagerCompat notificationManager =
249                         NotificationManagerCompat.from(context);
250                 notificationManager.notify(DEFER_PROVISIONING_NOTIFICATION_ID,
251                         notificationBuilder.build());
252             }
253 
254             @Override
255             public void onFailure(Throwable t) {
256                 LogUtil.e(TAG, "Failed to create deferred provisioning notification", t);
257             }
258         }, mListeningExecutorService);
259 
260     }
261 
cancelDeferredProvisioningNotification(Context context)262     void cancelDeferredProvisioningNotification(Context context) {
263         LogUtil.d(TAG, "cancelDeferredEnrollmentNotification");
264         NotificationManagerCompat.from(context).cancel(DEFER_PROVISIONING_NOTIFICATION_ID);
265     }
266 
createDeviceResetNotification(Context context, int days, boolean ongoing, String channelId)267     private ListenableFuture<Notification> createDeviceResetNotification(Context context,
268             int days, boolean ongoing, String channelId) {
269         return Futures.transform(SetupParametersClient.getInstance().getKioskAppProviderName(),
270                 providerName ->
271                         new NotificationCompat.Builder(context,
272                                 channelId)
273                                 .setSmallIcon(R.drawable.ic_action_lock)
274                                 .setColor(context.getResources()
275                                         .getColor(R.color.notification_background_color,
276                                                 context.getTheme()))
277                                 .setOngoing(ongoing)
278                                 .setContentTitle(StringUtil.getPluralString(context, days,
279                                         R.string.device_reset_in_days_notification_title))
280                                 .setContentText(context.getString(
281                                         R.string.device_reset_notification_content,
282                                         providerName))
283                                 .setContentIntent(
284                                         PendingIntent.getActivity(context, /* requestCode= */0,
285                                                 new Intent(context,
286                                                         ProvisioningActivity.class).putExtra(
287                                                         EXTRA_SHOW_PROVISION_FAILED_UI_ON_START,
288                                                         true),
289                                                 PendingIntent.FLAG_IMMUTABLE)).build(),
290                 context.getMainExecutor());
291     }
292 
getProvisionNotificationChannelId(Context context)293     private static String getProvisionNotificationChannelId(Context context) {
294         return PROVISION_NOTIFICATION_CHANNEL_ID_BASE + "-"
295                 + UserParameters.getNotificationChannelIdSuffix(context);
296     }
297 
createProvisionNotificationChannelId(Context context)298     private static String createProvisionNotificationChannelId(Context context) {
299         String notificationChannelIdSuffix = UUID.randomUUID().toString();
300         String provisioningChannelId = PROVISION_NOTIFICATION_CHANNEL_ID_BASE
301                 + "-" + notificationChannelIdSuffix;
302         UserParameters.setNotificationChannelIdSuffix(context, notificationChannelIdSuffix);
303 
304         return provisioningChannelId;
305     }
306 
307     // Create a notification channel and return a future with its ID.
createNotificationChannel(Context context)308     private ListenableFuture<String> createNotificationChannel(Context context) {
309         return mListeningExecutorService.submit(() -> {
310             String provisioningChannelId = getProvisionNotificationChannelId(context);
311             NotificationManager notificationManager =
312                     context.getSystemService(NotificationManager.class);
313             checkNotNull(notificationManager);
314             NotificationChannel notificationChannel =
315                     notificationManager.getNotificationChannel(provisioningChannelId);
316             if (notificationChannel != null
317                     && notificationChannel.getImportance() != NotificationManager.IMPORTANCE_HIGH) {
318                 // Channel importance has been changed by user
319                 LogUtil.w(TAG, "Channel " + provisioningChannelId
320                         + " importance changed by user, deleting it");
321                 notificationManager.deleteNotificationChannel(provisioningChannelId);
322                 notificationChannel = null;
323             }
324 
325             if (notificationChannel == null) {
326                 provisioningChannelId = createProvisionNotificationChannelId(context);
327                 notificationChannel = new NotificationChannel(
328                         provisioningChannelId,
329                         context.getString(R.string.provision_notification_channel_name),
330                         NotificationManager.IMPORTANCE_HIGH);
331                 LogUtil.i(TAG, "New notification channel: " + provisioningChannelId);
332             }
333             notificationManager.createNotificationChannel(notificationChannel);
334 
335             return provisioningChannelId;
336         });
337     }
338 }
339