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