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