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