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.ActivityTaskManager.RootTaskInfo;
30 import android.app.AppGlobals;
31 import android.app.Notification;
32 import android.app.NotificationManager;
33 import android.app.PendingIntent;
34 import android.app.SynchronousUserSwitchObserver;
35 import android.content.ComponentName;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.pm.ApplicationInfo;
39 import android.content.pm.IPackageManager;
40 import android.content.pm.PackageManager;
41 import android.graphics.drawable.Icon;
42 import android.net.Uri;
43 import android.os.Bundle;
44 import android.os.Handler;
45 import android.os.RemoteException;
46 import android.os.UserHandle;
47 import android.provider.Settings;
48 import android.service.notification.StatusBarNotification;
49 import android.util.ArraySet;
50 import android.util.Pair;
51 
52 import com.android.internal.messages.nano.SystemMessageProto;
53 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
54 import com.android.systemui.Dependency;
55 import com.android.systemui.R;
56 import com.android.systemui.SystemUI;
57 import com.android.systemui.dagger.SysUISingleton;
58 import com.android.systemui.dagger.qualifiers.UiBackground;
59 import com.android.systemui.statusbar.CommandQueue;
60 import com.android.systemui.statusbar.policy.KeyguardStateController;
61 import com.android.systemui.util.NotificationChannels;
62 import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
63 
64 import java.util.List;
65 import java.util.Optional;
66 import java.util.concurrent.Executor;
67 
68 import javax.inject.Inject;
69 
70 /** The class to show notification(s) of instant apps. This may show multiple notifications on
71  * splitted screen.
72  */
73 @SysUISingleton
74 public class InstantAppNotifier extends SystemUI
75         implements CommandQueue.Callbacks, KeyguardStateController.Callback {
76     private static final String TAG = "InstantAppNotifier";
77     public static final int NUM_TASKS_FOR_INSTANT_APP_INFO = 5;
78 
79     private final Handler mHandler = new Handler();
80     private final Executor mUiBgExecutor;
81     private final ArraySet<Pair<String, Integer>> mCurrentNotifs = new ArraySet<>();
82     private final CommandQueue mCommandQueue;
83     private boolean mDockedStackExists;
84     private KeyguardStateController mKeyguardStateController;
85     private final Optional<LegacySplitScreen> mSplitScreenOptional;
86 
87     @Inject
InstantAppNotifier(Context context, CommandQueue commandQueue, @UiBackground Executor uiBgExecutor, Optional<LegacySplitScreen> splitScreenOptional)88     public InstantAppNotifier(Context context, CommandQueue commandQueue,
89             @UiBackground Executor uiBgExecutor, Optional<LegacySplitScreen> splitScreenOptional) {
90         super(context);
91         mSplitScreenOptional = splitScreenOptional;
92         mCommandQueue = commandQueue;
93         mUiBgExecutor = uiBgExecutor;
94     }
95 
96     @Override
start()97     public void start() {
98         mKeyguardStateController = Dependency.get(KeyguardStateController.class);
99 
100         // listen for user / profile change.
101         try {
102             ActivityManager.getService().registerUserSwitchObserver(mUserSwitchListener, TAG);
103         } catch (RemoteException e) {
104             // Ignore
105         }
106 
107         mCommandQueue.addCallback(this);
108         mKeyguardStateController.addCallback(this);
109 
110         mSplitScreenOptional.ifPresent(splitScreen ->
111                 splitScreen.registerInSplitScreenListener(exists -> {
112                     mDockedStackExists = exists;
113                     updateForegroundInstantApps();
114                 }));
115 
116         // Clear out all old notifications on startup (only present in the case where sysui dies)
117         NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
118         for (StatusBarNotification notification : noMan.getActiveNotifications()) {
119             if (notification.getId() == SystemMessage.NOTE_INSTANT_APPS) {
120                 noMan.cancel(notification.getTag(), notification.getId());
121             }
122         }
123     }
124 
125     @Override
appTransitionStarting( int displayId, long startTime, long duration, boolean forced)126     public void appTransitionStarting(
127             int displayId, long startTime, long duration, boolean forced) {
128         if (mContext.getDisplayId() == displayId) {
129             updateForegroundInstantApps();
130         }
131     }
132 
133     @Override
onKeyguardShowingChanged()134     public void onKeyguardShowingChanged() {
135         updateForegroundInstantApps();
136     }
137 
138     @Override
preloadRecentApps()139     public void preloadRecentApps() {
140         updateForegroundInstantApps();
141     }
142 
143     private final SynchronousUserSwitchObserver mUserSwitchListener =
144             new SynchronousUserSwitchObserver() {
145                 @Override
146                 public void onUserSwitching(int newUserId) throws RemoteException {}
147 
148                 @Override
149                 public void onUserSwitchComplete(int newUserId) throws RemoteException {
150                     mHandler.post(
151                             () -> {
152                                 updateForegroundInstantApps();
153                             });
154                 }
155             };
156 
157 
updateForegroundInstantApps()158     private void updateForegroundInstantApps() {
159         NotificationManager noMan = mContext.getSystemService(NotificationManager.class);
160         IPackageManager pm = AppGlobals.getPackageManager();
161         mUiBgExecutor.execute(
162                 () -> {
163                     ArraySet<Pair<String, Integer>> notifs = new ArraySet<>(mCurrentNotifs);
164                     try {
165                         final RootTaskInfo focusedTask =
166                                 ActivityTaskManager.getService().getFocusedRootTaskInfo();
167                         if (focusedTask != null) {
168                             final int windowingMode =
169                                     focusedTask.configuration.windowConfiguration
170                                             .getWindowingMode();
171                             if (windowingMode == WINDOWING_MODE_FULLSCREEN
172                                     || windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
173                                     || windowingMode == WINDOWING_MODE_FREEFORM) {
174                                 checkAndPostForStack(focusedTask, notifs, noMan, pm);
175                             }
176                         }
177                         if (mDockedStackExists) {
178                             checkAndPostForPrimaryScreen(notifs, noMan, pm);
179                         }
180                     } catch (RemoteException e) {
181                         e.rethrowFromSystemServer();
182                     }
183 
184                     // Cancel all the leftover notifications that don't have a foreground
185                     // process anymore.
186                     notifs.forEach(
187                             v -> {
188                                 mCurrentNotifs.remove(v);
189 
190                                 noMan.cancelAsUser(
191                                         v.first,
192                                         SystemMessageProto.SystemMessage.NOTE_INSTANT_APPS,
193                                         new UserHandle(v.second));
194                             });
195                 });
196     }
197 
198     /**
199      * Posts an instant app notification if the top activity of the primary container in the
200      * splitted screen is an instant app and the corresponding instant app notification is not
201      * posted yet. If the notification already exists, this method removes it from {@code
202      * notifs} in the arguments.
203      */
checkAndPostForPrimaryScreen( @onNull ArraySet<Pair<String, Integer>> notifs, @NonNull NotificationManager noMan, @NonNull IPackageManager pm)204     private void checkAndPostForPrimaryScreen(
205             @NonNull ArraySet<Pair<String, Integer>> notifs,
206             @NonNull NotificationManager noMan,
207             @NonNull IPackageManager pm) {
208         try {
209             final RootTaskInfo info = ActivityTaskManager.getService().getRootTaskInfo(
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 RootTaskInfo info, @NonNull ArraySet<Pair<String, Integer>> notifs, @NonNull NotificationManager noMan, @NonNull IPackageManager pm)222     private void checkAndPostForStack(
223             @Nullable RootTaskInfo 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.childTaskIds[info.childTaskIds.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         final List<ActivityManager.RecentTaskInfo> tasks =
368                 ActivityTaskManager.getInstance().getRecentTasks(
369                         NUM_TASKS_FOR_INSTANT_APP_INFO, 0, userId);
370         for (int i = 0; i < tasks.size(); i++) {
371             if (tasks.get(i).id == taskId) {
372                 return tasks.get(i).baseIntent;
373             }
374         }
375         return null;
376     }
377 }
378