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