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