GlobalScreenshot.java revision 103bc3342efe2a8a9a3a26f2e115bd1b58e0ed77
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_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
85
86    private final int mNotificationId;
87    private final NotificationManager mNotificationManager;
88    private final Notification.Builder mNotificationBuilder;
89    private final File mScreenshotDir;
90    private final String mImageFileName;
91    private final String mImageFilePath;
92    private final long mImageTime;
93    private final BigPictureStyle mNotificationStyle;
94    private final int mImageWidth;
95    private final 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        mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate);
112
113        mScreenshotDir = new File(Environment.getExternalStoragePublicDirectory(
114                Environment.DIRECTORY_PICTURES), SCREENSHOTS_DIR_NAME);
115        mImageFilePath = new File(mScreenshotDir, mImageFileName).getAbsolutePath();
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            // Create screenshot directory if it doesn't exist
179            mScreenshotDir.mkdirs();
180
181            // Save the screenshot to the MediaStore
182            ContentValues values = new ContentValues();
183            ContentResolver resolver = context.getContentResolver();
184            values.put(MediaStore.Images.ImageColumns.DATA, mImageFilePath);
185            values.put(MediaStore.Images.ImageColumns.TITLE, mImageFileName);
186            values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, mImageFileName);
187            values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, mImageTime);
188            values.put(MediaStore.Images.ImageColumns.DATE_ADDED, mImageTime);
189            values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, mImageTime);
190            values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png");
191            values.put(MediaStore.Images.ImageColumns.WIDTH, mImageWidth);
192            values.put(MediaStore.Images.ImageColumns.HEIGHT, mImageHeight);
193            Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
194
195            String subjectDate = new SimpleDateFormat("hh:mma, MMM dd, yyyy")
196                .format(new Date(mImageTime));
197            String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
198            Intent sharingIntent = new Intent(Intent.ACTION_SEND);
199            sharingIntent.setType("image/png");
200            sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
201            sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
202
203            Intent chooserIntent = Intent.createChooser(sharingIntent, null);
204            chooserIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK
205                    | Intent.FLAG_ACTIVITY_NEW_TASK);
206
207            mNotificationBuilder.addAction(R.drawable.ic_menu_share,
208                     r.getString(com.android.internal.R.string.share),
209                     PendingIntent.getActivity(context, 0, chooserIntent,
210                             PendingIntent.FLAG_CANCEL_CURRENT));
211
212            OutputStream out = resolver.openOutputStream(uri);
213            image.compress(Bitmap.CompressFormat.PNG, 100, out);
214            out.flush();
215            out.close();
216
217            // update file size in the database
218            values.clear();
219            values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length());
220            resolver.update(uri, values, null, null);
221
222            params[0].imageUri = uri;
223            params[0].image = null;
224            params[0].result = 0;
225        } catch (Exception e) {
226            // IOException/UnsupportedOperationException may be thrown if external storage is not
227            // mounted
228            params[0].imageUri = null;
229            params[0].image = null;
230            params[0].result = 1;
231        }
232
233        // Recycle the bitmap data
234        if (image != null) {
235            image.recycle();
236        }
237
238        return params[0];
239    }
240
241    @Override
242    protected void onPostExecute(SaveImageInBackgroundData params) {
243        if (params.result > 0) {
244            // Show a message that we've failed to save the image to disk
245            GlobalScreenshot.notifyScreenshotError(params.context, mNotificationManager);
246        } else {
247            // Show the final notification to indicate screenshot saved
248            Resources r = params.context.getResources();
249
250            // Create the intent to show the screenshot in gallery
251            Intent launchIntent = new Intent(Intent.ACTION_VIEW);
252            launchIntent.setDataAndType(params.imageUri, "image/png");
253            launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
254
255            mNotificationBuilder
256                .setContentTitle(r.getString(R.string.screenshot_saved_title))
257                .setContentText(r.getString(R.string.screenshot_saved_text))
258                .setContentIntent(PendingIntent.getActivity(params.context, 0, launchIntent, 0))
259                .setWhen(System.currentTimeMillis())
260                .setAutoCancel(true);
261
262            Notification n = mNotificationBuilder.build();
263            n.flags &= ~Notification.FLAG_NO_CLEAR;
264            mNotificationManager.notify(mNotificationId, n);
265        }
266        params.finisher.run();
267    }
268}
269
270/**
271 * TODO:
272 *   - Performance when over gl surfaces? Ie. Gallery
273 *   - what do we say in the Toast? Which icon do we get if the user uses another
274 *     type of gallery?
275 */
276class GlobalScreenshot {
277    private static final int SCREENSHOT_NOTIFICATION_ID = 789;
278    private static final int SCREENSHOT_FLASH_TO_PEAK_DURATION = 130;
279    private static final int SCREENSHOT_DROP_IN_DURATION = 430;
280    private static final int SCREENSHOT_DROP_OUT_DELAY = 500;
281    private static final int SCREENSHOT_DROP_OUT_DURATION = 430;
282    private static final int SCREENSHOT_DROP_OUT_SCALE_DURATION = 370;
283    private static final int SCREENSHOT_FAST_DROP_OUT_DURATION = 320;
284    private static final float BACKGROUND_ALPHA = 0.5f;
285    private static final float SCREENSHOT_SCALE = 1f;
286    private static final float SCREENSHOT_DROP_IN_MIN_SCALE = SCREENSHOT_SCALE * 0.725f;
287    private static final float SCREENSHOT_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.45f;
288    private static final float SCREENSHOT_FAST_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.6f;
289    private static final float SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET = 0f;
290
291    private Context mContext;
292    private WindowManager mWindowManager;
293    private WindowManager.LayoutParams mWindowLayoutParams;
294    private NotificationManager mNotificationManager;
295    private Display mDisplay;
296    private DisplayMetrics mDisplayMetrics;
297    private Matrix mDisplayMatrix;
298
299    private Bitmap mScreenBitmap;
300    private View mScreenshotLayout;
301    private ImageView mBackgroundView;
302    private ImageView mScreenshotView;
303    private ImageView mScreenshotFlash;
304
305    private AnimatorSet mScreenshotAnimation;
306
307    private int mNotificationIconSize;
308    private float mBgPadding;
309    private float mBgPaddingScale;
310
311    private MediaActionSound mCameraSound;
312
313
314    /**
315     * @param context everything needs a context :(
316     */
317    public GlobalScreenshot(Context context) {
318        Resources r = context.getResources();
319        mContext = context;
320        LayoutInflater layoutInflater = (LayoutInflater)
321                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
322
323        // Inflate the screenshot layout
324        mDisplayMatrix = new Matrix();
325        mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null);
326        mBackgroundView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_background);
327        mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot);
328        mScreenshotFlash = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_flash);
329        mScreenshotLayout.setFocusable(true);
330        mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() {
331            @Override
332            public boolean onTouch(View v, MotionEvent event) {
333                // Intercept and ignore all touch events
334                return true;
335            }
336        });
337
338        // Setup the window that we are going to use
339        mWindowLayoutParams = new WindowManager.LayoutParams(
340                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0,
341                WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY,
342                WindowManager.LayoutParams.FLAG_FULLSCREEN
343                    | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
344                    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
345                    | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED,
346                PixelFormat.TRANSLUCENT);
347        mWindowLayoutParams.setTitle("ScreenshotAnimation");
348        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
349        mNotificationManager =
350            (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
351        mDisplay = mWindowManager.getDefaultDisplay();
352        mDisplayMetrics = new DisplayMetrics();
353        mDisplay.getRealMetrics(mDisplayMetrics);
354
355        // Get the various target sizes
356        mNotificationIconSize =
357            r.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
358
359        // Scale has to account for both sides of the bg
360        mBgPadding = (float) r.getDimensionPixelSize(R.dimen.global_screenshot_bg_padding);
361        mBgPaddingScale = mBgPadding /  mDisplayMetrics.widthPixels;
362
363        // Setup the Camera shutter sound
364        mCameraSound = new MediaActionSound();
365        mCameraSound.load(MediaActionSound.SHUTTER_CLICK);
366    }
367
368    /**
369     * Creates a new worker thread and saves the screenshot to the media store.
370     */
371    private void saveScreenshotInWorkerThread(Runnable finisher) {
372        SaveImageInBackgroundData data = new SaveImageInBackgroundData();
373        data.context = mContext;
374        data.image = mScreenBitmap;
375        data.iconSize = mNotificationIconSize;
376        data.finisher = finisher;
377        new SaveImageInBackgroundTask(mContext, data, mNotificationManager,
378                SCREENSHOT_NOTIFICATION_ID).execute(data);
379    }
380
381    /**
382     * @return the current display rotation in degrees
383     */
384    private float getDegreesForRotation(int value) {
385        switch (value) {
386        case Surface.ROTATION_90:
387            return 360f - 90f;
388        case Surface.ROTATION_180:
389            return 360f - 180f;
390        case Surface.ROTATION_270:
391            return 360f - 270f;
392        }
393        return 0f;
394    }
395
396    /**
397     * Takes a screenshot of the current display and shows an animation.
398     */
399    void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) {
400        // We need to orient the screenshot correctly (and the Surface api seems to take screenshots
401        // only in the natural orientation of the device :!)
402        mDisplay.getRealMetrics(mDisplayMetrics);
403        float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels};
404        float degrees = getDegreesForRotation(mDisplay.getRotation());
405        boolean requiresRotation = (degrees > 0);
406        if (requiresRotation) {
407            // Get the dimensions of the device in its native orientation
408            mDisplayMatrix.reset();
409            mDisplayMatrix.preRotate(-degrees);
410            mDisplayMatrix.mapPoints(dims);
411            dims[0] = Math.abs(dims[0]);
412            dims[1] = Math.abs(dims[1]);
413        }
414
415        // Take the screenshot
416        mScreenBitmap = Surface.screenshot((int) dims[0], (int) dims[1]);
417        if (mScreenBitmap == null) {
418            notifyScreenshotError(mContext, mNotificationManager);
419            finisher.run();
420            return;
421        }
422
423        if (requiresRotation) {
424            // Rotate the screenshot to the current orientation
425            Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
426                    mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);
427            Canvas c = new Canvas(ss);
428            c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
429            c.rotate(degrees);
430            c.translate(-dims[0] / 2, -dims[1] / 2);
431            c.drawBitmap(mScreenBitmap, 0, 0, null);
432            c.setBitmap(null);
433            mScreenBitmap = ss;
434        }
435
436        // Optimizations
437        mScreenBitmap.setHasAlpha(false);
438        mScreenBitmap.prepareToDraw();
439
440        // Start the post-screenshot animation
441        startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,
442                statusBarVisible, navBarVisible);
443    }
444
445
446    /**
447     * Starts the animation after taking the screenshot
448     */
449    private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible,
450            boolean navBarVisible) {
451        // Add the view for the animation
452        mScreenshotView.setImageBitmap(mScreenBitmap);
453        mScreenshotLayout.requestFocus();
454
455        // Setup the animation with the screenshot just taken
456        if (mScreenshotAnimation != null) {
457            mScreenshotAnimation.end();
458        }
459
460        mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
461        ValueAnimator screenshotDropInAnim = createScreenshotDropInAnimation();
462        ValueAnimator screenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h,
463                statusBarVisible, navBarVisible);
464        mScreenshotAnimation = new AnimatorSet();
465        mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim);
466        mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
467            @Override
468            public void onAnimationEnd(Animator animation) {
469                // Save the screenshot once we have a bit of time now
470                saveScreenshotInWorkerThread(finisher);
471                mWindowManager.removeView(mScreenshotLayout);
472
473                // Clear any references to the bitmap
474                mScreenBitmap = null;
475                mScreenshotView.setImageBitmap(null);
476            }
477        });
478        mScreenshotLayout.post(new Runnable() {
479            @Override
480            public void run() {
481                // Play the shutter sound to notify that we've taken a screenshot
482                mCameraSound.play(MediaActionSound.SHUTTER_CLICK);
483
484                mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
485                mScreenshotView.buildLayer();
486                mScreenshotAnimation.start();
487            }
488        });
489    }
490    private ValueAnimator createScreenshotDropInAnimation() {
491        final float flashPeakDurationPct = ((float) (SCREENSHOT_FLASH_TO_PEAK_DURATION)
492                / SCREENSHOT_DROP_IN_DURATION);
493        final float flashDurationPct = 2f * flashPeakDurationPct;
494        final Interpolator flashAlphaInterpolator = new Interpolator() {
495            @Override
496            public float getInterpolation(float x) {
497                // Flash the flash view in and out quickly
498                if (x <= flashDurationPct) {
499                    return (float) Math.sin(Math.PI * (x / flashDurationPct));
500                }
501                return 0;
502            }
503        };
504        final Interpolator scaleInterpolator = new Interpolator() {
505            @Override
506            public float getInterpolation(float x) {
507                // We start scaling when the flash is at it's peak
508                if (x < flashPeakDurationPct) {
509                    return 0;
510                }
511                return (x - flashDurationPct) / (1f - flashDurationPct);
512            }
513        };
514        ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
515        anim.setDuration(SCREENSHOT_DROP_IN_DURATION);
516        anim.addListener(new AnimatorListenerAdapter() {
517            @Override
518            public void onAnimationStart(Animator animation) {
519                mBackgroundView.setAlpha(0f);
520                mBackgroundView.setVisibility(View.VISIBLE);
521                mScreenshotView.setAlpha(0f);
522                mScreenshotView.setTranslationX(0f);
523                mScreenshotView.setTranslationY(0f);
524                mScreenshotView.setScaleX(SCREENSHOT_SCALE + mBgPaddingScale);
525                mScreenshotView.setScaleY(SCREENSHOT_SCALE + mBgPaddingScale);
526                mScreenshotView.setVisibility(View.VISIBLE);
527                mScreenshotFlash.setAlpha(0f);
528                mScreenshotFlash.setVisibility(View.VISIBLE);
529            }
530            @Override
531            public void onAnimationEnd(android.animation.Animator animation) {
532                mScreenshotFlash.setVisibility(View.GONE);
533            }
534        });
535        anim.addUpdateListener(new AnimatorUpdateListener() {
536            @Override
537            public void onAnimationUpdate(ValueAnimator animation) {
538                float t = (Float) animation.getAnimatedValue();
539                float scaleT = (SCREENSHOT_SCALE + mBgPaddingScale)
540                    - scaleInterpolator.getInterpolation(t)
541                        * (SCREENSHOT_SCALE - SCREENSHOT_DROP_IN_MIN_SCALE);
542                mBackgroundView.setAlpha(scaleInterpolator.getInterpolation(t) * BACKGROUND_ALPHA);
543                mScreenshotView.setAlpha(t);
544                mScreenshotView.setScaleX(scaleT);
545                mScreenshotView.setScaleY(scaleT);
546                mScreenshotFlash.setAlpha(flashAlphaInterpolator.getInterpolation(t));
547            }
548        });
549        return anim;
550    }
551    private ValueAnimator createScreenshotDropOutAnimation(int w, int h, boolean statusBarVisible,
552            boolean navBarVisible) {
553        ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
554        anim.setStartDelay(SCREENSHOT_DROP_OUT_DELAY);
555        anim.addListener(new AnimatorListenerAdapter() {
556            @Override
557            public void onAnimationEnd(Animator animation) {
558                mBackgroundView.setVisibility(View.GONE);
559                mScreenshotView.setVisibility(View.GONE);
560                mScreenshotView.setLayerType(View.LAYER_TYPE_NONE, null);
561            }
562        });
563
564        if (!statusBarVisible || !navBarVisible) {
565            // There is no status bar/nav bar, so just fade the screenshot away in place
566            anim.setDuration(SCREENSHOT_FAST_DROP_OUT_DURATION);
567            anim.addUpdateListener(new AnimatorUpdateListener() {
568                @Override
569                public void onAnimationUpdate(ValueAnimator animation) {
570                    float t = (Float) animation.getAnimatedValue();
571                    float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale)
572                            - t * (SCREENSHOT_DROP_IN_MIN_SCALE - SCREENSHOT_FAST_DROP_OUT_MIN_SCALE);
573                    mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA);
574                    mScreenshotView.setAlpha(1f - t);
575                    mScreenshotView.setScaleX(scaleT);
576                    mScreenshotView.setScaleY(scaleT);
577                }
578            });
579        } else {
580            // In the case where there is a status bar, animate to the origin of the bar (top-left)
581            final float scaleDurationPct = (float) SCREENSHOT_DROP_OUT_SCALE_DURATION
582                    / SCREENSHOT_DROP_OUT_DURATION;
583            final Interpolator scaleInterpolator = new Interpolator() {
584                @Override
585                public float getInterpolation(float x) {
586                    if (x < scaleDurationPct) {
587                        // Decelerate, and scale the input accordingly
588                        return (float) (1f - Math.pow(1f - (x / scaleDurationPct), 2f));
589                    }
590                    return 1f;
591                }
592            };
593
594            // Determine the bounds of how to scale
595            float halfScreenWidth = (w - 2f * mBgPadding) / 2f;
596            float halfScreenHeight = (h - 2f * mBgPadding) / 2f;
597            final float offsetPct = SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET;
598            final PointF finalPos = new PointF(
599                -halfScreenWidth + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenWidth,
600                -halfScreenHeight + (SCREENSHOT_DROP_OUT_MIN_SCALE + offsetPct) * halfScreenHeight);
601
602            // Animate the screenshot to the status bar
603            anim.setDuration(SCREENSHOT_DROP_OUT_DURATION);
604            anim.addUpdateListener(new AnimatorUpdateListener() {
605                @Override
606                public void onAnimationUpdate(ValueAnimator animation) {
607                    float t = (Float) animation.getAnimatedValue();
608                    float scaleT = (SCREENSHOT_DROP_IN_MIN_SCALE + mBgPaddingScale)
609                        - scaleInterpolator.getInterpolation(t)
610                            * (SCREENSHOT_DROP_IN_MIN_SCALE - SCREENSHOT_DROP_OUT_MIN_SCALE);
611                    mBackgroundView.setAlpha((1f - t) * BACKGROUND_ALPHA);
612                    mScreenshotView.setAlpha(1f - scaleInterpolator.getInterpolation(t));
613                    mScreenshotView.setScaleX(scaleT);
614                    mScreenshotView.setScaleY(scaleT);
615                    mScreenshotView.setTranslationX(t * finalPos.x);
616                    mScreenshotView.setTranslationY(t * finalPos.y);
617                }
618            });
619        }
620        return anim;
621    }
622
623    static void notifyScreenshotError(Context context, NotificationManager nManager) {
624        Resources r = context.getResources();
625
626        // Clear all existing notification, compose the new notification and show it
627        Notification.Builder b = new Notification.Builder(context)
628            .setTicker(r.getString(R.string.screenshot_failed_title))
629            .setContentTitle(r.getString(R.string.screenshot_failed_title))
630            .setContentText(r.getString(R.string.screenshot_failed_text))
631            .setSmallIcon(R.drawable.stat_notify_image_error)
632            .setWhen(System.currentTimeMillis())
633            .setAutoCancel(true);
634        Notification n =
635            new Notification.BigTextStyle(b)
636                .bigText(r.getString(R.string.screenshot_failed_text))
637                .build();
638        nManager.notify(SCREENSHOT_NOTIFICATION_ID, n);
639    }
640}
641