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