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