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