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