• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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