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