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