Home | History | Annotate | Download | only in screenshot
      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 
     80     void clearImage() {
     81         image = null;
     82         imageUri = null;
     83         iconSize = 0;
     84     }
     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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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     }
    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     }
    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 
    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