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