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