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 android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 20 21 import static com.android.systemui.screenshot.GlobalScreenshot.SHARING_INTENT; 22 import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorListenerAdapter; 26 import android.animation.AnimatorSet; 27 import android.animation.ValueAnimator; 28 import android.animation.ValueAnimator.AnimatorUpdateListener; 29 import android.app.ActivityManager; 30 import android.app.ActivityOptions; 31 import android.app.Notification; 32 import android.app.Notification.BigPictureStyle; 33 import android.app.NotificationManager; 34 import android.app.PendingIntent; 35 import android.app.admin.DevicePolicyManager; 36 import android.content.BroadcastReceiver; 37 import android.content.ComponentName; 38 import android.content.ContentResolver; 39 import android.content.ContentValues; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.content.res.Resources; 43 import android.graphics.Bitmap; 44 import android.graphics.Canvas; 45 import android.graphics.ColorMatrix; 46 import android.graphics.ColorMatrixColorFilter; 47 import android.graphics.Matrix; 48 import android.graphics.Paint; 49 import android.graphics.Picture; 50 import android.graphics.PixelFormat; 51 import android.graphics.PointF; 52 import android.graphics.Rect; 53 import android.media.MediaActionSound; 54 import android.net.Uri; 55 import android.os.AsyncTask; 56 import android.os.Environment; 57 import android.os.PowerManager; 58 import android.os.Process; 59 import android.os.RemoteException; 60 import android.os.UserHandle; 61 import android.provider.MediaStore; 62 import android.util.DisplayMetrics; 63 import android.util.Slog; 64 import android.view.Display; 65 import android.view.LayoutInflater; 66 import android.view.MotionEvent; 67 import android.view.Surface; 68 import android.view.SurfaceControl; 69 import android.view.View; 70 import android.view.ViewGroup; 71 import android.view.WindowManager; 72 import android.view.animation.Interpolator; 73 import android.widget.ImageView; 74 import android.widget.Toast; 75 76 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; 77 import com.android.systemui.R; 78 import com.android.systemui.SystemUI; 79 import com.android.systemui.util.NotificationChannels; 80 81 import java.io.File; 82 import java.io.FileOutputStream; 83 import java.io.OutputStream; 84 import java.text.DateFormat; 85 import java.text.SimpleDateFormat; 86 import java.util.Date; 87 88 /** 89 * POD used in the AsyncTask which saves an image in the background. 90 */ 91 class SaveImageInBackgroundData { 92 Context context; 93 Bitmap image; 94 Uri imageUri; 95 Runnable finisher; 96 int iconSize; 97 int previewWidth; 98 int previewheight; 99 int errorMsgResId; 100 101 void clearImage() { 102 image = null; 103 imageUri = null; 104 iconSize = 0; 105 } 106 void clearContext() { 107 context = null; 108 } 109 } 110 111 /** 112 * An AsyncTask that saves an image to the media store in the background. 113 */ 114 class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { 115 private static final String TAG = "SaveImageInBackgroundTask"; 116 117 private static final String SCREENSHOTS_DIR_NAME = "Screenshots"; 118 private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png"; 119 private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"; 120 121 private final SaveImageInBackgroundData mParams; 122 private final NotificationManager mNotificationManager; 123 private final Notification.Builder mNotificationBuilder, mPublicNotificationBuilder; 124 private final File mScreenshotDir; 125 private final String mImageFileName; 126 private final String mImageFilePath; 127 private final long mImageTime; 128 private final BigPictureStyle mNotificationStyle; 129 private final int mImageWidth; 130 private final int mImageHeight; 131 132 SaveImageInBackgroundTask(Context context, SaveImageInBackgroundData data, 133 NotificationManager nManager) { 134 Resources r = context.getResources(); 135 136 // Prepare all the output metadata 137 mParams = data; 138 mImageTime = System.currentTimeMillis(); 139 String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime)); 140 mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate); 141 142 mScreenshotDir = new File(Environment.getExternalStoragePublicDirectory( 143 Environment.DIRECTORY_PICTURES), SCREENSHOTS_DIR_NAME); 144 mImageFilePath = new File(mScreenshotDir, mImageFileName).getAbsolutePath(); 145 146 // Create the large notification icon 147 mImageWidth = data.image.getWidth(); 148 mImageHeight = data.image.getHeight(); 149 int iconSize = data.iconSize; 150 int previewWidth = data.previewWidth; 151 int previewHeight = data.previewheight; 152 153 Paint paint = new Paint(); 154 ColorMatrix desat = new ColorMatrix(); 155 desat.setSaturation(0.25f); 156 paint.setColorFilter(new ColorMatrixColorFilter(desat)); 157 Matrix matrix = new Matrix(); 158 int overlayColor = 0x40FFFFFF; 159 160 matrix.setTranslate((previewWidth - mImageWidth) / 2, (previewHeight - mImageHeight) / 2); 161 Bitmap picture = generateAdjustedHwBitmap(data.image, previewWidth, previewHeight, matrix, 162 paint, overlayColor); 163 164 // Note, we can't use the preview for the small icon, since it is non-square 165 float scale = (float) iconSize / Math.min(mImageWidth, mImageHeight); 166 matrix.setScale(scale, scale); 167 matrix.postTranslate((iconSize - (scale * mImageWidth)) / 2, 168 (iconSize - (scale * mImageHeight)) / 2); 169 Bitmap icon = generateAdjustedHwBitmap(data.image, iconSize, iconSize, matrix, paint, 170 overlayColor); 171 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 = 181 new Notification.Builder(context, NotificationChannels.SCREENSHOTS_HEADSUP) 182 .setContentTitle(r.getString(R.string.screenshot_saving_title)) 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, true); 190 191 mNotificationBuilder = new Notification.Builder(context, 192 NotificationChannels.SCREENSHOTS_HEADSUP) 193 .setContentTitle(r.getString(R.string.screenshot_saving_title)) 194 .setSmallIcon(R.drawable.stat_notify_image) 195 .setWhen(now) 196 .setShowWhen(true) 197 .setColor(r.getColor(com.android.internal.R.color.system_notification_accent_color)) 198 .setStyle(mNotificationStyle) 199 .setPublicVersion(mPublicNotificationBuilder.build()); 200 mNotificationBuilder.setFlag(Notification.FLAG_NO_CLEAR, true); 201 SystemUI.overrideNotificationAppName(context, mNotificationBuilder, true); 202 203 mNotificationManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT, 204 mNotificationBuilder.build()); 205 206 /** 207 * NOTE: The following code prepares the notification builder for updating the notification 208 * after the screenshot has been written to disk. 209 */ 210 211 // On the tablet, the large icon makes the notification appear as if it is clickable (and 212 // on small devices, the large icon is not shown) so defer showing the large icon until 213 // we compose the final post-save notification below. 214 mNotificationBuilder.setLargeIcon(icon.createAshmemBitmap()); 215 // But we still don't set it for the expanded view, allowing the smallIcon to show here. 216 mNotificationStyle.bigLargeIcon((Bitmap) null); 217 } 218 219 /** 220 * Generates a new hardware bitmap with specified values, copying the content from the passed 221 * in bitmap. 222 */ 223 private Bitmap generateAdjustedHwBitmap(Bitmap bitmap, int width, int height, Matrix matrix, 224 Paint paint, int color) { 225 Picture picture = new Picture(); 226 Canvas canvas = picture.beginRecording(width, height); 227 canvas.drawColor(color); 228 canvas.drawBitmap(bitmap, matrix, paint); 229 picture.endRecording(); 230 return Bitmap.createBitmap(picture); 231 } 232 233 @Override 234 protected Void doInBackground(Void... params) { 235 if (isCancelled()) { 236 return null; 237 } 238 239 // By default, AsyncTask sets the worker thread to have background thread priority, so bump 240 // it back up so that we save a little quicker. 241 Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); 242 243 Context context = mParams.context; 244 Bitmap image = mParams.image; 245 Resources r = context.getResources(); 246 247 try { 248 // Create screenshot directory if it doesn't exist 249 mScreenshotDir.mkdirs(); 250 251 // media provider uses seconds for DATE_MODIFIED and DATE_ADDED, but milliseconds 252 // for DATE_TAKEN 253 long dateSeconds = mImageTime / 1000; 254 255 // Save 256 OutputStream out = new FileOutputStream(mImageFilePath); 257 image.compress(Bitmap.CompressFormat.PNG, 100, out); 258 out.flush(); 259 out.close(); 260 261 // Save the screenshot to the MediaStore 262 ContentValues values = new ContentValues(); 263 ContentResolver resolver = context.getContentResolver(); 264 values.put(MediaStore.Images.ImageColumns.DATA, mImageFilePath); 265 values.put(MediaStore.Images.ImageColumns.TITLE, mImageFileName); 266 values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, mImageFileName); 267 values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, mImageTime); 268 values.put(MediaStore.Images.ImageColumns.DATE_ADDED, dateSeconds); 269 values.put(MediaStore.Images.ImageColumns.DATE_MODIFIED, dateSeconds); 270 values.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png"); 271 values.put(MediaStore.Images.ImageColumns.WIDTH, mImageWidth); 272 values.put(MediaStore.Images.ImageColumns.HEIGHT, mImageHeight); 273 values.put(MediaStore.Images.ImageColumns.SIZE, new File(mImageFilePath).length()); 274 Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); 275 276 // Create a share intent 277 String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime)); 278 String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); 279 Intent sharingIntent = new Intent(Intent.ACTION_SEND); 280 sharingIntent.setType("image/png"); 281 sharingIntent.putExtra(Intent.EXTRA_STREAM, uri); 282 sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject); 283 sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 284 285 // Create a share action for the notification. Note, we proxy the call to 286 // ScreenshotActionReceiver because RemoteViews currently forces an activity options 287 // on the PendingIntent being launched, and since we don't want to trigger the share 288 // sheet in this case, we start the chooser activity directly in 289 // ScreenshotActionReceiver. 290 PendingIntent shareAction = PendingIntent.getBroadcast(context, 0, 291 new Intent(context, GlobalScreenshot.ScreenshotActionReceiver.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 Intent editIntent = new Intent(Intent.ACTION_EDIT); 300 editIntent.setType("image/png"); 301 editIntent.setData(uri); 302 editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 303 editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 304 305 // Create a edit action for the notification the same way. 306 PendingIntent editAction = PendingIntent.getBroadcast(context, 1, 307 new Intent(context, GlobalScreenshot.ScreenshotActionReceiver.class) 308 .putExtra(SHARING_INTENT, editIntent), 309 PendingIntent.FLAG_CANCEL_CURRENT); 310 Notification.Action.Builder editActionBuilder = new Notification.Action.Builder( 311 R.drawable.ic_screenshot_edit, 312 r.getString(com.android.internal.R.string.screenshot_edit), editAction); 313 mNotificationBuilder.addAction(editActionBuilder.build()); 314 315 316 // Create a delete action for the notification 317 PendingIntent deleteAction = PendingIntent.getBroadcast(context, 0, 318 new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class) 319 .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()), 320 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); 321 Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder( 322 R.drawable.ic_screenshot_delete, 323 r.getString(com.android.internal.R.string.delete), deleteAction); 324 mNotificationBuilder.addAction(deleteActionBuilder.build()); 325 326 mParams.imageUri = uri; 327 mParams.image = null; 328 mParams.errorMsgResId = 0; 329 } catch (Exception e) { 330 // IOException/UnsupportedOperationException may be thrown if external storage is not 331 // mounted 332 Slog.e(TAG, "unable to save screenshot", e); 333 mParams.clearImage(); 334 mParams.errorMsgResId = R.string.screenshot_failed_to_save_text; 335 } 336 337 // Recycle the bitmap data 338 if (image != null) { 339 image.recycle(); 340 } 341 342 return null; 343 } 344 345 @Override 346 protected void onPostExecute(Void params) { 347 if (mParams.errorMsgResId != 0) { 348 // Show a message that we've failed to save the image to disk 349 GlobalScreenshot.notifyScreenshotError(mParams.context, mNotificationManager, 350 mParams.errorMsgResId); 351 } else { 352 // Show the final notification to indicate screenshot saved 353 Context context = mParams.context; 354 Resources r = context.getResources(); 355 356 // Create the intent to show the screenshot in gallery 357 Intent launchIntent = new Intent(Intent.ACTION_VIEW); 358 launchIntent.setDataAndType(mParams.imageUri, "image/png"); 359 launchIntent.setFlags( 360 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION); 361 362 final long now = System.currentTimeMillis(); 363 364 // Update the text and the icon for the existing notification 365 mPublicNotificationBuilder 366 .setContentTitle(r.getString(R.string.screenshot_saved_title)) 367 .setContentText(r.getString(R.string.screenshot_saved_text)) 368 .setContentIntent(PendingIntent.getActivity(mParams.context, 0, launchIntent, 0)) 369 .setWhen(now) 370 .setAutoCancel(true) 371 .setColor(context.getColor( 372 com.android.internal.R.color.system_notification_accent_color)); 373 mNotificationBuilder 374 .setContentTitle(r.getString(R.string.screenshot_saved_title)) 375 .setContentText(r.getString(R.string.screenshot_saved_text)) 376 .setContentIntent(PendingIntent.getActivity(mParams.context, 0, launchIntent, 0)) 377 .setWhen(now) 378 .setAutoCancel(true) 379 .setColor(context.getColor( 380 com.android.internal.R.color.system_notification_accent_color)) 381 .setPublicVersion(mPublicNotificationBuilder.build()) 382 .setFlag(Notification.FLAG_NO_CLEAR, false); 383 384 mNotificationManager.notify(SystemMessage.NOTE_GLOBAL_SCREENSHOT, 385 mNotificationBuilder.build()); 386 } 387 mParams.finisher.run(); 388 mParams.clearContext(); 389 } 390 391 @Override 392 protected void onCancelled(Void params) { 393 // If we are cancelled while the task is running in the background, we may get null params. 394 // The finisher is expected to always be called back, so just use the baked-in params from 395 // the ctor in any case. 396 mParams.finisher.run(); 397 mParams.clearImage(); 398 mParams.clearContext(); 399 400 // Cancel the posted notification 401 mNotificationManager.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT); 402 } 403 } 404 405 /** 406 * An AsyncTask that deletes an image from the media store in the background. 407 */ 408 class DeleteImageInBackgroundTask extends AsyncTask<Uri, Void, Void> { 409 private Context mContext; 410 411 DeleteImageInBackgroundTask(Context context) { 412 mContext = context; 413 } 414 415 @Override 416 protected Void doInBackground(Uri... params) { 417 if (params.length != 1) return null; 418 419 Uri screenshotUri = params[0]; 420 ContentResolver resolver = mContext.getContentResolver(); 421 resolver.delete(screenshotUri, null, null); 422 return null; 423 } 424 } 425 426 class GlobalScreenshot { 427 static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id"; 428 static final String SHARING_INTENT = "android:screenshot_sharing_intent"; 429 430 private static final int SCREENSHOT_FLASH_TO_PEAK_DURATION = 130; 431 private static final int SCREENSHOT_DROP_IN_DURATION = 430; 432 private static final int SCREENSHOT_DROP_OUT_DELAY = 500; 433 private static final int SCREENSHOT_DROP_OUT_DURATION = 430; 434 private static final int SCREENSHOT_DROP_OUT_SCALE_DURATION = 370; 435 private static final int SCREENSHOT_FAST_DROP_OUT_DURATION = 320; 436 private static final float BACKGROUND_ALPHA = 0.5f; 437 private static final float SCREENSHOT_SCALE = 1f; 438 private static final float SCREENSHOT_DROP_IN_MIN_SCALE = SCREENSHOT_SCALE * 0.725f; 439 private static final float SCREENSHOT_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.45f; 440 private static final float SCREENSHOT_FAST_DROP_OUT_MIN_SCALE = SCREENSHOT_SCALE * 0.6f; 441 private static final float SCREENSHOT_DROP_OUT_MIN_SCALE_OFFSET = 0f; 442 private final int mPreviewWidth; 443 private final int mPreviewHeight; 444 445 private Context mContext; 446 private WindowManager mWindowManager; 447 private WindowManager.LayoutParams mWindowLayoutParams; 448 private NotificationManager mNotificationManager; 449 private Display mDisplay; 450 private DisplayMetrics mDisplayMetrics; 451 private Matrix mDisplayMatrix; 452 453 private Bitmap mScreenBitmap; 454 private View mScreenshotLayout; 455 private ScreenshotSelectorView mScreenshotSelectorView; 456 private ImageView mBackgroundView; 457 private ImageView mScreenshotView; 458 private ImageView mScreenshotFlash; 459 460 private AnimatorSet mScreenshotAnimation; 461 462 private int mNotificationIconSize; 463 private float mBgPadding; 464 private float mBgPaddingScale; 465 466 private AsyncTask<Void, Void, Void> mSaveInBgTask; 467 468 private MediaActionSound mCameraSound; 469 470 471 /** 472 * @param context everything needs a context :( 473 */ 474 public GlobalScreenshot(Context context) { 475 Resources r = context.getResources(); 476 mContext = context; 477 LayoutInflater layoutInflater = (LayoutInflater) 478 context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 479 480 // Inflate the screenshot layout 481 mDisplayMatrix = new Matrix(); 482 mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null); 483 mBackgroundView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_background); 484 mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot); 485 mScreenshotFlash = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot_flash); 486 mScreenshotSelectorView = (ScreenshotSelectorView) mScreenshotLayout.findViewById( 487 R.id.global_screenshot_selector); 488 mScreenshotLayout.setFocusable(true); 489 mScreenshotSelectorView.setFocusable(true); 490 mScreenshotSelectorView.setFocusableInTouchMode(true); 491 mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() { 492 @Override 493 public boolean onTouch(View v, MotionEvent event) { 494 // Intercept and ignore all touch events 495 return true; 496 } 497 }); 498 499 // Setup the window that we are going to use 500 mWindowLayoutParams = new WindowManager.LayoutParams( 501 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, 502 WindowManager.LayoutParams.TYPE_SCREENSHOT, 503 WindowManager.LayoutParams.FLAG_FULLSCREEN 504 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 505 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED, 506 PixelFormat.TRANSLUCENT); 507 mWindowLayoutParams.setTitle("ScreenshotAnimation"); 508 mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 509 mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 510 mNotificationManager = 511 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 512 mDisplay = mWindowManager.getDefaultDisplay(); 513 mDisplayMetrics = new DisplayMetrics(); 514 mDisplay.getRealMetrics(mDisplayMetrics); 515 516 // Get the various target sizes 517 mNotificationIconSize = 518 r.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); 519 520 // Scale has to account for both sides of the bg 521 mBgPadding = (float) r.getDimensionPixelSize(R.dimen.global_screenshot_bg_padding); 522 mBgPaddingScale = mBgPadding / mDisplayMetrics.widthPixels; 523 524 // determine the optimal preview size 525 int panelWidth = 0; 526 try { 527 panelWidth = r.getDimensionPixelSize(R.dimen.notification_panel_width); 528 } catch (Resources.NotFoundException e) { 529 } 530 if (panelWidth <= 0) { 531 // includes notification_panel_width==match_parent (-1) 532 panelWidth = mDisplayMetrics.widthPixels; 533 } 534 mPreviewWidth = panelWidth; 535 mPreviewHeight = r.getDimensionPixelSize(R.dimen.notification_max_height); 536 537 // Setup the Camera shutter sound 538 mCameraSound = new MediaActionSound(); 539 mCameraSound.load(MediaActionSound.SHUTTER_CLICK); 540 } 541 542 /** 543 * Creates a new worker thread and saves the screenshot to the media store. 544 */ 545 private void saveScreenshotInWorkerThread(Runnable finisher) { 546 SaveImageInBackgroundData data = new SaveImageInBackgroundData(); 547 data.context = mContext; 548 data.image = mScreenBitmap; 549 data.iconSize = mNotificationIconSize; 550 data.finisher = finisher; 551 data.previewWidth = mPreviewWidth; 552 data.previewheight = mPreviewHeight; 553 if (mSaveInBgTask != null) { 554 mSaveInBgTask.cancel(false); 555 } 556 mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager) 557 .execute(); 558 } 559 560 /** 561 * @return the current display rotation in degrees 562 */ 563 private float getDegreesForRotation(int value) { 564 switch (value) { 565 case Surface.ROTATION_90: 566 return 360f - 90f; 567 case Surface.ROTATION_180: 568 return 360f - 180f; 569 case Surface.ROTATION_270: 570 return 360f - 270f; 571 } 572 return 0f; 573 } 574 575 /** 576 * Takes a screenshot of the current display and shows an animation. 577 */ 578 private void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible, 579 Rect crop) { 580 int rot = mDisplay.getRotation(); 581 int width = crop.width(); 582 int height = crop.height(); 583 584 // Take the screenshot 585 mScreenBitmap = SurfaceControl.screenshot(crop, width, height, rot); 586 if (mScreenBitmap == null) { 587 notifyScreenshotError(mContext, mNotificationManager, 588 R.string.screenshot_failed_to_capture_text); 589 finisher.run(); 590 return; 591 } 592 593 // Optimizations 594 mScreenBitmap.setHasAlpha(false); 595 mScreenBitmap.prepareToDraw(); 596 597 // Start the post-screenshot animation 598 startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels, 599 statusBarVisible, navBarVisible); 600 } 601 602 void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible) { 603 mDisplay.getRealMetrics(mDisplayMetrics); 604 takeScreenshot(finisher, statusBarVisible, navBarVisible, 605 new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels)); 606 } 607 608 /** 609 * Displays a screenshot selector 610 */ 611 void takeScreenshotPartial(final Runnable finisher, final boolean statusBarVisible, 612 final boolean navBarVisible) { 613 mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); 614 mScreenshotSelectorView.setOnTouchListener(new View.OnTouchListener() { 615 @Override 616 public boolean onTouch(View v, MotionEvent event) { 617 ScreenshotSelectorView view = (ScreenshotSelectorView) v; 618 switch (event.getAction()) { 619 case MotionEvent.ACTION_DOWN: 620 view.startSelection((int) event.getX(), (int) event.getY()); 621 return true; 622 case MotionEvent.ACTION_MOVE: 623 view.updateSelection((int) event.getX(), (int) event.getY()); 624 return true; 625 case MotionEvent.ACTION_UP: 626 view.setVisibility(View.GONE); 627 mWindowManager.removeView(mScreenshotLayout); 628 final Rect rect = view.getSelectionRect(); 629 if (rect != null) { 630 if (rect.width() != 0 && rect.height() != 0) { 631 // Need mScreenshotLayout to handle it after the view disappears 632 mScreenshotLayout.post(new Runnable() { 633 public void run() { 634 takeScreenshot(finisher, statusBarVisible, navBarVisible, 635 rect); 636 } 637 }); 638 } 639 } 640 641 view.stopSelection(); 642 return true; 643 } 644 645 return false; 646 } 647 }); 648 mScreenshotLayout.post(new Runnable() { 649 @Override 650 public void run() { 651 mScreenshotSelectorView.setVisibility(View.VISIBLE); 652 mScreenshotSelectorView.requestFocus(); 653 } 654 }); 655 } 656 657 /** 658 * Cancels screenshot request 659 */ 660 void stopScreenshot() { 661 // If the selector layer still presents on screen, we remove it and resets its state. 662 if (mScreenshotSelectorView.getSelectionRect() != null) { 663 mWindowManager.removeView(mScreenshotLayout); 664 mScreenshotSelectorView.stopSelection(); 665 } 666 } 667 668 /** 669 * Starts the animation after taking the screenshot 670 */ 671 private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible, 672 boolean navBarVisible) { 673 // If power save is on, show a toast so there is some visual indication that a screenshot 674 // has been taken. 675 PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 676 if (powerManager.isPowerSaveMode()) { 677 Toast.makeText(mContext, R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show(); 678 } 679 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, true); 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 * Receiver to proxy the share or edit intent. 891 */ 892 public static class ScreenshotActionReceiver extends BroadcastReceiver { 893 @Override 894 public void onReceive(Context context, Intent intent) { 895 try { 896 ActivityManager.getService().closeSystemDialogs(SYSTEM_DIALOG_REASON_SCREENSHOT); 897 } catch (RemoteException e) { 898 } 899 900 Intent actionIntent = intent.getParcelableExtra(SHARING_INTENT); 901 902 // If this is an edit & default editor exists, route straight there. 903 String editorPackage = context.getResources().getString(R.string.config_screenshotEditor); 904 if (actionIntent.getAction() == Intent.ACTION_EDIT && 905 editorPackage != null && editorPackage.length() > 0) { 906 actionIntent.setComponent(ComponentName.unflattenFromString(editorPackage)); 907 final NotificationManager nm = 908 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 909 nm.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT); 910 } else { 911 PendingIntent chooseAction = PendingIntent.getBroadcast(context, 0, 912 new Intent(context, GlobalScreenshot.TargetChosenReceiver.class), 913 PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); 914 actionIntent = Intent.createChooser(actionIntent, null, 915 chooseAction.getIntentSender()) 916 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); 917 } 918 919 ActivityOptions opts = ActivityOptions.makeBasic(); 920 opts.setDisallowEnterPictureInPictureWhileLaunching(true); 921 922 context.startActivityAsUser(actionIntent, opts.toBundle(), UserHandle.CURRENT); 923 } 924 } 925 926 /** 927 * Removes the notification for a screenshot after a share or edit target is chosen. 928 */ 929 public static class TargetChosenReceiver extends BroadcastReceiver { 930 @Override 931 public void onReceive(Context context, Intent intent) { 932 // Clear the notification 933 final NotificationManager nm = 934 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 935 nm.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT); 936 } 937 } 938 939 /** 940 * Removes the last screenshot. 941 */ 942 public static class DeleteScreenshotReceiver extends BroadcastReceiver { 943 @Override 944 public void onReceive(Context context, Intent intent) { 945 if (!intent.hasExtra(SCREENSHOT_URI_ID)) { 946 return; 947 } 948 949 // Clear the notification 950 final NotificationManager nm = 951 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 952 final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID)); 953 nm.cancel(SystemMessage.NOTE_GLOBAL_SCREENSHOT); 954 955 // And delete the image from the media store 956 new DeleteImageInBackgroundTask(context).execute(uri); 957 } 958 } 959 } 960