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