• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.screenshot;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ValueAnimator;
23 import android.animation.ValueAnimator.AnimatorUpdateListener;
24 import android.app.admin.DevicePolicyManager;
25 import android.app.Notification;
26 import android.app.Notification.BigPictureStyle;
27 import android.app.NotificationManager;
28 import android.app.PendingIntent;
29 import android.content.BroadcastReceiver;
30 import android.content.ContentResolver;
31 import android.content.ContentValues;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.res.Resources;
35 import android.graphics.Bitmap;
36 import android.graphics.Canvas;
37 import android.graphics.ColorMatrix;
38 import android.graphics.ColorMatrixColorFilter;
39 import android.graphics.Matrix;
40 import android.graphics.Paint;
41 import android.graphics.PixelFormat;
42 import android.graphics.PointF;
43 import android.graphics.Rect;
44 import android.media.MediaActionSound;
45 import android.net.Uri;
46 import android.os.AsyncTask;
47 import android.os.Bundle;
48 import android.os.Environment;
49 import android.os.Process;
50 import android.os.UserHandle;
51 import android.provider.MediaStore;
52 import android.util.DisplayMetrics;
53 import android.view.Display;
54 import android.view.LayoutInflater;
55 import android.view.MotionEvent;
56 import android.view.Surface;
57 import android.view.SurfaceControl;
58 import android.view.View;
59 import android.view.ViewGroup;
60 import android.view.WindowManager;
61 import android.view.animation.Interpolator;
62 import android.widget.ImageView;
63 
64 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
65 import com.android.systemui.R;
66 import com.android.systemui.SystemUI;
67 import com.android.systemui.util.NotificationChannels;
68 
69 import java.io.File;
70 import java.io.FileOutputStream;
71 import java.io.OutputStream;
72 import java.text.DateFormat;
73 import java.text.SimpleDateFormat;
74 import java.util.Date;
75 
76 /**
77  * POD used in the AsyncTask which saves an image in the background.
78  */
79 class SaveImageInBackgroundData {
80     Context context;
81     Bitmap image;
82     Uri imageUri;
83     Runnable finisher;
84     int iconSize;
85     int previewWidth;
86     int previewheight;
87     int errorMsgResId;
88 
clearImage()89     void clearImage() {
90         image = null;
91         imageUri = null;
92         iconSize = 0;
93     }
clearContext()94     void clearContext() {
95         context = null;
96     }
97 }
98 
99 /**
100  * An AsyncTask that saves an image to the media store in the background.
101  */
102 class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
103 
104     private static final String SCREENSHOTS_DIR_NAME = "Screenshots";
105     private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png";
106     private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
107 
108     private final SaveImageInBackgroundData mParams;
109     private final NotificationManager mNotificationManager;
110     private final Notification.Builder mNotificationBuilder, mPublicNotificationBuilder;
111     private final File mScreenshotDir;
112     private final String mImageFileName;
113     private final String mImageFilePath;
114     private final long mImageTime;
115     private final BigPictureStyle mNotificationStyle;
116     private final int mImageWidth;
117     private final int mImageHeight;
118 
119     // WORKAROUND: We want the same notification across screenshots that we update so that we don't
120     // spam a user's notification drawer.  However, we only show the ticker for the saving state
121     // and if the ticker text is the same as the previous notification, then it will not show. So
122     // for now, we just add and remove a space from the ticker text to trigger the animation when
123     // necessary.
124     private static boolean mTickerAddSpace;
125 
SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data, NotificationManager nManager)126     SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data,
127             NotificationManager nManager) {
128         Resources r = context.getResources();
129 
130         // Prepare all the output metadata
131         mParams = data;
132         mImageTime = System.currentTimeMillis();
133         String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime));
134         mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate);
135 
136         mScreenshotDir = new File(Environment.getExternalStoragePublicDirectory(
137                 Environment.DIRECTORY_PICTURES), SCREENSHOTS_DIR_NAME);
138         mImageFilePath = new File(mScreenshotDir, mImageFileName).getAbsolutePath();
139 
140         // Create the large notification icon
141         mImageWidth = data.image.getWidth();
142         mImageHeight = data.image.getHeight();
143         int iconSize = data.iconSize;
144         int previewWidth = data.previewWidth;
145         int previewHeight = data.previewheight;
146 
147         Canvas c = new Canvas();
148         Paint paint = new Paint();
149         ColorMatrix desat = new ColorMatrix();
150         desat.setSaturation(0.25f);
151         paint.setColorFilter(new ColorMatrixColorFilter(desat));
152         Matrix matrix = new Matrix();
153         int overlayColor = 0x40FFFFFF;
154 
155         Bitmap picture = Bitmap.createBitmap(previewWidth, previewHeight, data.image.getConfig());
156         matrix.setTranslate((previewWidth - mImageWidth) / 2, (previewHeight - mImageHeight) / 2);
157         c.setBitmap(picture);
158         c.drawBitmap(data.image, matrix, paint);
159         c.drawColor(overlayColor);
160         c.setBitmap(null);
161 
162         // Note, we can't use the preview for the small icon, since it is non-square
163         float scale = (float) iconSize / Math.min(mImageWidth, mImageHeight);
164         Bitmap icon = Bitmap.createBitmap(iconSize, iconSize, data.image.getConfig());
165         matrix.setScale(scale, scale);
166         matrix.postTranslate((iconSize - (scale * mImageWidth)) / 2,
167                 (iconSize - (scale * mImageHeight)) / 2);
168         c.setBitmap(icon);
169         c.drawBitmap(data.image, matrix, paint);
170         c.drawColor(overlayColor);
171         c.setBitmap(null);
172 
173         // Show the intermediate notification
174         mTickerAddSpace = !mTickerAddSpace;
175         mNotificationManager = nManager;
176         final long now = System.currentTimeMillis();
177 
178         // Setup the notification
179         mNotificationStyle = new Notification.BigPictureStyle()
180                 .bigPicture(picture.createAshmemBitmap());
181 
182         // The public notification will show similar info but with the actual screenshot omitted
183         mPublicNotificationBuilder =
184                 new Notification.Builder(context, NotificationChannels.SCREENSHOTS)
185                         .setContentTitle(r.getString(R.string.screenshot_saving_title))
186                         .setContentText(r.getString(R.string.screenshot_saving_text))
187                         .setSmallIcon(R.drawable.stat_notify_image)
188                         .setCategory(Notification.CATEGORY_PROGRESS)
189                         .setWhen(now)
190                         .setShowWhen(true)
191                         .setColor(r.getColor(
192                                 com.android.internal.R.color.system_notification_accent_color));
193         SystemUI.overrideNotificationAppName(context, mPublicNotificationBuilder);
194 
195         mNotificationBuilder = new Notification.Builder(context, NotificationChannels.SCREENSHOTS)
196             .setTicker(r.getString(R.string.screenshot_saving_ticker)
197                     + (mTickerAddSpace ? " " : ""))
198             .setContentTitle(r.getString(R.string.screenshot_saving_title))
199             .setContentText(r.getString(R.string.screenshot_saving_text))
200             .setSmallIcon(R.drawable.stat_notify_image)
201             .setWhen(now)
202             .setShowWhen(true)
203             .setColor(r.getColor(com.android.internal.R.color.system_notification_accent_color))
204             .setStyle(mNotificationStyle)
205             .setPublicVersion(mPublicNotificationBuilder.build());
206         mNotificationBuilder.setFlag(Notification.FLAG_NO_CLEAR, true);
207         SystemUI.overrideNotificationAppName(context, mNotificationBuilder);
208 
209         mNotificationManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT,
210                 mNotificationBuilder.build());
211 
212         /**
213          * NOTE: The following code prepares the notification builder for updating the notification
214          * after the screenshot has been written to disk.
215          */
216 
217         // On the tablet, the large icon makes the notification appear as if it is clickable (and
218         // on small devices, the large icon is not shown) so defer showing the large icon until
219         // we compose the final post-save notification below.
220         mNotificationBuilder.setLargeIcon(icon.createAshmemBitmap());
221         // But we still don't set it for the expanded view, allowing the smallIcon to show here.
222         mNotificationStyle.bigLargeIcon((Bitmap) null);
223     }
224 
225     @Override
doInBackground(Void... params)226     protected Void doInBackground(Void... params) {
227         if (isCancelled()) {
228             return null;
229         }
230 
231         // By default, AsyncTask sets the worker thread to have background thread priority, so bump
232         // it back up so that we save a little quicker.
233         Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
234 
235         Context context = mParams.context;
236         Bitmap image = mParams.image;
237         Resources r = context.getResources();
238 
239         try {
240             // Create screenshot directory if it doesn't exist
241             mScreenshotDir.mkdirs();
242 
243             // media provider uses seconds for DATE_MODIFIED and DATE_ADDED, but milliseconds
244             // for DATE_TAKEN
245             long dateSeconds = mImageTime / 1000;
246 
247             // Save
248             OutputStream out = new FileOutputStream(mImageFilePath);
249             image.compress(Bitmap.CompressFormat.PNG, 100, out);
250             out.flush();
251             out.close();
252 
253             // Save the screenshot to the MediaStore
254             ContentValues values = new ContentValues();
255             ContentResolver resolver = context.getContentResolver();
256             values.put(MediaStore.Images.ImageColumns.DATA, mImageFilePath);
257             values.put(MediaStore.Images.ImageColumns.TITLE, mImageFileName);
258             values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, mImageFileName);
259             values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, mImageTime);
260             values.put(MediaStore.Images.ImageColumns.DATE_ADDED, dateSeconds);
261             values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, dateSeconds);
262             values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png");
263             values.put(MediaStore.Images.ImageColumns.WIDTH, mImageWidth);
264             values.put(MediaStore.Images.ImageColumns.HEIGHT, mImageHeight);
265             values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length());
266             Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
267 
268             // Create a share intent
269             String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
270             String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
271             Intent sharingIntent = new Intent(Intent.ACTION_SEND);
272             sharingIntent.setType("image/png");
273             sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
274             sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
275 
276             // Create a share action for the notification
277             PendingIntent chooseAction = PendingIntent.getBroadcast(context, 0,
278                     new Intent(context, GlobalScreenshot.TargetChosenReceiver.class),
279                     PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
280             Intent chooserIntent = Intent.createChooser(sharingIntent, null,
281                     chooseAction.getIntentSender())
282                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
283             PendingIntent shareAction = PendingIntent.getActivity(context, 0, chooserIntent,
284                     PendingIntent.FLAG_CANCEL_CURRENT);
285             Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
286                     R.drawable.ic_screenshot_share,
287                     r.getString(com.android.internal.R.string.share), shareAction);
288             mNotificationBuilder.addAction(shareActionBuilder.build());
289 
290             // Create a delete action for the notification
291             PendingIntent deleteAction = PendingIntent.getBroadcast(context,  0,
292                     new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
293                             .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()),
294                     PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
295             Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
296                     R.drawable.ic_screenshot_delete,
297                     r.getString(com.android.internal.R.string.delete), deleteAction);
298             mNotificationBuilder.addAction(deleteActionBuilder.build());
299 
300             mParams.imageUri = uri;
301             mParams.image = null;
302             mParams.errorMsgResId = 0;
303         } catch (Exception e) {
304             // IOException/UnsupportedOperationException may be thrown if external storage is not
305             // mounted
306             mParams.clearImage();
307             mParams.errorMsgResId = R.string.screenshot_failed_to_save_text;
308         }
309 
310         // Recycle the bitmap data
311         if (image != null) {
312             image.recycle();
313         }
314 
315         return null;
316     }
317 
318     @Override
onPostExecute(Void params)319     protected void onPostExecute(Void params) {
320         if (mParams.errorMsgResId != 0) {
321             // Show a message that we've failed to save the image to disk
322             GlobalScreenshot.notifyScreenshotError(mParams.context, mNotificationManager,
323                     mParams.errorMsgResId);
324         } else {
325             // Show the final notification to indicate screenshot saved
326             Context context = mParams.context;
327             Resources r = context.getResources();
328 
329             // Create the intent to show the screenshot in gallery
330             Intent launchIntent = new Intent(Intent.ACTION_VIEW);
331             launchIntent.setDataAndType(mParams.imageUri, "image/png");
332             launchIntent.setFlags(
333                     Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
334 
335             final long now = System.currentTimeMillis();
336 
337             // Update the text and the icon for the existing notification
338             mPublicNotificationBuilder
339                     .setContentTitle(r.getString(R.string.screenshot_saved_title))
340                     .setContentText(r.getString(R.string.screenshot_saved_text))
341                     .setContentIntent(PendingIntent.getActivity(mParams.context, 0, launchIntent, 0))
342                     .setWhen(now)
343                     .setAutoCancel(true)
344                     .setColor(context.getColor(
345                             com.android.internal.R.color.system_notification_accent_color));
346             mNotificationBuilder
347                 .setContentTitle(r.getString(R.string.screenshot_saved_title))
348                 .setContentText(r.getString(R.string.screenshot_saved_text))
349                 .setContentIntent(PendingIntent.getActivity(mParams.context, 0, launchIntent, 0))
350                 .setWhen(now)
351                 .setAutoCancel(true)
352                 .setColor(context.getColor(
353                         com.android.internal.R.color.system_notification_accent_color))
354                 .setPublicVersion(mPublicNotificationBuilder.build())
355                 .setFlag(Notification.FLAG_NO_CLEAR, false);
356 
357             mNotificationManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT,
358                     mNotificationBuilder.build());
359         }
360         mParams.finisher.run();
361         mParams.clearContext();
362     }
363 
364     @Override
onCancelled(Void params)365     protected void onCancelled(Void params) {
366         // If we are cancelled while the task is running in the background, we may get null params.
367         // The finisher is expected to always be called back, so just use the baked-in params from
368         // the ctor in any case.
369         mParams.finisher.run();
370         mParams.clearImage();
371         mParams.clearContext();
372 
373         // Cancel the posted notification
374         mNotificationManager.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT);
375     }
376 }
377 
378 /**
379  * An AsyncTask that deletes an image from the media store in the background.
380  */
381 class DeleteImageInBackgroundTask extends AsyncTask<Uri, Void, Void> {
382     private static final String TAG = "DeleteImageInBackgroundTask";
383 
384     private Context mContext;
385 
DeleteImageInBackgroundTask(Context context)386     DeleteImageInBackgroundTask(Context context) {
387         mContext = context;
388     }
389 
390     @Override
doInBackground(Uri... params)391     protected Void doInBackground(Uri... params) {
392         if (params.length != 1) return null;
393 
394         Uri screenshotUri = params[0];
395         ContentResolver resolver = mContext.getContentResolver();
396         resolver.delete(screenshotUri, null, null);
397         return null;
398     }
399 }
400 
401 class GlobalScreenshot {
402     static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id";
403 
404     private static final int SCREENSHOT_FLASH_TO_PEAK_DURATION = 130;
405     private static final int SCREENSHOT_DROP_IN_DURATION = 430;
406     private static final int SCREENSHOT_DROP_OUT_DELAY = 500;
407     private static final int SCREENSHOT_DROP_OUT_DURATION = 430;
408     private static final int SCREENSHOT_DROP_OUT_SCALE_DURATION = 370;
409     private static final int SCREENSHOT_FAST_DROP_OUT_DURATION = 320;
410     private static final float BACKGROUND_ALPHA = 0.5f;
411     private static final float SCREENSHOT_SCALE = 1f;
412     private static final float SCREENSHOT_DROP_IN_MIN_SCALE = SCREENSHOT_SCALE * 0.725f;
413     private static final float SCREENSHOT_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.45f;
414     private static final float SCREENSHOT_FAST_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.6f;
415     private static final float SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET = 0f;
416     private final int mPreviewWidth;
417     private final int mPreviewHeight;
418 
419     private Context mContext;
420     private WindowManager mWindowManager;
421     private WindowManager.LayoutParams mWindowLayoutParams;
422     private NotificationManager mNotificationManager;
423     private Display mDisplay;
424     private DisplayMetrics mDisplayMetrics;
425     private Matrix mDisplayMatrix;
426 
427     private Bitmap mScreenBitmap;
428     private View mScreenshotLayout;
429     private ScreenshotSelectorView mScreenshotSelectorView;
430     private ImageView mBackgroundView;
431     private ImageView mScreenshotView;
432     private ImageView mScreenshotFlash;
433 
434     private AnimatorSet mScreenshotAnimation;
435 
436     private int mNotificationIconSize;
437     private float mBgPadding;
438     private float mBgPaddingScale;
439 
440     private AsyncTask<Void, Void, Void> mSaveInBgTask;
441 
442     private MediaActionSound mCameraSound;
443 
444 
445     /**
446      * @param context everything needs a context :(
447      */
GlobalScreenshot(Context context)448     public GlobalScreenshot(Context context) {
449         Resources r = context.getResources();
450         mContext = context;
451         LayoutInflater layoutInflater = (LayoutInflater)
452                 context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
453 
454         // Inflate the screenshot layout
455         mDisplayMatrix = new Matrix();
456         mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null);
457         mBackgroundView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_background);
458         mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot);
459         mScreenshotFlash = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_flash);
460         mScreenshotSelectorView = (ScreenshotSelectorView) mScreenshotLayout.findViewById(
461                 R.id.global_screenshot_selector);
462         mScreenshotLayout.setFocusable(true);
463         mScreenshotSelectorView.setFocusable(true);
464         mScreenshotSelectorView.setFocusableInTouchMode(true);
465         mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() {
466             @Override
467             public boolean onTouch(View v, MotionEvent event) {
468                 // Intercept and ignore all touch events
469                 return true;
470             }
471         });
472 
473         // Setup the window that we are going to use
474         mWindowLayoutParams = new WindowManager.LayoutParams(
475                 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0,
476                 WindowManager.LayoutParams.TYPE_SCREENSHOT,
477                 WindowManager.LayoutParams.FLAG_FULLSCREEN
478                     | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
479                     | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
480                     | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED,
481                 PixelFormat.TRANSLUCENT);
482         mWindowLayoutParams.setTitle("ScreenshotAnimation");
483         mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
484         mNotificationManager =
485             (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
486         mDisplay = mWindowManager.getDefaultDisplay();
487         mDisplayMetrics = new DisplayMetrics();
488         mDisplay.getRealMetrics(mDisplayMetrics);
489 
490         // Get the various target sizes
491         mNotificationIconSize =
492             r.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
493 
494         // Scale has to account for both sides of the bg
495         mBgPadding = (float) r.getDimensionPixelSize(R.dimen.global_screenshot_bg_padding);
496         mBgPaddingScale = mBgPadding /  mDisplayMetrics.widthPixels;
497 
498         // determine the optimal preview size
499         int panelWidth = 0;
500         try {
501             panelWidth = r.getDimensionPixelSize(R.dimen.notification_panel_width);
502         } catch (Resources.NotFoundException e) {
503         }
504         if (panelWidth <= 0) {
505             // includes notification_panel_width==match_parent (-1)
506             panelWidth = mDisplayMetrics.widthPixels;
507         }
508         mPreviewWidth = panelWidth;
509         mPreviewHeight = r.getDimensionPixelSize(R.dimen.notification_max_height);
510 
511         // Setup the Camera shutter sound
512         mCameraSound = new MediaActionSound();
513         mCameraSound.load(MediaActionSound.SHUTTER_CLICK);
514     }
515 
516     /**
517      * Creates a new worker thread and saves the screenshot to the media store.
518      */
saveScreenshotInWorkerThread(Runnable finisher)519     private void saveScreenshotInWorkerThread(Runnable finisher) {
520         SaveImageInBackgroundData data = new SaveImageInBackgroundData();
521         data.context = mContext;
522         data.image = mScreenBitmap;
523         data.iconSize = mNotificationIconSize;
524         data.finisher = finisher;
525         data.previewWidth = mPreviewWidth;
526         data.previewheight = mPreviewHeight;
527         if (mSaveInBgTask != null) {
528             mSaveInBgTask.cancel(false);
529         }
530         mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager)
531                 .execute();
532     }
533 
534     /**
535      * @return the current display rotation in degrees
536      */
getDegreesForRotation(int value)537     private float getDegreesForRotation(int value) {
538         switch (value) {
539         case Surface.ROTATION_90:
540             return 360f - 90f;
541         case Surface.ROTATION_180:
542             return 360f - 180f;
543         case Surface.ROTATION_270:
544             return 360f - 270f;
545         }
546         return 0f;
547     }
548 
549     /**
550      * Takes a screenshot of the current display and shows an animation.
551      */
takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible, int x, int y, int width, int height)552     void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible,
553             int x, int y, int width, int height) {
554         // We need to orient the screenshot correctly (and the Surface api seems to take screenshots
555         // only in the natural orientation of the device :!)
556         mDisplay.getRealMetrics(mDisplayMetrics);
557         float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels};
558         float degrees = getDegreesForRotation(mDisplay.getRotation());
559         boolean requiresRotation = (degrees > 0);
560         if (requiresRotation) {
561             // Get the dimensions of the device in its native orientation
562             mDisplayMatrix.reset();
563             mDisplayMatrix.preRotate(-degrees);
564             mDisplayMatrix.mapPoints(dims);
565             dims[0] = Math.abs(dims[0]);
566             dims[1] = Math.abs(dims[1]);
567         }
568 
569         // Take the screenshot
570         mScreenBitmap = SurfaceControl.screenshot((int) dims[0], (int) dims[1]);
571         if (mScreenBitmap == null) {
572             notifyScreenshotError(mContext, mNotificationManager,
573                     R.string.screenshot_failed_to_capture_text);
574             finisher.run();
575             return;
576         }
577 
578         if (requiresRotation) {
579             // Rotate the screenshot to the current orientation
580             Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
581                     mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);
582             Canvas c = new Canvas(ss);
583             c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
584             c.rotate(degrees);
585             c.translate(-dims[0] / 2, -dims[1] / 2);
586             c.drawBitmap(mScreenBitmap, 0, 0, null);
587             c.setBitmap(null);
588             // Recycle the previous bitmap
589             mScreenBitmap.recycle();
590             mScreenBitmap = ss;
591         }
592 
593         if (width != mDisplayMetrics.widthPixels || height != mDisplayMetrics.heightPixels) {
594             // Crop the screenshot to selected region
595             Bitmap cropped = Bitmap.createBitmap(mScreenBitmap, x, y, width, height);
596             mScreenBitmap.recycle();
597             mScreenBitmap = cropped;
598         }
599 
600         // Optimizations
601         mScreenBitmap.setHasAlpha(false);
602         mScreenBitmap.prepareToDraw();
603 
604         // Start the post-screenshot animation
605         startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,
606                 statusBarVisible, navBarVisible);
607     }
608 
takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible)609     void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) {
610         mDisplay.getRealMetrics(mDisplayMetrics);
611         takeScreenshot(finisher, statusBarVisible, navBarVisible, 0, 0, mDisplayMetrics.widthPixels,
612                 mDisplayMetrics.heightPixels);
613     }
614 
615     /**
616      * Displays a screenshot selector
617      */
takeScreenshotPartial(final Runnable finisher, final boolean statusBarVisible, final boolean navBarVisible)618     void takeScreenshotPartial(final Runnable finisher, final boolean statusBarVisible,
619             final boolean navBarVisible) {
620         mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
621         mScreenshotSelectorView.setOnTouchListener(new View.OnTouchListener() {
622             @Override
623             public boolean onTouch(View v, MotionEvent event) {
624                 ScreenshotSelectorView view = (ScreenshotSelectorView) v;
625                 switch (event.getAction()) {
626                     case MotionEvent.ACTION_DOWN:
627                         view.startSelection((int) event.getX(), (int) event.getY());
628                         return true;
629                     case MotionEvent.ACTION_MOVE:
630                         view.updateSelection((int) event.getX(), (int) event.getY());
631                         return true;
632                     case MotionEvent.ACTION_UP:
633                         view.setVisibility(View.GONE);
634                         mWindowManager.removeView(mScreenshotLayout);
635                         final Rect rect = view.getSelectionRect();
636                         if (rect != null) {
637                             if (rect.width() != 0 && rect.height() != 0) {
638                                 // Need mScreenshotLayout to handle it after the view disappears
639                                 mScreenshotLayout.post(new Runnable() {
640                                     public void run() {
641                                         takeScreenshot(finisher, statusBarVisible, navBarVisible,
642                                                 rect.left, rect.top, rect.width(), rect.height());
643                                     }
644                                 });
645                             }
646                         }
647 
648                         view.stopSelection();
649                         return true;
650                 }
651 
652                 return false;
653             }
654         });
655         mScreenshotLayout.post(new Runnable() {
656             @Override
657             public void run() {
658                 mScreenshotSelectorView.setVisibility(View.VISIBLE);
659                 mScreenshotSelectorView.requestFocus();
660             }
661         });
662     }
663 
664     /**
665      * Cancels screenshot request
666      */
stopScreenshot()667     void stopScreenshot() {
668         // If the selector layer still presents on screen, we remove it and resets its state.
669         if (mScreenshotSelectorView.getSelectionRect() != null) {
670             mWindowManager.removeView(mScreenshotLayout);
671             mScreenshotSelectorView.stopSelection();
672         }
673     }
674 
675     /**
676      * Starts the animation after taking the screenshot
677      */
startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible, boolean navBarVisible)678     private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible,
679             boolean navBarVisible) {
680         // Add the view for the animation
681         mScreenshotView.setImageBitmap(mScreenBitmap);
682         mScreenshotLayout.requestFocus();
683 
684         // Setup the animation with the screenshot just taken
685         if (mScreenshotAnimation != null) {
686             if (mScreenshotAnimation.isStarted()) {
687                 mScreenshotAnimation.end();
688             }
689             mScreenshotAnimation.removeAllListeners();
690         }
691 
692         mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
693         ValueAnimator screenshotDropInAnim = createScreenshotDropInAnimation();
694         ValueAnimator screenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h,
695                 statusBarVisible, navBarVisible);
696         mScreenshotAnimation = new AnimatorSet();
697         mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim);
698         mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
699             @Override
700             public void onAnimationEnd(Animator animation) {
701                 // Save the screenshot once we have a bit of time now
702                 saveScreenshotInWorkerThread(finisher);
703                 mWindowManager.removeView(mScreenshotLayout);
704 
705                 // Clear any references to the bitmap
706                 mScreenBitmap = null;
707                 mScreenshotView.setImageBitmap(null);
708             }
709         });
710         mScreenshotLayout.post(new Runnable() {
711             @Override
712             public void run() {
713                 // Play the shutter sound to notify that we've taken a screenshot
714                 mCameraSound.play(MediaActionSound.SHUTTER_CLICK);
715 
716                 mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
717                 mScreenshotView.buildLayer();
718                 mScreenshotAnimation.start();
719             }
720         });
721     }
createScreenshotDropInAnimation()722     private ValueAnimator createScreenshotDropInAnimation() {
723         final float flashPeakDurationPct = ((float) (SCREENSHOT_FLASH_TO_PEAK_DURATION)
724                 / SCREENSHOT_DROP_IN_DURATION);
725         final float flashDurationPct = 2f * flashPeakDurationPct;
726         final Interpolator flashAlphaInterpolator = new Interpolator() {
727             @Override
728             public float getInterpolation(float x) {
729                 // Flash the flash view in and out quickly
730                 if (x <= flashDurationPct) {
731                     return (float) Math.sin(Math.PI * (x / flashDurationPct));
732                 }
733                 return 0;
734             }
735         };
736         final Interpolator scaleInterpolator = new Interpolator() {
737             @Override
738             public float getInterpolation(float x) {
739                 // We start scaling when the flash is at it's peak
740                 if (x < flashPeakDurationPct) {
741                     return 0;
742                 }
743                 return (x - flashDurationPct) / (1f - flashDurationPct);
744             }
745         };
746         ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
747         anim.setDuration(SCREENSHOT_DROP_IN_DURATION);
748         anim.addListener(new AnimatorListenerAdapter() {
749             @Override
750             public void onAnimationStart(Animator animation) {
751                 mBackgroundView.setAlpha(0f);
752                 mBackgroundView.setVisibility(View.VISIBLE);
753                 mScreenshotView.setAlpha(0f);
754                 mScreenshotView.setTranslationX(0f);
755                 mScreenshotView.setTranslationY(0f);
756                 mScreenshotView.setScaleX(SCREENSHOT_SCALE + mBgPaddingScale);
757                 mScreenshotView.setScaleY(SCREENSHOT_SCALE + mBgPaddingScale);
758                 mScreenshotView.setVisibility(View.VISIBLE);
759                 mScreenshotFlash.setAlpha(0f);
760                 mScreenshotFlash.setVisibility(View.VISIBLE);
761             }
762             @Override
763             public void onAnimationEnd(android.animation.Animator animation) {
764                 mScreenshotFlash.setVisibility(View.GONE);
765             }
766         });
767         anim.addUpdateListener(new AnimatorUpdateListener() {
768             @Override
769             public void onAnimationUpdate(ValueAnimator animation) {
770                 float t = (Float) animation.getAnimatedValue();
771                 float scaleT = (SCREENSHOT_SCALE + mBgPaddingScale)
772                     - scaleInterpolator.getInterpolation(t)
773                         * (SCREENSHOT_SCALE - SCREENSHOT_DROP_IN_MIN_SCALE);
774                 mBackgroundView.setAlpha(scaleInterpolator.getInterpolation(t) * BACKGROUND_ALPHA);
775                 mScreenshotView.setAlpha(t);
776                 mScreenshotView.setScaleX(scaleT);
777                 mScreenshotView.setScaleY(scaleT);
778                 mScreenshotFlash.setAlpha(flashAlphaInterpolator.getInterpolation(t));
779             }
780         });
781         return anim;
782     }
createScreenshotDropOutAnimation(int w, int h, boolean statusBarVisible, boolean navBarVisible)783     private ValueAnimator createScreenshotDropOutAnimation(int w, int h, boolean statusBarVisible,
784             boolean navBarVisible) {
785         ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
786         anim.setStartDelay(SCREENSHOT_DROP_OUT_DELAY);
787         anim.addListener(new AnimatorListenerAdapter() {
788             @Override
789             public void onAnimationEnd(Animator animation) {
790                 mBackgroundView.setVisibility(View.GONE);
791                 mScreenshotView.setVisibility(View.GONE);
792                 mScreenshotView.setLayerType(View.LAYER_TYPE_NONE, null);
793             }
794         });
795 
796         if (!statusBarVisible || !navBarVisible) {
797             // There is no status bar/nav bar, so just fade the screenshot away in place
798             anim.setDuration(SCREENSHOT_FAST_DROP_OUT_DURATION);
799             anim.addUpdateListener(new AnimatorUpdateListener() {
800                 @Override
801                 public void onAnimationUpdate(ValueAnimator animation) {
802                     float t = (Float) animation.getAnimatedValue();
803                     float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale)
804                             - t * (SCREENSHOT_DROP_IN_MIN_SCALE - SCREENSHOT_FAST_DROP_OUT_MIN_SCALE);
805                     mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA);
806                     mScreenshotView.setAlpha(1f - t);
807                     mScreenshotView.setScaleX(scaleT);
808                     mScreenshotView.setScaleY(scaleT);
809                 }
810             });
811         } else {
812             // In the case where there is a status bar, animate to the origin of the bar (top-left)
813             final float scaleDurationPct = (float) SCREENSHOT_DROP_OUT_SCALE_DURATION
814                     / SCREENSHOT_DROP_OUT_DURATION;
815             final Interpolator scaleInterpolator = new Interpolator() {
816                 @Override
817                 public float getInterpolation(float x) {
818                     if (x < scaleDurationPct) {
819                         // Decelerate, and scale the input accordingly
820                         return (float) (1f - Math.pow(1f - (x / scaleDurationPct), 2f));
821                     }
822                     return 1f;
823                 }
824             };
825 
826             // Determine the bounds of how to scale
827             float halfScreenWidth = (w - 2f * mBgPadding) / 2f;
828             float halfScreenHeight = (h - 2f * mBgPadding) / 2f;
829             final float offsetPct = SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET;
830             final PointF finalPos = new PointF(
831                 -halfScreenWidth + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenWidth,
832                 -halfScreenHeight + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenHeight);
833 
834             // Animate the screenshot to the status bar
835             anim.setDuration(SCREENSHOT_DROP_OUT_DURATION);
836             anim.addUpdateListener(new AnimatorUpdateListener() {
837                 @Override
838                 public void onAnimationUpdate(ValueAnimator animation) {
839                     float t = (Float) animation.getAnimatedValue();
840                     float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale)
841                         - scaleInterpolator.getInterpolation(t)
842                             * (SCREENSHOT_DROP_IN_MIN_SCALE - SCREENSHOT_DROP_OUT_MIN_SCALE);
843                     mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA);
844                     mScreenshotView.setAlpha(1f - scaleInterpolator.getInterpolation(t));
845                     mScreenshotView.setScaleX(scaleT);
846                     mScreenshotView.setScaleY(scaleT);
847                     mScreenshotView.setTranslationX(t * finalPos.x);
848                     mScreenshotView.setTranslationY(t * finalPos.y);
849                 }
850             });
851         }
852         return anim;
853     }
854 
notifyScreenshotError(Context context, NotificationManager nManager, int msgResId)855     static void notifyScreenshotError(Context context, NotificationManager nManager, int msgResId) {
856         Resources r = context.getResources();
857         String errorMsg = r.getString(msgResId);
858 
859         // Repurpose the existing notification to notify the user of the error
860         Notification.Builder b = new Notification.Builder(context, NotificationChannels.ALERTS)
861             .setTicker(r.getString(R.string.screenshot_failed_title))
862             .setContentTitle(r.getString(R.string.screenshot_failed_title))
863             .setContentText(errorMsg)
864             .setSmallIcon(R.drawable.stat_notify_image_error)
865             .setWhen(System.currentTimeMillis())
866             .setVisibility(Notification.VISIBILITY_PUBLIC) // ok to show outside lockscreen
867             .setCategory(Notification.CATEGORY_ERROR)
868             .setAutoCancel(true)
869             .setColor(context.getColor(
870                         com.android.internal.R.color.system_notification_accent_color));
871         final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
872                 Context.DEVICE_POLICY_SERVICE);
873         final Intent intent = dpm.createAdminSupportIntent(
874                 DevicePolicyManager.POLICY_DISABLE_SCREEN_CAPTURE);
875         if (intent != null) {
876             final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
877                     context, 0, intent, 0, null, UserHandle.CURRENT);
878             b.setContentIntent(pendingIntent);
879         }
880 
881         SystemUI.overrideNotificationAppName(context, b);
882 
883         Notification n = new Notification.BigTextStyle(b)
884                 .bigText(errorMsg)
885                 .build();
886         nManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT, n);
887     }
888 
889     /**
890      * Removes the notification for a screenshot after a share target is chosen.
891      */
892     public static class TargetChosenReceiver extends BroadcastReceiver {
893         @Override
onReceive(Context context, Intent intent)894         public void onReceive(Context context, Intent intent) {
895             // Clear the notification
896             final NotificationManager nm =
897                     (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
898             nm.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT);
899         }
900     }
901 
902     /**
903      * Removes the last screenshot.
904      */
905     public static class DeleteScreenshotReceiver extends BroadcastReceiver {
906         @Override
onReceive(Context context, Intent intent)907         public void onReceive(Context context, Intent intent) {
908             if (!intent.hasExtra(SCREENSHOT_URI_ID)) {
909                 return;
910             }
911 
912             // Clear the notification
913             final NotificationManager nm =
914                     (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
915             final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID));
916             nm.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT);
917 
918             // And delete the image from the media store
919             new DeleteImageInBackgroundTask(context).execute(uri);
920         }
921     }
922 }
923