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