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