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