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