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.flags.Flags; 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 FeatureFlags mFlags; 77 private final ScreenshotSmartActions mScreenshotSmartActions; 78 private final ScreenshotController.SaveImageInBackgroundData mParams; 79 private final ScreenshotController.SavedImageData mImageData; 80 private final ScreenshotController.QuickShareData mQuickShareData; 81 82 private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider; 83 private String mScreenshotId; 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, FeatureFlags flags, ImageExporter exporter, ScreenshotSmartActions screenshotSmartActions, ScreenshotController.SaveImageInBackgroundData data, Supplier<ActionTransition> sharedElementTransition, ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider )89 SaveImageInBackgroundTask( 90 Context context, 91 FeatureFlags flags, 92 ImageExporter exporter, 93 ScreenshotSmartActions screenshotSmartActions, 94 ScreenshotController.SaveImageInBackgroundData data, 95 Supplier<ActionTransition> sharedElementTransition, 96 ScreenshotNotificationSmartActionsProvider 97 screenshotNotificationSmartActionsProvider 98 ) { 99 mContext = context; 100 mFlags = flags; 101 mScreenshotSmartActions = screenshotSmartActions; 102 mImageData = new ScreenshotController.SavedImageData(); 103 mQuickShareData = new ScreenshotController.QuickShareData(); 104 mSharedElementTransition = sharedElementTransition; 105 mImageExporter = exporter; 106 107 // Prepare all the output metadata 108 mParams = data; 109 110 // Initialize screenshot notification smart actions provider. 111 mSmartActionsProvider = screenshotNotificationSmartActionsProvider; 112 } 113 114 @Override doInBackground(Void... paramsUnused)115 protected Void doInBackground(Void... paramsUnused) { 116 if (isCancelled()) { 117 if (DEBUG_STORAGE) { 118 Log.d(TAG, "cancelled! returning null"); 119 } 120 return null; 121 } 122 // TODO: move to constructor / from ScreenshotRequest 123 final UUID requestId = UUID.randomUUID(); 124 final UserHandle user = mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY) 125 ? mParams.owner : getUserHandleOfForegroundApplication(mContext); 126 127 Thread.currentThread().setPriority(Thread.MAX_PRIORITY); 128 129 Bitmap image = mParams.image; 130 mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, requestId); 131 132 boolean savingToOtherUser = mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY) 133 && (user != Process.myUserHandle()); 134 // Smart actions don't yet work for cross-user saves. 135 boolean smartActionsEnabled = !savingToOtherUser 136 && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, 137 SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, 138 true); 139 try { 140 if (smartActionsEnabled && mParams.mQuickShareActionsReadyListener != null) { 141 // Since Quick Share target recommendation does not rely on image URL, it is 142 // queried and surfaced before image compress/export. Action intent would not be 143 // used, because it does not contain image URL. 144 queryQuickShareAction(image, user); 145 } 146 147 // Call synchronously here since already on a background thread. 148 ListenableFuture<ImageExporter.Result> future = 149 mImageExporter.export(Runnable::run, requestId, image, mParams.owner); 150 ImageExporter.Result result = future.get(); 151 Log.d(TAG, "Saved screenshot: " + result); 152 final Uri uri = result.uri; 153 mImageTime = result.timestamp; 154 155 CompletableFuture<List<Notification.Action>> smartActionsFuture = 156 mScreenshotSmartActions.getSmartActionsFuture( 157 mScreenshotId, uri, image, mSmartActionsProvider, 158 ScreenshotSmartActionType.REGULAR_SMART_ACTIONS, 159 smartActionsEnabled, user); 160 List<Notification.Action> smartActions = new ArrayList<>(); 161 if (smartActionsEnabled) { 162 int timeoutMs = DeviceConfig.getInt( 163 DeviceConfig.NAMESPACE_SYSTEMUI, 164 SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS, 165 1000); 166 smartActions.addAll(buildSmartActions( 167 mScreenshotSmartActions.getSmartActions( 168 mScreenshotId, smartActionsFuture, timeoutMs, 169 mSmartActionsProvider, 170 ScreenshotSmartActionType.REGULAR_SMART_ACTIONS), 171 mContext)); 172 } 173 174 mImageData.uri = uri; 175 mImageData.owner = user; 176 mImageData.smartActions = smartActions; 177 mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri, 178 smartActionsEnabled); 179 mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri, 180 smartActionsEnabled); 181 mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri, 182 smartActionsEnabled); 183 mImageData.quickShareAction = createQuickShareAction(mContext, 184 mQuickShareData.quickShareAction, uri); 185 mImageData.subject = getSubjectString(); 186 187 mParams.mActionsReadyListener.onActionsReady(mImageData); 188 if (DEBUG_CALLBACK) { 189 Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) " 190 + "finisher.accept(\"" + mImageData.uri + "\""); 191 } 192 mParams.finisher.accept(mImageData.uri); 193 mParams.image = null; 194 } catch (Exception e) { 195 // IOException/UnsupportedOperationException may be thrown if external storage is 196 // not mounted 197 if (DEBUG_STORAGE) { 198 Log.d(TAG, "Failed to store screenshot", e); 199 } 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()); 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 * Populate image uri into intent of Quick Share action. 427 */ 428 @VisibleForTesting 429 private Notification.Action createQuickShareAction(Context context, Notification.Action action, 430 Uri uri) { 431 if (action == null) { 432 return null; 433 } 434 // Populate image URI into Quick Share chip intent 435 Intent sharingIntent = action.actionIntent.getIntent(); 436 sharingIntent.setType("image/png"); 437 sharingIntent.putExtra(Intent.EXTRA_STREAM, uri); 438 String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime)); 439 String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); 440 sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject); 441 // Include URI in ClipData also, so that grantPermission picks it up. 442 // We don't use setData here because some apps interpret this as "to:". 443 ClipData clipdata = new ClipData(new ClipDescription("content", 444 new String[]{"image/png"}), 445 new ClipData.Item(uri)); 446 sharingIntent.setClipData(clipdata); 447 sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 448 PendingIntent updatedPendingIntent = PendingIntent.getActivity( 449 context, 0, sharingIntent, 450 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); 451 452 // Proxy smart actions through {@link SmartActionsReceiver} for logging smart actions. 453 Bundle extras = action.getExtras(); 454 String actionType = extras.getString( 455 ScreenshotNotificationSmartActionsProvider.ACTION_TYPE, 456 ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE); 457 Intent intent = new Intent(context, SmartActionsReceiver.class) 458 .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, updatedPendingIntent) 459 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 460 // We only query for quick share actions when smart actions are enabled, so we can assert 461 // that it's true here. 462 addIntentExtras(mScreenshotId, intent, actionType, true /* smartActionsEnabled */); 463 PendingIntent broadcastIntent = PendingIntent.getBroadcast(context, 464 mRandom.nextInt(), 465 intent, 466 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); 467 return new Notification.Action.Builder(action.getIcon(), action.title, 468 broadcastIntent).setContextual(true).addExtras(extras).build(); 469 } 470 471 /** 472 * Query and surface Quick Share chip if it is available. Action intent would not be used, 473 * because it does not contain image URL which would be populated in {@link 474 * #createQuickShareAction(Context, Notification.Action, Uri)} 475 */ 476 private void queryQuickShareAction(Bitmap image, UserHandle user) { 477 CompletableFuture<List<Notification.Action>> quickShareActionsFuture = 478 mScreenshotSmartActions.getSmartActionsFuture( 479 mScreenshotId, null, image, mSmartActionsProvider, 480 ScreenshotSmartActionType.QUICK_SHARE_ACTION, 481 true /* smartActionsEnabled */, user); 482 int timeoutMs = DeviceConfig.getInt( 483 DeviceConfig.NAMESPACE_SYSTEMUI, 484 SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_QUICK_SHARE_ACTIONS_TIMEOUT_MS, 485 500); 486 List<Notification.Action> quickShareActions = 487 mScreenshotSmartActions.getSmartActions( 488 mScreenshotId, quickShareActionsFuture, timeoutMs, 489 mSmartActionsProvider, 490 ScreenshotSmartActionType.QUICK_SHARE_ACTION); 491 if (!quickShareActions.isEmpty()) { 492 mQuickShareData.quickShareAction = quickShareActions.get(0); 493 mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData); 494 } 495 } 496 497 private String getSubjectString() { 498 String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime)); 499 return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); 500 } 501 } 502