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