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