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