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