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