1 /* 2 * Copyright (C) 2020 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.quickstep.util; 18 19 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 20 import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; 21 import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW; 22 import static android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE; 23 24 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 25 import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR; 26 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 27 28 import android.app.Activity; 29 import android.app.ActivityOptions; 30 import android.app.prediction.AppTarget; 31 import android.content.ActivityNotFoundException; 32 import android.content.ClipData; 33 import android.content.ClipDescription; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.content.Intent; 37 import android.content.pm.ShortcutInfo; 38 import android.graphics.Bitmap; 39 import android.graphics.Canvas; 40 import android.graphics.Insets; 41 import android.graphics.Picture; 42 import android.graphics.Rect; 43 import android.graphics.RectF; 44 import android.net.Uri; 45 import android.util.Log; 46 import android.view.View; 47 48 import androidx.annotation.WorkerThread; 49 import androidx.core.content.FileProvider; 50 51 import com.android.internal.app.ChooserActivity; 52 import com.android.internal.util.ScreenshotRequest; 53 import com.android.launcher3.BuildConfig; 54 import com.android.quickstep.SystemUiProxy; 55 import com.android.systemui.shared.recents.model.Task; 56 57 import java.io.File; 58 import java.io.FileOutputStream; 59 import java.io.IOException; 60 import java.util.function.BiFunction; 61 import java.util.function.Supplier; 62 63 /** 64 * Utility class containing methods to help manage image actions such as sharing, cropping, and 65 * saving image. 66 */ 67 public class ImageActionUtils { 68 69 private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".overview.fileprovider"; 70 private static final long FILE_LIFE = 1000L /*ms*/ * 60L /*s*/ * 60L /*m*/ * 24L /*h*/; 71 private static final String SUB_FOLDER = "Overview"; 72 private static final String BASE_NAME = "overview_image_"; 73 private static final String TAG = "ImageActionUtils"; 74 75 /** 76 * Saves screenshot to location determine by SystemUiProxy 77 */ saveScreenshot(SystemUiProxy systemUiProxy, Bitmap screenshot, Rect screenshotBounds, Insets visibleInsets, Task.TaskKey task)78 public static void saveScreenshot(SystemUiProxy systemUiProxy, Bitmap screenshot, 79 Rect screenshotBounds, Insets visibleInsets, Task.TaskKey task) { 80 ScreenshotRequest request = 81 new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OVERVIEW) 82 .setTopComponent(task.sourceComponent) 83 .setTaskId(task.id) 84 .setUserId(task.userId) 85 .setBitmap(screenshot) 86 .setBoundsOnScreen(screenshotBounds) 87 .setInsets(visibleInsets) 88 .build(); 89 systemUiProxy.takeScreenshot(request); 90 } 91 92 /** 93 * Launch the activity to share image for overview sharing. This is to share cropped bitmap 94 * with specific share targets (with shortcutInfo and appTarget) rendered in overview. 95 */ shareImage(Context context, Supplier<Bitmap> bitmapSupplier, RectF rectF, ShortcutInfo shortcutInfo, AppTarget appTarget, String tag)96 public static void shareImage(Context context, Supplier<Bitmap> bitmapSupplier, RectF rectF, 97 ShortcutInfo shortcutInfo, AppTarget appTarget, String tag) { 98 UI_HELPER_EXECUTOR.execute(() -> { 99 Bitmap bitmap = bitmapSupplier.get(); 100 if (bitmap == null) { 101 return; 102 } 103 Rect crop = new Rect(); 104 rectF.round(crop); 105 Intent intent = new Intent(); 106 Uri uri = getImageUri(bitmap, crop, context, tag); 107 ClipData clipdata = new ClipData(new ClipDescription("content", 108 new String[]{"image/png"}), 109 new ClipData.Item(uri)); 110 intent.setAction(Intent.ACTION_SEND) 111 .setComponent( 112 new ComponentName(appTarget.getPackageName(), appTarget.getClassName())) 113 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 114 .addFlags(FLAG_GRANT_READ_URI_PERMISSION) 115 .setType("image/png") 116 .putExtra(Intent.EXTRA_STREAM, uri) 117 .putExtra(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId()) 118 .setClipData(clipdata); 119 120 if (context.getUserId() != appTarget.getUser().getIdentifier()) { 121 intent.prepareToLeaveUser(context.getUserId()); 122 intent.fixUris(context.getUserId()); 123 context.startActivityAsUser(intent, appTarget.getUser()); 124 } else { 125 context.startActivity(intent); 126 } 127 }); 128 } 129 130 /** 131 * Launch the activity to share image. 132 */ startShareActivity(Context context, Supplier<Bitmap> bitmapSupplier, Rect crop, Intent intent, String tag)133 public static void startShareActivity(Context context, Supplier<Bitmap> bitmapSupplier, 134 Rect crop, Intent intent, String tag) { 135 UI_HELPER_EXECUTOR.execute(() -> { 136 Bitmap bitmap = bitmapSupplier.get(); 137 if (bitmap == null) { 138 Log.e(tag, "No snapshot available, not starting share."); 139 return; 140 } 141 persistBitmapAndStartActivity(context, bitmap, crop, intent, 142 ImageActionUtils::getShareIntentForImageUri, tag); 143 }); 144 } 145 146 /** 147 * Launch the activity to share image with shared element transition. 148 */ startShareActivity(Context context, Supplier<Bitmap> bitmapSupplier, Rect crop, Intent intent, String tag, View sharedElement)149 public static void startShareActivity(Context context, Supplier<Bitmap> bitmapSupplier, 150 Rect crop, Intent intent, String tag, View sharedElement) { 151 UI_HELPER_EXECUTOR.execute(() -> { 152 Bitmap bitmap = bitmapSupplier.get(); 153 if (bitmap == null) { 154 Log.e(tag, "No snapshot available, not starting share."); 155 return; 156 } 157 persistBitmapAndStartActivity(context, bitmap, 158 crop, intent, ImageActionUtils::getShareIntentForImageUri, tag, sharedElement); 159 }); 160 } 161 162 /** 163 * Starts activity based on given intent created from image uri. 164 */ 165 @WorkerThread persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag)166 public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, 167 Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag) { 168 persistBitmapAndStartActivity(context, bitmap, crop, intent, uriToIntentMap, tag, 169 (Runnable) null); 170 } 171 172 /** 173 * Starts activity based on given intent created from image uri. 174 * @param exceptionCallback An optional callback to be called when the intent can't be resolved 175 */ 176 @WorkerThread persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag, Runnable exceptionCallback)177 public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, 178 Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag, 179 Runnable exceptionCallback) { 180 Intent[] intents = uriToIntentMap.apply(getImageUri(bitmap, crop, context, tag), intent); 181 182 try { 183 // Work around b/159412574 184 if (intents.length == 1) { 185 context.startActivity(intents[0]); 186 } else { 187 context.startActivities(intents); 188 } 189 } catch (ActivityNotFoundException e) { 190 Log.e(TAG, "No activity found to receive image intent"); 191 if (exceptionCallback != null) { 192 exceptionCallback.run(); 193 } 194 } 195 } 196 197 /** 198 * Starts activity based on given intent created from image uri with shared element transition. 199 */ 200 @WorkerThread persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag, View scaledImage)201 public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, 202 Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag, 203 View scaledImage) { 204 Intent[] intents = uriToIntentMap.apply(getImageUri(bitmap, crop, context, tag), intent); 205 206 // Work around b/159412574 207 if (intents.length == 1) { 208 MAIN_EXECUTOR.execute(() -> context.startActivity(intents[0], 209 ActivityOptions.makeSceneTransitionAnimation((Activity) context, scaledImage, 210 ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle())); 211 212 } else { 213 MAIN_EXECUTOR.execute(() -> context.startActivities(intents, 214 ActivityOptions.makeSceneTransitionAnimation((Activity) context, scaledImage, 215 ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle())); 216 } 217 } 218 219 220 221 /** 222 * Converts image bitmap to Uri by temporarily saving bitmap to cache, and creating Uri pointing 223 * to that location. Used to be able to share an image with another app. 224 * 225 * @param bitmap The whole bitmap to be shared. 226 * @param crop The section of the bitmap to be shared. 227 * @param context The application context, used to interact with file system. 228 * @param tag Tag used to log errors. 229 * @return Uri that points to the cropped version of desired bitmap to share. 230 */ 231 @WorkerThread getImageUri(Bitmap bitmap, Rect crop, Context context, String tag)232 public static Uri getImageUri(Bitmap bitmap, Rect crop, Context context, String tag) { 233 clearOldCacheFiles(context); 234 Bitmap croppedBitmap = cropBitmap(bitmap, crop); 235 int cropHash = crop == null ? 0 : crop.hashCode(); 236 String baseName = BASE_NAME + bitmap.hashCode() + "_" + cropHash + ".png"; 237 File parent = new File(context.getCacheDir(), SUB_FOLDER); 238 parent.mkdir(); 239 File file = new File(parent, baseName); 240 241 try (FileOutputStream fos = new FileOutputStream(file)) { 242 croppedBitmap.compress(Bitmap.CompressFormat.PNG, 100, fos); 243 } catch (IOException e) { 244 Log.e(tag, "Error saving image", e); 245 } 246 247 return FileProvider.getUriForFile(context, AUTHORITY, file); 248 } 249 250 /** 251 * Crops the bitmap to the provided size and returns a software backed bitmap whenever possible. 252 * 253 * @param bitmap The bitmap to be cropped. 254 * @param crop The section of the bitmap in the crop. 255 * @return The cropped bitmap. 256 */ 257 @WorkerThread cropBitmap(Bitmap bitmap, Rect crop)258 public static Bitmap cropBitmap(Bitmap bitmap, Rect crop) { 259 Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); 260 if (crop == null) { 261 crop = new Rect(src); 262 } 263 if (crop.equals(src)) { 264 return bitmap; 265 } else { 266 if (bitmap.getConfig() != Bitmap.Config.HARDWARE) { 267 return Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), 268 crop.height()); 269 } 270 271 // For hardware bitmaps, use the Picture API to directly create a software bitmap 272 Picture picture = new Picture(); 273 Canvas canvas = picture.beginRecording(crop.width(), crop.height()); 274 canvas.drawBitmap(bitmap, -crop.left, -crop.top, null); 275 picture.endRecording(); 276 return Bitmap.createBitmap(picture, crop.width(), crop.height(), 277 Bitmap.Config.ARGB_8888); 278 } 279 } 280 281 /** 282 * Gets the intent used to share image. 283 */ 284 @WorkerThread getShareIntentForImageUri(Uri uri, Intent intent)285 private static Intent[] getShareIntentForImageUri(Uri uri, Intent intent) { 286 if (intent == null) { 287 intent = new Intent(); 288 } 289 ClipData clipdata = new ClipData(new ClipDescription("content", 290 new String[]{"image/png"}), 291 new ClipData.Item(uri)); 292 intent.setAction(Intent.ACTION_SEND) 293 .setComponent(null) 294 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 295 .addFlags(FLAG_GRANT_READ_URI_PERMISSION) 296 .setType("image/png") 297 .putExtra(Intent.EXTRA_STREAM, uri) 298 .setClipData(clipdata); 299 return new Intent[]{Intent.createChooser(intent, null).addFlags(FLAG_ACTIVITY_NEW_TASK)}; 300 } 301 clearOldCacheFiles(Context context)302 private static void clearOldCacheFiles(Context context) { 303 THREAD_POOL_EXECUTOR.execute(() -> { 304 File parent = new File(context.getCacheDir(), SUB_FOLDER); 305 File[] files = parent.listFiles((File f, String s) -> s.startsWith(BASE_NAME)); 306 if (files != null) { 307 for (File file: files) { 308 if (file.lastModified() + FILE_LIFE < System.currentTimeMillis()) { 309 file.delete(); 310 } 311 } 312 } 313 }); 314 315 } 316 } 317