From 4ad3a639d6d6354674acb1506e40bc732dbc9578 Mon Sep 17 00:00:00 2001 From: Miranda Kephart Date: Mon, 19 Oct 2020 16:56:22 -0400 Subject: [PATCH] Refactor GlobalScreenshot Pull apart into View vs Controller model, to separate views/ animations from state handling. This is a large change, but not as large as it looks since most sections are copied from GlobalScreenshot into either ScreenshotView or ScreenshotController. Roughly speaking, ScreenshotController handles the overall screenshot initiation, flow, and callbacks, while ScreenshotView handles the subviews and constructs the animations. Test: manual Change-Id: Ic3238571ac2d7aa749eff078c2297f2b6ed06b78 --- .../SystemUI/res/layout/global_screenshot.xml | 4 +- .../screenshot/ActionProxyReceiver.java | 12 +- .../screenshot/DeleteScreenshotReceiver.java | 8 +- .../systemui/screenshot/GlobalScreenshot.java | 1199 ----------------- .../screenshot/SaveImageInBackgroundTask.java | 38 +- .../screenshot/ScreenshotController.java | 665 +++++++++ .../ScreenshotNotificationsController.java | 2 +- .../screenshot/ScreenshotSelectorView.java | 83 +- .../systemui/screenshot/ScreenshotView.java | 611 +++++++++ .../screenshot/SmartActionsReceiver.java | 6 +- .../screenshot/TakeScreenshotService.java | 10 +- .../screenshot/ActionProxyReceiverTest.java | 8 +- .../DeleteScreenshotReceiverTest.java | 8 +- ...creenshotNotificationSmartActionsTest.java | 30 +- .../screenshot/SmartActionsReceiverTest.java | 6 +- 15 files changed, 1403 insertions(+), 1287 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java create mode 100644 packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java create mode 100644 packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java diff --git a/packages/SystemUI/res/layout/global_screenshot.xml b/packages/SystemUI/res/layout/global_screenshot.xml index ef7325ea8f380..e0333eb82d518 100644 --- a/packages/SystemUI/res/layout/global_screenshot.xml +++ b/packages/SystemUI/res/layout/global_screenshot.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - + diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java index 3fd7f94514f3d..5c26d9400c9f6 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java @@ -16,12 +16,12 @@ package com.android.systemui.screenshot; -import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_EDIT; -import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_SHARE; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_INTENT; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED; +import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_EDIT; +import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_SHARE; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_INTENT; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_DISALLOW_ENTER_PIP; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED; import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT; import android.app.ActivityOptions; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java index 9028bb57c8e5a..35839f39b491a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java @@ -16,10 +16,10 @@ package com.android.systemui.screenshot; -import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_DELETE; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED; -import static com.android.systemui.screenshot.GlobalScreenshot.SCREENSHOT_URI_ID; +import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_DELETE; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED; +import static com.android.systemui.screenshot.ScreenshotController.SCREENSHOT_URI_ID; import android.content.BroadcastReceiver; import android.content.ContentResolver; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java deleted file mode 100644 index 818bb9d8d78fc..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ /dev/null @@ -1,1199 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.screenshot; - -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; -import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.view.Display.DEFAULT_DISPLAY; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ValueAnimator; -import android.annotation.Nullable; -import android.annotation.SuppressLint; -import android.app.ActivityManager; -import android.app.Notification; -import android.app.PendingIntent; -import android.app.WindowContext; -import android.content.ComponentName; -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Insets; -import android.graphics.Outline; -import android.graphics.PixelFormat; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.Region; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.graphics.drawable.InsetDrawable; -import android.graphics.drawable.LayerDrawable; -import android.hardware.display.DisplayManager; -import android.media.MediaActionSound; -import android.net.Uri; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.RemoteException; -import android.provider.Settings; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.MathUtils; -import android.view.Display; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.SurfaceControl; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import android.view.ViewTreeObserver; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; -import android.widget.FrameLayout; -import android.widget.HorizontalScrollView; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.Toast; - -import com.android.internal.logging.UiEventLogger; -import com.android.systemui.R; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.shared.system.QuickStepContract; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -import javax.inject.Inject; - -/** - * Class for handling device screen shots - */ -@SysUISingleton -public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInsetsListener { - - /** - * POD used in the AsyncTask which saves an image in the background. - */ - static class SaveImageInBackgroundData { - public Bitmap image; - public Consumer finisher; - public GlobalScreenshot.ActionsReadyListener mActionsReadyListener; - - void clearImage() { - image = null; - } - } - - /** - * Structure returned by the SaveImageInBackgroundTask - */ - static class SavedImageData { - public Uri uri; - public Notification.Action shareAction; - public Notification.Action editAction; - public Notification.Action deleteAction; - public List smartActions; - - /** - * Used to reset the return data on error - */ - public void reset() { - uri = null; - shareAction = null; - editAction = null; - deleteAction = null; - smartActions = null; - } - } - - abstract static class ActionsReadyListener { - abstract void onActionsReady(SavedImageData imageData); - } - - // These strings are used for communicating the action invoked to - // ScreenshotNotificationSmartActionsProvider. - static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type"; - static final String EXTRA_ID = "android:screenshot_id"; - static final String ACTION_TYPE_DELETE = "Delete"; - static final String ACTION_TYPE_SHARE = "Share"; - static final String ACTION_TYPE_EDIT = "Edit"; - static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled"; - static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent"; - - static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id"; - static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification"; - static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip"; - - // From WizardManagerHelper.java - private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; - - private static final String TAG = "GlobalScreenshot"; - - private static final long SCREENSHOT_FLASH_IN_DURATION_MS = 133; - private static final long SCREENSHOT_FLASH_OUT_DURATION_MS = 217; - // delay before starting to fade in dismiss button - private static final long SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS = 200; - private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234; - private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500; - private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234; - private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; - private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100; - private static final long SCREENSHOT_DISMISS_Y_DURATION_MS = 350; - private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 183; - private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade - private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f; - private static final float ROUNDED_CORNER_RADIUS = .05f; - private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; - private static final int MESSAGE_CORNER_TIMEOUT = 2; - - private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator(); - - private final ScreenshotNotificationsController mNotificationsController; - private final UiEventLogger mUiEventLogger; - - private final Context mContext; - private final ScreenshotSmartActions mScreenshotSmartActions; - private final WindowManager mWindowManager; - private final WindowManager.LayoutParams mWindowLayoutParams; - private final Display mDisplay; - private final DisplayMetrics mDisplayMetrics; - private final AccessibilityManager mAccessibilityManager; - - private View mScreenshotLayout; - private ScreenshotSelectorView mScreenshotSelectorView; - private ImageView mScreenshotAnimatedView; - private ImageView mScreenshotPreview; - private ImageView mScreenshotFlash; - private ImageView mActionsContainerBackground; - private HorizontalScrollView mActionsContainer; - private LinearLayout mActionsView; - private ScreenshotActionChip mShareChip; - private ScreenshotActionChip mEditChip; - private ImageView mBackgroundProtection; - private FrameLayout mDismissButton; - - private Bitmap mScreenBitmap; - private SaveImageInBackgroundTask mSaveInBgTask; - private Animator mScreenshotAnimation; - private Runnable mOnCompleteRunnable; - private Animator mDismissAnimation; - private boolean mInDarkMode; - private boolean mDirectionLTR; - private boolean mOrientationPortrait; - - private float mCornerSizeX; - private float mDismissDeltaY; - - private MediaActionSound mCameraSound; - - private int mNavMode; - private int mLeftInset; - private int mRightInset; - - private ArrayList mSmartChips = new ArrayList<>(); - private PendingInteraction mPendingInteraction; - private enum PendingInteraction { - PREVIEW, - EDIT, - SHARE - } - - // standard material ease - private final Interpolator mFastOutSlowIn; - - private final Handler mScreenshotHandler = new Handler(Looper.getMainLooper()) { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MESSAGE_CORNER_TIMEOUT: - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT); - GlobalScreenshot.this.dismissScreenshot("timeout", false); - mOnCompleteRunnable.run(); - break; - default: - break; - } - } - }; - - @Inject - public GlobalScreenshot(Context context, - ScreenshotSmartActions screenshotSmartActions, - ScreenshotNotificationsController screenshotNotificationsController, - UiEventLogger uiEventLogger) { - - // Create a visual (Window) context - // After this, our windowToken is available from mContext.getActivityToken() - final DisplayManager dm = context.getSystemService(DisplayManager.class); - mDisplay = dm.getDisplay(DEFAULT_DISPLAY); - Context displayContext = context.createDisplayContext(mDisplay); - mContext = new WindowContext( - displayContext, WindowManager.LayoutParams.TYPE_SCREENSHOT, null); - mWindowManager = mContext.getSystemService(WindowManager.class); - - mScreenshotSmartActions = screenshotSmartActions; - mNotificationsController = screenshotNotificationsController; - mUiEventLogger = uiEventLogger; - mAccessibilityManager = AccessibilityManager.getInstance(mContext); - - reloadAssets(); - Configuration config = mContext.getResources().getConfiguration(); - mInDarkMode = config.isNightModeActive(); - mDirectionLTR = config.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; - mOrientationPortrait = config.orientation == ORIENTATION_PORTRAIT; - - // Setup the window that we are going to use - mWindowLayoutParams = new WindowManager.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, - WindowManager.LayoutParams.TYPE_SCREENSHOT, - WindowManager.LayoutParams.FLAG_FULLSCREEN - | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH - | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, - PixelFormat.TRANSLUCENT); - mWindowLayoutParams.setTitle("ScreenshotAnimation"); - mWindowLayoutParams.layoutInDisplayCutoutMode = - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - mWindowLayoutParams.setFitInsetsTypes(0 /* types */); - mDisplayMetrics = new DisplayMetrics(); - mDisplay.getRealMetrics(mDisplayMetrics); - - final Resources resources = mContext.getResources(); - mCornerSizeX = resources.getDimensionPixelSize(R.dimen.global_screenshot_x_scale); - mDismissDeltaY = resources.getDimensionPixelSize(R.dimen.screenshot_dismissal_height_delta); - - mFastOutSlowIn = - AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in); - - // Setup the Camera shutter sound - mCameraSound = new MediaActionSound(); - mCameraSound.load(MediaActionSound.SHUTTER_CLICK); - } - - @Override // ViewTreeObserver.OnComputeInternalInsetsListener - public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { - inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - Region touchRegion = new Region(); - - Rect screenshotRect = new Rect(); - mScreenshotPreview.getBoundsOnScreen(screenshotRect); - touchRegion.op(screenshotRect, Region.Op.UNION); - Rect actionsRect = new Rect(); - mActionsContainer.getBoundsOnScreen(actionsRect); - touchRegion.op(actionsRect, Region.Op.UNION); - Rect dismissRect = new Rect(); - mDismissButton.getBoundsOnScreen(dismissRect); - touchRegion.op(dismissRect, Region.Op.UNION); - - if (QuickStepContract.isGesturalMode(mNavMode)) { - // Receive touches in gesture insets such that they don't cause TOUCH_OUTSIDE - Rect inset = new Rect(0, 0, mLeftInset, mDisplayMetrics.heightPixels); - touchRegion.op(inset, Region.Op.UNION); - inset.set(mDisplayMetrics.widthPixels - mRightInset, 0, mDisplayMetrics.widthPixels, - mDisplayMetrics.heightPixels); - touchRegion.op(inset, Region.Op.UNION); - } - - inoutInfo.touchableRegion.set(touchRegion); - } - - void takeScreenshotFullscreen(Consumer finisher, Runnable onComplete) { - mOnCompleteRunnable = onComplete; - - mDisplay.getRealMetrics(mDisplayMetrics); - takeScreenshotInternal( - finisher, - new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels)); - } - - void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, - Insets visibleInsets, int taskId, int userId, ComponentName topComponent, - Consumer finisher, Runnable onComplete) { - // TODO: use task Id, userId, topComponent for smart handler - mOnCompleteRunnable = onComplete; - - if (screenshot == null) { - Log.e(TAG, "Got null bitmap from screenshot message"); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_capture_text); - finisher.accept(null); - mOnCompleteRunnable.run(); - return; - } - - if (aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) { - saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, false); - } else { - saveScreenshot(screenshot, finisher, - new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight()), Insets.NONE, - true); - } - } - - /** - * Displays a screenshot selector - */ - @SuppressLint("ClickableViewAccessibility") - void takeScreenshotPartial(final Consumer finisher, Runnable onComplete) { - dismissScreenshot("new screenshot requested", true); - mOnCompleteRunnable = onComplete; - - mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); - mScreenshotSelectorView.setOnTouchListener((v, event) -> { - ScreenshotSelectorView view = (ScreenshotSelectorView) v; - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - view.startSelection((int) event.getX(), (int) event.getY()); - return true; - case MotionEvent.ACTION_MOVE: - view.updateSelection((int) event.getX(), (int) event.getY()); - return true; - case MotionEvent.ACTION_UP: - view.setVisibility(View.GONE); - mWindowManager.removeView(mScreenshotLayout); - final Rect rect = view.getSelectionRect(); - if (rect != null) { - if (rect.width() != 0 && rect.height() != 0) { - // Need mScreenshotLayout to handle it after the view disappears - mScreenshotLayout.post(() -> takeScreenshotInternal(finisher, rect)); - } - } - - view.stopSelection(); - return true; - } - - return false; - }); - mScreenshotLayout.post(() -> { - mScreenshotSelectorView.setVisibility(View.VISIBLE); - mScreenshotSelectorView.requestFocus(); - }); - } - - /** - * Cancels screenshot request - */ - void stopScreenshot() { - // If the selector layer still presents on screen, we remove it and resets its state. - if (mScreenshotSelectorView.getSelectionRect() != null) { - mWindowManager.removeView(mScreenshotLayout); - mScreenshotSelectorView.stopSelection(); - } - } - - /** - * Clears current screenshot - */ - void dismissScreenshot(String reason, boolean immediate) { - Log.v(TAG, "clearing screenshot: " + reason); - mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); - mScreenshotLayout.getViewTreeObserver().removeOnComputeInternalInsetsListener(this); - if (!immediate) { - mDismissAnimation = createScreenshotDismissAnimation(); - mDismissAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - clearScreenshot(); - } - }); - mDismissAnimation.start(); - } else { - clearScreenshot(); - } - } - - private void onConfigChanged(Configuration newConfig) { - boolean needsUpdate = false; - // dark mode - if (newConfig.isNightModeActive()) { - // Night mode is active, we're using dark theme - if (!mInDarkMode) { - mInDarkMode = true; - needsUpdate = true; - } - } else { - // Night mode is not active, we're using the light theme - if (mInDarkMode) { - mInDarkMode = false; - needsUpdate = true; - } - } - - // RTL configuration - switch (newConfig.getLayoutDirection()) { - case View.LAYOUT_DIRECTION_LTR: - if (!mDirectionLTR) { - mDirectionLTR = true; - needsUpdate = true; - } - break; - case View.LAYOUT_DIRECTION_RTL: - if (mDirectionLTR) { - mDirectionLTR = false; - needsUpdate = true; - } - break; - } - - // portrait/landscape orientation - switch (newConfig.orientation) { - case ORIENTATION_PORTRAIT: - if (!mOrientationPortrait) { - mOrientationPortrait = true; - needsUpdate = true; - } - break; - case ORIENTATION_LANDSCAPE: - if (mOrientationPortrait) { - mOrientationPortrait = false; - needsUpdate = true; - } - break; - } - - if (needsUpdate) { - reloadAssets(); - } - - mNavMode = mContext.getResources().getInteger( - com.android.internal.R.integer.config_navBarInteractionMode); - } - - /** - * Update assets (called when the dark theme status changes). We only need to update the dismiss - * button and the actions container background, since the buttons are re-inflated on demand. - */ - private void reloadAssets() { - boolean wasAttached = mScreenshotLayout != null && mScreenshotLayout.isAttachedToWindow(); - if (wasAttached) { - mWindowManager.removeView(mScreenshotLayout); - } - - // Inflate the screenshot layout - mScreenshotLayout = LayoutInflater.from(mContext).inflate(R.layout.global_screenshot, null); - // TODO(159460485): Remove this when focus is handled properly in the system - mScreenshotLayout.setOnTouchListener((v, event) -> { - if (event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) { - // Once the user touches outside, stop listening for input - setWindowFocusable(false); - } - return false; - }); - mScreenshotLayout.setOnApplyWindowInsetsListener((v, insets) -> { - if (QuickStepContract.isGesturalMode(mNavMode)) { - Insets gestureInsets = insets.getInsets( - WindowInsets.Type.systemGestures()); - mLeftInset = gestureInsets.left; - mRightInset = gestureInsets.right; - } else { - mLeftInset = mRightInset = 0; - } - return mScreenshotLayout.onApplyWindowInsets(insets); - }); - mScreenshotLayout.setOnKeyListener((v, keyCode, event) -> { - if (keyCode == KeyEvent.KEYCODE_BACK) { - dismissScreenshot("back pressed", false); - return true; - } - return false; - }); - // Get focus so that the key events go to the layout. - mScreenshotLayout.setFocusableInTouchMode(true); - mScreenshotLayout.requestFocus(); - - mScreenshotAnimatedView = - mScreenshotLayout.findViewById(R.id.global_screenshot_animated_view); - mScreenshotAnimatedView.setClipToOutline(true); - mScreenshotAnimatedView.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRoundRect(new Rect(0, 0, view.getWidth(), view.getHeight()), - ROUNDED_CORNER_RADIUS * view.getWidth()); - } - }); - mScreenshotPreview = mScreenshotLayout.findViewById(R.id.global_screenshot_preview); - mScreenshotPreview.setClipToOutline(true); - mScreenshotPreview.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRoundRect(new Rect(0, 0, view.getWidth(), view.getHeight()), - ROUNDED_CORNER_RADIUS * view.getWidth()); - } - }); - - mActionsContainerBackground = mScreenshotLayout.findViewById( - R.id.global_screenshot_actions_container_background); - mActionsContainer = mScreenshotLayout.findViewById( - R.id.global_screenshot_actions_container); - mActionsView = mScreenshotLayout.findViewById(R.id.global_screenshot_actions); - mBackgroundProtection = mScreenshotLayout.findViewById( - R.id.global_screenshot_actions_background); - mDismissButton = mScreenshotLayout.findViewById(R.id.global_screenshot_dismiss_button); - mDismissButton.setOnClickListener(view -> { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL); - dismissScreenshot("dismiss_button", false); - mOnCompleteRunnable.run(); - }); - - mShareChip = mActionsContainer.findViewById(R.id.screenshot_share_chip); - mEditChip = mActionsContainer.findViewById(R.id.screenshot_edit_chip); - - mScreenshotFlash = mScreenshotLayout.findViewById(R.id.global_screenshot_flash); - mScreenshotSelectorView = mScreenshotLayout.findViewById(R.id.global_screenshot_selector); - mScreenshotLayout.setFocusable(true); - mScreenshotSelectorView.setFocusable(true); - mScreenshotSelectorView.setFocusableInTouchMode(true); - mScreenshotAnimatedView.setPivotX(0); - mScreenshotAnimatedView.setPivotY(0); - mActionsContainer.setScrollX(0); - - if (wasAttached) { - mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); - } - } - - /** - * Takes a screenshot of the current display and shows an animation. - */ - private void takeScreenshotInternal(Consumer finisher, Rect crop) { - // copy the input Rect, since SurfaceControl.screenshot can mutate it - Rect screenRect = new Rect(crop); - int width = crop.width(); - int height = crop.height(); - final IBinder displayToken = SurfaceControl.getInternalDisplayToken(); - final SurfaceControl.DisplayCaptureArgs captureArgs = - new SurfaceControl.DisplayCaptureArgs.Builder(displayToken) - .setSourceCrop(crop) - .setSize(width, height) - .build(); - final SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = - SurfaceControl.captureDisplay(captureArgs); - Bitmap screenshot = screenshotBuffer == null ? null : screenshotBuffer.asBitmap(); - - if (screenshot == null) { - Log.e(TAG, "Screenshot bitmap was null"); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_capture_text); - finisher.accept(null); - mOnCompleteRunnable.run(); - return; - } - - saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, true); - } - - private void saveScreenshot(Bitmap screenshot, Consumer finisher, Rect screenRect, - Insets screenInsets, boolean showFlash) { - if (mAccessibilityManager.isEnabled()) { - AccessibilityEvent event = - new AccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - event.setContentDescription( - mContext.getResources().getString(R.string.screenshot_saving_title)); - mAccessibilityManager.sendAccessibilityEvent(event); - } - - if (mScreenshotLayout.isAttachedToWindow()) { - // if we didn't already dismiss for another reason - if (mDismissAnimation == null || !mDismissAnimation.isRunning()) { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED); - } - dismissScreenshot("new screenshot requested", true); - } - - mScreenBitmap = screenshot; - - if (!isUserSetupComplete()) { - // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing - // and sharing shouldn't be exposed to the user. - saveScreenshotAndToast(finisher); - return; - } - - // Optimizations - mScreenBitmap.setHasAlpha(false); - mScreenBitmap.prepareToDraw(); - - onConfigChanged(mContext.getResources().getConfiguration()); - - if (mDismissAnimation != null && mDismissAnimation.isRunning()) { - mDismissAnimation.cancel(); - } - - // The window is focusable by default - setWindowFocusable(true); - - // Start the post-screenshot animation - startAnimation(finisher, screenRect, screenInsets, showFlash); - } - - /** - * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on - * failure). - */ - private void saveScreenshotAndToast(Consumer finisher) { - // Play the shutter sound to notify that we've taken a screenshot - mScreenshotHandler.post(() -> { - mCameraSound.play(MediaActionSound.SHUTTER_CLICK); - }); - - saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() { - @Override - void onActionsReady(SavedImageData imageData) { - finisher.accept(imageData.uri); - if (imageData.uri == null) { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_save_text); - } else { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); - - mScreenshotHandler.post(() -> { - Toast.makeText(mContext, R.string.screenshot_saved_title, - Toast.LENGTH_SHORT).show(); - }); - } - } - }); - } - - /** - * Starts the animation after taking the screenshot - */ - private void startAnimation(final Consumer finisher, Rect screenRect, Insets screenInsets, - boolean showFlash) { - mScreenshotHandler.post(() -> { - if (!mScreenshotLayout.isAttachedToWindow()) { - mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); - } - mScreenshotAnimatedView.setImageDrawable( - createScreenDrawable(mScreenBitmap, screenInsets)); - setAnimatedViewSize(screenRect.width(), screenRect.height()); - // Show when the animation starts - mScreenshotAnimatedView.setVisibility(View.GONE); - - mScreenshotPreview.setImageDrawable(createScreenDrawable(mScreenBitmap, screenInsets)); - // make static preview invisible (from gone) so we can query its location on screen - mScreenshotPreview.setVisibility(View.INVISIBLE); - - mScreenshotHandler.post(() -> { - mScreenshotLayout.getViewTreeObserver().addOnComputeInternalInsetsListener(this); - - mScreenshotAnimation = - createScreenshotDropInAnimation(screenRect, showFlash); - - saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() { - @Override - void onActionsReady(SavedImageData imageData) { - showUiOnActionsReady(imageData); - } - }); - - // Play the shutter sound to notify that we've taken a screenshot - mCameraSound.play(MediaActionSound.SHUTTER_CLICK); - - mScreenshotPreview.setLayerType(View.LAYER_TYPE_HARDWARE, null); - mScreenshotPreview.buildLayer(); - mScreenshotAnimation.start(); - }); - }); - } - - /** - * Creates a new worker thread and saves the screenshot to the media store. - */ - private void saveScreenshotInWorkerThread( - Consumer finisher, @Nullable ActionsReadyListener actionsReadyListener) { - SaveImageInBackgroundData data = new SaveImageInBackgroundData(); - data.image = mScreenBitmap; - data.finisher = finisher; - data.mActionsReadyListener = actionsReadyListener; - - if (mSaveInBgTask != null) { - // just log success/failure for the pre-existing screenshot - mSaveInBgTask.setActionsReadyListener(new ActionsReadyListener() { - @Override - void onActionsReady(SavedImageData imageData) { - logSuccessOnActionsReady(imageData); - } - }); - } - - mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data); - mSaveInBgTask.execute(); - } - - /** - * Sets up the action shade and its entrance animation, once we get the screenshot URI. - */ - private void showUiOnActionsReady(SavedImageData imageData) { - logSuccessOnActionsReady(imageData); - - AccessibilityManager accessibilityManager = (AccessibilityManager) - mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); - long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis( - SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS, - AccessibilityManager.FLAG_CONTENT_CONTROLS); - - mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); - mScreenshotHandler.sendMessageDelayed( - mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT), - timeoutMs); - - if (imageData.uri != null) { - mScreenshotHandler.post(() -> { - if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { - mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - setChipIntents(imageData); - } - }); - } else { - setChipIntents(imageData); - } - }); - } - } - - /** - * Logs success/failure of the screenshot saving task, and shows an error if it failed. - */ - private void logSuccessOnActionsReady(SavedImageData imageData) { - if (imageData.uri == null) { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_save_text); - } else { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); - } - } - - private AnimatorSet createScreenshotDropInAnimation(Rect bounds, boolean showFlash) { - Rect previewBounds = new Rect(); - mScreenshotPreview.getBoundsOnScreen(previewBounds); - - float cornerScale = - mCornerSizeX / (mOrientationPortrait ? bounds.width() : bounds.height()); - final float currentScale = 1f; - - mScreenshotAnimatedView.setScaleX(currentScale); - mScreenshotAnimatedView.setScaleY(currentScale); - - mDismissButton.setAlpha(0); - mDismissButton.setVisibility(View.VISIBLE); - - AnimatorSet dropInAnimation = new AnimatorSet(); - ValueAnimator flashInAnimator = ValueAnimator.ofFloat(0, 1); - flashInAnimator.setDuration(SCREENSHOT_FLASH_IN_DURATION_MS); - flashInAnimator.setInterpolator(mFastOutSlowIn); - flashInAnimator.addUpdateListener(animation -> - mScreenshotFlash.setAlpha((float) animation.getAnimatedValue())); - - ValueAnimator flashOutAnimator = ValueAnimator.ofFloat(1, 0); - flashOutAnimator.setDuration(SCREENSHOT_FLASH_OUT_DURATION_MS); - flashOutAnimator.setInterpolator(mFastOutSlowIn); - flashOutAnimator.addUpdateListener(animation -> - mScreenshotFlash.setAlpha((float) animation.getAnimatedValue())); - - // animate from the current location, to the static preview location - final PointF startPos = new PointF(bounds.centerX(), bounds.centerY()); - final PointF finalPos = new PointF(previewBounds.centerX(), previewBounds.centerY()); - - ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1); - toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS); - float xPositionPct = - SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; - float dismissPct = - SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; - float scalePct = - SCREENSHOT_TO_CORNER_SCALE_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; - toCorner.addUpdateListener(animation -> { - float t = animation.getAnimatedFraction(); - if (t < scalePct) { - float scale = MathUtils.lerp( - currentScale, cornerScale, mFastOutSlowIn.getInterpolation(t / scalePct)); - mScreenshotAnimatedView.setScaleX(scale); - mScreenshotAnimatedView.setScaleY(scale); - } else { - mScreenshotAnimatedView.setScaleX(cornerScale); - mScreenshotAnimatedView.setScaleY(cornerScale); - } - - float currentScaleX = mScreenshotAnimatedView.getScaleX(); - float currentScaleY = mScreenshotAnimatedView.getScaleY(); - - if (t < xPositionPct) { - float xCenter = MathUtils.lerp(startPos.x, finalPos.x, - mFastOutSlowIn.getInterpolation(t / xPositionPct)); - mScreenshotAnimatedView.setX(xCenter - bounds.width() * currentScaleX / 2f); - } else { - mScreenshotAnimatedView.setX(finalPos.x - bounds.width() * currentScaleX / 2f); - } - float yCenter = MathUtils.lerp( - startPos.y, finalPos.y, mFastOutSlowIn.getInterpolation(t)); - mScreenshotAnimatedView.setY(yCenter - bounds.height() * currentScaleY / 2f); - - if (t >= dismissPct) { - mDismissButton.setAlpha((t - dismissPct) / (1 - dismissPct)); - float currentX = mScreenshotAnimatedView.getX(); - float currentY = mScreenshotAnimatedView.getY(); - mDismissButton.setY(currentY - mDismissButton.getHeight() / 2f); - if (mDirectionLTR) { - mDismissButton.setX(currentX - + bounds.width() * currentScaleX - mDismissButton.getWidth() / 2f); - } else { - mDismissButton.setX(currentX - mDismissButton.getWidth() / 2f); - } - } - }); - - toCorner.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - super.onAnimationStart(animation); - mScreenshotAnimatedView.setVisibility(View.VISIBLE); - } - }); - - mScreenshotFlash.setAlpha(0f); - mScreenshotFlash.setVisibility(View.VISIBLE); - - if (showFlash) { - dropInAnimation.play(flashOutAnimator).after(flashInAnimator); - dropInAnimation.play(flashOutAnimator).with(toCorner); - } else { - dropInAnimation.play(toCorner); - } - - dropInAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - mDismissButton.setAlpha(1); - float dismissOffset = mDismissButton.getWidth() / 2f; - float finalDismissX = mDirectionLTR - ? finalPos.x - dismissOffset + bounds.width() * cornerScale / 2f - : finalPos.x - dismissOffset - bounds.width() * cornerScale / 2f; - mDismissButton.setX(finalDismissX); - mDismissButton.setY( - finalPos.y - dismissOffset - bounds.height() * cornerScale / 2f); - mScreenshotAnimatedView.setScaleX(1); - mScreenshotAnimatedView.setScaleY(1); - mScreenshotAnimatedView.setX(finalPos.x - bounds.width() * cornerScale / 2f); - mScreenshotAnimatedView.setY(finalPos.y - bounds.height() * cornerScale / 2f); - mScreenshotAnimatedView.setVisibility(View.GONE); - mScreenshotPreview.setVisibility(View.VISIBLE); - mScreenshotLayout.forceLayout(); - createScreenshotActionsShadeAnimation().start(); - } - }); - - return dropInAnimation; - } - - private ValueAnimator createScreenshotActionsShadeAnimation() { - // By default the activities won't be able to start immediately; override this to keep - // the same behavior as if started from a notification - try { - ActivityManager.getService().resumeAppSwitches(); - } catch (RemoteException e) { - } - - ArrayList chips = new ArrayList<>(); - - mShareChip.setText(mContext.getString(com.android.internal.R.string.share)); - mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); - mShareChip.setOnClickListener(v -> { - mShareChip.setIsPending(true); - mEditChip.setIsPending(false); - mPendingInteraction = PendingInteraction.SHARE; - }); - chips.add(mShareChip); - - mEditChip.setText(mContext.getString(com.android.internal.R.string.screenshot_edit)); - mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true); - mEditChip.setOnClickListener(v -> { - mEditChip.setIsPending(true); - mShareChip.setIsPending(false); - mPendingInteraction = PendingInteraction.EDIT; - }); - chips.add(mEditChip); - - mScreenshotPreview.setOnClickListener(v -> { - mShareChip.setIsPending(false); - mEditChip.setIsPending(false); - mPendingInteraction = PendingInteraction.PREVIEW; - }); - - // remove the margin from the last chip so that it's correctly aligned with the end - LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) - mActionsView.getChildAt(0).getLayoutParams(); - params.setMarginStart(0); - mActionsView.getChildAt(0).setLayoutParams(params); - - ValueAnimator animator = ValueAnimator.ofFloat(0, 1); - animator.setDuration(SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS); - float alphaFraction = (float) SCREENSHOT_ACTIONS_ALPHA_DURATION_MS - / SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS; - mActionsContainer.setAlpha(0f); - mActionsContainerBackground.setAlpha(0f); - mActionsContainer.setVisibility(View.VISIBLE); - mActionsContainerBackground.setVisibility(View.VISIBLE); - - animator.addUpdateListener(animation -> { - float t = animation.getAnimatedFraction(); - mBackgroundProtection.setAlpha(t); - float containerAlpha = t < alphaFraction ? t / alphaFraction : 1; - mActionsContainer.setAlpha(containerAlpha); - mActionsContainerBackground.setAlpha(containerAlpha); - float containerScale = SCREENSHOT_ACTIONS_START_SCALE_X - + (t * (1 - SCREENSHOT_ACTIONS_START_SCALE_X)); - mActionsContainer.setScaleX(containerScale); - mActionsContainerBackground.setScaleX(containerScale); - for (ScreenshotActionChip chip : chips) { - chip.setAlpha(t); - chip.setScaleX(1 / containerScale); // invert to keep size of children constant - } - mActionsContainer.setScrollX(mDirectionLTR ? 0 : mActionsContainer.getWidth()); - mActionsContainer.setPivotX(mDirectionLTR ? 0 : mActionsContainer.getWidth()); - mActionsContainerBackground.setPivotX( - mDirectionLTR ? 0 : mActionsContainerBackground.getWidth()); - }); - return animator; - } - - private void setChipIntents(SavedImageData imageData) { - mShareChip.setPendingIntent(imageData.shareAction.actionIntent, () -> { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED); - dismissScreenshot("chip tapped", false); - mOnCompleteRunnable.run(); - }); - - mEditChip.setPendingIntent(imageData.editAction.actionIntent, () -> { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED); - dismissScreenshot("chip tapped", false); - mOnCompleteRunnable.run(); - }); - - mScreenshotPreview.setOnClickListener(v -> { - try { - imageData.editAction.actionIntent.send(); - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED); - dismissScreenshot("screenshot preview tapped", false); - mOnCompleteRunnable.run(); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Intent cancelled", e); - } - }); - - if (mPendingInteraction != null) { - switch(mPendingInteraction) { - case PREVIEW: - mScreenshotPreview.callOnClick(); - break; - case SHARE: - mShareChip.callOnClick(); - break; - case EDIT: - mEditChip.callOnClick(); - break; - } - } else { - LayoutInflater inflater = LayoutInflater.from(mContext); - - for (Notification.Action smartAction : imageData.smartActions) { - ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate( - R.layout.global_screenshot_action_chip, mActionsView, false); - actionChip.setText(smartAction.title); - actionChip.setIcon(smartAction.getIcon(), false); - actionChip.setPendingIntent(smartAction.actionIntent, - () -> { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED); - dismissScreenshot("chip tapped", false); - mOnCompleteRunnable.run(); - }); - actionChip.setAlpha(1); - mActionsView.addView(actionChip); - mSmartChips.add(actionChip); - } - } - } - - private AnimatorSet createScreenshotDismissAnimation() { - ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); - alphaAnim.setStartDelay(SCREENSHOT_DISMISS_ALPHA_OFFSET_MS); - alphaAnim.setDuration(SCREENSHOT_DISMISS_ALPHA_DURATION_MS); - alphaAnim.addUpdateListener(animation -> { - mScreenshotLayout.setAlpha(1 - animation.getAnimatedFraction()); - }); - - ValueAnimator yAnim = ValueAnimator.ofFloat(0, 1); - yAnim.setInterpolator(mAccelerateInterpolator); - yAnim.setDuration(SCREENSHOT_DISMISS_Y_DURATION_MS); - float screenshotStartY = mScreenshotPreview.getTranslationY(); - float dismissStartY = mDismissButton.getTranslationY(); - yAnim.addUpdateListener(animation -> { - float yDelta = MathUtils.lerp(0, mDismissDeltaY, animation.getAnimatedFraction()); - mScreenshotPreview.setTranslationY(screenshotStartY + yDelta); - mDismissButton.setTranslationY(dismissStartY + yDelta); - mActionsContainer.setTranslationY(yDelta); - mActionsContainerBackground.setTranslationY(yDelta); - }); - - AnimatorSet animSet = new AnimatorSet(); - animSet.play(yAnim).with(alphaAnim); - - return animSet; - } - - private void clearScreenshot() { - if (mScreenshotLayout.isAttachedToWindow()) { - mWindowManager.removeView(mScreenshotLayout); - } - - // Clear any references to the bitmap - mScreenshotPreview.setImageDrawable(null); - mScreenshotAnimatedView.setImageDrawable(null); - mScreenshotAnimatedView.setVisibility(View.GONE); - mActionsContainerBackground.setVisibility(View.GONE); - mActionsContainer.setVisibility(View.GONE); - mBackgroundProtection.setAlpha(0f); - mDismissButton.setVisibility(View.GONE); - mScreenshotPreview.setVisibility(View.GONE); - mScreenshotPreview.setLayerType(View.LAYER_TYPE_NONE, null); - mScreenshotPreview.setOnClickListener(null); - mShareChip.setOnClickListener(null); - mEditChip.setOnClickListener(null); - mShareChip.setIsPending(false); - mEditChip.setIsPending(false); - mPendingInteraction = null; - for (ScreenshotActionChip chip : mSmartChips) { - mActionsView.removeView(chip); - } - mSmartChips.clear(); - mScreenshotLayout.setAlpha(1); - mDismissButton.setTranslationY(0); - mActionsContainer.setTranslationY(0); - mActionsContainerBackground.setTranslationY(0); - mScreenshotPreview.setTranslationY(0); - } - - private void setAnimatedViewSize(int width, int height) { - ViewGroup.LayoutParams layoutParams = mScreenshotAnimatedView.getLayoutParams(); - layoutParams.width = width; - layoutParams.height = height; - mScreenshotAnimatedView.setLayoutParams(layoutParams); - } - - /** - * Updates the window focusability. If the window is already showing, then it updates the - * window immediately, otherwise the layout params will be applied when the window is next - * shown. - */ - private void setWindowFocusable(boolean focusable) { - if (focusable) { - mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } else { - mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; - } - if (mScreenshotLayout.isAttachedToWindow()) { - mWindowManager.updateViewLayout(mScreenshotLayout, mWindowLayoutParams); - } - } - - private boolean isUserSetupComplete() { - return Settings.Secure.getInt(mContext.getContentResolver(), - SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; - } - - /** Does the aspect ratio of the bitmap with insets removed match the bounds. */ - private boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, Rect screenBounds) { - int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right; - int insettedHeight = bitmap.getHeight() - bitmapInsets.top - bitmapInsets.bottom; - - if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 - || bitmap.getHeight() == 0) { - Log.e(TAG, String.format( - "Provided bitmap and insets create degenerate region: %dx%d %s", - bitmap.getWidth(), bitmap.getHeight(), bitmapInsets)); - return false; - } - - float insettedBitmapAspect = ((float) insettedWidth) / insettedHeight; - float boundsAspect = ((float) screenBounds.width()) / screenBounds.height(); - - boolean matchWithinTolerance = Math.abs(insettedBitmapAspect - boundsAspect) < 0.1f; - if (!matchWithinTolerance) { - Log.d(TAG, String.format("aspectRatiosMatch: don't match bitmap: %f, bounds: %f", - insettedBitmapAspect, boundsAspect)); - } - - return matchWithinTolerance; - } - - /** - * Create a drawable using the size of the bitmap and insets as the fractional inset parameters. - */ - private Drawable createScreenDrawable(Bitmap bitmap, Insets insets) { - int insettedWidth = bitmap.getWidth() - insets.left - insets.right; - int insettedHeight = bitmap.getHeight() - insets.top - insets.bottom; - - BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap); - if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 - || bitmap.getHeight() == 0) { - Log.e(TAG, String.format( - "Can't create insetted drawable, using 0 insets " - + "bitmap and insets create degenerate region: %dx%d %s", - bitmap.getWidth(), bitmap.getHeight(), insets)); - return bitmapDrawable; - } - - InsetDrawable insetDrawable = new InsetDrawable(bitmapDrawable, - -1f * insets.left / insettedWidth, - -1f * insets.top / insettedHeight, - -1f * insets.right / insettedWidth, - -1f * insets.bottom / insettedHeight); - - if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) { - // Are any of the insets negative, meaning the bitmap is smaller than the bounds so need - // to fill in the background of the drawable. - return new LayerDrawable(new Drawable[]{ - new ColorDrawable(Color.BLACK), insetDrawable}); - } else { - return insetDrawable; - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index f0ea597c458dd..c0061ad972934 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -82,8 +82,8 @@ class SaveImageInBackgroundTask extends AsyncTask { private final Context mContext; private final ScreenshotSmartActions mScreenshotSmartActions; - private final GlobalScreenshot.SaveImageInBackgroundData mParams; - private final GlobalScreenshot.SavedImageData mImageData; + private final ScreenshotController.SaveImageInBackgroundData mParams; + private final ScreenshotController.SavedImageData mImageData; private final String mImageFileName; private final long mImageTime; private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider; @@ -92,10 +92,10 @@ class SaveImageInBackgroundTask extends AsyncTask { private final Random mRandom = new Random(); SaveImageInBackgroundTask(Context context, ScreenshotSmartActions screenshotSmartActions, - GlobalScreenshot.SaveImageInBackgroundData data) { + ScreenshotController.SaveImageInBackgroundData data) { mContext = context; mScreenshotSmartActions = screenshotSmartActions; - mImageData = new GlobalScreenshot.SavedImageData(); + mImageData = new ScreenshotController.SavedImageData(); // Prepare all the output metadata mParams = data; @@ -234,7 +234,7 @@ class SaveImageInBackgroundTask extends AsyncTask { * Update the listener run when the saving task completes. Used to avoid showing UI for the * first screenshot when a second one is taken. */ - void setActionsReadyListener(GlobalScreenshot.ActionsReadyListener listener) { + void setActionsReadyListener(ScreenshotController.ActionsReadyListener listener) { mParams.mActionsReadyListener = listener; } @@ -287,10 +287,10 @@ class SaveImageInBackgroundTask extends AsyncTask { // Create a share action for the notification PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode, new Intent(context, ActionProxyReceiver.class) - .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, pendingIntent) - .putExtra(GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP, true) - .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) - .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, + .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent) + .putExtra(ScreenshotController.EXTRA_DISALLOW_ENTER_PIP, true) + .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId) + .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, mSmartActionsEnabled) .setAction(Intent.ACTION_SEND) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), @@ -332,9 +332,9 @@ class SaveImageInBackgroundTask extends AsyncTask { // Create a edit action PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode, new Intent(context, ActionProxyReceiver.class) - .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, pendingIntent) - .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) - .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, + .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, pendingIntent) + .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId) + .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, mSmartActionsEnabled) .setAction(Intent.ACTION_EDIT) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), @@ -355,9 +355,9 @@ class SaveImageInBackgroundTask extends AsyncTask { // Create a delete action for the notification PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode, new Intent(context, DeleteScreenshotReceiver.class) - .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()) - .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) - .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, + .putExtra(ScreenshotController.SCREENSHOT_URI_ID, uri.toString()) + .putExtra(ScreenshotController.EXTRA_ID, mScreenshotId) + .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, mSmartActionsEnabled) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); @@ -395,7 +395,7 @@ class SaveImageInBackgroundTask extends AsyncTask { ScreenshotNotificationSmartActionsProvider.ACTION_TYPE, ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE); Intent intent = new Intent(context, SmartActionsReceiver.class) - .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, action.actionIntent) + .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, action.actionIntent) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); addIntentExtras(mScreenshotId, intent, actionType, mSmartActionsEnabled); PendingIntent broadcastIntent = PendingIntent.getBroadcast(context, @@ -411,9 +411,9 @@ class SaveImageInBackgroundTask extends AsyncTask { private static void addIntentExtras(String screenshotId, Intent intent, String actionType, boolean smartActionsEnabled) { intent - .putExtra(GlobalScreenshot.EXTRA_ACTION_TYPE, actionType) - .putExtra(GlobalScreenshot.EXTRA_ID, screenshotId) - .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, smartActionsEnabled); + .putExtra(ScreenshotController.EXTRA_ACTION_TYPE, actionType) + .putExtra(ScreenshotController.EXTRA_ID, screenshotId) + .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, smartActionsEnabled); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java new file mode 100644 index 0000000000000..5d8f70c4e4606 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -0,0 +1,665 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot; + +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.view.Display.DEFAULT_DISPLAY; + +import static java.util.Objects.requireNonNull; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.WindowContext; +import android.content.ComponentName; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Insets; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.media.MediaActionSound; +import android.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.widget.Toast; + +import com.android.internal.logging.UiEventLogger; +import com.android.systemui.R; + +import java.util.List; +import java.util.function.Consumer; + +import javax.inject.Inject; + +/** + * Controls the state and flow for screenshots. + */ +public class ScreenshotController { + /** + * POD used in the AsyncTask which saves an image in the background. + */ + static class SaveImageInBackgroundData { + public Bitmap image; + public Consumer finisher; + public ScreenshotController.ActionsReadyListener mActionsReadyListener; + + void clearImage() { + image = null; + } + } + + /** + * Structure returned by the SaveImageInBackgroundTask + */ + static class SavedImageData { + public Uri uri; + public Notification.Action shareAction; + public Notification.Action editAction; + public Notification.Action deleteAction; + public List smartActions; + + /** + * Used to reset the return data on error + */ + public void reset() { + uri = null; + shareAction = null; + editAction = null; + deleteAction = null; + smartActions = null; + } + } + + abstract static class ActionsReadyListener { + abstract void onActionsReady(ScreenshotController.SavedImageData imageData); + } + + private static final String TAG = "GlobalScreenshotController"; + + // These strings are used for communicating the action invoked to + // ScreenshotNotificationSmartActionsProvider. + static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type"; + static final String EXTRA_ID = "android:screenshot_id"; + static final String ACTION_TYPE_DELETE = "Delete"; + static final String ACTION_TYPE_SHARE = "Share"; + static final String ACTION_TYPE_EDIT = "Edit"; + static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled"; + static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent"; + + static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id"; + static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification"; + static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip"; + + + private static final int MESSAGE_CORNER_TIMEOUT = 2; + private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; + + // From WizardManagerHelper.java + private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; + + private final Context mContext; + private final ScreenshotNotificationsController mNotificationsController; + private final ScreenshotSmartActions mScreenshotSmartActions; + private final UiEventLogger mUiEventLogger; + + private final WindowManager mWindowManager; + private final WindowManager.LayoutParams mWindowLayoutParams; + private final Display mDisplay; + private final DisplayMetrics mDisplayMetrics; + private final AccessibilityManager mAccessibilityManager; + private final MediaActionSound mCameraSound; + + private ScreenshotView mScreenshotView; + private Bitmap mScreenBitmap; + private SaveImageInBackgroundTask mSaveInBgTask; + + private Animator mScreenshotAnimation; + private Animator mDismissAnimation; + + private Runnable mOnCompleteRunnable; + private boolean mInDarkMode; + private boolean mDirectionLTR; + private boolean mOrientationPortrait; + + private final Handler mScreenshotHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_CORNER_TIMEOUT: + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT); + ScreenshotController.this.dismissScreenshot(false); + mOnCompleteRunnable.run(); + break; + default: + break; + } + } + }; + + @Inject + ScreenshotController(Context context, ScreenshotSmartActions screenshotSmartActions, + ScreenshotNotificationsController screenshotNotificationsController, + UiEventLogger uiEventLogger) { + mScreenshotSmartActions = screenshotSmartActions; + mNotificationsController = screenshotNotificationsController; + mUiEventLogger = uiEventLogger; + + // Create a visual (Window) context + // After this, our windowToken is available from mContext.getActivityToken() + final DisplayManager dm = requireNonNull(context.getSystemService(DisplayManager.class)); + mDisplay = dm.getDisplay(DEFAULT_DISPLAY); + Context displayContext = context.createDisplayContext(mDisplay); + mContext = new WindowContext(displayContext, WindowManager.LayoutParams.TYPE_SCREENSHOT, + null); + mWindowManager = mContext.getSystemService(WindowManager.class); + + mAccessibilityManager = AccessibilityManager.getInstance(mContext); + + reloadAssets(); + Configuration config = mContext.getResources().getConfiguration(); + mInDarkMode = config.isNightModeActive(); + mDirectionLTR = config.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; + mOrientationPortrait = config.orientation == ORIENTATION_PORTRAIT; + + // Setup the window that we are going to use + mWindowLayoutParams = new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0, + WindowManager.LayoutParams.TYPE_SCREENSHOT, + WindowManager.LayoutParams.FLAG_FULLSCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, + PixelFormat.TRANSLUCENT); + mWindowLayoutParams.setTitle("ScreenshotAnimation"); + mWindowLayoutParams.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + mWindowLayoutParams.setFitInsetsTypes(0 /* types */); + + mDisplayMetrics = new DisplayMetrics(); + mDisplay.getRealMetrics(mDisplayMetrics); + + // Setup the Camera shutter sound + mCameraSound = new MediaActionSound(); + mCameraSound.load(MediaActionSound.SHUTTER_CLICK); + } + + void takeScreenshotFullscreen(Consumer finisher, Runnable onComplete) { + mOnCompleteRunnable = onComplete; + + mDisplay.getRealMetrics(mDisplayMetrics); + takeScreenshotInternal( + finisher, + new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels)); + } + + void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, + Insets visibleInsets, int taskId, int userId, ComponentName topComponent, + Consumer finisher, Runnable onComplete) { + // TODO: use task Id, userId, topComponent for smart handler + mOnCompleteRunnable = onComplete; + + if (screenshot == null) { + Log.e(TAG, "Got null bitmap from screenshot message"); + mNotificationsController.notifyScreenshotError( + R.string.screenshot_failed_to_capture_text); + finisher.accept(null); + mOnCompleteRunnable.run(); + return; + } + + if (aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) { + saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, false); + } else { + saveScreenshot(screenshot, finisher, + new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight()), Insets.NONE, + true); + } + } + + /** + * Displays a screenshot selector + */ + @SuppressLint("ClickableViewAccessibility") + void takeScreenshotPartial(final Consumer finisher, Runnable onComplete) { + dismissScreenshot(true); + mOnCompleteRunnable = onComplete; + + mWindowManager.addView(mScreenshotView, mWindowLayoutParams); + + mScreenshotView.takePartialScreenshot( + rect -> takeScreenshotInternal(finisher, rect)); + } + + boolean isDismissing() { + return (mDismissAnimation != null && mDismissAnimation.isRunning()); + } + + /** + * Clears current screenshot + */ + void dismissScreenshot(boolean immediate) { + Log.v(TAG, "clearing screenshot"); + mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); + mScreenshotView.getViewTreeObserver().removeOnComputeInternalInsetsListener( + mScreenshotView); + if (!immediate) { + mDismissAnimation = mScreenshotView.createScreenshotDismissAnimation(); + mDismissAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + clearScreenshot(); + } + }); + mDismissAnimation.start(); + } else { + clearScreenshot(); + } + } + + private void onConfigChanged(Configuration newConfig) { + boolean needsUpdate = false; + // dark mode + if (newConfig.isNightModeActive()) { + // Night mode is active, we're using dark theme + if (!mInDarkMode) { + mInDarkMode = true; + needsUpdate = true; + } + } else { + // Night mode is not active, we're using the light theme + if (mInDarkMode) { + mInDarkMode = false; + needsUpdate = true; + } + } + + // RTL configuration + switch (newConfig.getLayoutDirection()) { + case View.LAYOUT_DIRECTION_LTR: + if (!mDirectionLTR) { + mDirectionLTR = true; + needsUpdate = true; + } + break; + case View.LAYOUT_DIRECTION_RTL: + if (mDirectionLTR) { + mDirectionLTR = false; + needsUpdate = true; + } + break; + } + + // portrait/landscape orientation + switch (newConfig.orientation) { + case ORIENTATION_PORTRAIT: + if (!mOrientationPortrait) { + mOrientationPortrait = true; + needsUpdate = true; + } + break; + case ORIENTATION_LANDSCAPE: + if (mOrientationPortrait) { + mOrientationPortrait = false; + needsUpdate = true; + } + break; + } + + if (needsUpdate) { + reloadAssets(); + } + } + + /** + * Update assets (called when the dark theme status changes). We only need to update the dismiss + * button and the actions container background, since the buttons are re-inflated on demand. + */ + private void reloadAssets() { + boolean wasAttached = mScreenshotView != null && mScreenshotView.isAttachedToWindow(); + if (wasAttached) { + mWindowManager.removeView(mScreenshotView); + } + + // Inflate the screenshot layout + mScreenshotView = (ScreenshotView) + LayoutInflater.from(mContext).inflate(R.layout.global_screenshot, null); + + // TODO(159460485): Remove this when focus is handled properly in the system + mScreenshotView.setOnTouchListener((v, event) -> { + if (event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) { + // Once the user touches outside, stop listening for input + setWindowFocusable(false); + } + return false; + }); + + mScreenshotView.setOnKeyListener((v, keyCode, event) -> { + if (keyCode == KeyEvent.KEYCODE_BACK) { + dismissScreenshot(false); + return true; + } + return false; + }); + + if (wasAttached) { + mWindowManager.addView(mScreenshotView, mWindowLayoutParams); + } + } + + /** + * Takes a screenshot of the current display and shows an animation. + */ + private void takeScreenshotInternal(Consumer finisher, Rect crop) { + // copy the input Rect, since SurfaceControl.screenshot can mutate it + Rect screenRect = new Rect(crop); + int width = crop.width(); + int height = crop.height(); + final IBinder displayToken = SurfaceControl.getInternalDisplayToken(); + final SurfaceControl.DisplayCaptureArgs captureArgs = + new SurfaceControl.DisplayCaptureArgs.Builder(displayToken) + .setSourceCrop(crop) + .setSize(width, height) + .build(); + final SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = + SurfaceControl.captureDisplay(captureArgs); + Bitmap screenshot = screenshotBuffer == null ? null : screenshotBuffer.asBitmap(); + + if (screenshot == null) { + Log.e(TAG, "Screenshot bitmap was null"); + mNotificationsController.notifyScreenshotError( + R.string.screenshot_failed_to_capture_text); + finisher.accept(null); + mOnCompleteRunnable.run(); + return; + } + + saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, true); + } + + private void saveScreenshot(Bitmap screenshot, Consumer finisher, Rect screenRect, + Insets screenInsets, boolean showFlash) { + if (mAccessibilityManager.isEnabled()) { + AccessibilityEvent event = + new AccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + event.setContentDescription( + mContext.getResources().getString(R.string.screenshot_saving_title)); + mAccessibilityManager.sendAccessibilityEvent(event); + } + + if (mScreenshotView.isAttachedToWindow()) { + // if we didn't already dismiss for another reason + if (mDismissAnimation == null || !mDismissAnimation.isRunning()) { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED); + } + dismissScreenshot(true); + } + + mScreenBitmap = screenshot; + + if (!isUserSetupComplete()) { + // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing + // and sharing shouldn't be exposed to the user. + saveScreenshotAndToast(finisher); + return; + } + + // Optimizations + mScreenBitmap.setHasAlpha(false); + mScreenBitmap.prepareToDraw(); + + onConfigChanged(mContext.getResources().getConfiguration()); + + if (mDismissAnimation != null && mDismissAnimation.isRunning()) { + mDismissAnimation.cancel(); + } + + // The window is focusable by default + setWindowFocusable(true); + + // Start the post-screenshot animation + startAnimation(finisher, screenRect, screenInsets, showFlash); + } + + /** + * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on + * failure). + */ + private void saveScreenshotAndToast(Consumer finisher) { + // Play the shutter sound to notify that we've taken a screenshot + mScreenshotHandler.post(() -> { + mCameraSound.play(MediaActionSound.SHUTTER_CLICK); + }); + + saveScreenshotInWorkerThread(finisher, + new ScreenshotController.ActionsReadyListener() { + @Override + void onActionsReady(ScreenshotController.SavedImageData imageData) { + finisher.accept(imageData.uri); + if (imageData.uri == null) { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); + mNotificationsController.notifyScreenshotError( + R.string.screenshot_failed_to_save_text); + } else { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); + + mScreenshotHandler.post(() -> { + Toast.makeText(mContext, R.string.screenshot_saved_title, + Toast.LENGTH_SHORT).show(); + }); + } + } + }); + } + + /** + * Starts the animation after taking the screenshot + */ + private void startAnimation(final Consumer finisher, Rect screenRect, Insets screenInsets, + boolean showFlash) { + mScreenshotHandler.post(() -> { + if (!mScreenshotView.isAttachedToWindow()) { + mWindowManager.addView(mScreenshotView, mWindowLayoutParams); + } + + mScreenshotView.prepareForAnimation(mScreenBitmap, screenRect, screenInsets); + + mScreenshotHandler.post(() -> { + mScreenshotView.getViewTreeObserver().addOnComputeInternalInsetsListener( + mScreenshotView); + + mScreenshotAnimation = + mScreenshotView.createScreenshotDropInAnimation(screenRect, showFlash, + this::onElementTapped); + + saveScreenshotInWorkerThread(finisher, + new ScreenshotController.ActionsReadyListener() { + @Override + void onActionsReady( + ScreenshotController.SavedImageData imageData) { + showUiOnActionsReady(imageData); + } + }); + + // Play the shutter sound to notify that we've taken a screenshot + mCameraSound.play(MediaActionSound.SHUTTER_CLICK); + + mScreenshotAnimation.start(); + }); + }); + } + + /** + * Creates a new worker thread and saves the screenshot to the media store. + */ + private void saveScreenshotInWorkerThread( + Consumer finisher, + @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener) { + ScreenshotController.SaveImageInBackgroundData + data = new ScreenshotController.SaveImageInBackgroundData(); + data.image = mScreenBitmap; + data.finisher = finisher; + data.mActionsReadyListener = actionsReadyListener; + + if (mSaveInBgTask != null) { + // just log success/failure for the pre-existing screenshot + mSaveInBgTask.setActionsReadyListener( + new ScreenshotController.ActionsReadyListener() { + @Override + void onActionsReady(ScreenshotController.SavedImageData imageData) { + logSuccessOnActionsReady(imageData); + } + }); + } + + mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data); + mSaveInBgTask.execute(); + } + + /** + * Sets up the action shade and its entrance animation, once we get the screenshot URI. + */ + private void showUiOnActionsReady(ScreenshotController.SavedImageData imageData) { + logSuccessOnActionsReady(imageData); + + AccessibilityManager accessibilityManager = (AccessibilityManager) + mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis( + SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS, + AccessibilityManager.FLAG_CONTENT_CONTROLS); + + mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); + mScreenshotHandler.sendMessageDelayed( + mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT), + timeoutMs); + + if (imageData.uri != null) { + mScreenshotHandler.post(() -> { + if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { + mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mScreenshotView.setChipIntents( + imageData, event -> onElementTapped(event)); + } + }); + } else { + mScreenshotView.setChipIntents( + imageData, this::onElementTapped); + } + }); + } + } + + private void onElementTapped(ScreenshotEvent event) { + mUiEventLogger.log(event); + dismissScreenshot(false); + mOnCompleteRunnable.run(); + } + + /** + * Logs success/failure of the screenshot saving task, and shows an error if it failed. + */ + private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) { + if (imageData.uri == null) { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); + mNotificationsController.notifyScreenshotError( + R.string.screenshot_failed_to_save_text); + } else { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); + } + } + + private void clearScreenshot() { + if (mScreenshotView.isAttachedToWindow()) { + mWindowManager.removeView(mScreenshotView); + } + + mScreenshotView.reset(); + } + + private boolean isUserSetupComplete() { + return Settings.Secure.getInt(mContext.getContentResolver(), + SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; + } + + + /** + * Updates the window focusability. If the window is already showing, then it updates the + * window immediately, otherwise the layout params will be applied when the window is next + * shown. + */ + private void setWindowFocusable(boolean focusable) { + if (focusable) { + mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } else { + mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + } + if (mScreenshotView.isAttachedToWindow()) { + mWindowManager.updateViewLayout(mScreenshotView, mWindowLayoutParams); + } + } + + /** Does the aspect ratio of the bitmap with insets removed match the bounds. */ + private static boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, + Rect screenBounds) { + int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right; + int insettedHeight = bitmap.getHeight() - bitmapInsets.top - bitmapInsets.bottom; + + if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 + || bitmap.getHeight() == 0) { + Log.e(TAG, String.format( + "Provided bitmap and insets create degenerate region: %dx%d %s", + bitmap.getWidth(), bitmap.getHeight(), bitmapInsets)); + return false; + } + + float insettedBitmapAspect = ((float) insettedWidth) / insettedHeight; + float boundsAspect = ((float) screenBounds.width()) / screenBounds.height(); + + boolean matchWithinTolerance = Math.abs(insettedBitmapAspect - boundsAspect) < 0.1f; + if (!matchWithinTolerance) { + Log.d(TAG, String.format("aspectRatiosMatch: don't match bitmap: %f, bounds: %f", + insettedBitmapAspect, boundsAspect)); + } + + return matchWithinTolerance; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java index 6d1299ba98acc..a946513870c90 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java @@ -185,7 +185,7 @@ public class ScreenshotNotificationsController { * @param actionData SavedImageData struct with image URI and actions */ public void showScreenshotActionsNotification( - GlobalScreenshot.SavedImageData actionData) { + ScreenshotController.SavedImageData actionData) { mNotificationBuilder.addAction(actionData.shareAction); mNotificationBuilder.addAction(actionData.editAction); mNotificationBuilder.addAction(actionData.deleteAction); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java index 07a92460f3ead..c793b5b9639e3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java @@ -26,8 +26,11 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; +import java.util.function.Consumer; + /** * Draws a selection rectangle while taking screenshot */ @@ -36,6 +39,8 @@ public class ScreenshotSelectorView extends View { private Rect mSelectionRect; private final Paint mPaintSelection, mPaintBackground; + private Consumer mOnScreenshotSelected; + public ScreenshotSelectorView(Context context) { this(context, null); } @@ -46,30 +51,28 @@ public class ScreenshotSelectorView extends View { mPaintBackground.setAlpha(160); mPaintSelection = new Paint(Color.TRANSPARENT); mPaintSelection.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); - } - public void startSelection(int x, int y) { - mStartPoint = new Point(x, y); - mSelectionRect = new Rect(x, y, x, y); - } - - public void updateSelection(int x, int y) { - if (mSelectionRect != null) { - mSelectionRect.left = Math.min(mStartPoint.x, x); - mSelectionRect.right = Math.max(mStartPoint.x, x); - mSelectionRect.top = Math.min(mStartPoint.y, y); - mSelectionRect.bottom = Math.max(mStartPoint.y, y); - invalidate(); - } - } - - public Rect getSelectionRect() { - return mSelectionRect; - } - - public void stopSelection() { - mStartPoint = null; - mSelectionRect = null; + setOnTouchListener((v, event) -> { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + startSelection((int) event.getX(), (int) event.getY()); + return true; + case MotionEvent.ACTION_MOVE: + updateSelection((int) event.getX(), (int) event.getY()); + return true; + case MotionEvent.ACTION_UP: + setVisibility(View.GONE); + final Rect rect = getSelectionRect(); + if (mOnScreenshotSelected != null + && rect != null + && rect.width() != 0 && rect.height() != 0) { + mOnScreenshotSelected.accept(rect); + } + stopSelection(); + return true; + } + return false; + }); } @Override @@ -79,4 +82,38 @@ public class ScreenshotSelectorView extends View { canvas.drawRect(mSelectionRect, mPaintSelection); } } + + void setOnScreenshotSelected(Consumer onScreenshotSelected) { + mOnScreenshotSelected = onScreenshotSelected; + } + + void stop() { + if (getSelectionRect() != null) { + stopSelection(); + } + } + + private void startSelection(int x, int y) { + mStartPoint = new Point(x, y); + mSelectionRect = new Rect(x, y, x, y); + } + + private void updateSelection(int x, int y) { + if (mSelectionRect != null) { + mSelectionRect.left = Math.min(mStartPoint.x, x); + mSelectionRect.right = Math.max(mStartPoint.x, x); + mSelectionRect.top = Math.min(mStartPoint.y, y); + mSelectionRect.bottom = Math.max(mStartPoint.y, y); + invalidate(); + } + } + + private Rect getSelectionRect() { + return mSelectionRect; + } + + private void stopSelection() { + mStartPoint = null; + mSelectionRect = null; + } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java new file mode 100644 index 0000000000000..03fe2920405f7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -0,0 +1,611 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot; + +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; + +import static java.util.Objects.requireNonNull; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.app.ActivityManager; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Insets; +import android.graphics.Outline; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.graphics.drawable.InsetDrawable; +import android.graphics.drawable.LayerDrawable; +import android.os.RemoteException; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.MathUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; +import android.widget.HorizontalScrollView; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.android.systemui.R; +import com.android.systemui.shared.system.QuickStepContract; + +import java.util.ArrayList; +import java.util.function.Consumer; + +/** + * Handles the visual elements and animations for the screenshot flow. + */ +public class ScreenshotView extends FrameLayout implements + ViewTreeObserver.OnComputeInternalInsetsListener { + + private static final String TAG = "GlobalScreenshotView"; + + private static final long SCREENSHOT_FLASH_IN_DURATION_MS = 133; + private static final long SCREENSHOT_FLASH_OUT_DURATION_MS = 217; + // delay before starting to fade in dismiss button + private static final long SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS = 200; + private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234; + private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500; + private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234; + private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400; + private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100; + private static final long SCREENSHOT_DISMISS_Y_DURATION_MS = 350; + private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 183; + private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade + private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f; + private static final float ROUNDED_CORNER_RADIUS = .05f; + + private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator(); + + private final Resources mResources; + private final Interpolator mFastOutSlowIn; + private final DisplayMetrics mDisplayMetrics; + private final float mCornerSizeX; + private final float mDismissDeltaY; + + private int mNavMode; + private int mLeftInset; + private int mRightInset; + private boolean mOrientationPortrait; + private boolean mDirectionLTR; + + private ScreenshotSelectorView mScreenshotSelectorView; + private ImageView mScreenshotAnimatedView; + private ImageView mScreenshotPreview; + private ImageView mScreenshotFlash; + private ImageView mActionsContainerBackground; + private HorizontalScrollView mActionsContainer; + private LinearLayout mActionsView; + private ImageView mBackgroundProtection; + private FrameLayout mDismissButton; + private ScreenshotActionChip mShareChip; + private ScreenshotActionChip mEditChip; + + private ArrayList mSmartChips = new ArrayList<>(); + private PendingInteraction mPendingInteraction; + + private enum PendingInteraction { + PREVIEW, + EDIT, + SHARE + } + + public ScreenshotView(Context context) { + this(context, null); + } + + public ScreenshotView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ScreenshotView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ScreenshotView( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + mResources = mContext.getResources(); + + mCornerSizeX = mResources.getDimensionPixelSize(R.dimen.global_screenshot_x_scale); + mDismissDeltaY = mResources.getDimensionPixelSize( + R.dimen.screenshot_dismissal_height_delta); + + // standard material ease + mFastOutSlowIn = + AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in); + + mDisplayMetrics = new DisplayMetrics(); + mContext.getDisplay().getRealMetrics(mDisplayMetrics); + } + + @Override // ViewTreeObserver.OnComputeInternalInsetsListener + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + Region touchRegion = new Region(); + + Rect screenshotRect = new Rect(); + mScreenshotPreview.getBoundsOnScreen(screenshotRect); + touchRegion.op(screenshotRect, Region.Op.UNION); + Rect actionsRect = new Rect(); + mActionsContainer.getBoundsOnScreen(actionsRect); + touchRegion.op(actionsRect, Region.Op.UNION); + Rect dismissRect = new Rect(); + mDismissButton.getBoundsOnScreen(dismissRect); + touchRegion.op(dismissRect, Region.Op.UNION); + + if (QuickStepContract.isGesturalMode(mNavMode)) { + // Receive touches in gesture insets such that they don't cause TOUCH_OUTSIDE + Rect inset = new Rect(0, 0, mLeftInset, mDisplayMetrics.heightPixels); + touchRegion.op(inset, Region.Op.UNION); + inset.set(mDisplayMetrics.widthPixels - mRightInset, 0, mDisplayMetrics.widthPixels, + mDisplayMetrics.heightPixels); + touchRegion.op(inset, Region.Op.UNION); + } + + inoutInfo.touchableRegion.set(touchRegion); + } + + @Override // View + protected void onFinishInflate() { + mScreenshotAnimatedView = requireNonNull( + findViewById(R.id.global_screenshot_animated_view)); + mScreenshotPreview = requireNonNull(findViewById(R.id.global_screenshot_preview)); + mActionsContainerBackground = requireNonNull(findViewById( + R.id.global_screenshot_actions_container_background)); + mActionsContainer = requireNonNull(findViewById(R.id.global_screenshot_actions_container)); + mActionsView = requireNonNull(findViewById(R.id.global_screenshot_actions)); + mBackgroundProtection = requireNonNull( + findViewById(R.id.global_screenshot_actions_background)); + mDismissButton = requireNonNull(findViewById(R.id.global_screenshot_dismiss_button)); + mScreenshotFlash = requireNonNull(findViewById(R.id.global_screenshot_flash)); + mScreenshotSelectorView = requireNonNull(findViewById(R.id.global_screenshot_selector)); + mShareChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_share_chip)); + mEditChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_edit_chip)); + + mScreenshotAnimatedView.setClipToOutline(true); + mScreenshotAnimatedView.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(new Rect(0, 0, view.getWidth(), view.getHeight()), + ROUNDED_CORNER_RADIUS * view.getWidth()); + } + }); + mScreenshotPreview.setClipToOutline(true); + mScreenshotPreview.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(new Rect(0, 0, view.getWidth(), view.getHeight()), + ROUNDED_CORNER_RADIUS * view.getWidth()); + } + }); + + setFocusable(true); + mScreenshotSelectorView.setFocusable(true); + mScreenshotSelectorView.setFocusableInTouchMode(true); + mScreenshotAnimatedView.setPivotX(0); + mScreenshotAnimatedView.setPivotY(0); + mActionsContainer.setScrollX(0); + + mNavMode = getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode); + mOrientationPortrait = + getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; + mDirectionLTR = + getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; + + setOnApplyWindowInsetsListener((v, insets) -> { + if (QuickStepContract.isGesturalMode(mNavMode)) { + Insets gestureInsets = insets.getInsets( + WindowInsets.Type.systemGestures()); + mLeftInset = gestureInsets.left; + mRightInset = gestureInsets.right; + } else { + mLeftInset = mRightInset = 0; + } + return ScreenshotView.this.onApplyWindowInsets(insets); + }); + + // Get focus so that the key events go to the layout. + setFocusableInTouchMode(true); + requestFocus(); + } + + void takePartialScreenshot(Consumer onPartialScreenshotSelected) { + mScreenshotSelectorView.setOnScreenshotSelected(onPartialScreenshotSelected); + mScreenshotSelectorView.setVisibility(View.VISIBLE); + mScreenshotSelectorView.requestFocus(); + } + + void prepareForAnimation(Bitmap bitmap, Rect screenRect, Insets screenInsets) { + mScreenshotAnimatedView.setImageDrawable( + createScreenDrawable(mResources, bitmap, screenInsets)); + setAnimatedViewSize(screenRect.width(), screenRect.height()); + + // will show when the animation starts + mScreenshotAnimatedView.setVisibility(View.GONE); + + mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, bitmap, screenInsets)); + // make static preview invisible (from gone) so we can query its location on screen + mScreenshotPreview.setVisibility(View.INVISIBLE); + } + + AnimatorSet createScreenshotDropInAnimation(Rect bounds, boolean showFlash, + Consumer onElementTapped) { + mScreenshotPreview.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mScreenshotPreview.buildLayer(); + + Rect previewBounds = new Rect(); + mScreenshotPreview.getBoundsOnScreen(previewBounds); + + float cornerScale = + mCornerSizeX / (mOrientationPortrait ? bounds.width() : bounds.height()); + final float currentScale = 1f; + + mScreenshotAnimatedView.setScaleX(currentScale); + mScreenshotAnimatedView.setScaleY(currentScale); + + mDismissButton.setAlpha(0); + mDismissButton.setVisibility(View.VISIBLE); + + AnimatorSet dropInAnimation = new AnimatorSet(); + ValueAnimator flashInAnimator = ValueAnimator.ofFloat(0, 1); + flashInAnimator.setDuration(SCREENSHOT_FLASH_IN_DURATION_MS); + flashInAnimator.setInterpolator(mFastOutSlowIn); + flashInAnimator.addUpdateListener(animation -> + mScreenshotFlash.setAlpha((float) animation.getAnimatedValue())); + + ValueAnimator flashOutAnimator = ValueAnimator.ofFloat(1, 0); + flashOutAnimator.setDuration(SCREENSHOT_FLASH_OUT_DURATION_MS); + flashOutAnimator.setInterpolator(mFastOutSlowIn); + flashOutAnimator.addUpdateListener(animation -> + mScreenshotFlash.setAlpha((float) animation.getAnimatedValue())); + + // animate from the current location, to the static preview location + final PointF startPos = new PointF(bounds.centerX(), bounds.centerY()); + final PointF finalPos = new PointF(previewBounds.centerX(), previewBounds.centerY()); + + ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1); + toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS); + float xPositionPct = + SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; + float dismissPct = + SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; + float scalePct = + SCREENSHOT_TO_CORNER_SCALE_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS; + toCorner.addUpdateListener(animation -> { + float t = animation.getAnimatedFraction(); + if (t < scalePct) { + float scale = MathUtils.lerp( + currentScale, cornerScale, mFastOutSlowIn.getInterpolation(t / scalePct)); + mScreenshotAnimatedView.setScaleX(scale); + mScreenshotAnimatedView.setScaleY(scale); + } else { + mScreenshotAnimatedView.setScaleX(cornerScale); + mScreenshotAnimatedView.setScaleY(cornerScale); + } + + float currentScaleX = mScreenshotAnimatedView.getScaleX(); + float currentScaleY = mScreenshotAnimatedView.getScaleY(); + + if (t < xPositionPct) { + float xCenter = MathUtils.lerp(startPos.x, finalPos.x, + mFastOutSlowIn.getInterpolation(t / xPositionPct)); + mScreenshotAnimatedView.setX(xCenter - bounds.width() * currentScaleX / 2f); + } else { + mScreenshotAnimatedView.setX(finalPos.x - bounds.width() * currentScaleX / 2f); + } + float yCenter = MathUtils.lerp( + startPos.y, finalPos.y, mFastOutSlowIn.getInterpolation(t)); + mScreenshotAnimatedView.setY(yCenter - bounds.height() * currentScaleY / 2f); + + if (t >= dismissPct) { + mDismissButton.setAlpha((t - dismissPct) / (1 - dismissPct)); + float currentX = mScreenshotAnimatedView.getX(); + float currentY = mScreenshotAnimatedView.getY(); + mDismissButton.setY(currentY - mDismissButton.getHeight() / 2f); + if (mDirectionLTR) { + mDismissButton.setX(currentX + + bounds.width() * currentScaleX - mDismissButton.getWidth() / 2f); + } else { + mDismissButton.setX(currentX - mDismissButton.getWidth() / 2f); + } + } + }); + + toCorner.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + mScreenshotAnimatedView.setVisibility(View.VISIBLE); + } + }); + + mScreenshotFlash.setAlpha(0f); + mScreenshotFlash.setVisibility(View.VISIBLE); + + if (showFlash) { + dropInAnimation.play(flashOutAnimator).after(flashInAnimator); + dropInAnimation.play(flashOutAnimator).with(toCorner); + } else { + dropInAnimation.play(toCorner); + } + + dropInAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mDismissButton.setOnClickListener(view -> + onElementTapped.accept(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL)); + mDismissButton.setAlpha(1); + float dismissOffset = mDismissButton.getWidth() / 2f; + float finalDismissX = mDirectionLTR + ? finalPos.x - dismissOffset + bounds.width() * cornerScale / 2f + : finalPos.x - dismissOffset - bounds.width() * cornerScale / 2f; + mDismissButton.setX(finalDismissX); + mDismissButton.setY( + finalPos.y - dismissOffset - bounds.height() * cornerScale / 2f); + mScreenshotAnimatedView.setScaleX(1); + mScreenshotAnimatedView.setScaleY(1); + mScreenshotAnimatedView.setX(finalPos.x - bounds.width() * cornerScale / 2f); + mScreenshotAnimatedView.setY(finalPos.y - bounds.height() * cornerScale / 2f); + mScreenshotAnimatedView.setVisibility(View.GONE); + mScreenshotPreview.setVisibility(View.VISIBLE); + forceLayout(); + createScreenshotActionsShadeAnimation().start(); + } + }); + + return dropInAnimation; + } + + ValueAnimator createScreenshotActionsShadeAnimation() { + // By default the activities won't be able to start immediately; override this to keep + // the same behavior as if started from a notification + try { + ActivityManager.getService().resumeAppSwitches(); + } catch (RemoteException e) { + } + + ArrayList chips = new ArrayList<>(); + + mShareChip.setText(mContext.getString(com.android.internal.R.string.share)); + mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); + mShareChip.setOnClickListener(v -> { + mShareChip.setIsPending(true); + mEditChip.setIsPending(false); + mPendingInteraction = PendingInteraction.SHARE; + }); + chips.add(mShareChip); + + mEditChip.setText(mContext.getString(com.android.internal.R.string.screenshot_edit)); + mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true); + mEditChip.setOnClickListener(v -> { + mEditChip.setIsPending(true); + mShareChip.setIsPending(false); + mPendingInteraction = PendingInteraction.EDIT; + }); + chips.add(mEditChip); + + mScreenshotPreview.setOnClickListener(v -> { + mShareChip.setIsPending(false); + mEditChip.setIsPending(false); + mPendingInteraction = PendingInteraction.PREVIEW; + }); + + // remove the margin from the last chip so that it's correctly aligned with the end + LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) + mActionsView.getChildAt(0).getLayoutParams(); + params.setMarginEnd(0); + mActionsView.getChildAt(0).setLayoutParams(params); + + ValueAnimator animator = ValueAnimator.ofFloat(0, 1); + animator.setDuration(SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS); + float alphaFraction = (float) SCREENSHOT_ACTIONS_ALPHA_DURATION_MS + / SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS; + mActionsContainer.setAlpha(0f); + mActionsContainerBackground.setAlpha(0f); + mActionsContainer.setVisibility(View.VISIBLE); + mActionsContainerBackground.setVisibility(View.VISIBLE); + + animator.addUpdateListener(animation -> { + float t = animation.getAnimatedFraction(); + mBackgroundProtection.setAlpha(t); + float containerAlpha = t < alphaFraction ? t / alphaFraction : 1; + mActionsContainer.setAlpha(containerAlpha); + mActionsContainerBackground.setAlpha(containerAlpha); + float containerScale = SCREENSHOT_ACTIONS_START_SCALE_X + + (t * (1 - SCREENSHOT_ACTIONS_START_SCALE_X)); + mActionsContainer.setScaleX(containerScale); + mActionsContainerBackground.setScaleX(containerScale); + for (ScreenshotActionChip chip : chips) { + chip.setAlpha(t); + chip.setScaleX(1 / containerScale); // invert to keep size of children constant + } + mActionsContainer.setScrollX(mDirectionLTR ? 0 : mActionsContainer.getWidth()); + mActionsContainer.setPivotX(mDirectionLTR ? 0 : mActionsContainer.getWidth()); + mActionsContainerBackground.setPivotX( + mDirectionLTR ? 0 : mActionsContainerBackground.getWidth()); + }); + return animator; + } + + void setChipIntents(ScreenshotController.SavedImageData imageData, + Consumer onElementTapped) { + mShareChip.setPendingIntent(imageData.shareAction.actionIntent, + () -> onElementTapped.accept(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED)); + mEditChip.setPendingIntent(imageData.editAction.actionIntent, + () -> onElementTapped.accept(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED)); + mScreenshotPreview.setOnClickListener(v -> { + try { + imageData.editAction.actionIntent.send(); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Intent cancelled", e); + } + onElementTapped.accept(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED); + }); + + if (mPendingInteraction != null) { + switch (mPendingInteraction) { + case PREVIEW: + mScreenshotPreview.callOnClick(); + break; + case SHARE: + mShareChip.callOnClick(); + break; + case EDIT: + mEditChip.callOnClick(); + break; + } + } else { + LayoutInflater inflater = LayoutInflater.from(mContext); + + for (Notification.Action smartAction : imageData.smartActions) { + ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate( + R.layout.global_screenshot_action_chip, mActionsView, false); + actionChip.setText(smartAction.title); + actionChip.setIcon(smartAction.getIcon(), false); + actionChip.setPendingIntent(smartAction.actionIntent, + () -> onElementTapped.accept( + ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED)); + mActionsView.addView(actionChip); + mSmartChips.add(actionChip); + } + } + } + + + AnimatorSet createScreenshotDismissAnimation() { + ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1); + alphaAnim.setStartDelay(SCREENSHOT_DISMISS_ALPHA_OFFSET_MS); + alphaAnim.setDuration(SCREENSHOT_DISMISS_ALPHA_DURATION_MS); + alphaAnim.addUpdateListener(animation -> { + setAlpha(1 - animation.getAnimatedFraction()); + }); + + ValueAnimator yAnim = ValueAnimator.ofFloat(0, 1); + yAnim.setInterpolator(mAccelerateInterpolator); + yAnim.setDuration(SCREENSHOT_DISMISS_Y_DURATION_MS); + float screenshotStartY = mScreenshotPreview.getTranslationY(); + float dismissStartY = mDismissButton.getTranslationY(); + yAnim.addUpdateListener(animation -> { + float yDelta = MathUtils.lerp(0, mDismissDeltaY, animation.getAnimatedFraction()); + mScreenshotPreview.setTranslationY(screenshotStartY + yDelta); + mDismissButton.setTranslationY(dismissStartY + yDelta); + mActionsContainer.setTranslationY(yDelta); + mActionsContainerBackground.setTranslationY(yDelta); + }); + + AnimatorSet animSet = new AnimatorSet(); + animSet.play(yAnim).with(alphaAnim); + + return animSet; + } + + void reset() { + // Clear any references to the bitmap + mScreenshotPreview.setImageDrawable(null); + mScreenshotAnimatedView.setImageDrawable(null); + mScreenshotAnimatedView.setVisibility(View.GONE); + mActionsContainerBackground.setVisibility(View.GONE); + mActionsContainer.setVisibility(View.GONE); + mBackgroundProtection.setAlpha(0f); + mDismissButton.setVisibility(View.GONE); + mScreenshotPreview.setVisibility(View.GONE); + mScreenshotPreview.setLayerType(View.LAYER_TYPE_NONE, null); + mScreenshotPreview.setContentDescription( + mContext.getResources().getString(R.string.screenshot_preview_description)); + mScreenshotPreview.setOnClickListener(null); + mShareChip.setOnClickListener(null); + mEditChip.setOnClickListener(null); + mShareChip.setIsPending(false); + mEditChip.setIsPending(false); + mPendingInteraction = null; + for (ScreenshotActionChip chip : mSmartChips) { + mActionsView.removeView(chip); + } + mSmartChips.clear(); + setAlpha(1); + mDismissButton.setTranslationY(0); + mActionsContainer.setTranslationY(0); + mActionsContainerBackground.setTranslationY(0); + mScreenshotPreview.setTranslationY(0); + mScreenshotSelectorView.stop(); + } + + private void setAnimatedViewSize(int width, int height) { + ViewGroup.LayoutParams layoutParams = mScreenshotAnimatedView.getLayoutParams(); + layoutParams.width = width; + layoutParams.height = height; + mScreenshotAnimatedView.setLayoutParams(layoutParams); + } + + /** + * Create a drawable using the size of the bitmap and insets as the fractional inset parameters. + */ + private static Drawable createScreenDrawable(Resources res, Bitmap bitmap, Insets insets) { + int insettedWidth = bitmap.getWidth() - insets.left - insets.right; + int insettedHeight = bitmap.getHeight() - insets.top - insets.bottom; + + BitmapDrawable bitmapDrawable = new BitmapDrawable(res, bitmap); + if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 + || bitmap.getHeight() == 0) { + Log.e(TAG, String.format( + "Can't create insetted drawable, using 0 insets " + + "bitmap and insets create degenerate region: %dx%d %s", + bitmap.getWidth(), bitmap.getHeight(), insets)); + return bitmapDrawable; + } + + InsetDrawable insetDrawable = new InsetDrawable(bitmapDrawable, + -1f * insets.left / insettedWidth, + -1f * insets.top / insettedHeight, + -1f * insets.right / insettedWidth, + -1f * insets.bottom / insettedHeight); + + if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) { + // Are any of the insets negative, meaning the bitmap is smaller than the bounds so need + // to fill in the background of the drawable. + return new LayerDrawable(new Drawable[]{ + new ColorDrawable(Color.BLACK), insetDrawable}); + } else { + return insetDrawable; + } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java index 217235b16ecf5..f32529fdaf04c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java @@ -16,9 +16,9 @@ package com.android.systemui.screenshot; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_INTENT; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_TYPE; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_INTENT; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_TYPE; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; import android.app.ActivityOptions; import android.app.PendingIntent; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index a043f0f1e50c5..4e2283396e257 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -52,7 +52,7 @@ import javax.inject.Inject; public class TakeScreenshotService extends Service { private static final String TAG = "TakeScreenshotService"; - private final GlobalScreenshot mScreenshot; + private final ScreenshotController mScreenshot; private final UserManager mUserManager; private final UiEventLogger mUiEventLogger; @@ -61,7 +61,7 @@ public class TakeScreenshotService extends Service { @Override public void onReceive(Context context, Intent intent) { if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction()) && mScreenshot != null) { - mScreenshot.dismissScreenshot("close system dialogs", false); + mScreenshot.dismissScreenshot(false); } } }; @@ -125,7 +125,7 @@ public class TakeScreenshotService extends Service { }; @Inject - public TakeScreenshotService(GlobalScreenshot globalScreenshot, UserManager userManager, + public TakeScreenshotService(ScreenshotController globalScreenshot, UserManager userManager, UiEventLogger uiEventLogger) { mScreenshot = globalScreenshot; mUserManager = userManager; @@ -144,7 +144,9 @@ public class TakeScreenshotService extends Service { @Override public boolean onUnbind(Intent intent) { - if (mScreenshot != null) mScreenshot.stopScreenshot(); + if (mScreenshot != null && !mScreenshot.isDismissing()) { + mScreenshot.dismissScreenshot(true); + } unregisterReceiver(mBroadcastReceiver); return true; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java index 4aaafbdaec1d8..de176b84ac4cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java @@ -16,9 +16,9 @@ package com.android.systemui.screenshot; -import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_SHARE; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED; +import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_SHARE; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED; import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT; import static org.mockito.ArgumentMatchers.any; @@ -79,7 +79,7 @@ public class ActionProxyReceiverTest extends SysuiTestCase { public void setup() throws InterruptedException, ExecutionException, TimeoutException { MockitoAnnotations.initMocks(this); mIntent = new Intent(mContext, ActionProxyReceiver.class) - .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, mMockPendingIntent); + .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, mMockPendingIntent); when(mMockActivityManagerWrapper.closeSystemWindows(anyString())).thenReturn(mMockFuture); when(mMockFuture.get(anyLong(), any(TimeUnit.class))).thenReturn(null); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DeleteScreenshotReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DeleteScreenshotReceiverTest.java index b9249131c191b..14c76798e0efe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DeleteScreenshotReceiverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DeleteScreenshotReceiverTest.java @@ -16,10 +16,10 @@ package com.android.systemui.screenshot; -import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_DELETE; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED; -import static com.android.systemui.screenshot.GlobalScreenshot.SCREENSHOT_URI_ID; +import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_DELETE; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED; +import static com.android.systemui.screenshot.ScreenshotController.SCREENSHOT_URI_ID; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java index e23f926165651..2374b82aa9ff7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java @@ -169,8 +169,8 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { Looper.prepare(); } - GlobalScreenshot.SaveImageInBackgroundData - data = new GlobalScreenshot.SaveImageInBackgroundData(); + ScreenshotController.SaveImageInBackgroundData + data = new ScreenshotController.SaveImageInBackgroundData(); data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); data.finisher = null; data.mActionsReadyListener = null; @@ -183,9 +183,9 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { Intent intent = shareAction.actionIntent.getIntent(); assertNotNull(intent); Bundle bundle = intent.getExtras(); - assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID)); - assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED)); - assertEquals(GlobalScreenshot.ACTION_TYPE_SHARE, shareAction.title); + assertTrue(bundle.containsKey(ScreenshotController.EXTRA_ID)); + assertTrue(bundle.containsKey(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED)); + assertEquals(ScreenshotController.ACTION_TYPE_SHARE, shareAction.title); assertEquals(Intent.ACTION_SEND, intent.getAction()); } @@ -196,8 +196,8 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { Looper.prepare(); } - GlobalScreenshot.SaveImageInBackgroundData - data = new GlobalScreenshot.SaveImageInBackgroundData(); + ScreenshotController.SaveImageInBackgroundData + data = new ScreenshotController.SaveImageInBackgroundData(); data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); data.finisher = null; data.mActionsReadyListener = null; @@ -210,9 +210,9 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { Intent intent = editAction.actionIntent.getIntent(); assertNotNull(intent); Bundle bundle = intent.getExtras(); - assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID)); - assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED)); - assertEquals(GlobalScreenshot.ACTION_TYPE_EDIT, editAction.title); + assertTrue(bundle.containsKey(ScreenshotController.EXTRA_ID)); + assertTrue(bundle.containsKey(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED)); + assertEquals(ScreenshotController.ACTION_TYPE_EDIT, editAction.title); assertEquals(Intent.ACTION_EDIT, intent.getAction()); } @@ -223,8 +223,8 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { Looper.prepare(); } - GlobalScreenshot.SaveImageInBackgroundData - data = new GlobalScreenshot.SaveImageInBackgroundData(); + ScreenshotController.SaveImageInBackgroundData + data = new ScreenshotController.SaveImageInBackgroundData(); data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); data.finisher = null; data.mActionsReadyListener = null; @@ -238,9 +238,9 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { Intent intent = deleteAction.actionIntent.getIntent(); assertNotNull(intent); Bundle bundle = intent.getExtras(); - assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID)); - assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED)); - assertEquals(deleteAction.title, GlobalScreenshot.ACTION_TYPE_DELETE); + assertTrue(bundle.containsKey(ScreenshotController.EXTRA_ID)); + assertTrue(bundle.containsKey(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED)); + assertEquals(deleteAction.title, ScreenshotController.ACTION_TYPE_DELETE); assertNull(intent.getAction()); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SmartActionsReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SmartActionsReceiverTest.java index ce6f0736ec335..6f3a4a17a4a59 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SmartActionsReceiverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SmartActionsReceiverTest.java @@ -16,8 +16,8 @@ package com.android.systemui.screenshot; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_TYPE; -import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_TYPE; +import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -56,7 +56,7 @@ public class SmartActionsReceiverTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mSmartActionsReceiver = new SmartActionsReceiver(mMockScreenshotSmartActions); mIntent = new Intent(mContext, SmartActionsReceiver.class) - .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, mMockPendingIntent); + .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, mMockPendingIntent); } @Test