1 /* 2 * Copyright (C) 2020 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.res.Configuration.ORIENTATION_LANDSCAPE; 20 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 21 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 22 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 23 24 import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT; 25 26 import android.animation.Animator; 27 import android.animation.AnimatorListenerAdapter; 28 import android.animation.AnimatorSet; 29 import android.animation.ValueAnimator; 30 import android.annotation.Nullable; 31 import android.annotation.SuppressLint; 32 import android.app.ActivityManager; 33 import android.app.ActivityOptions; 34 import android.app.Notification; 35 import android.app.PendingIntent; 36 import android.content.BroadcastReceiver; 37 import android.content.ComponentName; 38 import android.content.Context; 39 import android.content.Intent; 40 import android.content.res.Configuration; 41 import android.content.res.Resources; 42 import android.graphics.Bitmap; 43 import android.graphics.Color; 44 import android.graphics.Insets; 45 import android.graphics.Outline; 46 import android.graphics.PixelFormat; 47 import android.graphics.PointF; 48 import android.graphics.Rect; 49 import android.graphics.Region; 50 import android.graphics.drawable.BitmapDrawable; 51 import android.graphics.drawable.ColorDrawable; 52 import android.graphics.drawable.Drawable; 53 import android.graphics.drawable.InsetDrawable; 54 import android.graphics.drawable.LayerDrawable; 55 import android.media.MediaActionSound; 56 import android.net.Uri; 57 import android.os.Handler; 58 import android.os.Looper; 59 import android.os.Message; 60 import android.os.PowerManager; 61 import android.os.RemoteException; 62 import android.provider.Settings; 63 import android.util.DisplayMetrics; 64 import android.util.Log; 65 import android.util.MathUtils; 66 import android.util.Slog; 67 import android.view.Display; 68 import android.view.KeyEvent; 69 import android.view.LayoutInflater; 70 import android.view.MotionEvent; 71 import android.view.SurfaceControl; 72 import android.view.View; 73 import android.view.ViewGroup; 74 import android.view.ViewOutlineProvider; 75 import android.view.ViewTreeObserver; 76 import android.view.WindowInsets; 77 import android.view.WindowManager; 78 import android.view.accessibility.AccessibilityManager; 79 import android.view.animation.AccelerateInterpolator; 80 import android.view.animation.AnimationUtils; 81 import android.view.animation.Interpolator; 82 import android.widget.FrameLayout; 83 import android.widget.HorizontalScrollView; 84 import android.widget.ImageView; 85 import android.widget.LinearLayout; 86 import android.widget.Toast; 87 88 import com.android.internal.logging.UiEventLogger; 89 import com.android.systemui.R; 90 import com.android.systemui.dagger.qualifiers.Main; 91 import com.android.systemui.shared.system.ActivityManagerWrapper; 92 import com.android.systemui.shared.system.QuickStepContract; 93 import com.android.systemui.statusbar.phone.StatusBar; 94 95 import java.util.ArrayList; 96 import java.util.List; 97 import java.util.Optional; 98 import java.util.concurrent.ExecutionException; 99 import java.util.concurrent.TimeUnit; 100 import java.util.concurrent.TimeoutException; 101 import java.util.function.Consumer; 102 103 import javax.inject.Inject; 104 import javax.inject.Singleton; 105 106 import dagger.Lazy; 107 108 /** 109 * Class for handling device screen shots 110 */ 111 @Singleton 112 public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInsetsListener { 113 114 /** 115 * POD used in the AsyncTask which saves an image in the background. 116 */ 117 static class SaveImageInBackgroundData { 118 public Bitmap image; 119 public Consumer<Uri> finisher; 120 public GlobalScreenshot.ActionsReadyListener mActionsReadyListener; 121 public int errorMsgResId; 122 clearImage()123 void clearImage() { 124 image = null; 125 } 126 } 127 128 /** 129 * Structure returned by the SaveImageInBackgroundTask 130 */ 131 static class SavedImageData { 132 public Uri uri; 133 public Notification.Action shareAction; 134 public Notification.Action editAction; 135 public Notification.Action deleteAction; 136 public List<Notification.Action> smartActions; 137 138 /** 139 * Used to reset the return data on error 140 */ reset()141 public void reset() { 142 uri = null; 143 shareAction = null; 144 editAction = null; 145 deleteAction = null; 146 smartActions = null; 147 } 148 } 149 150 abstract static class ActionsReadyListener { onActionsReady(SavedImageData imageData)151 abstract void onActionsReady(SavedImageData imageData); 152 } 153 154 // These strings are used for communicating the action invoked to 155 // ScreenshotNotificationSmartActionsProvider. 156 static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type"; 157 static final String EXTRA_ID = "android:screenshot_id"; 158 static final String ACTION_TYPE_DELETE = "Delete"; 159 static final String ACTION_TYPE_SHARE = "Share"; 160 static final String ACTION_TYPE_EDIT = "Edit"; 161 static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled"; 162 static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent"; 163 164 static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id"; 165 static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification"; 166 static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip"; 167 168 // From WizardManagerHelper.java 169 private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; 170 171 private static final String TAG = "GlobalScreenshot"; 172 173 private static final long SCREENSHOT_FLASH_IN_DURATION_MS = 133; 174 private static final long SCREENSHOT_FLASH_OUT_DURATION_MS = 217; 175 // delay before starting to fade in dismiss button 176 private static final long SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS = 200; 177 private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234; 178 private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500; 179 private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234; 180 private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; 181 private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100; 182 private static final long SCREENSHOT_DISMISS_Y_DURATION_MS = 350; 183 private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 183; 184 private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade 185 private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f; 186 private static final float ROUNDED_CORNER_RADIUS = .05f; 187 private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; 188 private static final int MESSAGE_CORNER_TIMEOUT = 2; 189 190 private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator(); 191 192 private final ScreenshotNotificationsController mNotificationsController; 193 private final UiEventLogger mUiEventLogger; 194 195 private final Context mContext; 196 private final WindowManager mWindowManager; 197 private final WindowManager.LayoutParams mWindowLayoutParams; 198 private final Display mDisplay; 199 private final DisplayMetrics mDisplayMetrics; 200 201 private View mScreenshotLayout; 202 private ScreenshotSelectorView mScreenshotSelectorView; 203 private ImageView mScreenshotAnimatedView; 204 private ImageView mScreenshotPreview; 205 private ImageView mScreenshotFlash; 206 private ImageView mActionsContainerBackground; 207 private HorizontalScrollView mActionsContainer; 208 private LinearLayout mActionsView; 209 private ImageView mBackgroundProtection; 210 private FrameLayout mDismissButton; 211 212 private Bitmap mScreenBitmap; 213 private SaveImageInBackgroundTask mSaveInBgTask; 214 private Animator mScreenshotAnimation; 215 private Runnable mOnCompleteRunnable; 216 private Animator mDismissAnimation; 217 private boolean mInDarkMode = false; 218 private boolean mDirectionLTR = true; 219 private boolean mOrientationPortrait = true; 220 221 private float mCornerSizeX; 222 private float mDismissDeltaY; 223 224 private MediaActionSound mCameraSound; 225 226 private int mNavMode; 227 private int mLeftInset; 228 private int mRightInset; 229 230 // standard material ease 231 private final Interpolator mFastOutSlowIn; 232 233 private final Handler mScreenshotHandler = new Handler(Looper.getMainLooper()) { 234 @Override 235 public void handleMessage(Message msg) { 236 switch (msg.what) { 237 case MESSAGE_CORNER_TIMEOUT: 238 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT); 239 GlobalScreenshot.this.dismissScreenshot("timeout", false); 240 mOnCompleteRunnable.run(); 241 break; 242 default: 243 break; 244 } 245 } 246 }; 247 248 /** 249 * @param context everything needs a context :( 250 */ 251 @Inject GlobalScreenshot( Context context, @Main Resources resources, ScreenshotNotificationsController screenshotNotificationsController, UiEventLogger uiEventLogger)252 public GlobalScreenshot( 253 Context context, @Main Resources resources, 254 ScreenshotNotificationsController screenshotNotificationsController, 255 UiEventLogger uiEventLogger) { 256 mContext = context; 257 mNotificationsController = screenshotNotificationsController; 258 mUiEventLogger = uiEventLogger; 259 260 reloadAssets(); 261 Configuration config = mContext.getResources().getConfiguration(); 262 mInDarkMode = config.isNightModeActive(); 263 mDirectionLTR = config.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; 264 mOrientationPortrait = config.orientation == ORIENTATION_PORTRAIT; 265 266 // Setup the window that we are going to use 267 mWindowLayoutParams = new WindowManager.LayoutParams( 268 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, 269 WindowManager.LayoutParams.TYPE_SCREENSHOT, 270 WindowManager.LayoutParams.FLAG_FULLSCREEN 271 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 272 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 273 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH 274 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 275 | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 276 PixelFormat.TRANSLUCENT); 277 mWindowLayoutParams.setTitle("ScreenshotAnimation"); 278 mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 279 mWindowLayoutParams.setFitInsetsTypes(0 /* types */); 280 mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 281 mDisplay = mWindowManager.getDefaultDisplay(); 282 mDisplayMetrics = new DisplayMetrics(); 283 mDisplay.getRealMetrics(mDisplayMetrics); 284 285 mCornerSizeX = resources.getDimensionPixelSize(R.dimen.global_screenshot_x_scale); 286 mDismissDeltaY = resources.getDimensionPixelSize(R.dimen.screenshot_dismissal_height_delta); 287 288 mFastOutSlowIn = 289 AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in); 290 291 // Setup the Camera shutter sound 292 mCameraSound = new MediaActionSound(); 293 mCameraSound.load(MediaActionSound.SHUTTER_CLICK); 294 } 295 296 @Override // ViewTreeObserver.OnComputeInternalInsetsListener onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)297 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { 298 inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 299 Region touchRegion = new Region(); 300 301 Rect screenshotRect = new Rect(); 302 mScreenshotPreview.getBoundsOnScreen(screenshotRect); 303 touchRegion.op(screenshotRect, Region.Op.UNION); 304 Rect actionsRect = new Rect(); 305 mActionsContainer.getBoundsOnScreen(actionsRect); 306 touchRegion.op(actionsRect, Region.Op.UNION); 307 Rect dismissRect = new Rect(); 308 mDismissButton.getBoundsOnScreen(dismissRect); 309 touchRegion.op(dismissRect, Region.Op.UNION); 310 311 if (QuickStepContract.isGesturalMode(mNavMode)) { 312 // Receive touches in gesture insets such that they don't cause TOUCH_OUTSIDE 313 Rect inset = new Rect(0, 0, mLeftInset, mDisplayMetrics.heightPixels); 314 touchRegion.op(inset, Region.Op.UNION); 315 inset.set(mDisplayMetrics.widthPixels - mRightInset, 0, mDisplayMetrics.widthPixels, 316 mDisplayMetrics.heightPixels); 317 touchRegion.op(inset, Region.Op.UNION); 318 } 319 320 inoutInfo.touchableRegion.set(touchRegion); 321 } 322 onConfigChanged(Configuration newConfig)323 private void onConfigChanged(Configuration newConfig) { 324 boolean needsUpdate = false; 325 // dark mode 326 if (newConfig.isNightModeActive()) { 327 // Night mode is active, we're using dark theme 328 if (!mInDarkMode) { 329 mInDarkMode = true; 330 needsUpdate = true; 331 } 332 } else { 333 // Night mode is not active, we're using the light theme 334 if (mInDarkMode) { 335 mInDarkMode = false; 336 needsUpdate = true; 337 } 338 } 339 340 // RTL configuration 341 switch (newConfig.getLayoutDirection()) { 342 case View.LAYOUT_DIRECTION_LTR: 343 if (!mDirectionLTR) { 344 mDirectionLTR = true; 345 needsUpdate = true; 346 } 347 break; 348 case View.LAYOUT_DIRECTION_RTL: 349 if (mDirectionLTR) { 350 mDirectionLTR = false; 351 needsUpdate = true; 352 } 353 break; 354 } 355 356 // portrait/landscape orientation 357 switch (newConfig.orientation) { 358 case ORIENTATION_PORTRAIT: 359 if (!mOrientationPortrait) { 360 mOrientationPortrait = true; 361 needsUpdate = true; 362 } 363 break; 364 case ORIENTATION_LANDSCAPE: 365 if (mOrientationPortrait) { 366 mOrientationPortrait = false; 367 needsUpdate = true; 368 } 369 break; 370 } 371 372 if (needsUpdate) { 373 reloadAssets(); 374 } 375 376 mNavMode = mContext.getResources().getInteger( 377 com.android.internal.R.integer.config_navBarInteractionMode); 378 } 379 380 /** 381 * Update assets (called when the dark theme status changes). We only need to update the dismiss 382 * button and the actions container background, since the buttons are re-inflated on demand. 383 */ reloadAssets()384 private void reloadAssets() { 385 boolean wasAttached = mScreenshotLayout != null && mScreenshotLayout.isAttachedToWindow(); 386 if (wasAttached) { 387 mWindowManager.removeView(mScreenshotLayout); 388 } 389 390 // Inflate the screenshot layout 391 mScreenshotLayout = LayoutInflater.from(mContext).inflate(R.layout.global_screenshot, null); 392 // TODO(159460485): Remove this when focus is handled properly in the system 393 mScreenshotLayout.setOnTouchListener((v, event) -> { 394 if (event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) { 395 // Once the user touches outside, stop listening for input 396 setWindowFocusable(false); 397 } 398 return false; 399 }); 400 mScreenshotLayout.setOnApplyWindowInsetsListener((v, insets) -> { 401 if (QuickStepContract.isGesturalMode(mNavMode)) { 402 Insets gestureInsets = insets.getInsets( 403 WindowInsets.Type.systemGestures()); 404 mLeftInset = gestureInsets.left; 405 mRightInset = gestureInsets.right; 406 } else { 407 mLeftInset = mRightInset = 0; 408 } 409 return mScreenshotLayout.onApplyWindowInsets(insets); 410 }); 411 mScreenshotLayout.setOnKeyListener(new View.OnKeyListener() { 412 @Override 413 public boolean onKey(View v, int keyCode, KeyEvent event) { 414 if (keyCode == KeyEvent.KEYCODE_BACK) { 415 dismissScreenshot("back pressed", true); 416 return true; 417 } 418 return false; 419 } 420 }); 421 // Get focus so that the key events go to the layout. 422 mScreenshotLayout.setFocusableInTouchMode(true); 423 mScreenshotLayout.requestFocus(); 424 425 mScreenshotAnimatedView = 426 mScreenshotLayout.findViewById(R.id.global_screenshot_animated_view); 427 mScreenshotAnimatedView.setClipToOutline(true); 428 mScreenshotAnimatedView.setOutlineProvider(new ViewOutlineProvider() { 429 @Override 430 public void getOutline(View view, Outline outline) { 431 outline.setRoundRect(new Rect(0, 0, view.getWidth(), view.getHeight()), 432 ROUNDED_CORNER_RADIUS * view.getWidth()); 433 } 434 }); 435 mScreenshotPreview = mScreenshotLayout.findViewById(R.id.global_screenshot_preview); 436 mScreenshotPreview.setClipToOutline(true); 437 mScreenshotPreview.setOutlineProvider(new ViewOutlineProvider() { 438 @Override 439 public void getOutline(View view, Outline outline) { 440 outline.setRoundRect(new Rect(0, 0, view.getWidth(), view.getHeight()), 441 ROUNDED_CORNER_RADIUS * view.getWidth()); 442 } 443 }); 444 445 mActionsContainerBackground = mScreenshotLayout.findViewById( 446 R.id.global_screenshot_actions_container_background); 447 mActionsContainer = mScreenshotLayout.findViewById( 448 R.id.global_screenshot_actions_container); 449 mActionsView = mScreenshotLayout.findViewById(R.id.global_screenshot_actions); 450 mBackgroundProtection = mScreenshotLayout.findViewById( 451 R.id.global_screenshot_actions_background); 452 mDismissButton = mScreenshotLayout.findViewById(R.id.global_screenshot_dismiss_button); 453 mDismissButton.setOnClickListener(view -> { 454 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL); 455 dismissScreenshot("dismiss_button", false); 456 mOnCompleteRunnable.run(); 457 }); 458 459 mScreenshotFlash = mScreenshotLayout.findViewById(R.id.global_screenshot_flash); 460 mScreenshotSelectorView = mScreenshotLayout.findViewById(R.id.global_screenshot_selector); 461 mScreenshotLayout.setFocusable(true); 462 mScreenshotSelectorView.setFocusable(true); 463 mScreenshotSelectorView.setFocusableInTouchMode(true); 464 mScreenshotAnimatedView.setPivotX(0); 465 mScreenshotAnimatedView.setPivotY(0); 466 mActionsContainer.setScrollX(0); 467 468 if (wasAttached) { 469 mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); 470 } 471 } 472 473 /** 474 * Updates the window focusability. If the window is already showing, then it updates the 475 * window immediately, otherwise the layout params will be applied when the window is next 476 * shown. 477 */ setWindowFocusable(boolean focusable)478 private void setWindowFocusable(boolean focusable) { 479 if (focusable) { 480 mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE; 481 } else { 482 mWindowLayoutParams.flags |= FLAG_NOT_FOCUSABLE; 483 } 484 if (mScreenshotLayout.isAttachedToWindow()) { 485 mWindowManager.updateViewLayout(mScreenshotLayout, mWindowLayoutParams); 486 } 487 } 488 489 /** 490 * Creates a new worker thread and saves the screenshot to the media store. 491 */ saveScreenshotInWorkerThread( Consumer<Uri> finisher, @Nullable ActionsReadyListener actionsReadyListener)492 private void saveScreenshotInWorkerThread( 493 Consumer<Uri> finisher, @Nullable ActionsReadyListener actionsReadyListener) { 494 SaveImageInBackgroundData data = new SaveImageInBackgroundData(); 495 data.image = mScreenBitmap; 496 data.finisher = finisher; 497 data.mActionsReadyListener = actionsReadyListener; 498 499 if (mSaveInBgTask != null) { 500 // just log success/failure for the pre-existing screenshot 501 mSaveInBgTask.setActionsReadyListener(new ActionsReadyListener() { 502 @Override 503 void onActionsReady(SavedImageData imageData) { 504 logSuccessOnActionsReady(imageData); 505 } 506 }); 507 } 508 509 mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data); 510 mSaveInBgTask.execute(); 511 } 512 513 /** 514 * Takes a screenshot of the current display and shows an animation. 515 */ takeScreenshot(Consumer<Uri> finisher, Rect crop)516 private void takeScreenshot(Consumer<Uri> finisher, Rect crop) { 517 // copy the input Rect, since SurfaceControl.screenshot can mutate it 518 Rect screenRect = new Rect(crop); 519 int rot = mDisplay.getRotation(); 520 int width = crop.width(); 521 int height = crop.height(); 522 takeScreenshot(SurfaceControl.screenshot(crop, width, height, rot), finisher, screenRect, 523 Insets.NONE, true); 524 } 525 takeScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, boolean showFlash)526 private void takeScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, 527 Insets screenInsets, boolean showFlash) { 528 dismissScreenshot("new screenshot requested", true); 529 530 mScreenBitmap = screenshot; 531 532 if (mScreenBitmap == null) { 533 mNotificationsController.notifyScreenshotError( 534 R.string.screenshot_failed_to_capture_text); 535 finisher.accept(null); 536 mOnCompleteRunnable.run(); 537 return; 538 } 539 540 if (!isUserSetupComplete()) { 541 // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing 542 // and sharing shouldn't be exposed to the user. 543 saveScreenshotAndToast(finisher); 544 return; 545 } 546 547 // Optimizations 548 mScreenBitmap.setHasAlpha(false); 549 mScreenBitmap.prepareToDraw(); 550 551 onConfigChanged(mContext.getResources().getConfiguration()); 552 553 if (mDismissAnimation != null && mDismissAnimation.isRunning()) { 554 mDismissAnimation.cancel(); 555 } 556 557 // The window is focusable by default 558 setWindowFocusable(true); 559 560 // Start the post-screenshot animation 561 startAnimation(finisher, screenRect, screenInsets, showFlash); 562 } 563 takeScreenshot(Consumer<Uri> finisher, Runnable onComplete)564 void takeScreenshot(Consumer<Uri> finisher, Runnable onComplete) { 565 mOnCompleteRunnable = onComplete; 566 567 mDisplay.getRealMetrics(mDisplayMetrics); 568 takeScreenshot( 569 finisher, 570 new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels)); 571 } 572 handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, Insets visibleInsets, int taskId, int userId, ComponentName topComponent, Consumer<Uri> finisher, Runnable onComplete)573 void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, 574 Insets visibleInsets, int taskId, int userId, ComponentName topComponent, 575 Consumer<Uri> finisher, Runnable onComplete) { 576 // TODO: use task Id, userId, topComponent for smart handler 577 578 mOnCompleteRunnable = onComplete; 579 if (aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) { 580 takeScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, false); 581 } else { 582 takeScreenshot(screenshot, finisher, 583 new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight()), Insets.NONE, 584 true); 585 } 586 } 587 588 /** 589 * Displays a screenshot selector 590 */ 591 @SuppressLint("ClickableViewAccessibility") takeScreenshotPartial(final Consumer<Uri> finisher, Runnable onComplete)592 void takeScreenshotPartial(final Consumer<Uri> finisher, Runnable onComplete) { 593 dismissScreenshot("new screenshot requested", true); 594 mOnCompleteRunnable = onComplete; 595 596 mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); 597 mScreenshotSelectorView.setOnTouchListener(new View.OnTouchListener() { 598 @Override 599 public boolean onTouch(View v, MotionEvent event) { 600 ScreenshotSelectorView view = (ScreenshotSelectorView) v; 601 switch (event.getAction()) { 602 case MotionEvent.ACTION_DOWN: 603 view.startSelection((int) event.getX(), (int) event.getY()); 604 return true; 605 case MotionEvent.ACTION_MOVE: 606 view.updateSelection((int) event.getX(), (int) event.getY()); 607 return true; 608 case MotionEvent.ACTION_UP: 609 view.setVisibility(View.GONE); 610 mWindowManager.removeView(mScreenshotLayout); 611 final Rect rect = view.getSelectionRect(); 612 if (rect != null) { 613 if (rect.width() != 0 && rect.height() != 0) { 614 // Need mScreenshotLayout to handle it after the view disappears 615 mScreenshotLayout.post(() -> takeScreenshot(finisher, rect)); 616 } 617 } 618 619 view.stopSelection(); 620 return true; 621 } 622 623 return false; 624 } 625 }); 626 mScreenshotLayout.post(() -> { 627 mScreenshotSelectorView.setVisibility(View.VISIBLE); 628 mScreenshotSelectorView.requestFocus(); 629 }); 630 } 631 632 /** 633 * Cancels screenshot request 634 */ stopScreenshot()635 void stopScreenshot() { 636 // If the selector layer still presents on screen, we remove it and resets its state. 637 if (mScreenshotSelectorView.getSelectionRect() != null) { 638 mWindowManager.removeView(mScreenshotLayout); 639 mScreenshotSelectorView.stopSelection(); 640 } 641 } 642 643 /** 644 * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on 645 * failure). 646 */ saveScreenshotAndToast(Consumer<Uri> finisher)647 private void saveScreenshotAndToast(Consumer<Uri> finisher) { 648 // Play the shutter sound to notify that we've taken a screenshot 649 mScreenshotHandler.post(() -> { 650 mCameraSound.play(MediaActionSound.SHUTTER_CLICK); 651 }); 652 653 saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() { 654 @Override 655 void onActionsReady(SavedImageData imageData) { 656 finisher.accept(imageData.uri); 657 if (imageData.uri == null) { 658 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); 659 mNotificationsController.notifyScreenshotError( 660 R.string.screenshot_failed_to_capture_text); 661 } else { 662 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); 663 664 mScreenshotHandler.post(() -> { 665 Toast.makeText(mContext, R.string.screenshot_saved_title, 666 Toast.LENGTH_SHORT).show(); 667 }); 668 } 669 } 670 }); 671 } 672 isUserSetupComplete()673 private boolean isUserSetupComplete() { 674 return Settings.Secure.getInt(mContext.getContentResolver(), 675 SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; 676 } 677 678 /** 679 * Clears current screenshot 680 */ dismissScreenshot(String reason, boolean immediate)681 void dismissScreenshot(String reason, boolean immediate) { 682 Log.v(TAG, "clearing screenshot: " + reason); 683 mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); 684 mScreenshotLayout.getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 685 if (!immediate) { 686 mDismissAnimation = createScreenshotDismissAnimation(); 687 mDismissAnimation.addListener(new AnimatorListenerAdapter() { 688 @Override 689 public void onAnimationEnd(Animator animation) { 690 super.onAnimationEnd(animation); 691 clearScreenshot(); 692 } 693 }); 694 mDismissAnimation.start(); 695 } else { 696 clearScreenshot(); 697 } 698 } 699 clearScreenshot()700 private void clearScreenshot() { 701 if (mScreenshotLayout.isAttachedToWindow()) { 702 mWindowManager.removeView(mScreenshotLayout); 703 } 704 705 // Clear any references to the bitmap 706 mScreenshotPreview.setImageDrawable(null); 707 mScreenshotAnimatedView.setImageDrawable(null); 708 mScreenshotAnimatedView.setVisibility(View.GONE); 709 mActionsContainerBackground.setVisibility(View.GONE); 710 mActionsContainer.setVisibility(View.GONE); 711 mBackgroundProtection.setAlpha(0f); 712 mDismissButton.setVisibility(View.GONE); 713 mScreenshotPreview.setVisibility(View.GONE); 714 mScreenshotPreview.setLayerType(View.LAYER_TYPE_NONE, null); 715 mScreenshotPreview.setContentDescription( 716 mContext.getResources().getString(R.string.screenshot_preview_description)); 717 mScreenshotLayout.setAlpha(1); 718 mDismissButton.setTranslationY(0); 719 mActionsContainer.setTranslationY(0); 720 mActionsContainerBackground.setTranslationY(0); 721 mScreenshotPreview.setTranslationY(0); 722 } 723 724 /** 725 * Sets up the action shade and its entrance animation, once we get the screenshot URI. 726 */ showUiOnActionsReady(SavedImageData imageData)727 private void showUiOnActionsReady(SavedImageData imageData) { 728 logSuccessOnActionsReady(imageData); 729 730 AccessibilityManager accessibilityManager = (AccessibilityManager) 731 mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 732 long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis( 733 SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS, 734 AccessibilityManager.FLAG_CONTENT_CONTROLS); 735 736 mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); 737 mScreenshotHandler.sendMessageDelayed( 738 mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT), 739 timeoutMs); 740 741 if (imageData.uri != null) { 742 mScreenshotHandler.post(() -> { 743 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { 744 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { 745 @Override 746 public void onAnimationEnd(Animator animation) { 747 super.onAnimationEnd(animation); 748 createScreenshotActionsShadeAnimation(imageData).start(); 749 } 750 }); 751 } else { 752 createScreenshotActionsShadeAnimation(imageData).start(); 753 } 754 }); 755 } 756 } 757 758 /** 759 * Logs success/failure of the screenshot saving task, and shows an error if it failed. 760 */ logSuccessOnActionsReady(SavedImageData imageData)761 private void logSuccessOnActionsReady(SavedImageData imageData) { 762 if (imageData.uri == null) { 763 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); 764 mNotificationsController.notifyScreenshotError( 765 R.string.screenshot_failed_to_capture_text); 766 } else { 767 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); 768 } 769 } 770 771 /** 772 * Starts the animation after taking the screenshot 773 */ startAnimation(final Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, boolean showFlash)774 private void startAnimation(final Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, 775 boolean showFlash) { 776 777 // If power save is on, show a toast so there is some visual indication that a 778 // screenshot has been taken. 779 PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 780 if (powerManager.isPowerSaveMode()) { 781 Toast.makeText(mContext, R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show(); 782 } 783 784 mScreenshotHandler.post(() -> { 785 if (!mScreenshotLayout.isAttachedToWindow()) { 786 mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); 787 } 788 mScreenshotAnimatedView.setImageDrawable( 789 createScreenDrawable(mScreenBitmap, screenInsets)); 790 setAnimatedViewSize(screenRect.width(), screenRect.height()); 791 // Show when the animation starts 792 mScreenshotAnimatedView.setVisibility(View.GONE); 793 794 mScreenshotPreview.setImageDrawable(createScreenDrawable(mScreenBitmap, screenInsets)); 795 // make static preview invisible (from gone) so we can query its location on screen 796 mScreenshotPreview.setVisibility(View.INVISIBLE); 797 798 mScreenshotHandler.post(() -> { 799 mScreenshotLayout.getViewTreeObserver().addOnComputeInternalInsetsListener(this); 800 801 mScreenshotAnimation = 802 createScreenshotDropInAnimation(screenRect, showFlash); 803 804 saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() { 805 @Override 806 void onActionsReady(SavedImageData imageData) { 807 showUiOnActionsReady(imageData); 808 } 809 }); 810 811 // Play the shutter sound to notify that we've taken a screenshot 812 mCameraSound.play(MediaActionSound.SHUTTER_CLICK); 813 814 mScreenshotPreview.setLayerType(View.LAYER_TYPE_HARDWARE, null); 815 mScreenshotPreview.buildLayer(); 816 mScreenshotAnimation.start(); 817 }); 818 }); 819 } 820 createScreenshotDropInAnimation(Rect bounds, boolean showFlash)821 private AnimatorSet createScreenshotDropInAnimation(Rect bounds, boolean showFlash) { 822 Rect previewBounds = new Rect(); 823 mScreenshotPreview.getBoundsOnScreen(previewBounds); 824 825 float cornerScale = 826 mCornerSizeX / (mOrientationPortrait ? bounds.width() : bounds.height()); 827 final float currentScale = 1f; 828 829 mScreenshotAnimatedView.setScaleX(currentScale); 830 mScreenshotAnimatedView.setScaleY(currentScale); 831 832 mDismissButton.setAlpha(0); 833 mDismissButton.setVisibility(View.VISIBLE); 834 835 AnimatorSet dropInAnimation = new AnimatorSet(); 836 ValueAnimator flashInAnimator = ValueAnimator.ofFloat(0, 1); 837 flashInAnimator.setDuration(SCREENSHOT_FLASH_IN_DURATION_MS); 838 flashInAnimator.setInterpolator(mFastOutSlowIn); 839 flashInAnimator.addUpdateListener(animation -> 840 mScreenshotFlash.setAlpha((float) animation.getAnimatedValue())); 841 842 ValueAnimator flashOutAnimator = ValueAnimator.ofFloat(1, 0); 843 flashOutAnimator.setDuration(SCREENSHOT_FLASH_OUT_DURATION_MS); 844 flashOutAnimator.setInterpolator(mFastOutSlowIn); 845 flashOutAnimator.addUpdateListener(animation -> 846 mScreenshotFlash.setAlpha((float) animation.getAnimatedValue())); 847 848 // animate from the current location, to the static preview location 849 final PointF startPos = new PointF(bounds.centerX(), bounds.centerY()); 850 final PointF finalPos = new PointF(previewBounds.centerX(), previewBounds.centerY()); 851 852 ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1); 853 toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS); 854 float xPositionPct = 855 SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; 856 float dismissPct = 857 SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; 858 float scalePct = 859 SCREENSHOT_TO_CORNER_SCALE_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; 860 toCorner.addUpdateListener(animation -> { 861 float t = animation.getAnimatedFraction(); 862 if (t < scalePct) { 863 float scale = MathUtils.lerp( 864 currentScale, cornerScale, mFastOutSlowIn.getInterpolation(t / scalePct)); 865 mScreenshotAnimatedView.setScaleX(scale); 866 mScreenshotAnimatedView.setScaleY(scale); 867 } else { 868 mScreenshotAnimatedView.setScaleX(cornerScale); 869 mScreenshotAnimatedView.setScaleY(cornerScale); 870 } 871 872 float currentScaleX = mScreenshotAnimatedView.getScaleX(); 873 float currentScaleY = mScreenshotAnimatedView.getScaleY(); 874 875 if (t < xPositionPct) { 876 float xCenter = MathUtils.lerp(startPos.x, finalPos.x, 877 mFastOutSlowIn.getInterpolation(t / xPositionPct)); 878 mScreenshotAnimatedView.setX(xCenter - bounds.width() * currentScaleX / 2f); 879 } else { 880 mScreenshotAnimatedView.setX(finalPos.x - bounds.width() * currentScaleX / 2f); 881 } 882 float yCenter = MathUtils.lerp( 883 startPos.y, finalPos.y, mFastOutSlowIn.getInterpolation(t)); 884 mScreenshotAnimatedView.setY(yCenter - bounds.height() * currentScaleY / 2f); 885 886 if (t >= dismissPct) { 887 mDismissButton.setAlpha((t - dismissPct) / (1 - dismissPct)); 888 float currentX = mScreenshotAnimatedView.getX(); 889 float currentY = mScreenshotAnimatedView.getY(); 890 mDismissButton.setY(currentY - mDismissButton.getHeight() / 2f); 891 if (mDirectionLTR) { 892 mDismissButton.setX(currentX 893 + bounds.width() * currentScaleX - mDismissButton.getWidth() / 2f); 894 } else { 895 mDismissButton.setX(currentX - mDismissButton.getWidth() / 2f); 896 } 897 } 898 }); 899 900 toCorner.addListener(new AnimatorListenerAdapter() { 901 @Override 902 public void onAnimationStart(Animator animation) { 903 super.onAnimationStart(animation); 904 mScreenshotAnimatedView.setVisibility(View.VISIBLE); 905 } 906 }); 907 908 mScreenshotFlash.setAlpha(0f); 909 mScreenshotFlash.setVisibility(View.VISIBLE); 910 911 if (showFlash) { 912 dropInAnimation.play(flashOutAnimator).after(flashInAnimator); 913 dropInAnimation.play(flashOutAnimator).with(toCorner); 914 } else { 915 dropInAnimation.play(toCorner); 916 } 917 918 dropInAnimation.addListener(new AnimatorListenerAdapter() { 919 @Override 920 public void onAnimationEnd(Animator animation) { 921 super.onAnimationEnd(animation); 922 mDismissButton.setAlpha(1); 923 float dismissOffset = mDismissButton.getWidth() / 2f; 924 float finalDismissX = mDirectionLTR 925 ? finalPos.x - dismissOffset + bounds.width() * cornerScale / 2f 926 : finalPos.x - dismissOffset - bounds.width() * cornerScale / 2f; 927 mDismissButton.setX(finalDismissX); 928 mDismissButton.setY( 929 finalPos.y - dismissOffset - bounds.height() * cornerScale / 2f); 930 mScreenshotAnimatedView.setScaleX(1); 931 mScreenshotAnimatedView.setScaleY(1); 932 mScreenshotAnimatedView.setX(finalPos.x - bounds.width() * cornerScale / 2f); 933 mScreenshotAnimatedView.setY(finalPos.y - bounds.height() * cornerScale / 2f); 934 mScreenshotAnimatedView.setVisibility(View.GONE); 935 mScreenshotPreview.setVisibility(View.VISIBLE); 936 mScreenshotLayout.forceLayout(); 937 } 938 }); 939 940 return dropInAnimation; 941 } 942 createScreenshotActionsShadeAnimation(SavedImageData imageData)943 private ValueAnimator createScreenshotActionsShadeAnimation(SavedImageData imageData) { 944 LayoutInflater inflater = LayoutInflater.from(mContext); 945 mActionsView.removeAllViews(); 946 mScreenshotLayout.invalidate(); 947 mScreenshotLayout.requestLayout(); 948 mScreenshotLayout.getViewTreeObserver().dispatchOnGlobalLayout(); 949 950 // By default the activities won't be able to start immediately; override this to keep 951 // the same behavior as if started from a notification 952 try { 953 ActivityManager.getService().resumeAppSwitches(); 954 } catch (RemoteException e) { 955 } 956 957 ArrayList<ScreenshotActionChip> chips = new ArrayList<>(); 958 959 for (Notification.Action smartAction : imageData.smartActions) { 960 ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate( 961 R.layout.global_screenshot_action_chip, mActionsView, false); 962 actionChip.setText(smartAction.title); 963 actionChip.setIcon(smartAction.getIcon(), false); 964 actionChip.setPendingIntent(smartAction.actionIntent, 965 () -> { 966 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED); 967 dismissScreenshot("chip tapped", false); 968 mOnCompleteRunnable.run(); 969 }); 970 mActionsView.addView(actionChip); 971 chips.add(actionChip); 972 } 973 974 ScreenshotActionChip shareChip = (ScreenshotActionChip) inflater.inflate( 975 R.layout.global_screenshot_action_chip, mActionsView, false); 976 shareChip.setText(imageData.shareAction.title); 977 shareChip.setIcon(imageData.shareAction.getIcon(), true); 978 shareChip.setPendingIntent(imageData.shareAction.actionIntent, () -> { 979 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED); 980 dismissScreenshot("chip tapped", false); 981 mOnCompleteRunnable.run(); 982 }); 983 mActionsView.addView(shareChip); 984 chips.add(shareChip); 985 986 ScreenshotActionChip editChip = (ScreenshotActionChip) inflater.inflate( 987 R.layout.global_screenshot_action_chip, mActionsView, false); 988 editChip.setText(imageData.editAction.title); 989 editChip.setIcon(imageData.editAction.getIcon(), true); 990 editChip.setPendingIntent(imageData.editAction.actionIntent, () -> { 991 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED); 992 dismissScreenshot("chip tapped", false); 993 mOnCompleteRunnable.run(); 994 }); 995 mActionsView.addView(editChip); 996 chips.add(editChip); 997 998 mScreenshotPreview.setOnClickListener(v -> { 999 try { 1000 imageData.editAction.actionIntent.send(); 1001 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED); 1002 dismissScreenshot("screenshot preview tapped", false); 1003 mOnCompleteRunnable.run(); 1004 } catch (PendingIntent.CanceledException e) { 1005 Log.e(TAG, "Intent cancelled", e); 1006 } 1007 }); 1008 mScreenshotPreview.setContentDescription(imageData.editAction.title); 1009 1010 // remove the margin from the last chip so that it's correctly aligned with the end 1011 LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) 1012 mActionsView.getChildAt(mActionsView.getChildCount() - 1).getLayoutParams(); 1013 params.setMarginEnd(0); 1014 1015 ValueAnimator animator = ValueAnimator.ofFloat(0, 1); 1016 animator.setDuration(SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS); 1017 float alphaFraction = (float) SCREENSHOT_ACTIONS_ALPHA_DURATION_MS 1018 / SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS; 1019 mActionsContainer.setAlpha(0f); 1020 mActionsContainerBackground.setAlpha(0f); 1021 mActionsContainer.setVisibility(View.VISIBLE); 1022 mActionsContainerBackground.setVisibility(View.VISIBLE); 1023 1024 animator.addUpdateListener(animation -> { 1025 float t = animation.getAnimatedFraction(); 1026 mBackgroundProtection.setAlpha(t); 1027 float containerAlpha = t < alphaFraction ? t / alphaFraction : 1; 1028 mActionsContainer.setAlpha(containerAlpha); 1029 mActionsContainerBackground.setAlpha(containerAlpha); 1030 float containerScale = SCREENSHOT_ACTIONS_START_SCALE_X 1031 + (t * (1 - SCREENSHOT_ACTIONS_START_SCALE_X)); 1032 mActionsContainer.setScaleX(containerScale); 1033 mActionsContainerBackground.setScaleX(containerScale); 1034 for (ScreenshotActionChip chip : chips) { 1035 chip.setAlpha(t); 1036 chip.setScaleX(1 / containerScale); // invert to keep size of children constant 1037 } 1038 mActionsContainer.setScrollX(mDirectionLTR ? 0 : mActionsContainer.getWidth()); 1039 mActionsContainer.setPivotX(mDirectionLTR ? 0 : mActionsContainer.getWidth()); 1040 mActionsContainerBackground.setPivotX( 1041 mDirectionLTR ? 0 : mActionsContainerBackground.getWidth()); 1042 }); 1043 return animator; 1044 } 1045 createScreenshotDismissAnimation()1046 private AnimatorSet createScreenshotDismissAnimation() { 1047 ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); 1048 alphaAnim.setStartDelay(SCREENSHOT_DISMISS_ALPHA_OFFSET_MS); 1049 alphaAnim.setDuration(SCREENSHOT_DISMISS_ALPHA_DURATION_MS); 1050 alphaAnim.addUpdateListener(animation -> { 1051 mScreenshotLayout.setAlpha(1 - animation.getAnimatedFraction()); 1052 }); 1053 1054 ValueAnimator yAnim = ValueAnimator.ofFloat(0, 1); 1055 yAnim.setInterpolator(mAccelerateInterpolator); 1056 yAnim.setDuration(SCREENSHOT_DISMISS_Y_DURATION_MS); 1057 float screenshotStartY = mScreenshotPreview.getTranslationY(); 1058 float dismissStartY = mDismissButton.getTranslationY(); 1059 yAnim.addUpdateListener(animation -> { 1060 float yDelta = MathUtils.lerp(0, mDismissDeltaY, animation.getAnimatedFraction()); 1061 mScreenshotPreview.setTranslationY(screenshotStartY + yDelta); 1062 mDismissButton.setTranslationY(dismissStartY + yDelta); 1063 mActionsContainer.setTranslationY(yDelta); 1064 mActionsContainerBackground.setTranslationY(yDelta); 1065 }); 1066 1067 AnimatorSet animSet = new AnimatorSet(); 1068 animSet.play(yAnim).with(alphaAnim); 1069 1070 return animSet; 1071 } 1072 setAnimatedViewSize(int width, int height)1073 private void setAnimatedViewSize(int width, int height) { 1074 ViewGroup.LayoutParams layoutParams = mScreenshotAnimatedView.getLayoutParams(); 1075 layoutParams.width = width; 1076 layoutParams.height = height; 1077 mScreenshotAnimatedView.setLayoutParams(layoutParams); 1078 } 1079 1080 /** Does the aspect ratio of the bitmap with insets removed match the bounds. */ aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, Rect screenBounds)1081 private boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, Rect screenBounds) { 1082 int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right; 1083 int insettedHeight = bitmap.getHeight() - bitmapInsets.top - bitmapInsets.bottom; 1084 1085 if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 1086 || bitmap.getHeight() == 0) { 1087 Log.e(TAG, String.format( 1088 "Provided bitmap and insets create degenerate region: %dx%d %s", 1089 bitmap.getWidth(), bitmap.getHeight(), bitmapInsets)); 1090 return false; 1091 } 1092 1093 float insettedBitmapAspect = ((float) insettedWidth) / insettedHeight; 1094 float boundsAspect = ((float) screenBounds.width()) / screenBounds.height(); 1095 1096 boolean matchWithinTolerance = Math.abs(insettedBitmapAspect - boundsAspect) < 0.1f; 1097 if (!matchWithinTolerance) { 1098 Log.d(TAG, String.format("aspectRatiosMatch: don't match bitmap: %f, bounds: %f", 1099 insettedBitmapAspect, boundsAspect)); 1100 } 1101 1102 return matchWithinTolerance; 1103 } 1104 1105 /** 1106 * Create a drawable using the size of the bitmap and insets as the fractional inset parameters. 1107 */ 1108 private Drawable createScreenDrawable(Bitmap bitmap, Insets insets) { 1109 int insettedWidth = bitmap.getWidth() - insets.left - insets.right; 1110 int insettedHeight = bitmap.getHeight() - insets.top - insets.bottom; 1111 1112 BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap); 1113 if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 1114 || bitmap.getHeight() == 0) { 1115 Log.e(TAG, String.format( 1116 "Can't create insetted drawable, using 0 insets " 1117 + "bitmap and insets create degenerate region: %dx%d %s", 1118 bitmap.getWidth(), bitmap.getHeight(), insets)); 1119 return bitmapDrawable; 1120 } 1121 1122 InsetDrawable insetDrawable = new InsetDrawable(bitmapDrawable, 1123 -1f * insets.left / insettedWidth, 1124 -1f * insets.top / insettedHeight, 1125 -1f * insets.right / insettedWidth, 1126 -1f * insets.bottom / insettedHeight); 1127 1128 if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) { 1129 // Are any of the insets negative, meaning the bitmap is smaller than the bounds so need 1130 // to fill in the background of the drawable. 1131 return new LayerDrawable(new Drawable[] { 1132 new ColorDrawable(Color.BLACK), insetDrawable}); 1133 } else { 1134 return insetDrawable; 1135 } 1136 } 1137 1138 /** 1139 * Receiver to proxy the share or edit intent, used to clean up the notification and send 1140 * appropriate signals to the system (ie. to dismiss the keyguard if necessary). 1141 */ 1142 public static class ActionProxyReceiver extends BroadcastReceiver { 1143 static final int CLOSE_WINDOWS_TIMEOUT_MILLIS = 3000; 1144 private final StatusBar mStatusBar; 1145 1146 @Inject 1147 public ActionProxyReceiver(Optional<Lazy<StatusBar>> statusBarLazy) { 1148 Lazy<StatusBar> statusBar = statusBarLazy.orElse(null); 1149 mStatusBar = statusBar != null ? statusBar.get() : null; 1150 } 1151 1152 @Override 1153 public void onReceive(Context context, final Intent intent) { 1154 Runnable startActivityRunnable = () -> { 1155 try { 1156 ActivityManagerWrapper.getInstance().closeSystemWindows( 1157 SYSTEM_DIALOG_REASON_SCREENSHOT).get( 1158 CLOSE_WINDOWS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 1159 } catch (TimeoutException | InterruptedException | ExecutionException e) { 1160 Slog.e(TAG, "Unable to share screenshot", e); 1161 return; 1162 } 1163 1164 PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT); 1165 if (intent.getBooleanExtra(EXTRA_CANCEL_NOTIFICATION, false)) { 1166 ScreenshotNotificationsController.cancelScreenshotNotification(context); 1167 } 1168 ActivityOptions opts = ActivityOptions.makeBasic(); 1169 opts.setDisallowEnterPictureInPictureWhileLaunching( 1170 intent.getBooleanExtra(EXTRA_DISALLOW_ENTER_PIP, false)); 1171 try { 1172 actionIntent.send(context, 0, null, null, null, null, opts.toBundle()); 1173 } catch (PendingIntent.CanceledException e) { 1174 Log.e(TAG, "Pending intent canceled", e); 1175 } 1176 1177 }; 1178 1179 if (mStatusBar != null) { 1180 mStatusBar.executeRunnableDismissingKeyguard(startActivityRunnable, null, 1181 true /* dismissShade */, true /* afterKeyguardGone */, 1182 true /* deferred */); 1183 } else { 1184 startActivityRunnable.run(); 1185 } 1186 1187 if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) { 1188 String actionType = Intent.ACTION_EDIT.equals(intent.getAction()) 1189 ? ACTION_TYPE_EDIT 1190 : ACTION_TYPE_SHARE; 1191 ScreenshotSmartActions.notifyScreenshotAction( 1192 context, intent.getStringExtra(EXTRA_ID), actionType, false); 1193 } 1194 } 1195 } 1196 1197 /** 1198 * Removes the notification for a screenshot after a share target is chosen. 1199 */ 1200 public static class TargetChosenReceiver extends BroadcastReceiver { 1201 @Override 1202 public void onReceive(Context context, Intent intent) { 1203 // Clear the notification only after the user has chosen a share action 1204 ScreenshotNotificationsController.cancelScreenshotNotification(context); 1205 } 1206 } 1207 1208 /** 1209 * Removes the last screenshot. 1210 */ 1211 public static class DeleteScreenshotReceiver extends BroadcastReceiver { 1212 @Override 1213 public void onReceive(Context context, Intent intent) { 1214 if (!intent.hasExtra(SCREENSHOT_URI_ID)) { 1215 return; 1216 } 1217 1218 // Clear the notification when the image is deleted 1219 ScreenshotNotificationsController.cancelScreenshotNotification(context); 1220 1221 // And delete the image from the media store 1222 final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID)); 1223 new DeleteImageInBackgroundTask(context).execute(uri); 1224 if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) { 1225 ScreenshotSmartActions.notifyScreenshotAction( 1226 context, intent.getStringExtra(EXTRA_ID), ACTION_TYPE_DELETE, false); 1227 } 1228 } 1229 } 1230 1231 /** 1232 * Executes the smart action tapped by the user in the notification. 1233 */ 1234 public static class SmartActionsReceiver extends BroadcastReceiver { 1235 @Override 1236 public void onReceive(Context context, Intent intent) { 1237 PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT); 1238 String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE); 1239 Slog.d(TAG, "Executing smart action [" + actionType + "]:" + pendingIntent.getIntent()); 1240 ActivityOptions opts = ActivityOptions.makeBasic(); 1241 1242 try { 1243 pendingIntent.send(context, 0, null, null, null, null, opts.toBundle()); 1244 } catch (PendingIntent.CanceledException e) { 1245 Log.e(TAG, "Pending intent canceled", e); 1246 } 1247 1248 ScreenshotSmartActions.notifyScreenshotAction( 1249 context, intent.getStringExtra(EXTRA_ID), actionType, true); 1250 } 1251 } 1252 } 1253