/* * Copyright (C) 2011 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.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.app.admin.DevicePolicyManager; import android.app.Notification; import android.app.Notification.BigPictureStyle; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; 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.PixelFormat; import android.graphics.PointF; import android.graphics.Rect; import android.media.MediaActionSound; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.os.Process; import android.os.UserHandle; import android.provider.MediaStore; import android.util.DisplayMetrics; import android.view.Display; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceControl; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.animation.Interpolator; import android.widget.ImageView; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.systemui.R; import com.android.systemui.SystemUI; import com.android.systemui.util.NotificationChannels; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; /** * POD used in the AsyncTask which saves an image in the background. */ class SaveImageInBackgroundData { Context context; Bitmap image; Uri imageUri; Runnable finisher; int iconSize; int previewWidth; int previewheight; int errorMsgResId; void clearImage() { image = null; imageUri = null; iconSize = 0; } void clearContext() { context = null; } } /** * An AsyncTask that saves an image to the media store in the background. */ class SaveImageInBackgroundTask extends AsyncTask { private static final String SCREENSHOTS_DIR_NAME = "Screenshots"; 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 File mScreenshotDir; private final String mImageFileName; private final String mImageFilePath; private final long mImageTime; private final BigPictureStyle mNotificationStyle; private final int mImageWidth; private final int mImageHeight; // WORKAROUND: We want the same notification across screenshots that we update so that we don't // spam a user's notification drawer. However, we only show the ticker for the saving state // and if the ticker text is the same as the previous notification, then it will not show. So // for now, we just add and remove a space from the ticker text to trigger the animation when // necessary. private static boolean mTickerAddSpace; 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); mScreenshotDir = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES), SCREENSHOTS_DIR_NAME); mImageFilePath = new File(mScreenshotDir, mImageFileName).getAbsolutePath(); // 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; Canvas c = new Canvas(); Paint paint = new Paint(); ColorMatrix desat = new ColorMatrix(); desat.setSaturation(0.25f); paint.setColorFilter(new ColorMatrixColorFilter(desat)); Matrix matrix = new Matrix(); int overlayColor = 0x40FFFFFF; Bitmap picture = Bitmap.createBitmap(previewWidth, previewHeight, data.image.getConfig()); matrix.setTranslate((previewWidth - mImageWidth) / 2, (previewHeight - mImageHeight) / 2); c.setBitmap(picture); c.drawBitmap(data.image, matrix, paint); c.drawColor(overlayColor); c.setBitmap(null); // Note, we can't use the preview for the small icon, since it is non-square float scale = (float) iconSize / Math.min(mImageWidth, mImageHeight); Bitmap icon = Bitmap.createBitmap(iconSize, iconSize, data.image.getConfig()); matrix.setScale(scale, scale); matrix.postTranslate((iconSize - (scale * mImageWidth)) / 2, (iconSize - (scale * mImageHeight)) / 2); c.setBitmap(icon); c.drawBitmap(data.image, matrix, paint); c.drawColor(overlayColor); c.setBitmap(null); // Show the intermediate notification mTickerAddSpace = !mTickerAddSpace; 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) .setContentTitle(r.getString(R.string.screenshot_saving_title)) .setContentText(r.getString(R.string.screenshot_saving_text)) .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); mNotificationBuilder = new Notification.Builder(context, NotificationChannels.SCREENSHOTS) .setTicker(r.getString(R.string.screenshot_saving_ticker) + (mTickerAddSpace ? " " : "")) .setContentTitle(r.getString(R.string.screenshot_saving_title)) .setContentText(r.getString(R.string.screenshot_saving_text)) .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); 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); } @Override protected Void doInBackground(Void... params) { 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; Resources r = context.getResources(); try { // Create screenshot directory if it doesn't exist mScreenshotDir.mkdirs(); // media provider uses seconds for DATE_MODIFIED and DATE_ADDED, but milliseconds // for DATE_TAKEN long dateSeconds = mImageTime / 1000; // Save OutputStream out = new FileOutputStream(mImageFilePath); image.compress(Bitmap.CompressFormat.PNG, 100, out); out.flush(); out.close(); // Save the screenshot to the MediaStore ContentValues values = new ContentValues(); ContentResolver resolver = context.getContentResolver(); values.put(MediaStore.Images.ImageColumns.DATA, mImageFilePath); values.put(MediaStore.Images.ImageColumns.TITLE, mImageFileName); values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, mImageFileName); values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, mImageTime); values.put(MediaStore.Images.ImageColumns.DATE_ADDED, dateSeconds); values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, dateSeconds); values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png"); values.put(MediaStore.Images.ImageColumns.WIDTH, mImageWidth); values.put(MediaStore.Images.ImageColumns.HEIGHT, mImageHeight); values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length()); Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); // Create a share intent 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); sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject); // Create a share action for the notification PendingIntent chooseAction = PendingIntent.getBroadcast(context, 0, new Intent(context, GlobalScreenshot.TargetChosenReceiver.class), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); Intent chooserIntent = Intent.createChooser(sharingIntent, null, chooseAction.getIntentSender()) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent shareAction = PendingIntent.getActivity(context, 0, chooserIntent, PendingIntent.FLAG_CANCEL_CURRENT); 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 a delete action for the notification PendingIntent deleteAction = PendingIntent.getBroadcast(context, 0, 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; } catch (Exception e) { // IOException/UnsupportedOperationException may be thrown if external storage is not // mounted 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 { // 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. */ class DeleteImageInBackgroundTask extends AsyncTask { private static final String TAG = "DeleteImageInBackgroundTask"; 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; } } class GlobalScreenshot { static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id"; private static final int SCREENSHOT_FLASH_TO_PEAK_DURATION = 130; private static final int SCREENSHOT_DROP_IN_DURATION = 430; private static final int SCREENSHOT_DROP_OUT_DELAY = 500; private static final int SCREENSHOT_DROP_OUT_DURATION = 430; private static final int SCREENSHOT_DROP_OUT_SCALE_DURATION = 370; private static final int SCREENSHOT_FAST_DROP_OUT_DURATION = 320; private static final float BACKGROUND_ALPHA = 0.5f; private static final float SCREENSHOT_SCALE = 1f; private static final float SCREENSHOT_DROP_IN_MIN_SCALE = SCREENSHOT_SCALE * 0.725f; private static final float SCREENSHOT_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.45f; private static final float SCREENSHOT_FAST_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.6f; private static final float SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET = 0f; private final int mPreviewWidth; private final int mPreviewHeight; private Context mContext; private WindowManager mWindowManager; private WindowManager.LayoutParams mWindowLayoutParams; private NotificationManager mNotificationManager; private Display mDisplay; private DisplayMetrics mDisplayMetrics; private Matrix mDisplayMatrix; private Bitmap mScreenBitmap; private View mScreenshotLayout; private ScreenshotSelectorView mScreenshotSelectorView; private ImageView mBackgroundView; private ImageView mScreenshotView; private ImageView mScreenshotFlash; private AnimatorSet mScreenshotAnimation; private int mNotificationIconSize; private float mBgPadding; private float mBgPaddingScale; private AsyncTask mSaveInBgTask; private MediaActionSound mCameraSound; /** * @param context everything needs a context :( */ public GlobalScreenshot(Context context) { Resources r = context.getResources(); mContext = context; LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); // Inflate the screenshot layout mDisplayMatrix = new Matrix(); mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null); mBackgroundView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_background); mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot); mScreenshotFlash = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_flash); mScreenshotSelectorView = (ScreenshotSelectorView) mScreenshotLayout.findViewById( R.id.global_screenshot_selector); mScreenshotLayout.setFocusable(true); mScreenshotSelectorView.setFocusable(true); mScreenshotSelectorView.setFocusableInTouchMode(true); mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // Intercept and ignore all touch events return true; } }); // Setup the window that we are going to use mWindowLayoutParams = new WindowManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, WindowManager.LayoutParams.TYPE_SCREENSHOT, WindowManager.LayoutParams.FLAG_FULLSCREEN | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED, PixelFormat.TRANSLUCENT); mWindowLayoutParams.setTitle("ScreenshotAnimation"); mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); mDisplay = mWindowManager.getDefaultDisplay(); mDisplayMetrics = new DisplayMetrics(); mDisplay.getRealMetrics(mDisplayMetrics); // Get the various target sizes mNotificationIconSize = r.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); // Scale has to account for both sides of the bg mBgPadding = (float) r.getDimensionPixelSize(R.dimen.global_screenshot_bg_padding); mBgPaddingScale = mBgPadding / mDisplayMetrics.widthPixels; // determine the optimal preview size int panelWidth = 0; try { panelWidth = r.getDimensionPixelSize(R.dimen.notification_panel_width); } catch (Resources.NotFoundException e) { } if (panelWidth <= 0) { // includes notification_panel_width==match_parent (-1) panelWidth = mDisplayMetrics.widthPixels; } mPreviewWidth = panelWidth; mPreviewHeight = r.getDimensionPixelSize(R.dimen.notification_max_height); // Setup the Camera shutter sound mCameraSound = new MediaActionSound(); mCameraSound.load(MediaActionSound.SHUTTER_CLICK); } /** * Creates a new worker thread and saves the screenshot to the media store. */ private void saveScreenshotInWorkerThread(Runnable finisher) { SaveImageInBackgroundData data = new SaveImageInBackgroundData(); data.context = mContext; data.image = mScreenBitmap; data.iconSize = mNotificationIconSize; data.finisher = finisher; data.previewWidth = mPreviewWidth; data.previewheight = mPreviewHeight; if (mSaveInBgTask != null) { mSaveInBgTask.cancel(false); } mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager) .execute(); } /** * @return the current display rotation in degrees */ private float getDegreesForRotation(int value) { switch (value) { case Surface.ROTATION_90: return 360f - 90f; case Surface.ROTATION_180: return 360f - 180f; case Surface.ROTATION_270: return 360f - 270f; } return 0f; } /** * Takes a screenshot of the current display and shows an animation. */ void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible, int x, int y, int width, int height) { // We need to orient the screenshot correctly (and the Surface api seems to take screenshots // only in the natural orientation of the device :!) mDisplay.getRealMetrics(mDisplayMetrics); float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels}; float degrees = getDegreesForRotation(mDisplay.getRotation()); boolean requiresRotation = (degrees > 0); if (requiresRotation) { // Get the dimensions of the device in its native orientation mDisplayMatrix.reset(); mDisplayMatrix.preRotate(-degrees); mDisplayMatrix.mapPoints(dims); dims[0] = Math.abs(dims[0]); dims[1] = Math.abs(dims[1]); } // Take the screenshot mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]); if (mScreenBitmap == null) { notifyScreenshotError(mContext, mNotificationManager, R.string.screenshot_failed_to_capture_text); finisher.run(); return; } if (requiresRotation) { // Rotate the screenshot to the current orientation Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(ss); c.translate(ss.getWidth() / 2, ss.getHeight() / 2); c.rotate(degrees); c.translate(-dims[0] / 2, -dims[1] / 2); c.drawBitmap(mScreenBitmap, 0, 0, null); c.setBitmap(null); // Recycle the previous bitmap mScreenBitmap.recycle(); mScreenBitmap = ss; } if (width != mDisplayMetrics.widthPixels || height != mDisplayMetrics.heightPixels) { // Crop the screenshot to selected region Bitmap cropped = Bitmap.createBitmap(mScreenBitmap, x, y, width, height); mScreenBitmap.recycle(); mScreenBitmap = cropped; } // Optimizations mScreenBitmap.setHasAlpha(false); mScreenBitmap.prepareToDraw(); // Start the post-screenshot animation startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels, statusBarVisible, navBarVisible); } void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) { mDisplay.getRealMetrics(mDisplayMetrics); takeScreenshot(finisher, statusBarVisible, navBarVisible, 0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels); } /** * Displays a screenshot selector */ void takeScreenshotPartial(final Runnable finisher, final boolean statusBarVisible, final boolean navBarVisible) { mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); mScreenshotSelectorView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { ScreenshotSelectorView view = (ScreenshotSelectorView) v; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: view.startSelection((int) event.getX(), (int) event.getY()); return true; case MotionEvent.ACTION_MOVE: view.updateSelection((int) event.getX(), (int) event.getY()); return true; case MotionEvent.ACTION_UP: view.setVisibility(View.GONE); mWindowManager.removeView(mScreenshotLayout); final Rect rect = view.getSelectionRect(); if (rect != null) { if (rect.width() != 0 && rect.height() != 0) { // Need mScreenshotLayout to handle it after the view disappears mScreenshotLayout.post(new Runnable() { public void run() { takeScreenshot(finisher, statusBarVisible, navBarVisible, rect.left, rect.top, rect.width(), rect.height()); } }); } } view.stopSelection(); return true; } return false; } }); mScreenshotLayout.post(new Runnable() { @Override public void run() { mScreenshotSelectorView.setVisibility(View.VISIBLE); mScreenshotSelectorView.requestFocus(); } }); } /** * Cancels screenshot request */ void stopScreenshot() { // If the selector layer still presents on screen, we remove it and resets its state. if (mScreenshotSelectorView.getSelectionRect() != null) { mWindowManager.removeView(mScreenshotLayout); mScreenshotSelectorView.stopSelection(); } } /** * Starts the animation after taking the screenshot */ private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible, boolean navBarVisible) { // Add the view for the animation mScreenshotView.setImageBitmap(mScreenBitmap); mScreenshotLayout.requestFocus(); // Setup the animation with the screenshot just taken if (mScreenshotAnimation != null) { if (mScreenshotAnimation.isStarted()) { mScreenshotAnimation.end(); } mScreenshotAnimation.removeAllListeners(); } mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); ValueAnimator screenshotDropInAnim = createScreenshotDropInAnimation(); ValueAnimator screenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h, statusBarVisible, navBarVisible); mScreenshotAnimation = new AnimatorSet(); mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim); mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { // Save the screenshot once we have a bit of time now saveScreenshotInWorkerThread(finisher); mWindowManager.removeView(mScreenshotLayout); // Clear any references to the bitmap mScreenBitmap = null; mScreenshotView.setImageBitmap(null); } }); mScreenshotLayout.post(new Runnable() { @Override public void run() { // Play the shutter sound to notify that we've taken a screenshot mCameraSound.play(MediaActionSound.SHUTTER_CLICK); mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null); mScreenshotView.buildLayer(); mScreenshotAnimation.start(); } }); } private ValueAnimator createScreenshotDropInAnimation() { final float flashPeakDurationPct = ((float) (SCREENSHOT_FLASH_TO_PEAK_DURATION) / SCREENSHOT_DROP_IN_DURATION); final float flashDurationPct = 2f * flashPeakDurationPct; final Interpolator flashAlphaInterpolator = new Interpolator() { @Override public float getInterpolation(float x) { // Flash the flash view in and out quickly if (x <= flashDurationPct) { return (float) Math.sin(Math.PI * (x / flashDurationPct)); } return 0; } }; final Interpolator scaleInterpolator = new Interpolator() { @Override public float getInterpolation(float x) { // We start scaling when the flash is at it's peak if (x < flashPeakDurationPct) { return 0; } return (x - flashDurationPct) / (1f - flashDurationPct); } }; ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); anim.setDuration(SCREENSHOT_DROP_IN_DURATION); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mBackgroundView.setAlpha(0f); mBackgroundView.setVisibility(View.VISIBLE); mScreenshotView.setAlpha(0f); mScreenshotView.setTranslationX(0f); mScreenshotView.setTranslationY(0f); mScreenshotView.setScaleX(SCREENSHOT_SCALE + mBgPaddingScale); mScreenshotView.setScaleY(SCREENSHOT_SCALE + mBgPaddingScale); mScreenshotView.setVisibility(View.VISIBLE); mScreenshotFlash.setAlpha(0f); mScreenshotFlash.setVisibility(View.VISIBLE); } @Override public void onAnimationEnd(android.animation.Animator animation) { mScreenshotFlash.setVisibility(View.GONE); } }); anim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float t = (Float) animation.getAnimatedValue(); float scaleT = (SCREENSHOT_SCALE + mBgPaddingScale) - scaleInterpolator.getInterpolation(t) * (SCREENSHOT_SCALE - SCREENSHOT_DROP_IN_MIN_SCALE); mBackgroundView.setAlpha(scaleInterpolator.getInterpolation(t) * BACKGROUND_ALPHA); mScreenshotView.setAlpha(t); mScreenshotView.setScaleX(scaleT); mScreenshotView.setScaleY(scaleT); mScreenshotFlash.setAlpha(flashAlphaInterpolator.getInterpolation(t)); } }); return anim; } private ValueAnimator createScreenshotDropOutAnimation(int w, int h, boolean statusBarVisible, boolean navBarVisible) { ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); anim.setStartDelay(SCREENSHOT_DROP_OUT_DELAY); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mBackgroundView.setVisibility(View.GONE); mScreenshotView.setVisibility(View.GONE); mScreenshotView.setLayerType(View.LAYER_TYPE_NONE, null); } }); if (!statusBarVisible || !navBarVisible) { // There is no status bar/nav bar, so just fade the screenshot away in place anim.setDuration(SCREENSHOT_FAST_DROP_OUT_DURATION); anim.addUpdateListener(new AnimatorUpdateListener() { @Override 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); mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA); mScreenshotView.setAlpha(1f - t); mScreenshotView.setScaleX(scaleT); mScreenshotView.setScaleY(scaleT); } }); } else { // In the case where there is a status bar, animate to the origin of the bar (top-left) final float scaleDurationPct = (float) SCREENSHOT_DROP_OUT_SCALE_DURATION / SCREENSHOT_DROP_OUT_DURATION; final Interpolator scaleInterpolator = new Interpolator() { @Override public float getInterpolation(float x) { if (x < scaleDurationPct) { // Decelerate, and scale the input accordingly return (float) (1f - Math.pow(1f - (x / scaleDurationPct), 2f)); } return 1f; } }; // Determine the bounds of how to scale float halfScreenWidth = (w - 2f * mBgPadding) / 2f; 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); // Animate the screenshot to the status bar anim.setDuration(SCREENSHOT_DROP_OUT_DURATION); anim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float t = (Float) animation.getAnimatedValue(); float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale) - 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)); mScreenshotView.setScaleX(scaleT); mScreenshotView.setScaleY(scaleT); mScreenshotView.setTranslationX(t * finalPos.x); mScreenshotView.setTranslationY(t * finalPos.y); } }); } return anim; } static void notifyScreenshotError(Context context, NotificationManager nManager, int msgResId) { Resources r = context.getResources(); String errorMsg = r.getString(msgResId); // 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( com.android.internal.R.color.system_notification_accent_color)); final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService( Context.DEVICE_POLICY_SERVICE); final Intent intent = dpm.createAdminSupportIntent( DevicePolicyManager.POLICY_DISABLE_SCREEN_CAPTURE); if (intent != null) { final PendingIntent pendingIntent = PendingIntent.getActivityAsUser( context, 0, intent, 0, null, UserHandle.CURRENT); b.setContentIntent(pendingIntent); } SystemUI.overrideNotificationAppName(context, b); Notification n = new Notification.BigTextStyle(b) .bigText(errorMsg) .build(); nManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT, n); } /** * Removes the notification for a screenshot after a share target is chosen. */ public static class TargetChosenReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // Clear the notification final NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT); } } /** * Removes the last screenshot. */ public static class DeleteScreenshotReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (!intent.hasExtra(SCREENSHOT_URI_ID)) { return; } // Clear the notification final NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID)); nm.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT); // And delete the image from the media store new DeleteImageInBackgroundTask(context).execute(uri); } } }