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