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