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