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.screenshot; 18 19 import static com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS; 20 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; 21 import static com.android.systemui.screenshot.LogConfig.DEBUG_STORAGE; 22 import static com.android.systemui.screenshot.LogConfig.logTag; 23 import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.QUICK_SHARE_ACTION; 24 import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.REGULAR_SMART_ACTIONS; 25 26 import android.app.ActivityTaskManager; 27 import android.app.Notification; 28 import android.app.PendingIntent; 29 import android.content.ClipData; 30 import android.content.ClipDescription; 31 import android.content.ComponentName; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.pm.UserInfo; 35 import android.content.res.Resources; 36 import android.graphics.Bitmap; 37 import android.graphics.drawable.Icon; 38 import android.net.Uri; 39 import android.os.AsyncTask; 40 import android.os.Bundle; 41 import android.os.Handler; 42 import android.os.RemoteException; 43 import android.os.UserHandle; 44 import android.os.UserManager; 45 import android.provider.DeviceConfig; 46 import android.text.TextUtils; 47 import android.util.Log; 48 49 import com.android.internal.annotations.VisibleForTesting; 50 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; 51 import com.android.systemui.R; 52 import com.android.systemui.SystemUIFactory; 53 import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition; 54 55 import com.google.common.util.concurrent.ListenableFuture; 56 57 import java.text.DateFormat; 58 import java.util.ArrayList; 59 import java.util.Date; 60 import java.util.List; 61 import java.util.Random; 62 import java.util.UUID; 63 import java.util.concurrent.CompletableFuture; 64 import java.util.function.Supplier; 65 66 /** 67 * An AsyncTask that saves an image to the media store in the background. 68 */ 69 class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { 70 private static final String TAG = logTag(SaveImageInBackgroundTask.class); 71 72 private static final String SCREENSHOT_ID_TEMPLATE = "Screenshot_%s"; 73 private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"; 74 75 private final Context mContext; 76 private final ScreenshotSmartActions mScreenshotSmartActions; 77 private final ScreenshotController.SaveImageInBackgroundData mParams; 78 private final ScreenshotController.SavedImageData mImageData; 79 private final ScreenshotController.QuickShareData mQuickShareData; 80 81 private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider; 82 private String mScreenshotId; 83 private final boolean mSmartActionsEnabled; 84 private final Random mRandom = new Random(); 85 private final Supplier<ActionTransition> mSharedElementTransition; 86 private final ImageExporter mImageExporter; 87 private long mImageTime; 88 SaveImageInBackgroundTask(Context context, ImageExporter exporter, ScreenshotSmartActions screenshotSmartActions, ScreenshotController.SaveImageInBackgroundData data, Supplier<ActionTransition> sharedElementTransition)89 SaveImageInBackgroundTask(Context context, ImageExporter exporter, 90 ScreenshotSmartActions screenshotSmartActions, 91 ScreenshotController.SaveImageInBackgroundData data, 92 Supplier<ActionTransition> sharedElementTransition) { 93 mContext = context; 94 mScreenshotSmartActions = screenshotSmartActions; 95 mImageData = new ScreenshotController.SavedImageData(); 96 mQuickShareData = new ScreenshotController.QuickShareData(); 97 mSharedElementTransition = sharedElementTransition; 98 mImageExporter = exporter; 99 100 // Prepare all the output metadata 101 mParams = data; 102 103 // Initialize screenshot notification smart actions provider. 104 mSmartActionsEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, 105 SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, true); 106 if (mSmartActionsEnabled) { 107 mSmartActionsProvider = 108 SystemUIFactory.getInstance() 109 .createScreenshotNotificationSmartActionsProvider( 110 context, THREAD_POOL_EXECUTOR, new Handler()); 111 } else { 112 // If smart actions is not enabled use empty implementation. 113 mSmartActionsProvider = new ScreenshotNotificationSmartActionsProvider(); 114 } 115 } 116 117 @Override doInBackground(Void... paramsUnused)118 protected Void doInBackground(Void... paramsUnused) { 119 if (isCancelled()) { 120 if (DEBUG_STORAGE) { 121 Log.d(TAG, "cancelled! returning null"); 122 } 123 return null; 124 } 125 // TODO: move to constructor / from ScreenshotRequest 126 final UUID requestId = UUID.randomUUID(); 127 final UserHandle user = getUserHandleOfForegroundApplication(mContext); 128 129 Thread.currentThread().setPriority(Thread.MAX_PRIORITY); 130 131 Bitmap image = mParams.image; 132 mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, requestId); 133 try { 134 if (mSmartActionsEnabled && mParams.mQuickShareActionsReadyListener != null) { 135 // Since Quick Share target recommendation does not rely on image URL, it is 136 // queried and surfaced before image compress/export. Action intent would not be 137 // used, because it does not contain image URL. 138 queryQuickShareAction(image, user); 139 } 140 141 // Call synchronously here since already on a background thread. 142 ListenableFuture<ImageExporter.Result> future = 143 mImageExporter.export(Runnable::run, requestId, image); 144 ImageExporter.Result result = future.get(); 145 final Uri uri = result.uri; 146 mImageTime = result.timestamp; 147 148 CompletableFuture<List<Notification.Action>> smartActionsFuture = 149 mScreenshotSmartActions.getSmartActionsFuture( 150 mScreenshotId, uri, image, mSmartActionsProvider, REGULAR_SMART_ACTIONS, 151 mSmartActionsEnabled, user); 152 153 List<Notification.Action> smartActions = new ArrayList<>(); 154 if (mSmartActionsEnabled) { 155 int timeoutMs = DeviceConfig.getInt( 156 DeviceConfig.NAMESPACE_SYSTEMUI, 157 SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS, 158 1000); 159 smartActions.addAll(buildSmartActions( 160 mScreenshotSmartActions.getSmartActions( 161 mScreenshotId, smartActionsFuture, timeoutMs, 162 mSmartActionsProvider, REGULAR_SMART_ACTIONS), 163 mContext)); 164 } 165 166 mImageData.uri = uri; 167 mImageData.smartActions = smartActions; 168 mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri); 169 mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri); 170 mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri); 171 mImageData.quickShareAction = createQuickShareAction(mContext, 172 mQuickShareData.quickShareAction, uri); 173 174 mParams.mActionsReadyListener.onActionsReady(mImageData); 175 if (DEBUG_CALLBACK) { 176 Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) " 177 + "finisher.accept(\"" + mImageData.uri + "\""); 178 } 179 mParams.finisher.accept(mImageData.uri); 180 mParams.image = null; 181 } catch (Exception e) { 182 // IOException/UnsupportedOperationException may be thrown if external storage is 183 // not mounted 184 if (DEBUG_STORAGE) { 185 Log.d(TAG, "Failed to store screenshot", e); 186 } 187 mParams.clearImage(); 188 mImageData.reset(); 189 mQuickShareData.reset(); 190 mParams.mActionsReadyListener.onActionsReady(mImageData); 191 if (DEBUG_CALLBACK) { 192 Log.d(TAG, "Calling (Consumer<Uri>) finisher.accept(null)"); 193 } 194 mParams.finisher.accept(null); 195 } 196 197 return null; 198 } 199 200 /** 201 * Update the listener run when the saving task completes. Used to avoid showing UI for the 202 * first screenshot when a second one is taken. 203 */ setActionsReadyListener(ScreenshotController.ActionsReadyListener listener)204 void setActionsReadyListener(ScreenshotController.ActionsReadyListener listener) { 205 mParams.mActionsReadyListener = listener; 206 } 207 208 @Override onCancelled(Void params)209 protected void onCancelled(Void params) { 210 // If we are cancelled while the task is running in the background, we may get null 211 // params. The finisher is expected to always be called back, so just use the baked-in 212 // params from the ctor in any case. 213 mImageData.reset(); 214 mQuickShareData.reset(); 215 mParams.mActionsReadyListener.onActionsReady(mImageData); 216 if (DEBUG_CALLBACK) { 217 Log.d(TAG, "onCancelled, calling (Consumer<Uri>) finisher.accept(null)"); 218 } 219 mParams.finisher.accept(null); 220 mParams.clearImage(); 221 } 222 223 /** 224 * Assumes that the action intent is sent immediately after being supplied. 225 */ 226 @VisibleForTesting createShareAction(Context context, Resources r, Uri uri)227 Supplier<ActionTransition> createShareAction(Context context, Resources r, Uri uri) { 228 return () -> { 229 ActionTransition transition = mSharedElementTransition.get(); 230 231 // Note: Both the share and edit actions are proxied through ActionProxyReceiver in 232 // order to do some common work like dismissing the keyguard and sending 233 // closeSystemWindows 234 235 // Create a share intent, this will always go through the chooser activity first 236 // which should not trigger auto-enter PiP 237 String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime)); 238 String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); 239 Intent sharingIntent = new Intent(Intent.ACTION_SEND); 240 sharingIntent.setType("image/png"); 241 sharingIntent.putExtra(Intent.EXTRA_STREAM, uri); 242 // Include URI in ClipData also, so that grantPermission picks it up. 243 // We don't use setData here because some apps interpret this as "to:". 244 ClipData clipdata = new ClipData(new ClipDescription("content", 245 new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}), 246 new ClipData.Item(uri)); 247 sharingIntent.setClipData(clipdata); 248 sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject); 249 sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 250 251 // Make sure pending intents for the system user are still unique across users 252 // by setting the (otherwise unused) request code to the current user id. 253 int requestCode = context.getUserId(); 254 255 Intent sharingChooserIntent = Intent.createChooser(sharingIntent, null) 256 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK) 257 .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 258 259 // cancel current pending intent (if any) since clipData isn't used for matching 260 PendingIntent pendingIntent = PendingIntent.getActivityAsUser( 261 context, 0, sharingChooserIntent, 262 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 263 transition.bundle, UserHandle.CURRENT); 264 265 // Create a share action for the notification 266 PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode, 267 new Intent(context, ActionProxyReceiver.class) 268 .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent) 269 .putExtra(ScreenshotController.EXTRA_DISALLOW_ENTER_PIP, true) 270 .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId) 271 .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, 272 mSmartActionsEnabled) 273 .setAction(Intent.ACTION_SEND) 274 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), 275 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 276 UserHandle.SYSTEM); 277 278 Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder( 279 Icon.createWithResource(r, R.drawable.ic_screenshot_share), 280 r.getString(com.android.internal.R.string.share), shareAction); 281 282 transition.action = shareActionBuilder.build(); 283 return transition; 284 }; 285 } 286 287 @VisibleForTesting createEditAction(Context context, Resources r, Uri uri)288 Supplier<ActionTransition> createEditAction(Context context, Resources r, Uri uri) { 289 return () -> { 290 ActionTransition transition = mSharedElementTransition.get(); 291 // Note: Both the share and edit actions are proxied through ActionProxyReceiver in 292 // order to do some common work like dismissing the keyguard and sending 293 // closeSystemWindows 294 295 // Create an edit intent, if a specific package is provided as the editor, then 296 // launch that directly 297 String editorPackage = context.getString(R.string.config_screenshotEditor); 298 Intent editIntent = new Intent(Intent.ACTION_EDIT); 299 if (!TextUtils.isEmpty(editorPackage)) { 300 editIntent.setComponent(ComponentName.unflattenFromString(editorPackage)); 301 } 302 editIntent.setDataAndType(uri, "image/png"); 303 editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 304 editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 305 editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 306 307 PendingIntent pendingIntent = PendingIntent.getActivityAsUser( 308 context, 0, editIntent, PendingIntent.FLAG_IMMUTABLE, 309 transition.bundle, UserHandle.CURRENT); 310 311 // Make sure pending intents for the system user are still unique across users 312 // by setting the (otherwise unused) request code to the current user id. 313 int requestCode = mContext.getUserId(); 314 315 // Create a edit action 316 PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode, 317 new Intent(context, ActionProxyReceiver.class) 318 .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent) 319 .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId) 320 .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, 321 mSmartActionsEnabled) 322 .putExtra(ScreenshotController.EXTRA_OVERRIDE_TRANSITION, true) 323 .setAction(Intent.ACTION_EDIT) 324 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), 325 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, 326 UserHandle.SYSTEM); 327 Notification.Action.Builder editActionBuilder = new Notification.Action.Builder( 328 Icon.createWithResource(r, R.drawable.ic_screenshot_edit), 329 r.getString(com.android.internal.R.string.screenshot_edit), editAction); 330 331 transition.action = editActionBuilder.build(); 332 return transition; 333 }; 334 } 335 336 @VisibleForTesting 337 Notification.Action createDeleteAction(Context context, Resources r, Uri uri) { 338 // Make sure pending intents for the system user are still unique across users 339 // by setting the (otherwise unused) request code to the current user id. 340 int requestCode = mContext.getUserId(); 341 342 // Create a delete action for the notification 343 PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode, 344 new Intent(context, DeleteScreenshotReceiver.class) 345 .putExtra(ScreenshotController.SCREENSHOT_URI_ID, uri.toString()) 346 .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId) 347 .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, 348 mSmartActionsEnabled) 349 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), 350 PendingIntent.FLAG_CANCEL_CURRENT 351 | PendingIntent.FLAG_ONE_SHOT 352 | PendingIntent.FLAG_IMMUTABLE); 353 Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder( 354 Icon.createWithResource(r, R.drawable.ic_screenshot_delete), 355 r.getString(com.android.internal.R.string.delete), deleteAction); 356 357 return deleteActionBuilder.build(); 358 } 359 360 private UserHandle getUserHandleOfForegroundApplication(Context context) { 361 UserManager manager = UserManager.get(context); 362 int result; 363 // This logic matches 364 // com.android.systemui.statusbar.phone.PhoneStatusBarPolicy#updateManagedProfile 365 try { 366 result = ActivityTaskManager.getService().getLastResumedActivityUserId(); 367 } catch (RemoteException e) { 368 if (DEBUG_ACTIONS) { 369 Log.d(TAG, "Failed to get UserHandle of foreground app: ", e); 370 } 371 result = context.getUserId(); 372 } 373 UserInfo userInfo = manager.getUserInfo(result); 374 return userInfo.getUserHandle(); 375 } 376 377 private List<Notification.Action> buildSmartActions( 378 List<Notification.Action> actions, Context context) { 379 List<Notification.Action> broadcastActions = new ArrayList<>(); 380 for (Notification.Action action : actions) { 381 // Proxy smart actions through {@link GlobalScreenshot.SmartActionsReceiver} 382 // for logging smart actions. 383 Bundle extras = action.getExtras(); 384 String actionType = extras.getString( 385 ScreenshotNotificationSmartActionsProvider.ACTION_TYPE, 386 ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE); 387 Intent intent = new Intent(context, SmartActionsReceiver.class) 388 .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, action.actionIntent) 389 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 390 addIntentExtras(mScreenshotId, intent, actionType, mSmartActionsEnabled); 391 PendingIntent broadcastIntent = PendingIntent.getBroadcast(context, 392 mRandom.nextInt(), 393 intent, 394 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); 395 broadcastActions.add(new Notification.Action.Builder(action.getIcon(), action.title, 396 broadcastIntent).setContextual(true).addExtras(extras).build()); 397 } 398 return broadcastActions; 399 } 400 401 private static void addIntentExtras(String screenshotId, Intent intent, String actionType, 402 boolean smartActionsEnabled) { 403 intent 404 .putExtra(ScreenshotController.EXTRA_ACTION_TYPE, actionType) 405 .putExtra(ScreenshotController.EXTRA_ID, screenshotId) 406 .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, smartActionsEnabled); 407 } 408 409 /** 410 * Populate image uri into intent of Quick Share action. 411 */ 412 @VisibleForTesting 413 private Notification.Action createQuickShareAction(Context context, Notification.Action action, 414 Uri uri) { 415 if (action == null) { 416 return null; 417 } 418 // Populate image URI into Quick Share chip intent 419 Intent sharingIntent = action.actionIntent.getIntent(); 420 sharingIntent.setType("image/png"); 421 sharingIntent.putExtra(Intent.EXTRA_STREAM, uri); 422 String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime)); 423 String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); 424 sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject); 425 // Include URI in ClipData also, so that grantPermission picks it up. 426 // We don't use setData here because some apps interpret this as "to:". 427 ClipData clipdata = new ClipData(new ClipDescription("content", 428 new String[]{"image/png"}), 429 new ClipData.Item(uri)); 430 sharingIntent.setClipData(clipdata); 431 sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 432 PendingIntent updatedPendingIntent = PendingIntent.getActivity( 433 context, 0, sharingIntent, 434 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); 435 436 // Proxy smart actions through {@link GlobalScreenshot.SmartActionsReceiver} 437 // for logging smart actions. 438 Bundle extras = action.getExtras(); 439 String actionType = extras.getString( 440 ScreenshotNotificationSmartActionsProvider.ACTION_TYPE, 441 ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE); 442 Intent intent = new Intent(context, SmartActionsReceiver.class) 443 .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, updatedPendingIntent) 444 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 445 addIntentExtras(mScreenshotId, intent, actionType, mSmartActionsEnabled); 446 PendingIntent broadcastIntent = PendingIntent.getBroadcast(context, 447 mRandom.nextInt(), 448 intent, 449 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); 450 return new Notification.Action.Builder(action.getIcon(), action.title, 451 broadcastIntent).setContextual(true).addExtras(extras).build(); 452 } 453 454 /** 455 * Query and surface Quick Share chip if it is available. Action intent would not be used, 456 * because it does not contain image URL which would be populated in {@link 457 * #createQuickShareAction(Context, Notification.Action, Uri)} 458 */ 459 private void queryQuickShareAction(Bitmap image, UserHandle user) { 460 CompletableFuture<List<Notification.Action>> quickShareActionsFuture = 461 mScreenshotSmartActions.getSmartActionsFuture( 462 mScreenshotId, null, image, mSmartActionsProvider, 463 QUICK_SHARE_ACTION, 464 mSmartActionsEnabled, user); 465 int timeoutMs = DeviceConfig.getInt( 466 DeviceConfig.NAMESPACE_SYSTEMUI, 467 SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_QUICK_SHARE_ACTIONS_TIMEOUT_MS, 468 500); 469 List<Notification.Action> quickShareActions = 470 mScreenshotSmartActions.getSmartActions( 471 mScreenshotId, quickShareActionsFuture, timeoutMs, 472 mSmartActionsProvider, QUICK_SHARE_ACTION); 473 if (!quickShareActions.isEmpty()) { 474 mQuickShareData.quickShareAction = quickShareActions.get(0); 475 mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData); 476 } 477 } 478 } 479