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_CALLBACK;
20 import static com.android.systemui.screenshot.LogConfig.DEBUG_STORAGE;
21 import static com.android.systemui.screenshot.LogConfig.logTag;
22 import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType;
23 
24 import android.app.Notification;
25 import android.app.PendingIntent;
26 import android.content.ClipData;
27 import android.content.ClipDescription;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.graphics.Bitmap;
31 import android.net.Uri;
32 import android.os.AsyncTask;
33 import android.os.Bundle;
34 import android.os.Process;
35 import android.os.UserHandle;
36 import android.provider.DeviceConfig;
37 import android.util.Log;
38 
39 import com.android.internal.annotations.VisibleForTesting;
40 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
41 import com.android.systemui.flags.FeatureFlags;
42 
43 import com.google.common.util.concurrent.ListenableFuture;
44 
45 import java.text.DateFormat;
46 import java.util.ArrayList;
47 import java.util.Date;
48 import java.util.List;
49 import java.util.Random;
50 import java.util.UUID;
51 import java.util.concurrent.CompletableFuture;
52 
53 /**
54  * An AsyncTask that saves an image to the media store in the background.
55  */
56 class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
57     private static final String TAG = logTag(SaveImageInBackgroundTask.class);
58 
59     private static final String SCREENSHOT_ID_TEMPLATE = "Screenshot_%s";
60     private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
61 
62     private final Context mContext;
63     private FeatureFlags mFlags;
64     private final ScreenshotSmartActions mScreenshotSmartActions;
65     private final ScreenshotController.SaveImageInBackgroundData mParams;
66     private final ScreenshotController.SavedImageData mImageData;
67     private final ScreenshotController.QuickShareData mQuickShareData;
68 
69     private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
70     private String mScreenshotId;
71     private final Random mRandom = new Random();
72     private final ImageExporter mImageExporter;
73     private long mImageTime;
74 
SaveImageInBackgroundTask( Context context, FeatureFlags flags, ImageExporter exporter, ScreenshotSmartActions screenshotSmartActions, ScreenshotController.SaveImageInBackgroundData data, ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider )75     SaveImageInBackgroundTask(
76             Context context,
77             FeatureFlags flags,
78             ImageExporter exporter,
79             ScreenshotSmartActions screenshotSmartActions,
80             ScreenshotController.SaveImageInBackgroundData data,
81             ScreenshotNotificationSmartActionsProvider
82                     screenshotNotificationSmartActionsProvider
83     ) {
84         mContext = context;
85         mFlags = flags;
86         mScreenshotSmartActions = screenshotSmartActions;
87         mImageData = new ScreenshotController.SavedImageData();
88         mQuickShareData = new ScreenshotController.QuickShareData();
89         mImageExporter = exporter;
90 
91         // Prepare all the output metadata
92         mParams = data;
93 
94         // Initialize screenshot notification smart actions provider.
95         mSmartActionsProvider = screenshotNotificationSmartActionsProvider;
96     }
97 
98     @Override
doInBackground(Void... paramsUnused)99     protected Void doInBackground(Void... paramsUnused) {
100         if (isCancelled()) {
101             if (DEBUG_STORAGE) {
102                 Log.d(TAG, "cancelled! returning null");
103             }
104             return null;
105         }
106         // TODO: move to constructor / from ScreenshotRequest
107         final UUID requestId = UUID.randomUUID();
108 
109         Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
110 
111         Bitmap image = mParams.image;
112         mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, requestId);
113 
114         boolean savingToOtherUser = mParams.owner != Process.myUserHandle();
115         // Smart actions don't yet work for cross-user saves.
116         boolean smartActionsEnabled = !savingToOtherUser
117                 && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
118                 SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS,
119                 true);
120         try {
121             if (smartActionsEnabled && mParams.mQuickShareActionsReadyListener != null) {
122                 // Since Quick Share target recommendation does not rely on image URL, it is
123                 // queried and surfaced before image compress/export. Action intent would not be
124                 // used, because it does not contain image URL.
125                 Notification.Action quickShare =
126                         queryQuickShareAction(mScreenshotId, image, mParams.owner, null);
127                 if (quickShare != null) {
128                     mQuickShareData.quickShareAction = quickShare;
129                     mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
130                 }
131             }
132 
133             // Call synchronously here since already on a background thread.
134             ListenableFuture<ImageExporter.Result> future =
135                     mImageExporter.export(Runnable::run, requestId, image, mParams.owner,
136                             mParams.displayId);
137             ImageExporter.Result result = future.get();
138             Log.d(TAG, "Saved screenshot: " + result);
139             final Uri uri = result.uri;
140             mImageTime = result.timestamp;
141 
142             CompletableFuture<List<Notification.Action>> smartActionsFuture =
143                     mScreenshotSmartActions.getSmartActionsFuture(
144                             mScreenshotId, uri, image, mSmartActionsProvider,
145                             ScreenshotSmartActionType.REGULAR_SMART_ACTIONS,
146                             smartActionsEnabled, mParams.owner);
147             List<Notification.Action> smartActions = new ArrayList<>();
148             if (smartActionsEnabled) {
149                 int timeoutMs = DeviceConfig.getInt(
150                         DeviceConfig.NAMESPACE_SYSTEMUI,
151                         SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS,
152                         1000);
153                 smartActions.addAll(buildSmartActions(
154                         mScreenshotSmartActions.getSmartActions(
155                                 mScreenshotId, smartActionsFuture, timeoutMs,
156                                 mSmartActionsProvider,
157                                 ScreenshotSmartActionType.REGULAR_SMART_ACTIONS),
158                         mContext));
159             }
160 
161             mImageData.uri = uri;
162             mImageData.owner = mParams.owner;
163             mImageData.smartActions = smartActions;
164             mImageData.quickShareAction = createQuickShareAction(
165                     mQuickShareData.quickShareAction, mScreenshotId, uri, mImageTime, image,
166                     mParams.owner);
167             mImageData.subject = getSubjectString(mImageTime);
168             mImageData.imageTime = mImageTime;
169 
170             mParams.mActionsReadyListener.onActionsReady(mImageData);
171             if (DEBUG_CALLBACK) {
172                 Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) "
173                         + "finisher.accept(\"" + mImageData.uri + "\"");
174             }
175             mParams.finisher.accept(mImageData.uri);
176             mParams.image = null;
177         } catch (Exception e) {
178             // IOException/UnsupportedOperationException may be thrown if external storage is
179             // not mounted
180             Log.d(TAG, "Failed to store screenshot", e);
181             mParams.clearImage();
182             mImageData.reset();
183             mQuickShareData.reset();
184             mParams.mActionsReadyListener.onActionsReady(mImageData);
185             if (DEBUG_CALLBACK) {
186                 Log.d(TAG, "Calling (Consumer<Uri>) finisher.accept(null)");
187             }
188             mParams.finisher.accept(null);
189         }
190 
191         return null;
192     }
193 
194     /**
195      * Update the listener run when the saving task completes. Used to avoid showing UI for the
196      * first screenshot when a second one is taken.
197      */
setActionsReadyListener(ScreenshotController.ActionsReadyListener listener)198     void setActionsReadyListener(ScreenshotController.ActionsReadyListener listener) {
199         mParams.mActionsReadyListener = listener;
200     }
201 
202     @Override
onCancelled(Void params)203     protected void onCancelled(Void params) {
204         // If we are cancelled while the task is running in the background, we may get null
205         // params. The finisher is expected to always be called back, so just use the baked-in
206         // params from the ctor in any case.
207         mImageData.reset();
208         mQuickShareData.reset();
209         mParams.mActionsReadyListener.onActionsReady(mImageData);
210         if (DEBUG_CALLBACK) {
211             Log.d(TAG, "onCancelled, calling (Consumer<Uri>) finisher.accept(null)");
212         }
213         mParams.finisher.accept(null);
214         mParams.clearImage();
215     }
216 
buildSmartActions( List<Notification.Action> actions, Context context)217     private List<Notification.Action> buildSmartActions(
218             List<Notification.Action> actions, Context context) {
219         List<Notification.Action> broadcastActions = new ArrayList<>();
220         for (Notification.Action action : actions) {
221             // Proxy smart actions through {@link SmartActionsReceiver} for logging smart actions.
222             Bundle extras = action.getExtras();
223             String actionType = extras.getString(
224                     ScreenshotNotificationSmartActionsProvider.ACTION_TYPE,
225                     ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE);
226             Intent intent = new Intent(context, SmartActionsReceiver.class)
227                     .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, action.actionIntent)
228                     .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
229             addIntentExtras(mScreenshotId, intent, actionType, true /* smartActionsEnabled */);
230             PendingIntent broadcastIntent = PendingIntent.getBroadcast(context,
231                     mRandom.nextInt(),
232                     intent,
233                     PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
234             broadcastActions.add(new Notification.Action.Builder(action.getIcon(), action.title,
235                     broadcastIntent).setContextual(true).addExtras(extras).build());
236         }
237         return broadcastActions;
238     }
239 
addIntentExtras(String screenshotId, Intent intent, String actionType, boolean smartActionsEnabled)240     private static void addIntentExtras(String screenshotId, Intent intent, String actionType,
241             boolean smartActionsEnabled) {
242         intent
243                 .putExtra(ScreenshotController.EXTRA_ACTION_TYPE, actionType)
244                 .putExtra(ScreenshotController.EXTRA_ID, screenshotId)
245                 .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, smartActionsEnabled);
246     }
247 
248     /**
249      * Wrap the quickshare intent and populate the fillin intent with the URI
250      */
251     @VisibleForTesting
createQuickShareAction( Notification.Action quickShare, String screenshotId, Uri uri, long imageTime, Bitmap image, UserHandle user)252     Notification.Action createQuickShareAction(
253             Notification.Action quickShare, String screenshotId, Uri uri, long imageTime,
254             Bitmap image, UserHandle user) {
255         if (quickShare == null) {
256             return null;
257         } else if (quickShare.actionIntent.isImmutable()) {
258             Notification.Action quickShareWithUri =
259                     queryQuickShareAction(screenshotId, image, user, uri);
260             if (quickShareWithUri == null
261                     || !quickShareWithUri.title.toString().contentEquals(quickShare.title)) {
262                 return null;
263             }
264             quickShare = quickShareWithUri;
265         }
266 
267         Intent wrappedIntent = new Intent(mContext, SmartActionsReceiver.class)
268                 .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, quickShare.actionIntent)
269                 .putExtra(ScreenshotController.EXTRA_ACTION_INTENT_FILLIN,
270                         createFillInIntent(uri, imageTime))
271                 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
272         Bundle extras = quickShare.getExtras();
273         String actionType = extras.getString(
274                 ScreenshotNotificationSmartActionsProvider.ACTION_TYPE,
275                 ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE);
276         // We only query for quick share actions when smart actions are enabled, so we can assert
277         // that it's true here.
278         addIntentExtras(screenshotId, wrappedIntent, actionType, true /* smartActionsEnabled */);
279         PendingIntent broadcastIntent =
280                 PendingIntent.getBroadcast(mContext, mRandom.nextInt(), wrappedIntent,
281                         PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
282         return new Notification.Action.Builder(quickShare.getIcon(), quickShare.title,
283                 broadcastIntent)
284                 .setContextual(true)
285                 .addExtras(extras)
286                 .build();
287     }
288 
createFillInIntent(Uri uri, long imageTime)289     private Intent createFillInIntent(Uri uri, long imageTime) {
290         Intent fillIn = new Intent();
291         fillIn.setType("image/png");
292         fillIn.putExtra(Intent.EXTRA_STREAM, uri);
293         fillIn.putExtra(Intent.EXTRA_SUBJECT, getSubjectString(imageTime));
294         // Include URI in ClipData also, so that grantPermission picks it up.
295         // We don't use setData here because some apps interpret this as "to:".
296         ClipData clipData = new ClipData(
297                 new ClipDescription("content", new String[]{"image/png"}),
298                 new ClipData.Item(uri));
299         fillIn.setClipData(clipData);
300         fillIn.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
301         return fillIn;
302     }
303 
304     /**
305      * Query and surface Quick Share chip if it is available. Action intent would not be used,
306      * because it does not contain image URL which would be populated in {@link
307      * #createQuickShareAction(Notification.Action, String, Uri, long, Bitmap, UserHandle)}
308      */
309 
310     @VisibleForTesting
queryQuickShareAction( String screenshotId, Bitmap image, UserHandle user, Uri uri)311     Notification.Action queryQuickShareAction(
312             String screenshotId, Bitmap image, UserHandle user, Uri uri) {
313         CompletableFuture<List<Notification.Action>> quickShareActionsFuture =
314                 mScreenshotSmartActions.getSmartActionsFuture(
315                         screenshotId, uri, image, mSmartActionsProvider,
316                         ScreenshotSmartActionType.QUICK_SHARE_ACTION,
317                         true /* smartActionsEnabled */, user);
318         int timeoutMs = DeviceConfig.getInt(
319                 DeviceConfig.NAMESPACE_SYSTEMUI,
320                 SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_QUICK_SHARE_ACTIONS_TIMEOUT_MS,
321                 500);
322         List<Notification.Action> quickShareActions =
323                 mScreenshotSmartActions.getSmartActions(
324                         screenshotId, quickShareActionsFuture, timeoutMs,
325                         mSmartActionsProvider,
326                         ScreenshotSmartActionType.QUICK_SHARE_ACTION);
327         if (!quickShareActions.isEmpty()) {
328             return quickShareActions.get(0);
329         }
330         return null;
331     }
332 
getSubjectString(long imageTime)333     private static String getSubjectString(long imageTime) {
334         String subjectDate = DateFormat.getDateTimeInstance().format(new Date(imageTime));
335         return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
336     }
337 }
338