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