1 /* 2 * Copyright (C) 2021 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.car.admin; 18 19 import android.annotation.NonNull; 20 import android.app.ActivityManager; 21 import android.app.Notification; 22 import android.app.NotificationChannel; 23 import android.app.NotificationManager; 24 import android.app.PendingIntent; 25 import android.car.ICarResultReceiver; 26 import android.car.builtin.util.Slogf; 27 import android.content.ComponentName; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.ApplicationInfo; 31 import android.content.pm.PackageManager; 32 import android.os.Bundle; 33 import android.os.UserHandle; 34 import android.text.TextUtils; 35 import android.util.SparseArray; 36 37 import com.android.car.R; 38 import com.android.car.admin.ui.ManagedDeviceTextView; 39 import com.android.car.internal.NotificationHelperBase; 40 import com.android.internal.annotations.VisibleForTesting; 41 import com.android.internal.util.Preconditions; 42 43 import java.util.Objects; 44 45 // TODO(b/196947649): move this class to CarSettings or at least to some common package (not 46 // car-admin-ui-lib) 47 /** 48 * Helper for notification-related tasks 49 */ 50 public final class NotificationHelper extends NotificationHelperBase { 51 // TODO: Move these constants to a common place. Right now a copy of these is present in 52 // CarSettings' FactoryResetActivity. 53 public static final String EXTRA_FACTORY_RESET_CALLBACK = "factory_reset_callback"; 54 public static final int FACTORY_RESET_NOTIFICATION_ID = 42; 55 public static final int NEW_USER_DISCLAIMER_NOTIFICATION_ID = 108; 56 57 public static final String CAR_SERVICE_PACKAGE_NAME = "com.android.car"; 58 @VisibleForTesting 59 public static final String CHANNEL_ID_DEFAULT = "channel_id_default"; 60 @VisibleForTesting 61 public static final String CHANNEL_ID_HIGH = "channel_id_high"; 62 private static final boolean DEBUG = false; 63 @VisibleForTesting 64 static final String TAG = NotificationHelper.class.getSimpleName(); 65 66 /** 67 * Creates a notification (and its notification channel) for the given importance type, setting 68 * its name to be {@code Android System}. 69 * 70 * @param context context for showing the notification 71 * @param importance notification importance. Currently only 72 * {@link NotificationManager.IMPORTANCE_HIGH} is supported. 73 */ 74 @NonNull newNotificationBuilder(Context context, @NotificationManager.Importance int importance)75 public static Notification.Builder newNotificationBuilder(Context context, 76 @NotificationManager.Importance int importance) { 77 Objects.requireNonNull(context, "context cannot be null"); 78 79 String channelId, importanceName; 80 switch (importance) { 81 case NotificationManager.IMPORTANCE_DEFAULT: 82 channelId = CHANNEL_ID_DEFAULT; 83 importanceName = context.getString(R.string.importance_default); 84 break; 85 case NotificationManager.IMPORTANCE_HIGH: 86 channelId = CHANNEL_ID_HIGH; 87 importanceName = context.getString(R.string.importance_high); 88 break; 89 default: 90 throw new IllegalArgumentException("Unsupported importance: " + importance); 91 } 92 NotificationManager notificationMgr = context.getSystemService(NotificationManager.class); 93 notificationMgr.createNotificationChannel( 94 new NotificationChannel(channelId, importanceName, importance)); 95 96 Bundle extras = new Bundle(); 97 extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, 98 context.getString(com.android.internal.R.string.android_system_label)); 99 100 return new Notification.Builder(context, channelId).addExtras(extras); 101 } 102 NotificationHelper(Context context)103 public NotificationHelper(Context context) { 104 super(context); 105 } 106 107 @Override cancelNotificationAsUser(UserHandle user, int notificationId)108 public void cancelNotificationAsUser(UserHandle user, int notificationId) { 109 if (DEBUG) { 110 Slogf.d(TAG, "Canceling notification %d for user %s", notificationId, user); 111 } 112 getContext().getSystemService(NotificationManager.class).cancelAsUser(TAG, notificationId, 113 user); 114 } 115 116 @Override showUserDisclaimerNotification(UserHandle user)117 public void showUserDisclaimerNotification(UserHandle user) { 118 // TODO(b/175057848) persist status so it's shown again if car service crashes? 119 PendingIntent pendingIntent = getPendingUserDisclaimerIntent(getContext(), 120 /* extraFlags= */ 0, user); 121 122 Notification notification = NotificationHelper 123 .newNotificationBuilder(getContext(), NotificationManager.IMPORTANCE_DEFAULT) 124 // TODO(b/177552737): Use a better icon? 125 .setSmallIcon(R.drawable.car_ic_mode) 126 .setContentTitle( 127 getContext().getString(R.string.new_user_managed_notification_title)) 128 .setContentText(ManagedDeviceTextView.getManagedDeviceText(getContext())) 129 .setCategory(Notification.CATEGORY_CAR_INFORMATION) 130 .setContentIntent(pendingIntent) 131 .setOngoing(true) 132 .build(); 133 134 if (DEBUG) { 135 Slogf.d(TAG, "Showing new managed notification (id " 136 + NEW_USER_DISCLAIMER_NOTIFICATION_ID + " on user " + user); 137 } 138 getContext().getSystemService(NotificationManager.class) 139 .notifyAsUser(TAG, NEW_USER_DISCLAIMER_NOTIFICATION_ID, 140 notification, user); 141 } 142 143 @Override cancelUserDisclaimerNotification(UserHandle user)144 public void cancelUserDisclaimerNotification(UserHandle user) { 145 if (DEBUG) { 146 Slogf.d(TAG, "Canceling notification " + NEW_USER_DISCLAIMER_NOTIFICATION_ID 147 + " for user " + user); 148 } 149 getContext().getSystemService(NotificationManager.class) 150 .cancelAsUser(TAG, NEW_USER_DISCLAIMER_NOTIFICATION_ID, user); 151 getPendingUserDisclaimerIntent(getContext(), 152 PendingIntent.FLAG_UPDATE_CURRENT, user).cancel(); 153 } 154 155 /** 156 * Creates and returns the PendingIntent for User Disclaimer notification. 157 */ 158 @VisibleForTesting getPendingUserDisclaimerIntent(Context context, int extraFlags, UserHandle user)159 public static PendingIntent getPendingUserDisclaimerIntent(Context context, int extraFlags, 160 UserHandle user) { 161 return PendingIntent 162 .getActivityAsUser(context, NEW_USER_DISCLAIMER_NOTIFICATION_ID, 163 new Intent().setComponent(ComponentName.unflattenFromString( 164 context.getString(R.string.config_newUserDisclaimerActivity) 165 )), 166 PendingIntent.FLAG_IMMUTABLE | extraFlags, null, user); 167 } 168 169 @Override showResourceOveruseNotificationsAsUser( UserHandle user, SparseArray<String> headsUpNotificationPackagesById, SparseArray<String> notificationCenterPackagesById)170 public void showResourceOveruseNotificationsAsUser( 171 UserHandle user, SparseArray<String> headsUpNotificationPackagesById, 172 SparseArray<String> notificationCenterPackagesById) { 173 Preconditions.checkArgument(user.getIdentifier() >= 0, 174 "Invalid user: %s. Must provide the user handle for a specific user.", user); 175 176 SparseArray<SparseArray<String>> packagesByImportance = new SparseArray<>(2); 177 packagesByImportance.put(NotificationManager.IMPORTANCE_HIGH, 178 headsUpNotificationPackagesById); 179 packagesByImportance.put(NotificationManager.IMPORTANCE_DEFAULT, 180 notificationCenterPackagesById); 181 showResourceOveruseNotificationsAsUser(getContext(), user, packagesByImportance); 182 } 183 184 @Override showFactoryResetNotification(ICarResultReceiver callback)185 public void showFactoryResetNotification(ICarResultReceiver callback) { 186 // The factory request is received by CarService - which runs on system user - but the 187 // notification will be sent to all users. 188 UserHandle currentUser = UserHandle.of(ActivityManager.getCurrentUser()); 189 190 ComponentName factoryResetActivity = ComponentName.unflattenFromString( 191 getContext().getString(R.string.config_factoryResetActivity)); 192 @SuppressWarnings("deprecation") 193 Intent intent = new Intent() 194 .setComponent(factoryResetActivity) 195 .putExtra(EXTRA_FACTORY_RESET_CALLBACK, callback.asBinder()); 196 PendingIntent pendingIntent = PendingIntent.getActivityAsUser(getContext(), 197 FACTORY_RESET_NOTIFICATION_ID, intent, PendingIntent.FLAG_IMMUTABLE, 198 /* options= */ null, currentUser); 199 200 Notification notification = NotificationHelper 201 .newNotificationBuilder(getContext(), NotificationManager.IMPORTANCE_HIGH) 202 .setSmallIcon(R.drawable.car_ic_warning) 203 .setColor(getContext().getColor(R.color.red_warning)) 204 .setContentTitle(getContext().getString(R.string.factory_reset_notification_title)) 205 .setContentText(getContext().getString(R.string.factory_reset_notification_text)) 206 .setCategory(Notification.CATEGORY_CAR_WARNING) 207 .setOngoing(true) 208 .addAction(/* icon= */ 0, 209 getContext().getString(R.string.factory_reset_notification_button), 210 pendingIntent) 211 .build(); 212 213 Slogf.i(TAG, "Showing factory reset notification on all users"); 214 getContext().getSystemService(NotificationManager.class) 215 .notifyAsUser(TAG, FACTORY_RESET_NOTIFICATION_ID, notification, UserHandle.ALL); 216 } 217 showResourceOveruseNotificationsAsUser(Context context, UserHandle user, SparseArray<SparseArray<String>> packagesByImportance)218 private static void showResourceOveruseNotificationsAsUser(Context context, UserHandle user, 219 SparseArray<SparseArray<String>> packagesByImportance) { 220 PackageManager packageManager = context.getPackageManager(); 221 NotificationManager notificationManager = 222 context.getSystemService(NotificationManager.class); 223 224 CharSequence titleTemplate = context.getText(R.string.resource_overuse_notification_title); 225 String textDisabledApp = 226 context.getString(R.string.resource_overuse_notification_text_disabled_app); 227 String actionTitlePrioritizeApp = 228 context.getString(R.string.resource_overuse_notification_button_prioritize_app); 229 String actionTitleCloseNotification = 230 context.getString(R.string.resource_overuse_notification_button_close_app); 231 232 for (int i = 0; i < packagesByImportance.size(); i++) { 233 int importance = packagesByImportance.keyAt(i); 234 SparseArray<String> packagesById = packagesByImportance.valueAt(i); 235 for (int pkgIdx = 0; pkgIdx < packagesById.size(); pkgIdx++) { 236 int notificationId = packagesById.keyAt(pkgIdx); 237 String packageName = packagesById.valueAt(pkgIdx); 238 239 CharSequence appName; 240 try { 241 ApplicationInfo info = packageManager.getApplicationInfoAsUser(packageName, 242 /* flags= */ 0, user); 243 appName = info.loadLabel(packageManager); 244 } catch (PackageManager.NameNotFoundException e) { 245 Slogf.e(TAG, e, "Package '%s' not found for user %s", packageName, user); 246 continue; 247 } 248 PendingIntent closeActionPendingIntent = getPendingIntent(context, 249 CAR_WATCHDOG_ACTION_DISMISS_RESOURCE_OVERUSE_NOTIFICATION, user, 250 packageName, notificationId); 251 PendingIntent prioritizeActionPendingIntent = getPendingIntent(context, 252 CAR_WATCHDOG_ACTION_LAUNCH_APP_SETTINGS, user, packageName, notificationId); 253 Notification notification = NotificationHelper 254 .newNotificationBuilder(context, importance) 255 .setSmallIcon(R.drawable.car_ic_warning) 256 .setContentTitle(TextUtils.expandTemplate(titleTemplate, appName)) 257 .setContentText(textDisabledApp) 258 .setCategory(Notification.CATEGORY_CAR_WARNING) 259 .addAction(new Notification.Action.Builder(/* icon= */ null, 260 actionTitleCloseNotification, closeActionPendingIntent).build()) 261 .addAction(new Notification.Action.Builder(/* icon= */ null, 262 actionTitlePrioritizeApp, prioritizeActionPendingIntent).build()) 263 .setDeleteIntent(closeActionPendingIntent) 264 .build(); 265 266 notificationManager.notifyAsUser(TAG, notificationId, notification, user); 267 268 if (DEBUG) { 269 Slogf.d(TAG, 270 "Sent user notification (id %d) for resource overuse for " 271 + "user %s.\nNotification { App name: %s, Importance: %d, " 272 + "Description: %s, Positive button text: %s, Negative button " 273 + "text: %s }", 274 notificationId, user, appName, importance, textDisabledApp, 275 actionTitleCloseNotification, actionTitlePrioritizeApp); 276 } 277 } 278 } 279 } 280 281 @VisibleForTesting getPendingIntent(Context context, String action, UserHandle user, String packageName, int notificationId)282 static PendingIntent getPendingIntent(Context context, String action, UserHandle user, 283 String packageName, int notificationId) { 284 Intent intent = new Intent(action) 285 .putExtra(Intent.EXTRA_USER, user) 286 .putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) 287 .putExtra(INTENT_EXTRA_NOTIFICATION_ID, notificationId) 288 .setPackage(context.getPackageName()) 289 .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); 290 return PendingIntent.getBroadcastAsUser(context, notificationId, intent, 291 PendingIntent.FLAG_IMMUTABLE, user); 292 } 293 } 294