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.THREAD_POOL_EXECUTOR; 23 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 24 25 import android.content.ClipData; 26 import android.content.ClipDescription; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.graphics.Bitmap; 30 import android.graphics.Canvas; 31 import android.graphics.Insets; 32 import android.graphics.Picture; 33 import android.graphics.Rect; 34 import android.net.Uri; 35 import android.util.Log; 36 37 import androidx.annotation.UiThread; 38 import androidx.annotation.WorkerThread; 39 import androidx.core.content.FileProvider; 40 41 import com.android.launcher3.BuildConfig; 42 import com.android.quickstep.SystemUiProxy; 43 import com.android.systemui.shared.recents.model.Task; 44 import com.android.systemui.shared.recents.utilities.BitmapUtil; 45 46 import java.io.File; 47 import java.io.FileOutputStream; 48 import java.io.IOException; 49 import java.util.function.BiFunction; 50 import java.util.function.Supplier; 51 52 /** 53 * Utility class containing methods to help manage image actions such as sharing, cropping, and 54 * saving image. 55 */ 56 public class ImageActionUtils { 57 58 private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".overview.fileprovider"; 59 private static final long FILE_LIFE = 1000L /*ms*/ * 60L /*s*/ * 60L /*m*/ * 24L /*h*/; 60 private static final String SUB_FOLDER = "Overview"; 61 private static final String BASE_NAME = "overview_image_"; 62 63 /** 64 * Saves screenshot to location determine by SystemUiProxy 65 */ saveScreenshot(SystemUiProxy systemUiProxy, Bitmap screenshot, Rect screenshotBounds, Insets visibleInsets, Task.TaskKey task)66 public static void saveScreenshot(SystemUiProxy systemUiProxy, Bitmap screenshot, 67 Rect screenshotBounds, 68 Insets visibleInsets, Task.TaskKey task) { 69 systemUiProxy.handleImageBundleAsScreenshot(BitmapUtil.hardwareBitmapToBundle(screenshot), 70 screenshotBounds, visibleInsets, task); 71 } 72 73 /** 74 * Launch the activity to share image. 75 */ 76 @UiThread startShareActivity(Context context, Supplier<Bitmap> bitmapSupplier, Rect crop, Intent intent, String tag)77 public static void startShareActivity(Context context, Supplier<Bitmap> bitmapSupplier, 78 Rect crop, Intent intent, String tag) { 79 if (bitmapSupplier.get() == null) { 80 Log.e(tag, "No snapshot available, not starting share."); 81 return; 82 } 83 84 UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(context, 85 bitmapSupplier.get(), crop, intent, ImageActionUtils::getShareIntentForImageUri, 86 tag)); 87 } 88 89 /** 90 * Starts activity based on given intent created from image uri. 91 */ 92 @WorkerThread persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag)93 public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, 94 Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag) { 95 Intent[] intents = uriToIntentMap.apply(getImageUri(bitmap, crop, context, tag), intent); 96 97 // Work around b/159412574 98 if (intents.length == 1) { 99 context.startActivity(intents[0]); 100 } else { 101 context.startActivities(intents); 102 } 103 } 104 105 /** 106 * Converts image bitmap to Uri by temporarily saving bitmap to cache, and creating Uri pointing 107 * to that location. Used to be able to share an image with another app. 108 * 109 * @param bitmap The whole bitmap to be shared. 110 * @param crop The section of the bitmap to be shared. 111 * @param context The application context, used to interact with file system. 112 * @param tag Tag used to log errors. 113 * @return Uri that points to the cropped version of desired bitmap to share. 114 */ 115 @WorkerThread getImageUri(Bitmap bitmap, Rect crop, Context context, String tag)116 public static Uri getImageUri(Bitmap bitmap, Rect crop, Context context, String tag) { 117 clearOldCacheFiles(context); 118 Bitmap croppedBitmap = cropBitmap(bitmap, crop); 119 int cropHash = crop == null ? 0 : crop.hashCode(); 120 String baseName = BASE_NAME + bitmap.hashCode() + "_" + cropHash + ".png"; 121 File parent = new File(context.getCacheDir(), SUB_FOLDER); 122 parent.mkdir(); 123 File file = new File(parent, baseName); 124 125 try (FileOutputStream fos = new FileOutputStream(file)) { 126 croppedBitmap.compress(Bitmap.CompressFormat.PNG, 100, fos); 127 } catch (IOException e) { 128 Log.e(tag, "Error saving image", e); 129 } 130 131 return FileProvider.getUriForFile(context, AUTHORITY, file); 132 } 133 134 /** 135 * Crops the bitmap to the provided size and returns a software backed bitmap whenever possible. 136 * 137 * @param bitmap The bitmap to be cropped. 138 * @param crop The section of the bitmap in the crop. 139 * @return The cropped bitmap. 140 */ 141 @WorkerThread cropBitmap(Bitmap bitmap, Rect crop)142 public static Bitmap cropBitmap(Bitmap bitmap, Rect crop) { 143 Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); 144 if (crop == null) { 145 crop = new Rect(src); 146 } 147 if (crop.equals(src)) { 148 return bitmap; 149 } else { 150 if (bitmap.getConfig() != Bitmap.Config.HARDWARE) { 151 return Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), 152 crop.height()); 153 } 154 155 // For hardware bitmaps, use the Picture API to directly create a software bitmap 156 Picture picture = new Picture(); 157 Canvas canvas = picture.beginRecording(crop.width(), crop.height()); 158 canvas.drawBitmap(bitmap, -crop.left, -crop.top, null); 159 picture.endRecording(); 160 return Bitmap.createBitmap(picture, crop.width(), crop.height(), 161 Bitmap.Config.ARGB_8888); 162 } 163 } 164 165 /** 166 * Gets the intent used to share image. 167 */ 168 @WorkerThread getShareIntentForImageUri(Uri uri, Intent intent)169 private static Intent[] getShareIntentForImageUri(Uri uri, Intent intent) { 170 if (intent == null) { 171 intent = new Intent(); 172 } 173 ClipData clipdata = new ClipData(new ClipDescription("content", 174 new String[]{"image/png"}), 175 new ClipData.Item(uri)); 176 intent.setAction(Intent.ACTION_SEND) 177 .setComponent(null) 178 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 179 .addFlags(FLAG_GRANT_READ_URI_PERMISSION) 180 .setType("image/png") 181 .putExtra(Intent.EXTRA_STREAM, uri) 182 .setClipData(clipdata); 183 return new Intent[]{Intent.createChooser(intent, null).addFlags(FLAG_ACTIVITY_NEW_TASK)}; 184 } 185 clearOldCacheFiles(Context context)186 private static void clearOldCacheFiles(Context context) { 187 THREAD_POOL_EXECUTOR.execute(() -> { 188 File parent = new File(context.getCacheDir(), SUB_FOLDER); 189 File[] files = parent.listFiles((File f, String s) -> s.startsWith(BASE_NAME)); 190 if (files != null) { 191 for (File file: files) { 192 if (file.lastModified() + FILE_LIFE < System.currentTimeMillis()) { 193 file.delete(); 194 } 195 } 196 } 197 }); 198 199 } 200 } 201