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