• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.systemui.statusbar.notification;
18 
19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
21 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
22 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.app.ActivityManager;
27 import android.app.ActivityTaskManager;
28 import android.app.AppGlobals;
29 import android.app.Notification;
30 import android.app.NotificationManager;
31 import android.app.PendingIntent;
32 import android.app.SynchronousUserSwitchObserver;
33 import android.content.ComponentName;
34 import android.content.Intent;
35 import android.content.pm.ApplicationInfo;
36 import android.content.pm.IPackageManager;
37 import android.content.pm.PackageManager;
38 import android.graphics.drawable.Icon;
39 import android.net.Uri;
40 import android.os.Bundle;
41 import android.os.Handler;
42 import android.os.RemoteException;
43 import android.os.UserHandle;
44 import android.provider.Settings;
45 import android.service.notification.StatusBarNotification;
46 import android.util.ArraySet;
47 import android.util.Pair;
48 
49 import com.android.internal.messages.nano.SystemMessageProto;
50 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
51 import com.android.systemui.Dependency;
52 import com.android.systemui.DockedStackExistsListener;
53 import com.android.systemui.R;
54 import com.android.systemui.SysUiServiceProvider;
55 import com.android.systemui.SystemUI;
56 import com.android.systemui.UiOffloadThread;
57 import com.android.systemui.shared.system.TaskStackChangeListener;
58 import com.android.systemui.statusbar.CommandQueue;
59 import com.android.systemui.statusbar.policy.KeyguardMonitor;
60 import com.android.systemui.util.NotificationChannels;
61 
62 import java.util.List;
63 
64 /** The clsss to show notification(s) of instant apps. This may show multiple notifications on
65  * splitted screen.
66  */
67 public class InstantAppNotifier extends SystemUI
68         implements CommandQueue.Callbacks, KeyguardMonitor.Callback {
69     private static final String TAG = "InstantAppNotifier";
70     public static final int NUM_TASKS_FOR_INSTANT_APP_INFO = 5;
71 
72     private final Handler mHandler = new Handler();
73     private final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class);
74     private final ArraySet<Pair<String, Integer>> mCurrentNotifs = new ArraySet<>();
75     private boolean mDockedStackExists;
76     private KeyguardMonitor mKeyguardMonitor;
77 
InstantAppNotifier()78     public InstantAppNotifier() {}
79 
80     @Override
start()81     public void start() {
82         mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
83 
84         // listen for user / profile change.
85         try {
86             ActivityManager.getService().registerUserSwitchObserver(mUserSwitchListener, TAG);
87         } catch (RemoteException e) {
88             // Ignore
89         }
90 
91         SysUiServiceProvider.getComponent(mContext, CommandQueue.class).addCallback(this);
92         mKeyguardMonitor.addCallback(this);
93 
94         DockedStackExistsListener.register(
95                 exists -> {
96                     mDockedStackExists = exists;
97                     updateForegroundInstantApps();
98                 });
99 
100         // Clear out all old notifications on startup (only present in the case where sysui dies)
101         NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
102         for (StatusBarNotification notification : noMan.getActiveNotifications()) {
103             if (notification.getId() == SystemMessage.NOTE_INSTANT_APPS) {
104                 noMan.cancel(notification.getTag(), notification.getId());
105             }
106         }
107     }
108 
109     @Override
appTransitionStarting( int displayId, long startTime, long duration, boolean forced)110     public void appTransitionStarting(
111             int displayId, long startTime, long duration, boolean forced) {
112         if (mContext.getDisplayId() == displayId) {
113             updateForegroundInstantApps();
114         }
115     }
116 
117     @Override
onKeyguardShowingChanged()118     public void onKeyguardShowingChanged() {
119         updateForegroundInstantApps();
120     }
121 
122     @Override
preloadRecentApps()123     public void preloadRecentApps() {
124         updateForegroundInstantApps();
125     }
126 
127     private final SynchronousUserSwitchObserver mUserSwitchListener =
128             new SynchronousUserSwitchObserver() {
129                 @Override
130                 public void onUserSwitching(int newUserId) throws RemoteException {}
131 
132                 @Override
133                 public void onUserSwitchComplete(int newUserId) throws RemoteException {
134                     mHandler.post(
135                             () -> {
136                                 updateForegroundInstantApps();
137                             });
138                 }
139             };
140 
141     private final TaskStackChangeListener mTaskListener =
142             new TaskStackChangeListener() {
143                 @Override
144                 public void onTaskStackChanged() {
145                     // Listen for changes to stacks and then check which instant apps are
146                     // foreground.
147                     updateForegroundInstantApps();
148                 }
149             };
150 
updateForegroundInstantApps()151     private void updateForegroundInstantApps() {
152         NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
153         IPackageManager pm = AppGlobals.getPackageManager();
154         mUiOffloadThread.submit(
155                 () -> {
156                     ArraySet<Pair<String, Integer>> notifs = new ArraySet<>(mCurrentNotifs);
157                     try {
158                         final ActivityManager.StackInfo focusedStack =
159                                 ActivityTaskManager.getService().getFocusedStackInfo();
160                         if (focusedStack != null) {
161                             final int windowingMode =
162                                     focusedStack.configuration.windowConfiguration
163                                             .getWindowingMode();
164                             if (windowingMode == WINDOWING_MODE_FULLSCREEN
165                                     || windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) {
166                                 checkAndPostForStack(focusedStack, notifs, noMan, pm);
167                             }
168                         }
169                         if (mDockedStackExists) {
170                             checkAndPostForPrimaryScreen(notifs, noMan, pm);
171                         }
172                     } catch (RemoteException e) {
173                         e.rethrowFromSystemServer();
174                     }
175 
176                     // Cancel all the leftover notifications that don't have a foreground
177                     // process anymore.
178                     notifs.forEach(
179                             v -> {
180                                 mCurrentNotifs.remove(v);
181 
182                                 noMan.cancelAsUser(
183                                         v.first,
184                                         SystemMessageProto.SystemMessage.NOTE_INSTANT_APPS,
185                                         new UserHandle(v.second));
186                             });
187                 });
188     }
189 
190     /**
191      * Posts an instant app notification if the top activity of the primary container in the
192      * splitted screen is an instant app and the corresponding instant app notification is not
193      * posted yet. If the notification already exists, this method removes it from {@code
194      * notifs} in the arguments.
195      */
checkAndPostForPrimaryScreen( @onNull ArraySet<Pair<String, Integer>> notifs, @NonNull NotificationManager noMan, @NonNull IPackageManager pm)196     private void checkAndPostForPrimaryScreen(
197             @NonNull ArraySet<Pair<String, Integer>> notifs,
198             @NonNull NotificationManager noMan,
199             @NonNull IPackageManager pm) {
200         try {
201             final ActivityManager.StackInfo info =
202                     ActivityTaskManager.getService()
203                             .getStackInfo(
204                                     WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_UNDEFINED);
205             checkAndPostForStack(info, notifs, noMan, pm);
206         } catch (RemoteException e) {
207             e.rethrowFromSystemServer();
208         }
209     }
210 
211     /**
212      * Posts an instant app notification if the top activity of the given stack is an instant app
213      * and the corresponding instant app notification is not posted yet. If the notification already
214      * exists, this method removes it from {@code notifs} in the arguments.
215      */
checkAndPostForStack( @ullable ActivityManager.StackInfo info, @NonNull ArraySet<Pair<String, Integer>> notifs, @NonNull NotificationManager noMan, @NonNull IPackageManager pm)216     private void checkAndPostForStack(
217             @Nullable ActivityManager.StackInfo info,
218             @NonNull ArraySet<Pair<String, Integer>> notifs,
219             @NonNull NotificationManager noMan,
220             @NonNull IPackageManager pm) {
221         try {
222             if (info == null || info.topActivity == null) return;
223             String pkg = info.topActivity.getPackageName();
224             Pair<String, Integer> key = new Pair<>(pkg, info.userId);
225             if (!notifs.remove(key)) {
226                 // TODO: Optimize by not always needing to get application info.
227                 // Maybe cache non-instant-app packages?
228                 ApplicationInfo appInfo =
229                         pm.getApplicationInfo(
230                                 pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES, info.userId);
231                 if (appInfo.isInstantApp()) {
232                     postInstantAppNotif(
233                             pkg,
234                             info.userId,
235                             appInfo,
236                             noMan,
237                             info.taskIds[info.taskIds.length - 1]);
238                 }
239             }
240         } catch (RemoteException e) {
241             e.rethrowFromSystemServer();
242         }
243     }
244 
245     /** Posts an instant app notification. */
postInstantAppNotif( @onNull String pkg, int userId, @NonNull ApplicationInfo appInfo, @NonNull NotificationManager noMan, int taskId)246     private void postInstantAppNotif(
247             @NonNull String pkg,
248             int userId,
249             @NonNull ApplicationInfo appInfo,
250             @NonNull NotificationManager noMan,
251             int taskId) {
252         final Bundle extras = new Bundle();
253         extras.putString(
254                 Notification.EXTRA_SUBSTITUTE_APP_NAME, mContext.getString(R.string.instant_apps));
255         mCurrentNotifs.add(new Pair<>(pkg, userId));
256 
257         String helpUrl = mContext.getString(R.string.instant_apps_help_url);
258         boolean hasHelpUrl = !helpUrl.isEmpty();
259         String message =
260                 mContext.getString(
261                         hasHelpUrl
262                                 ? R.string.instant_apps_message_with_help
263                                 : R.string.instant_apps_message);
264 
265         UserHandle user = UserHandle.of(userId);
266         PendingIntent appInfoAction =
267                 PendingIntent.getActivityAsUser(
268                         mContext,
269                         0,
270                         new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
271                                 .setData(Uri.fromParts("package", pkg, null)),
272                         PendingIntent.FLAG_IMMUTABLE,
273                         null,
274                         user);
275         Notification.Action action =
276                 new Notification.Action.Builder(
277                                 null, mContext.getString(R.string.app_info), appInfoAction)
278                         .build();
279         PendingIntent helpCenterIntent =
280                 hasHelpUrl
281                         ? PendingIntent.getActivityAsUser(
282                                 mContext,
283                                 0,
284                                 new Intent(Intent.ACTION_VIEW).setData(Uri.parse(helpUrl)),
285                                 PendingIntent.FLAG_IMMUTABLE,
286                                 null,
287                                 user)
288                         : null;
289 
290         Intent browserIntent = getTaskIntent(taskId, userId);
291         Notification.Builder builder =
292                 new Notification.Builder(mContext, NotificationChannels.GENERAL);
293         if (browserIntent != null && browserIntent.isWebIntent()) {
294             // Make sure that this doesn't resolve back to an instant app
295             browserIntent
296                     .setComponent(null)
297                     .setPackage(null)
298                     .addFlags(Intent.FLAG_IGNORE_EPHEMERAL)
299                     .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
300 
301             PendingIntent pendingIntent =
302                     PendingIntent.getActivityAsUser(
303                             mContext,
304                             0 /* requestCode */,
305                             browserIntent,
306                             PendingIntent.FLAG_IMMUTABLE /* flags */,
307                             null,
308                             user);
309             ComponentName aiaComponent = null;
310             try {
311                 aiaComponent = AppGlobals.getPackageManager().getInstantAppInstallerComponent();
312             } catch (RemoteException e) {
313                 e.rethrowFromSystemServer();
314             }
315             Intent goToWebIntent =
316                     new Intent()
317                             .setComponent(aiaComponent)
318                             .setAction(Intent.ACTION_VIEW)
319                             .addCategory(Intent.CATEGORY_BROWSABLE)
320                             .addCategory("unique:" + System.currentTimeMillis())
321                             .putExtra(Intent.EXTRA_PACKAGE_NAME, appInfo.packageName)
322                             .putExtra(
323                                     Intent.EXTRA_VERSION_CODE,
324                                     (int) (appInfo.versionCode & 0x7fffffff))
325                             .putExtra(Intent.EXTRA_LONG_VERSION_CODE, appInfo.longVersionCode)
326                             .putExtra(Intent.EXTRA_INSTANT_APP_FAILURE, pendingIntent);
327 
328             PendingIntent webPendingIntent = PendingIntent.getActivityAsUser(mContext, 0,
329                     goToWebIntent, PendingIntent.FLAG_IMMUTABLE, null, user);
330             Notification.Action webAction =
331                     new Notification.Action.Builder(
332                                     null, mContext.getString(R.string.go_to_web), webPendingIntent)
333                             .build();
334             builder.addAction(webAction);
335         }
336 
337         noMan.notifyAsUser(
338                 pkg,
339                 SystemMessage.NOTE_INSTANT_APPS,
340                 builder.addExtras(extras)
341                         .addAction(action)
342                         .setContentIntent(helpCenterIntent)
343                         .setColor(mContext.getColor(R.color.instant_apps_color))
344                         .setContentTitle(
345                                 mContext.getString(
346                                         R.string.instant_apps_title,
347                                         appInfo.loadLabel(mContext.getPackageManager())))
348                         .setLargeIcon(Icon.createWithResource(pkg, appInfo.icon))
349                         .setSmallIcon(
350                                 Icon.createWithResource(
351                                         mContext.getPackageName(), R.drawable.instant_icon))
352                         .setContentText(message)
353                         .setStyle(new Notification.BigTextStyle().bigText(message))
354                         .setOngoing(true)
355                         .build(),
356                 new UserHandle(userId));
357     }
358 
359     @Nullable
getTaskIntent(int taskId, int userId)360     private Intent getTaskIntent(int taskId, int userId) {
361         try {
362             final List<ActivityManager.RecentTaskInfo> tasks =
363                     ActivityTaskManager.getService()
364                             .getRecentTasks(NUM_TASKS_FOR_INSTANT_APP_INFO, 0, userId)
365                             .getList();
366             for (int i = 0; i < tasks.size(); i++) {
367                 if (tasks.get(i).id == taskId) {
368                     return tasks.get(i).baseIntent;
369                 }
370             }
371         } catch (RemoteException e) {
372             // Fall through
373         }
374         return null;
375     }
376 }
377