diff options
15 files changed, 1392 insertions, 1276 deletions
diff --git a/packages/SystemUI/res/layout/global_screenshot.xml b/packages/SystemUI/res/layout/global_screenshot.xml index ef7325ea8f38..e0333eb82d51 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. --> -<FrameLayout +<com.android.systemui.screenshot.ScreenshotView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/global_screenshot_frame" android:layout_width="match_parent" @@ -62,4 +62,4 @@ android:layout_margin="@dimen/screenshot_dismiss_button_margin" android:src="@drawable/screenshot_cancel"/> </FrameLayout> -</FrameLayout> +</com.android.systemui.screenshot.ScreenshotView> diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java index 3fd7f94514f3..5c26d9400c9f 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 9028bb57c8e5..35839f39b491 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 818bb9d8d78f..000000000000 --- 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<Uri> 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<Notification.Action> 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<ScreenshotActionChip> 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<Uri> 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<Uri> 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<Uri> 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<Uri> 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<Uri> 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<Uri> 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<Uri> 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<Uri> 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<ScreenshotActionChip> 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 f0ea597c458d..c0061ad97293 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<Void, Void, Void> { 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<Void, Void, Void> { 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<Void, Void, Void> { * 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<Void, Void, Void> { // 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<Void, Void, Void> { // 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<Void, Void, Void> { // 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<Void, Void, Void> { 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<Void, Void, Void> { 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 000000000000..5d8f70c4e460 --- /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<Uri> 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<Notification.Action> 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<Uri> 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<Uri> 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<Uri> 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<Uri> 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<Uri> 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<Uri> 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<Uri> 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<Uri> 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 6d1299ba98ac..a946513870c9 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 07a92460f3ea..c793b5b9639e 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<Rect> mOnScreenshotSelected; + public ScreenshotSelectorView(Context context) { this(context, null); } @@ -46,14 +51,54 @@ public class ScreenshotSelectorView extends View { mPaintBackground.setAlpha(160); mPaintSelection = new Paint(Color.TRANSPARENT); mPaintSelection.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + + 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 + public void draw(Canvas canvas) { + canvas.drawRect(mLeft, mTop, mRight, mBottom, mPaintBackground); + if (mSelectionRect != null) { + canvas.drawRect(mSelectionRect, mPaintSelection); + } + } + + void setOnScreenshotSelected(Consumer<Rect> onScreenshotSelected) { + mOnScreenshotSelected = onScreenshotSelected; + } + + void stop() { + if (getSelectionRect() != null) { + stopSelection(); + } } - public void startSelection(int x, int y) { + private 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) { + private void updateSelection(int x, int y) { if (mSelectionRect != null) { mSelectionRect.left = Math.min(mStartPoint.x, x); mSelectionRect.right = Math.max(mStartPoint.x, x); @@ -63,20 +108,12 @@ public class ScreenshotSelectorView extends View { } } - public Rect getSelectionRect() { + private Rect getSelectionRect() { return mSelectionRect; } - public void stopSelection() { + private void stopSelection() { mStartPoint = null; mSelectionRect = null; } - - @Override - public void draw(Canvas canvas) { - canvas.drawRect(mLeft, mTop, mRight, mBottom, mPaintBackground); - if (mSelectionRect != null) { - canvas.drawRect(mSelectionRect, mPaintSelection); - } - } } 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 000000000000..03fe2920405f --- /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<ScreenshotActionChip> 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<Rect> 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<ScreenshotEvent> 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<ScreenshotActionChip> 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<ScreenshotEvent> 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 217235b16ecf..f32529fdaf04 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 a043f0f1e50c..4e2283396e25 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 4aaafbdaec1d..de176b84ac4c 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 b9249131c191..14c76798e0ef 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 e23f92616565..2374b82aa9ff 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 ce6f0736ec33..6f3a4a17a4a5 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 |