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