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