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