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.watchdog; 18 19 import static com.android.car.watchdog.CarWatchdogService.ACTION_DISMISS_RESOURCE_OVERUSE_NOTIFICATION; 20 import static com.android.car.watchdog.CarWatchdogService.ACTION_LAUNCH_APP_SETTINGS; 21 import static com.android.car.watchdog.CarWatchdogService.ACTION_RESOURCE_OVERUSE_DISABLE_APP; 22 import static com.android.car.watchdog.CarWatchdogService.DEBUG; 23 import static com.android.car.watchdog.CarWatchdogService.TAG; 24 import static com.android.car.watchdog.WatchdogPerfHandler.INTENT_EXTRA_ID; 25 26 import android.annotation.Nullable; 27 import android.app.Notification; 28 import android.app.NotificationManager; 29 import android.app.PendingIntent; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.pm.ApplicationInfo; 33 import android.content.pm.PackageManager; 34 import android.os.UserHandle; 35 import android.text.TextUtils; 36 37 import com.android.car.R; 38 import com.android.car.admin.NotificationHelper; 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.internal.util.Preconditions; 41 import com.android.server.utils.Slogf; 42 43 import java.util.List; 44 import java.util.Objects; 45 46 /** Helper for sending notifications to the end-user. */ 47 public final class UserNotificationHelper { 48 private final Context mContext; 49 private final CharSequence mUserNotificationTitleTemplate; 50 private final String mUserNotificationTextPrioritizeApp; 51 private final String mUserNotificationTextDisableApp; 52 private final String mUserNotificationTextUninstallApp; 53 private final String mUserNotificationActionTextPrioritizeApp; 54 private final String mUserNotificationActionTextDisableApp; 55 private final String mUserNotificationActionTextUninstallApp; 56 UserNotificationHelper(Context context)57 UserNotificationHelper(Context context) { 58 mContext = context; 59 mUserNotificationTitleTemplate = 60 mContext.getText(R.string.resource_overuse_notification_title); 61 mUserNotificationTextPrioritizeApp = 62 mContext.getString(R.string.resource_overuse_notification_text_prioritize_app); 63 mUserNotificationTextDisableApp = 64 mContext.getString(R.string.resource_overuse_notification_text_disable_app); 65 mUserNotificationTextUninstallApp = 66 mContext.getString(R.string.resource_overuse_notification_text_uninstall_app); 67 mUserNotificationActionTextPrioritizeApp = 68 mContext.getString(R.string.resource_overuse_notification_button_prioritize_app); 69 mUserNotificationActionTextDisableApp = 70 mContext.getString(R.string.resource_overuse_notification_button_disable_app); 71 mUserNotificationActionTextUninstallApp = 72 mContext.getString(R.string.resource_overuse_notification_button_uninstall_app); 73 } 74 showResourceOveruseNotificationsAsUser(List<PackageNotificationInfo> infos, UserHandle userHandle)75 void showResourceOveruseNotificationsAsUser(List<PackageNotificationInfo> infos, 76 UserHandle userHandle) { 77 Preconditions.checkArgument(userHandle.getIdentifier() >= 0, 78 "Must provide the user handle for a specific user"); 79 PackageManager packageManager = mContext.getPackageManager(); 80 NotificationManager notificationManager = 81 mContext.getSystemService(NotificationManager.class); 82 int sentNotifications = 0; 83 for (int i = 0; i < infos.size(); i++) { 84 PackageNotificationInfo info = infos.get(i); 85 Notification notification = constructNotification(packageManager, userHandle, 86 info.packageName, info.importance, info.notificationId); 87 if (notification == null) { 88 continue; 89 } 90 notificationManager.notifyAsUser(TAG, info.notificationId, notification, userHandle); 91 ++sentNotifications; 92 } 93 if (DEBUG) { 94 Slogf.d(TAG, "Sent %d resource overuse notifications successfully", sentNotifications); 95 } 96 } 97 constructNotification(PackageManager packageManager, UserHandle userHandle, String packageName, @NotificationManager.Importance int importance, int notificationId)98 private @Nullable Notification constructNotification(PackageManager packageManager, 99 UserHandle userHandle, String packageName, 100 @NotificationManager.Importance int importance, int notificationId) { 101 CharSequence appName; 102 boolean isBundledApp; 103 try { 104 ApplicationInfo info = packageManager.getApplicationInfoAsUser(packageName, 105 /* flags= */ 0, userHandle); 106 appName = info.loadLabel(packageManager); 107 isBundledApp = (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 108 } catch (PackageManager.NameNotFoundException e) { 109 Slogf.e(TAG, e, "Package '%s' not found for user %s", packageName, userHandle); 110 return null; 111 } 112 PendingIntent positiveActionIntent = getCarServicePendingIntent(mContext, 113 ACTION_LAUNCH_APP_SETTINGS, userHandle, packageName, notificationId); 114 PendingIntent deleteIntent = getCarServicePendingIntent(mContext, 115 ACTION_DISMISS_RESOURCE_OVERUSE_NOTIFICATION, userHandle, packageName, 116 notificationId); 117 String text; 118 String negativeActionText; 119 PendingIntent negativeActionIntent; 120 // Bundled apps have disable button rather than uninstall button in the settings app. Follow 121 // the same pattern here. 122 if (isBundledApp) { 123 text = mUserNotificationTextDisableApp + " " + mUserNotificationTextPrioritizeApp; 124 negativeActionText = mUserNotificationActionTextDisableApp; 125 negativeActionIntent = getCarServicePendingIntent(mContext, 126 ACTION_RESOURCE_OVERUSE_DISABLE_APP, userHandle, packageName, notificationId); 127 } else { 128 text = mUserNotificationTextUninstallApp + " " + mUserNotificationTextPrioritizeApp; 129 negativeActionText = mUserNotificationActionTextUninstallApp; 130 negativeActionIntent = positiveActionIntent; 131 } 132 133 return NotificationHelper.newNotificationBuilder(mContext, importance) 134 .setSmallIcon(R.drawable.car_ic_warning) 135 .setContentTitle(TextUtils.expandTemplate(mUserNotificationTitleTemplate, appName)) 136 .setContentText(text) 137 .setCategory(Notification.CATEGORY_CAR_WARNING) 138 .addAction(new Notification.Action.Builder(/* icon= */ null, 139 mUserNotificationActionTextPrioritizeApp, positiveActionIntent).build()) 140 .addAction(new Notification.Action.Builder(/* icon= */ null, negativeActionText, 141 negativeActionIntent).build()) 142 .setDeleteIntent(deleteIntent) 143 .build(); 144 } 145 146 @VisibleForTesting getCarServicePendingIntent(Context context, String intentAction, UserHandle userHandle, String packageName, int notificationId)147 static PendingIntent getCarServicePendingIntent(Context context, String intentAction, 148 UserHandle userHandle, String packageName, int notificationId) { 149 Intent intent = new Intent(intentAction) 150 .putExtra(INTENT_EXTRA_ID, notificationId) 151 .putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) 152 .putExtra(Intent.EXTRA_USER, userHandle) 153 .setPackage(context.getPackageName()) 154 .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); 155 return PendingIntent.getBroadcastAsUser(context, notificationId, intent, 156 PendingIntent.FLAG_IMMUTABLE, userHandle); 157 } 158 159 /** Defines the notification info for a specific package. */ 160 static final class PackageNotificationInfo { 161 public final String packageName; 162 public final int importance; 163 public final int notificationId; PackageNotificationInfo(String packageName, @NotificationManager.Importance int importance, int notificationId)164 PackageNotificationInfo(String packageName, @NotificationManager.Importance int importance, 165 int notificationId) { 166 this.packageName = packageName; 167 this.importance = importance; 168 this.notificationId = notificationId; 169 } 170 171 @Override equals(Object obj)172 public boolean equals(Object obj) { 173 if (obj == this) { 174 return true; 175 } 176 if (!(obj instanceof PackageNotificationInfo)) { 177 return false; 178 } 179 PackageNotificationInfo other = (PackageNotificationInfo) obj; 180 return packageName.equals(other.packageName) && importance == other.importance 181 && notificationId == other.notificationId; 182 } 183 184 @Override hashCode()185 public int hashCode() { 186 return Objects.hash(packageName, importance, notificationId); 187 } 188 189 @Override toString()190 public String toString() { 191 return new StringBuilder().append("PackageNotificationInfo{packageName: ") 192 .append(packageName).append(", importance: ").append(importance) 193 .append(", notificationId: ").append(notificationId).append("}").toString(); 194 } 195 } 196 } 197