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