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