Move SaveImage-, DeleteImage- to separate files
SaveImageInBackgroundTask and DeleteImageInBackgroundTask were
recently moved to inner classes of GlobalScreenshot to make the
latter injectable. This change moves them instead to separate
files, to make GlobalScreenshot shorter and more easily
comprehensible.
Test: tested old and new flows; no changes other than moving the
classes and formatting
Bug: 137153302
Change-Id: Iec27fdef541809c24e194fe377e3fad3dc19e213
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
new file mode 100644
index 0000000..083f971
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -0,0 +1,472 @@
+/*
+ * Copyright (C) 2019 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.systemui.screenshot;
+
+import android.app.ActivityTaskManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Picture;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.DeviceConfig;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.internal.messages.nano.SystemMessageProto;
+import com.android.systemui.R;
+import com.android.systemui.SystemUI;
+import com.android.systemui.SystemUIFactory;
+import com.android.systemui.util.NotificationChannels;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * An AsyncTask that saves an image to the media store in the background.
+ */
+class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
+ private static final String TAG = "SaveImageInBackgroundTask";
+
+ private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png";
+ private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
+
+ private final GlobalScreenshot.SaveImageInBackgroundData mParams;
+ private final NotificationManager mNotificationManager;
+ private final Notification.Builder mNotificationBuilder, mPublicNotificationBuilder;
+ private final String mImageFileName;
+ private final long mImageTime;
+ private final Notification.BigPictureStyle mNotificationStyle;
+ private final int mImageWidth;
+ private final int mImageHeight;
+ private final Handler mHandler;
+ private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
+
+ SaveImageInBackgroundTask(Context context, GlobalScreenshot.SaveImageInBackgroundData data,
+ NotificationManager nManager) {
+ Resources r = context.getResources();
+
+ // Prepare all the output metadata
+ mParams = data;
+ mImageTime = System.currentTimeMillis();
+ String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime));
+ mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate);
+
+ // Initialize screenshot notification smart actions provider.
+ mHandler = new Handler();
+ mSmartActionsProvider =
+ SystemUIFactory.getInstance().createScreenshotNotificationSmartActionsProvider();
+
+ // Create the large notification icon
+ mImageWidth = data.image.getWidth();
+ mImageHeight = data.image.getHeight();
+ int iconSize = data.iconSize;
+ int previewWidth = data.previewWidth;
+ int previewHeight = data.previewheight;
+
+ Paint paint = new Paint();
+ ColorMatrix desat = new ColorMatrix();
+ desat.setSaturation(0.25f);
+ paint.setColorFilter(new ColorMatrixColorFilter(desat));
+ Matrix matrix = new Matrix();
+ int overlayColor = 0x40FFFFFF;
+
+ matrix.setTranslate((previewWidth - mImageWidth) / 2,
+ (previewHeight - mImageHeight) / 2);
+ Bitmap picture = generateAdjustedHwBitmap(data.image, previewWidth, previewHeight,
+ matrix, paint, overlayColor);
+
+ // Note, we can't use the preview for the small icon, since it is non-square
+ float scale = (float) iconSize / Math.min(mImageWidth, mImageHeight);
+ matrix.setScale(scale, scale);
+ matrix.postTranslate((iconSize - (scale * mImageWidth)) / 2,
+ (iconSize - (scale * mImageHeight)) / 2);
+ Bitmap icon = generateAdjustedHwBitmap(data.image, iconSize, iconSize, matrix, paint,
+ overlayColor);
+
+ mNotificationManager = nManager;
+ final long now = System.currentTimeMillis();
+
+ // Setup the notification
+ mNotificationStyle = new Notification.BigPictureStyle()
+ .bigPicture(picture.createAshmemBitmap());
+
+ // The public notification will show similar info but with the actual screenshot omitted
+ mPublicNotificationBuilder =
+ new Notification.Builder(context, NotificationChannels.SCREENSHOTS_HEADSUP)
+ .setContentTitle(r.getString(R.string.screenshot_saving_title))
+ .setSmallIcon(R.drawable.stat_notify_image)
+ .setCategory(Notification.CATEGORY_PROGRESS)
+ .setWhen(now)
+ .setShowWhen(true)
+ .setColor(r.getColor(
+ com.android.internal.R.color.system_notification_accent_color));
+ SystemUI.overrideNotificationAppName(context, mPublicNotificationBuilder, true);
+
+ mNotificationBuilder = new Notification.Builder(context,
+ NotificationChannels.SCREENSHOTS_HEADSUP)
+ .setContentTitle(r.getString(R.string.screenshot_saving_title))
+ .setSmallIcon(R.drawable.stat_notify_image)
+ .setWhen(now)
+ .setShowWhen(true)
+ .setColor(r.getColor(
+ com.android.internal.R.color.system_notification_accent_color))
+ .setStyle(mNotificationStyle)
+ .setPublicVersion(mPublicNotificationBuilder.build());
+ mNotificationBuilder.setFlag(Notification.FLAG_NO_CLEAR, true);
+ SystemUI.overrideNotificationAppName(context, mNotificationBuilder, true);
+
+ mNotificationManager.notify(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT,
+ mNotificationBuilder.build());
+
+ /**
+ * NOTE: The following code prepares the notification builder for updating the
+ * notification after the screenshot has been written to disk.
+ */
+
+ // On the tablet, the large icon makes the notification appear as if it is clickable
+ // (and on small devices, the large icon is not shown) so defer showing the large icon
+ // until we compose the final post-save notification below.
+ mNotificationBuilder.setLargeIcon(icon.createAshmemBitmap());
+ // But we still don't set it for the expanded view, allowing the smallIcon to show here.
+ mNotificationStyle.bigLargeIcon((Bitmap) null);
+ }
+
+ private int getUserHandleOfForegroundApplication(Context context) {
+ // This logic matches
+ // com.android.systemui.statusbar.phone.PhoneStatusBarPolicy#updateManagedProfile
+ try {
+ return ActivityTaskManager.getService().getLastResumedActivityUserId();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "getUserHandleOfForegroundApplication: ", e);
+ return context.getUserId();
+ }
+ }
+
+ private boolean isManagedProfile(Context context) {
+ UserManager manager = UserManager.get(context);
+ UserInfo info = manager.getUserInfo(getUserHandleOfForegroundApplication(context));
+ return info.isManagedProfile();
+ }
+
+ /**
+ * Generates a new hardware bitmap with specified values, copying the content from the
+ * passed in bitmap.
+ */
+ private Bitmap generateAdjustedHwBitmap(Bitmap bitmap, int width, int height, Matrix matrix,
+ Paint paint, int color) {
+ Picture picture = new Picture();
+ Canvas canvas = picture.beginRecording(width, height);
+ canvas.drawColor(color);
+ canvas.drawBitmap(bitmap, matrix, paint);
+ picture.endRecording();
+ return Bitmap.createBitmap(picture);
+ }
+
+ @Override
+ protected Void doInBackground(Void... paramsUnused) {
+ if (isCancelled()) {
+ return null;
+ }
+
+ // By default, AsyncTask sets the worker thread to have background thread priority,
+ // so bump it back up so that we save a little quicker.
+ Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
+
+ Context context = mParams.context;
+ Bitmap image = mParams.image;
+ boolean smartActionsEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, true);
+ CompletableFuture<List<Notification.Action>>
+ smartActionsFuture = GlobalScreenshot.getSmartActionsFuture(
+ context, image, mSmartActionsProvider, mHandler, smartActionsEnabled,
+ isManagedProfile(context));
+
+ Resources r = context.getResources();
+
+ try {
+ // Save the screenshot to the MediaStore
+ final MediaStore.PendingParams params = new MediaStore.PendingParams(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mImageFileName, "image/png");
+ params.setRelativePath(Environment.DIRECTORY_PICTURES + File.separator
+ + Environment.DIRECTORY_SCREENSHOTS);
+
+ final Uri uri = MediaStore.createPending(context, params);
+ final MediaStore.PendingSession session = MediaStore.openPending(context, uri);
+ try {
+ // First, write the actual data for our screenshot
+ try (OutputStream out = session.openOutputStream()) {
+ if (!image.compress(Bitmap.CompressFormat.PNG, 100, out)) {
+ throw new IOException("Failed to compress");
+ }
+ }
+
+ // Next, write metadata to help index the screenshot
+ try (ParcelFileDescriptor pfd = session.open()) {
+ final ExifInterface exif = new ExifInterface(pfd.getFileDescriptor());
+
+ exif.setAttribute(ExifInterface.TAG_SOFTWARE,
+ "Android " + Build.DISPLAY);
+
+ exif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH,
+ Integer.toString(image.getWidth()));
+ exif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH,
+ Integer.toString(image.getHeight()));
+
+ final ZonedDateTime time = ZonedDateTime.ofInstant(
+ Instant.ofEpochMilli(mImageTime), ZoneId.systemDefault());
+ exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL,
+ DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss").format(time));
+ exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
+ DateTimeFormatter.ofPattern("SSS").format(time));
+
+ if (Objects.equals(time.getOffset(), ZoneOffset.UTC)) {
+ exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00");
+ } else {
+ exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL,
+ DateTimeFormatter.ofPattern("XXX").format(time));
+ }
+
+ exif.saveAttributes();
+ }
+ session.publish();
+ } catch (Exception e) {
+ session.abandon();
+ throw e;
+ } finally {
+ IoUtils.closeQuietly(session);
+ }
+
+ // Note: Both the share and edit actions are proxied through ActionProxyReceiver in
+ // order to do some common work like dismissing the keyguard and sending
+ // closeSystemWindows
+
+ // Create a share intent, this will always go through the chooser activity first
+ // which should not trigger auto-enter PiP
+ String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
+ String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
+ Intent sharingIntent = new Intent(Intent.ACTION_SEND);
+ sharingIntent.setType("image/png");
+ sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
+ // Include URI in ClipData also, so that grantPermission picks it up.
+ // We don't use setData here because some apps interpret this as "to:".
+ ClipData clipdata = new ClipData(new ClipDescription("content",
+ new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
+ new ClipData.Item(uri));
+ sharingIntent.setClipData(clipdata);
+ sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
+ sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ // Make sure pending intents for the system user are still unique across users
+ // by setting the (otherwise unused) request code to the current user id.
+ int requestCode = context.getUserId();
+
+ PendingIntent chooserAction = PendingIntent.getBroadcast(context, requestCode,
+ new Intent(context, GlobalScreenshot.TargetChosenReceiver.class),
+ PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
+ Intent sharingChooserIntent = Intent.createChooser(sharingIntent, null,
+ chooserAction.getIntentSender())
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ // Create a share action for the notification
+ PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode,
+ new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
+ .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, sharingChooserIntent)
+ .putExtra(GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP, true)
+ .setAction(Intent.ACTION_SEND),
+ PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM);
+ Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
+ R.drawable.ic_screenshot_share,
+ r.getString(com.android.internal.R.string.share), shareAction);
+ mNotificationBuilder.addAction(shareActionBuilder.build());
+
+ // Create an edit intent, if a specific package is provided as the editor, then
+ // launch that directly
+ String editorPackage = context.getString(R.string.config_screenshotEditor);
+ Intent editIntent = new Intent(Intent.ACTION_EDIT);
+ if (!TextUtils.isEmpty(editorPackage)) {
+ editIntent.setComponent(ComponentName.unflattenFromString(editorPackage));
+ }
+ editIntent.setType("image/png");
+ editIntent.setData(uri);
+ editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
+ // Create a edit action
+ PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode,
+ new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
+ .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, editIntent)
+ .putExtra(GlobalScreenshot.EXTRA_CANCEL_NOTIFICATION,
+ editIntent.getComponent() != null)
+ .setAction(Intent.ACTION_EDIT),
+ PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM);
+ Notification.Action.Builder editActionBuilder = new Notification.Action.Builder(
+ R.drawable.ic_screenshot_edit,
+ r.getString(com.android.internal.R.string.screenshot_edit), editAction);
+ mNotificationBuilder.addAction(editActionBuilder.build());
+ if (editAction != null && mParams.onEditReady != null) {
+ mParams.onEditReady.apply(editAction);
+ }
+
+ // Create a delete action for the notification
+ PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode,
+ new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
+ .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()),
+ PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
+ Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
+ R.drawable.ic_screenshot_delete,
+ r.getString(com.android.internal.R.string.delete), deleteAction);
+ mNotificationBuilder.addAction(deleteActionBuilder.build());
+
+ mParams.imageUri = uri;
+ mParams.image = null;
+ mParams.errorMsgResId = 0;
+
+ if (smartActionsEnabled) {
+ int timeoutMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags
+ .SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS,
+ 1000);
+ List<Notification.Action> smartActions = GlobalScreenshot.getSmartActions(
+ smartActionsFuture,
+ timeoutMs);
+ for (Notification.Action action : smartActions) {
+ mNotificationBuilder.addAction(action);
+ }
+ }
+ } catch (Exception e) {
+ // IOException/UnsupportedOperationException may be thrown if external storage is
+ // not mounted
+ Slog.e(TAG, "unable to save screenshot", e);
+ mParams.clearImage();
+ mParams.errorMsgResId = R.string.screenshot_failed_to_save_text;
+ }
+
+ // Recycle the bitmap data
+ if (image != null) {
+ image.recycle();
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void params) {
+ if (mParams.errorMsgResId != 0) {
+ // Show a message that we've failed to save the image to disk
+ GlobalScreenshot.notifyScreenshotError(mParams.context, mNotificationManager,
+ mParams.errorMsgResId);
+ } else {
+ if (mParams.onEditReady != null) {
+ // Cancel the "saving screenshot" notification
+ mNotificationManager.cancel(
+ SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT);
+ } else {
+ // Show the final notification to indicate screenshot saved
+ Context context = mParams.context;
+ Resources r = context.getResources();
+
+ // Create the intent to show the screenshot in gallery
+ Intent launchIntent = new Intent(Intent.ACTION_VIEW);
+ launchIntent.setDataAndType(mParams.imageUri, "image/png");
+ launchIntent.setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ final long now = System.currentTimeMillis();
+
+ // Update the text and the icon for the existing notification
+ mPublicNotificationBuilder
+ .setContentTitle(r.getString(R.string.screenshot_saved_title))
+ .setContentText(r.getString(R.string.screenshot_saved_text))
+ .setContentIntent(
+ PendingIntent.getActivity(mParams.context, 0, launchIntent, 0))
+ .setWhen(now)
+ .setAutoCancel(true)
+ .setColor(context.getColor(
+ com.android.internal.R.color.system_notification_accent_color));
+ mNotificationBuilder
+ .setContentTitle(r.getString(R.string.screenshot_saved_title))
+ .setContentText(r.getString(R.string.screenshot_saved_text))
+ .setContentIntent(PendingIntent.getActivity(mParams.context, 0,
+ launchIntent, 0))
+ .setWhen(now)
+ .setAutoCancel(true)
+ .setColor(context.getColor(
+ com.android.internal.R.color.system_notification_accent_color))
+ .setPublicVersion(mPublicNotificationBuilder.build())
+ .setFlag(Notification.FLAG_NO_CLEAR, false);
+
+ mNotificationManager.notify(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT,
+ mNotificationBuilder.build());
+ }
+ }
+ mParams.finisher.run();
+ mParams.clearContext();
+ }
+
+ @Override
+ protected void onCancelled(Void params) {
+ // If we are cancelled while the task is running in the background, we may get null
+ // params. The finisher is expected to always be called back, so just use the baked-in
+ // params from the ctor in any case.
+ mParams.finisher.run();
+ mParams.clearImage();
+ mParams.clearContext();
+
+ // Cancel the posted notification
+ mNotificationManager.cancel(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT);
+ }
+}