diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/DeleteImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/DeleteImageInBackgroundTask.java new file mode 100644 index 0000000000000..8c4865510ed1c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/DeleteImageInBackgroundTask.java @@ -0,0 +1,43 @@ +/* + * 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.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.os.AsyncTask; + +/** + * An AsyncTask that deletes an image from the media store in the background. + */ +class DeleteImageInBackgroundTask extends AsyncTask { + private Context mContext; + + DeleteImageInBackgroundTask(Context context) { + mContext = context; + } + + @Override + protected Void doInBackground(Uri... params) { + if (params.length != 1) return null; + + Uri screenshotUri = params[0]; + ContentResolver resolver = mContext.getContentResolver(); + resolver.delete(screenshotUri, null, null); + return null; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index 7dc3236915e8f..79632039c8327 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -32,52 +32,31 @@ import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; -import android.app.ActivityTaskManager; import android.app.Notification; -import android.app.Notification.BigPictureStyle; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; -import android.content.ClipData; -import android.content.ClipDescription; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.pm.UserInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.ColorMatrix; -import android.graphics.ColorMatrixColorFilter; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Picture; import android.graphics.PixelFormat; import android.graphics.PointF; import android.graphics.Rect; -import android.media.ExifInterface; import android.media.MediaActionSound; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; -import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.ParcelFileDescriptor; import android.os.PowerManager; -import android.os.Process; -import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; -import android.os.UserManager; import android.provider.DeviceConfig; -import android.provider.MediaStore; -import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; import android.util.Slog; @@ -93,32 +72,16 @@ import android.widget.ImageView; import android.widget.Toast; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.systemui.R; import com.android.systemui.SystemUI; -import com.android.systemui.SystemUIFactory; import com.android.systemui.dagger.qualifiers.MainResources; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.statusbar.phone.StatusBar; 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.Collections; -import java.util.Date; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -140,7 +103,7 @@ public class GlobalScreenshot { /** * POD used in the AsyncTask which saves an image in the background. */ - private static class SaveImageInBackgroundData { + static class SaveImageInBackgroundData { public Context context; public Bitmap image; public Uri imageUri; @@ -156,426 +119,12 @@ public class GlobalScreenshot { imageUri = null; iconSize = 0; } + void clearContext() { context = null; } } - /** - * An AsyncTask that saves an image to the media store in the background. - */ - private static class SaveImageInBackgroundTask extends AsyncTask { - 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 SaveImageInBackgroundData mParams; - private final NotificationManager mNotificationManager; - private final Notification.Builder mNotificationBuilder, mPublicNotificationBuilder; - private final String mImageFileName; - private final long mImageTime; - private final BigPictureStyle mNotificationStyle; - private final int mImageWidth; - private final int mImageHeight; - private final Handler mHandler; - private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider; - - SaveImageInBackgroundTask(Context context, 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(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> smartActionsFuture = 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(EXTRA_ACTION_INTENT, sharingChooserIntent) - .putExtra(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(EXTRA_ACTION_INTENT, editIntent) - .putExtra(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 smartActions = 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(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(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(SystemMessage.NOTE_GLOBAL_SCREENSHOT); - } - } - - /** - * An AsyncTask that deletes an image from the media store in the background. - */ - private static class DeleteImageInBackgroundTask extends AsyncTask { - private Context mContext; - - DeleteImageInBackgroundTask(Context context) { - mContext = context; - } - - @Override - protected Void doInBackground(Uri... params) { - if (params.length != 1) return null; - - Uri screenshotUri = params[0]; - ContentResolver resolver = mContext.getContentResolver(); - resolver.delete(screenshotUri, null, null); - return null; - } - } - static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id"; static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent"; static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification"; @@ -639,7 +188,6 @@ public class GlobalScreenshot { } }; - /** * @param context everything needs a context :( */ @@ -670,25 +218,25 @@ public class GlobalScreenshot { ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, WindowManager.LayoutParams.TYPE_SCREENSHOT, WindowManager.LayoutParams.FLAG_FULLSCREEN - | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED, + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED, PixelFormat.TRANSLUCENT); mWindowLayoutParams.setTitle("ScreenshotAnimation"); mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mNotificationManager = - (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); + (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); mDisplay = mWindowManager.getDefaultDisplay(); mDisplayMetrics = new DisplayMetrics(); mDisplay.getRealMetrics(mDisplayMetrics); // Get the various target sizes mNotificationIconSize = - resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); + resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); // Scale has to account for both sides of the bg mBgPadding = (float) resources.getDimensionPixelSize(R.dimen.global_screenshot_bg_padding); - mBgPaddingScale = mBgPadding / mDisplayMetrics.widthPixels; + mBgPaddingScale = mBgPadding / mDisplayMetrics.widthPixels; // determine the optimal preview size int panelWidth = 0; @@ -916,6 +464,7 @@ public class GlobalScreenshot { mScreenshotAnimation.start(); }); } + private ValueAnimator createScreenshotDropInAnimation() { final float flashPeakDurationPct = ((float) (SCREENSHOT_FLASH_TO_PEAK_DURATION) / SCREENSHOT_DROP_IN_DURATION); @@ -965,6 +514,7 @@ public class GlobalScreenshot { mScreenshotFlash.setAlpha(0f); mScreenshotFlash.setVisibility(View.VISIBLE); } + @Override public void onAnimationEnd(android.animation.Animator animation) { mScreenshotFlash.setVisibility(View.GONE); @@ -975,7 +525,7 @@ public class GlobalScreenshot { public void onAnimationUpdate(ValueAnimator animation) { float t = (Float) animation.getAnimatedValue(); float scaleT = (SCREENSHOT_SCALE + mBgPaddingScale) - - scaleInterpolator.getInterpolation(t) + - scaleInterpolator.getInterpolation(t) * (SCREENSHOT_SCALE - SCREENSHOT_DROP_IN_MIN_SCALE); mBackgroundView.setAlpha(scaleInterpolator.getInterpolation(t) * BACKGROUND_ALPHA); mScreenshotView.setAlpha(t); @@ -986,6 +536,7 @@ public class GlobalScreenshot { }); return anim; } + private ValueAnimator createScreenshotDropOutAnimation(int w, int h, boolean statusBarVisible, boolean navBarVisible) { ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); @@ -1007,7 +558,8 @@ public class GlobalScreenshot { public void onAnimationUpdate(ValueAnimator animation) { float t = (Float) animation.getAnimatedValue(); float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale) - - t * (SCREENSHOT_DROP_IN_MIN_SCALE - SCREENSHOT_FAST_DROP_OUT_MIN_SCALE); + - t * (SCREENSHOT_DROP_IN_MIN_SCALE + - SCREENSHOT_FAST_DROP_OUT_MIN_SCALE); mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA); mScreenshotView.setAlpha(1f - t); mScreenshotView.setScaleX(scaleT); @@ -1034,8 +586,10 @@ public class GlobalScreenshot { float halfScreenHeight = (h - 2f * mBgPadding) / 2f; final float offsetPct = SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET; final PointF finalPos = new PointF( - -halfScreenWidth + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenWidth, - -halfScreenHeight + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenHeight); + -halfScreenWidth + + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenWidth, + -halfScreenHeight + + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenHeight); // Animate the screenshot to the status bar anim.setDuration(SCREENSHOT_DROP_OUT_DURATION); @@ -1044,7 +598,7 @@ public class GlobalScreenshot { public void onAnimationUpdate(ValueAnimator animation) { float t = (Float) animation.getAnimatedValue(); float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale) - - scaleInterpolator.getInterpolation(t) + - scaleInterpolator.getInterpolation(t) * (SCREENSHOT_DROP_IN_MIN_SCALE - SCREENSHOT_DROP_OUT_MIN_SCALE); mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA); mScreenshotView.setAlpha(1f - scaleInterpolator.getInterpolation(t)); @@ -1105,15 +659,15 @@ public class GlobalScreenshot { // Repurpose the existing notification to notify the user of the error Notification.Builder b = new Notification.Builder(context, NotificationChannels.ALERTS) - .setTicker(r.getString(R.string.screenshot_failed_title)) - .setContentTitle(r.getString(R.string.screenshot_failed_title)) - .setContentText(errorMsg) - .setSmallIcon(R.drawable.stat_notify_image_error) - .setWhen(System.currentTimeMillis()) - .setVisibility(Notification.VISIBILITY_PUBLIC) // ok to show outside lockscreen - .setCategory(Notification.CATEGORY_ERROR) - .setAutoCancel(true) - .setColor(context.getColor( + .setTicker(r.getString(R.string.screenshot_failed_title)) + .setContentTitle(r.getString(R.string.screenshot_failed_title)) + .setContentText(errorMsg) + .setSmallIcon(R.drawable.stat_notify_image_error) + .setWhen(System.currentTimeMillis()) + .setVisibility(Notification.VISIBILITY_PUBLIC) // ok to show outside lockscreen + .setCategory(Notification.CATEGORY_ERROR) + .setAutoCancel(true) + .setColor(context.getColor( com.android.internal.R.color.system_notification_accent_color)); final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService( Context.DEVICE_POLICY_SERVICE); 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 0000000000000..083f9712d7050 --- /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 { + 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> + 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 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); + } +}