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