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