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