| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.quickstep.util; |
| |
| import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; |
| import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; |
| |
| import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; |
| import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR; |
| import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; |
| |
| import android.app.Activity; |
| import android.app.ActivityOptions; |
| import android.app.prediction.AppTarget; |
| import android.content.ActivityNotFoundException; |
| import android.content.ClipData; |
| import android.content.ClipDescription; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.ShortcutInfo; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Insets; |
| import android.graphics.Picture; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.net.Uri; |
| import android.util.Log; |
| import android.view.View; |
| |
| import androidx.annotation.UiThread; |
| import androidx.annotation.WorkerThread; |
| import androidx.core.content.FileProvider; |
| |
| import com.android.internal.app.ChooserActivity; |
| import com.android.internal.util.ScreenshotHelper; |
| import com.android.launcher3.BuildConfig; |
| import com.android.quickstep.SystemUiProxy; |
| import com.android.systemui.shared.recents.model.Task; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.function.BiFunction; |
| import java.util.function.Supplier; |
| |
| /** |
| * Utility class containing methods to help manage image actions such as sharing, cropping, and |
| * saving image. |
| */ |
| public class ImageActionUtils { |
| |
| private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".overview.fileprovider"; |
| private static final long FILE_LIFE = 1000L /*ms*/ * 60L /*s*/ * 60L /*m*/ * 24L /*h*/; |
| private static final String SUB_FOLDER = "Overview"; |
| private static final String BASE_NAME = "overview_image_"; |
| private static final String TAG = "ImageActionUtils"; |
| |
| /** |
| * Saves screenshot to location determine by SystemUiProxy |
| */ |
| public static void saveScreenshot(SystemUiProxy systemUiProxy, Bitmap screenshot, |
| Rect screenshotBounds, |
| Insets visibleInsets, Task.TaskKey task) { |
| systemUiProxy.handleImageBundleAsScreenshot( |
| ScreenshotHelper.HardwareBitmapBundler.hardwareBitmapToBundle(screenshot), |
| screenshotBounds, visibleInsets, task); |
| } |
| |
| /** |
| * Launch the activity to share image for overview sharing. This is to share cropped bitmap |
| * with specific share targets (with shortcutInfo and appTarget) rendered in overview. |
| */ |
| @UiThread |
| public static void shareImage(Context context, Supplier<Bitmap> bitmapSupplier, RectF rectF, |
| ShortcutInfo shortcutInfo, AppTarget appTarget, String tag) { |
| if (bitmapSupplier.get() == null) { |
| return; |
| } |
| Rect crop = new Rect(); |
| rectF.round(crop); |
| Intent intent = new Intent(); |
| Uri uri = getImageUri(bitmapSupplier.get(), crop, context, tag); |
| ClipData clipdata = new ClipData(new ClipDescription("content", |
| new String[]{"image/png"}), |
| new ClipData.Item(uri)); |
| intent.setAction(Intent.ACTION_SEND) |
| .setComponent(new ComponentName(appTarget.getPackageName(), appTarget.getClassName())) |
| .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |
| .addFlags(FLAG_GRANT_READ_URI_PERMISSION) |
| .setType("image/png") |
| .putExtra(Intent.EXTRA_STREAM, uri) |
| .putExtra(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId()) |
| .setClipData(clipdata); |
| |
| if (context.getUserId() != appTarget.getUser().getIdentifier()) { |
| intent.prepareToLeaveUser(context.getUserId()); |
| intent.fixUris(context.getUserId()); |
| context.startActivityAsUser(intent, appTarget.getUser()); |
| } else { |
| context.startActivity(intent); |
| } |
| } |
| |
| /** |
| * Launch the activity to share image. |
| */ |
| @UiThread |
| public static void startShareActivity(Context context, Supplier<Bitmap> bitmapSupplier, |
| Rect crop, Intent intent, String tag) { |
| if (bitmapSupplier.get() == null) { |
| Log.e(tag, "No snapshot available, not starting share."); |
| return; |
| } |
| |
| UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(context, |
| bitmapSupplier.get(), crop, intent, ImageActionUtils::getShareIntentForImageUri, |
| tag)); |
| } |
| |
| /** |
| * Launch the activity to share image with shared element transition. |
| */ |
| @UiThread |
| public static void startShareActivity(Context context, Supplier<Bitmap> bitmapSupplier, |
| Rect crop, Intent intent, String tag, View sharedElement) { |
| if (bitmapSupplier.get() == null) { |
| Log.e(tag, "No snapshot available, not starting share."); |
| return; |
| } |
| |
| UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(context, |
| bitmapSupplier.get(), crop, intent, ImageActionUtils::getShareIntentForImageUri, |
| tag, sharedElement)); |
| } |
| |
| /** |
| * Starts activity based on given intent created from image uri. |
| */ |
| @WorkerThread |
| public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, |
| Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag) { |
| persistBitmapAndStartActivity(context, bitmap, crop, intent, uriToIntentMap, tag, |
| (Runnable) null); |
| } |
| |
| /** |
| * Starts activity based on given intent created from image uri. |
| * @param exceptionCallback An optional callback to be called when the intent can't be resolved |
| */ |
| @WorkerThread |
| public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, |
| Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag, |
| Runnable exceptionCallback) { |
| Intent[] intents = uriToIntentMap.apply(getImageUri(bitmap, crop, context, tag), intent); |
| |
| try { |
| // Work around b/159412574 |
| if (intents.length == 1) { |
| context.startActivity(intents[0]); |
| } else { |
| context.startActivities(intents); |
| } |
| } catch (ActivityNotFoundException e) { |
| Log.e(TAG, "No activity found to receive image intent"); |
| if (exceptionCallback != null) { |
| exceptionCallback.run(); |
| } |
| } |
| } |
| |
| /** |
| * Starts activity based on given intent created from image uri with shared element transition. |
| */ |
| @WorkerThread |
| public static void persistBitmapAndStartActivity(Context context, Bitmap bitmap, Rect crop, |
| Intent intent, BiFunction<Uri, Intent, Intent[]> uriToIntentMap, String tag, |
| View scaledImage) { |
| Intent[] intents = uriToIntentMap.apply(getImageUri(bitmap, crop, context, tag), intent); |
| |
| // Work around b/159412574 |
| if (intents.length == 1) { |
| MAIN_EXECUTOR.execute(() -> context.startActivity(intents[0], |
| ActivityOptions.makeSceneTransitionAnimation((Activity) context, scaledImage, |
| ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle())); |
| |
| } else { |
| MAIN_EXECUTOR.execute(() -> context.startActivities(intents, |
| ActivityOptions.makeSceneTransitionAnimation((Activity) context, scaledImage, |
| ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle())); |
| } |
| } |
| |
| |
| |
| /** |
| * Converts image bitmap to Uri by temporarily saving bitmap to cache, and creating Uri pointing |
| * to that location. Used to be able to share an image with another app. |
| * |
| * @param bitmap The whole bitmap to be shared. |
| * @param crop The section of the bitmap to be shared. |
| * @param context The application context, used to interact with file system. |
| * @param tag Tag used to log errors. |
| * @return Uri that points to the cropped version of desired bitmap to share. |
| */ |
| @WorkerThread |
| public static Uri getImageUri(Bitmap bitmap, Rect crop, Context context, String tag) { |
| clearOldCacheFiles(context); |
| Bitmap croppedBitmap = cropBitmap(bitmap, crop); |
| int cropHash = crop == null ? 0 : crop.hashCode(); |
| String baseName = BASE_NAME + bitmap.hashCode() + "_" + cropHash + ".png"; |
| File parent = new File(context.getCacheDir(), SUB_FOLDER); |
| parent.mkdir(); |
| File file = new File(parent, baseName); |
| |
| try (FileOutputStream fos = new FileOutputStream(file)) { |
| croppedBitmap.compress(Bitmap.CompressFormat.PNG, 100, fos); |
| } catch (IOException e) { |
| Log.e(tag, "Error saving image", e); |
| } |
| |
| return FileProvider.getUriForFile(context, AUTHORITY, file); |
| } |
| |
| /** |
| * Crops the bitmap to the provided size and returns a software backed bitmap whenever possible. |
| * |
| * @param bitmap The bitmap to be cropped. |
| * @param crop The section of the bitmap in the crop. |
| * @return The cropped bitmap. |
| */ |
| @WorkerThread |
| public static Bitmap cropBitmap(Bitmap bitmap, Rect crop) { |
| Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); |
| if (crop == null) { |
| crop = new Rect(src); |
| } |
| if (crop.equals(src)) { |
| return bitmap; |
| } else { |
| if (bitmap.getConfig() != Bitmap.Config.HARDWARE) { |
| return Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), |
| crop.height()); |
| } |
| |
| // For hardware bitmaps, use the Picture API to directly create a software bitmap |
| Picture picture = new Picture(); |
| Canvas canvas = picture.beginRecording(crop.width(), crop.height()); |
| canvas.drawBitmap(bitmap, -crop.left, -crop.top, null); |
| picture.endRecording(); |
| return Bitmap.createBitmap(picture, crop.width(), crop.height(), |
| Bitmap.Config.ARGB_8888); |
| } |
| } |
| |
| /** |
| * Gets the intent used to share image. |
| */ |
| @WorkerThread |
| private static Intent[] getShareIntentForImageUri(Uri uri, Intent intent) { |
| if (intent == null) { |
| intent = new Intent(); |
| } |
| ClipData clipdata = new ClipData(new ClipDescription("content", |
| new String[]{"image/png"}), |
| new ClipData.Item(uri)); |
| intent.setAction(Intent.ACTION_SEND) |
| .setComponent(null) |
| .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |
| .addFlags(FLAG_GRANT_READ_URI_PERMISSION) |
| .setType("image/png") |
| .putExtra(Intent.EXTRA_STREAM, uri) |
| .setClipData(clipdata); |
| return new Intent[]{Intent.createChooser(intent, null).addFlags(FLAG_ACTIVITY_NEW_TASK)}; |
| } |
| |
| private static void clearOldCacheFiles(Context context) { |
| THREAD_POOL_EXECUTOR.execute(() -> { |
| File parent = new File(context.getCacheDir(), SUB_FOLDER); |
| File[] files = parent.listFiles((File f, String s) -> s.startsWith(BASE_NAME)); |
| if (files != null) { |
| for (File file: files) { |
| if (file.lastModified() + FILE_LIFE < System.currentTimeMillis()) { |
| file.delete(); |
| } |
| } |
| } |
| }); |
| |
| } |
| } |