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