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