GlobalScreenshot.java revision c57ccf01fe24ce508404c99b449e9097e6d8b270
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.TimeInterpolator;
23import android.animation.ValueAnimator;
24import android.animation.ValueAnimator.AnimatorUpdateListener;
25import android.app.Notification;
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.Matrix;
36import android.graphics.PixelFormat;
37import android.net.Uri;
38import android.os.AsyncTask;
39import android.os.Environment;
40import android.os.ServiceManager;
41import android.provider.MediaStore;
42import android.util.DisplayMetrics;
43import android.view.Display;
44import android.view.IWindowManager;
45import android.view.LayoutInflater;
46import android.view.MotionEvent;
47import android.view.Surface;
48import android.view.View;
49import android.view.ViewGroup;
50import android.view.WindowManager;
51import android.widget.FrameLayout;
52import android.widget.ImageView;
53
54import com.android.systemui.R;
55
56import java.io.File;
57import java.io.OutputStream;
58import java.text.SimpleDateFormat;
59import java.util.Date;
60
61/**
62 * POD used in the AsyncTask which saves an image in the background.
63 */
64class SaveImageInBackgroundData {
65    Context context;
66    Bitmap image;
67    Runnable finisher;
68    int result;
69}
70
71/**
72 * An AsyncTask that saves an image to the media store in the background.
73 */
74class SaveImageInBackgroundTask extends AsyncTask<SaveImageInBackgroundData, Void,
75        SaveImageInBackgroundData> {
76    private static final String TAG = "SaveImageInBackgroundTask";
77    private static final String SCREENSHOTS_DIR_NAME = "Screenshots";
78    private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png";
79    private static final String SCREENSHOT_FILE_PATH_TEMPLATE = "%s/%s/%s";
80
81    private int mNotificationId;
82    private NotificationManager mNotificationManager;
83    private Notification.Builder mNotificationBuilder;
84    private Intent mLaunchIntent;
85    private String mImageDir;
86    private String mImageFileName;
87    private String mImageFilePath;
88    private String mImageDate;
89    private long mImageTime;
90
91    SaveImageInBackgroundTask(Context context, NotificationManager nManager, int nId) {
92        Resources r = context.getResources();
93
94        // Prepare all the output metadata
95        mImageTime = System.currentTimeMillis();
96        mImageDate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date(mImageTime));
97        mImageDir = Environment.getExternalStoragePublicDirectory(
98                Environment.DIRECTORY_PICTURES).getAbsolutePath();
99        mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, mImageDate);
100        mImageFilePath = String.format(SCREENSHOT_FILE_PATH_TEMPLATE, mImageDir,
101                SCREENSHOTS_DIR_NAME, mImageFileName);
102
103        // Show the intermediate notification
104        mLaunchIntent = new Intent(Intent.ACTION_VIEW);
105        mLaunchIntent.setDataAndType(Uri.fromFile(new File(mImageFilePath)), "image/png");
106        mLaunchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
107        mNotificationId = nId;
108        mNotificationBuilder = new Notification.Builder(context)
109            .setTicker(r.getString(R.string.screenshot_saving_ticker))
110            .setContentTitle(r.getString(R.string.screenshot_saving_title))
111            .setContentText(r.getString(R.string.screenshot_saving_text))
112            .setSmallIcon(android.R.drawable.ic_menu_gallery)
113            .setWhen(System.currentTimeMillis());
114        Notification n = mNotificationBuilder.getNotification();
115        n.flags |= Notification.FLAG_NO_CLEAR;
116
117        mNotificationManager = nManager;
118        mNotificationManager.notify(nId, n);
119    }
120
121    @Override
122    protected SaveImageInBackgroundData doInBackground(SaveImageInBackgroundData... params) {
123        if (params.length != 1) return null;
124
125        Context context = params[0].context;
126        Bitmap image = params[0].image;
127
128        try {
129            // Save the screenshot to the MediaStore
130            ContentValues values = new ContentValues();
131            ContentResolver resolver = context.getContentResolver();
132            values.put(MediaStore.Images.ImageColumns.DATA, mImageFilePath);
133            values.put(MediaStore.Images.ImageColumns.TITLE, mImageFileName);
134            values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, mImageFileName);
135            values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, mImageTime);
136            values.put(MediaStore.Images.ImageColumns.DATE_ADDED, mImageTime);
137            values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, mImageTime);
138            values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png");
139            Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
140
141            OutputStream out = resolver.openOutputStream(uri);
142            image.compress(Bitmap.CompressFormat.PNG, 100, out);
143            out.flush();
144            out.close();
145
146            // update file size in the database
147            values.clear();
148            values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length());
149            resolver.update(uri, values, null, null);
150
151            params[0].result = 0;
152        } catch (Exception e) {
153            // IOException/UnsupportedOperationException may be thrown if external storage is not
154            // mounted
155            params[0].result = 1;
156        }
157
158        return params[0];
159    };
160
161    @Override
162    protected void onPostExecute(SaveImageInBackgroundData params) {
163        if (params.result > 0) {
164            // Show a message that we've failed to save the image to disk
165            GlobalScreenshot.notifyScreenshotError(params.context, mNotificationManager);
166        } else {
167            // Show the final notification to indicate screenshot saved
168            Resources r = params.context.getResources();
169
170            mNotificationBuilder
171                .setTicker(r.getString(R.string.screenshot_saved_title))
172                .setContentTitle(r.getString(R.string.screenshot_saved_title))
173                .setContentText(r.getString(R.string.screenshot_saved_text))
174                .setContentIntent(PendingIntent.getActivity(params.context, 0, mLaunchIntent, 0))
175                .setWhen(System.currentTimeMillis())
176                .setAutoCancel(true);
177
178            Notification n = mNotificationBuilder.getNotification();
179            n.flags &= ~Notification.FLAG_NO_CLEAR;
180            mNotificationManager.notify(mNotificationId, n);
181        }
182        params.finisher.run();
183    };
184}
185
186/**
187 * TODO:
188 *   - Performance when over gl surfaces? Ie. Gallery
189 *   - what do we say in the Toast? Which icon do we get if the user uses another
190 *     type of gallery?
191 */
192class GlobalScreenshot {
193    private static final String TAG = "GlobalScreenshot";
194    private static final int SCREENSHOT_NOTIFICATION_ID = 789;
195    private static final int SCREENSHOT_FADE_IN_DURATION = 500;
196    private static final int SCREENSHOT_FADE_OUT_DELAY = 1000;
197    private static final int SCREENSHOT_FADE_OUT_DURATION = 300;
198    private static final float BACKGROUND_ALPHA = 0.65f;
199    private static final float SCREENSHOT_SCALE_FUDGE = 0.075f; // To account for the border padding
200    private static final float SCREENSHOT_SCALE = 0.8f;
201    private static final float SCREENSHOT_MIN_SCALE = 0.775f;
202
203    private Context mContext;
204    private LayoutInflater mLayoutInflater;
205    private IWindowManager mIWindowManager;
206    private WindowManager mWindowManager;
207    private WindowManager.LayoutParams mWindowLayoutParams;
208    private NotificationManager mNotificationManager;
209    private Display mDisplay;
210    private DisplayMetrics mDisplayMetrics;
211    private Matrix mDisplayMatrix;
212
213    private Bitmap mScreenBitmap;
214    private View mScreenshotLayout;
215    private ImageView mBackgroundView;
216    private FrameLayout mScreenshotContainerView;
217    private ImageView mScreenshotView;
218
219    private AnimatorSet mScreenshotAnimation;
220
221    // Fade interpolators
222    final TimeInterpolator mFadeInInterpolator = new TimeInterpolator() {
223        public float getInterpolation(float t) {
224            return (float) Math.pow(t, 1.5f);
225        }
226    };
227    final TimeInterpolator mFadeOutInterpolator = new TimeInterpolator() {
228        public float getInterpolation(float t) {
229            return (float) t;
230        }
231    };
232    // The interpolator used to control the background alpha at the start of the animation
233    final TimeInterpolator mBackgroundViewAlphaInterpolator = new TimeInterpolator() {
234        public float getInterpolation(float t) {
235            float tStep = 0.35f;
236            if (t < tStep) {
237                return t * (1f / tStep);
238            } else {
239                return 1f;
240            }
241        }
242    };
243
244    /**
245     * @param context everything needs a context :(
246     */
247    public GlobalScreenshot(Context context) {
248        mContext = context;
249        mLayoutInflater = (LayoutInflater)
250                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
251
252        // Inflate the screenshot layout
253        mDisplayMetrics = new DisplayMetrics();
254        mDisplayMatrix = new Matrix();
255        mScreenshotLayout = mLayoutInflater.inflate(R.layout.global_screenshot, null);
256        mBackgroundView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_background);
257        mScreenshotContainerView = (FrameLayout) mScreenshotLayout.findViewById(R.id.global_screenshot_container);
258        mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot);
259        mScreenshotLayout.setFocusable(true);
260        mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() {
261            @Override
262            public boolean onTouch(View v, MotionEvent event) {
263                // Intercept and ignore all touch events
264                return true;
265            }
266        });
267
268        // Setup the window that we are going to use
269        mIWindowManager = IWindowManager.Stub.asInterface(
270                ServiceManager.getService(Context.WINDOW_SERVICE));
271        mWindowLayoutParams = new WindowManager.LayoutParams(
272                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0,
273                WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY,
274                WindowManager.LayoutParams.FLAG_FULLSCREEN
275                    | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
276                    | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
277                    | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED,
278                PixelFormat.TRANSLUCENT);
279        mWindowLayoutParams.setTitle("ScreenshotAnimation");
280        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
281        mNotificationManager =
282            (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
283        mDisplay = mWindowManager.getDefaultDisplay();
284    }
285
286    /**
287     * Creates a new worker thread and saves the screenshot to the media store.
288     */
289    private void saveScreenshotInWorkerThread(Runnable finisher) {
290        SaveImageInBackgroundData data = new SaveImageInBackgroundData();
291        data.context = mContext;
292        data.image = mScreenBitmap;
293        data.finisher = finisher;
294        new SaveImageInBackgroundTask(mContext, mNotificationManager, SCREENSHOT_NOTIFICATION_ID)
295                .execute(data);
296    }
297
298    /**
299     * @return the current display rotation in degrees
300     */
301    private float getDegreesForRotation(int value) {
302        switch (value) {
303        case Surface.ROTATION_90:
304            return 90f;
305        case Surface.ROTATION_180:
306            return 180f;
307        case Surface.ROTATION_270:
308            return 270f;
309        }
310        return 0f;
311    }
312
313    /**
314     * Takes a screenshot of the current display and shows an animation.
315     */
316    void takeScreenshot(Runnable finisher) {
317        // We need to orient the screenshot correctly (and the Surface api seems to take screenshots
318        // only in the natural orientation of the device :!)
319        mDisplay.getRealMetrics(mDisplayMetrics);
320        float[] dims = {mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels};
321        float degrees = getDegreesForRotation(mDisplay.getRotation());
322        boolean requiresRotation = (degrees > 0);
323        if (requiresRotation) {
324            // Get the dimensions of the device in its native orientation
325            mDisplayMatrix.reset();
326            mDisplayMatrix.preRotate(-degrees);
327            mDisplayMatrix.mapPoints(dims);
328            dims[0] = Math.abs(dims[0]);
329            dims[1] = Math.abs(dims[1]);
330        }
331        mScreenBitmap = Surface.screenshot((int) dims[0], (int) dims[1]);
332        if (requiresRotation) {
333            // Rotate the screenshot to the current orientation
334            Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
335                    mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);
336            Canvas c = new Canvas(ss);
337            c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
338            c.rotate(360f - degrees);
339            c.translate(-dims[0] / 2, -dims[1] / 2);
340            c.drawBitmap(mScreenBitmap, 0, 0, null);
341            c.setBitmap(null);
342            mScreenBitmap = ss;
343        }
344
345        // If we couldn't take the screenshot, notify the user
346        if (mScreenBitmap == null) {
347            notifyScreenshotError(mContext, mNotificationManager);
348            finisher.run();
349            return;
350        }
351
352        // Start the post-screenshot animation
353        startAnimation(finisher);
354    }
355
356
357    /**
358     * Starts the animation after taking the screenshot
359     */
360    private void startAnimation(final Runnable finisher) {
361        // Add the view for the animation
362        mScreenshotView.setImageBitmap(mScreenBitmap);
363        mScreenshotLayout.requestFocus();
364
365        // Setup the animation with the screenshot just taken
366        if (mScreenshotAnimation != null) {
367            mScreenshotAnimation.end();
368        }
369
370        mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
371        ValueAnimator screenshotFadeInAnim = createScreenshotFadeInAnimation();
372        ValueAnimator screenshotFadeOutAnim = createScreenshotFadeOutAnimation();
373        mScreenshotAnimation = new AnimatorSet();
374        mScreenshotAnimation.play(screenshotFadeInAnim).before(screenshotFadeOutAnim);
375        mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
376            @Override
377            public void onAnimationEnd(Animator animation) {
378                // Save the screenshot once we have a bit of time now
379                saveScreenshotInWorkerThread(finisher);
380                mWindowManager.removeView(mScreenshotLayout);
381            }
382        });
383        mScreenshotAnimation.start();
384    }
385    private ValueAnimator createScreenshotFadeInAnimation() {
386        ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
387        anim.setInterpolator(mFadeInInterpolator);
388        anim.setDuration(SCREENSHOT_FADE_IN_DURATION);
389        anim.addListener(new AnimatorListenerAdapter() {
390            @Override
391            public void onAnimationStart(Animator animation) {
392                mBackgroundView.setVisibility(View.VISIBLE);
393                mScreenshotContainerView.setTranslationY(0f);
394                mScreenshotContainerView.setVisibility(View.VISIBLE);
395            }
396        });
397        anim.addUpdateListener(new AnimatorUpdateListener() {
398            @Override
399            public void onAnimationUpdate(ValueAnimator animation) {
400                float t = ((Float) animation.getAnimatedValue()).floatValue();
401                mBackgroundView.setAlpha(mBackgroundViewAlphaInterpolator.getInterpolation(t) *
402                        BACKGROUND_ALPHA);
403                float scaleT = SCREENSHOT_SCALE
404                        + (1f - t) * (1f - SCREENSHOT_SCALE)
405                        + SCREENSHOT_SCALE_FUDGE;
406                mScreenshotContainerView.setAlpha(t*t*t*t);
407                mScreenshotContainerView.setScaleX(scaleT);
408                mScreenshotContainerView.setScaleY(scaleT);
409            }
410        });
411        return anim;
412    }
413    private ValueAnimator createScreenshotFadeOutAnimation() {
414        ValueAnimator anim = ValueAnimator.ofFloat(1f, 0f);
415        anim.setInterpolator(mFadeOutInterpolator);
416        anim.setStartDelay(SCREENSHOT_FADE_OUT_DELAY);
417        anim.setDuration(SCREENSHOT_FADE_OUT_DURATION);
418        anim.addListener(new AnimatorListenerAdapter() {
419            @Override
420            public void onAnimationEnd(Animator animation) {
421                mBackgroundView.setVisibility(View.GONE);
422                mScreenshotContainerView.setVisibility(View.GONE);
423            }
424        });
425        anim.addUpdateListener(new AnimatorUpdateListener() {
426            @Override
427            public void onAnimationUpdate(ValueAnimator animation) {
428                float t = ((Float) animation.getAnimatedValue()).floatValue();
429                float scaleT = SCREENSHOT_MIN_SCALE
430                        + t * (SCREENSHOT_SCALE - SCREENSHOT_MIN_SCALE)
431                        + SCREENSHOT_SCALE_FUDGE;
432                mScreenshotContainerView.setAlpha(t);
433                mScreenshotContainerView.setScaleX(scaleT);
434                mScreenshotContainerView.setScaleY(scaleT);
435                mBackgroundView.setAlpha(t * t * BACKGROUND_ALPHA);
436            }
437        });
438        return anim;
439    }
440
441    static void notifyScreenshotError(Context context, NotificationManager nManager) {
442        Resources r = context.getResources();
443
444        // Clear all existing notification, compose the new notification and show it
445        Notification n = new Notification.Builder(context)
446            .setTicker(r.getString(R.string.screenshot_failed_title))
447            .setContentTitle(r.getString(R.string.screenshot_failed_title))
448            .setContentText(r.getString(R.string.screenshot_failed_text))
449            .setSmallIcon(android.R.drawable.ic_menu_report_image)
450            .setWhen(System.currentTimeMillis())
451            .setAutoCancel(true)
452            .getNotification();
453        nManager.notify(SCREENSHOT_NOTIFICATION_ID, n);
454    }
455}
456