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