summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/layout/global_screenshot.xml4
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java1199
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java38
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java665
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSelectorView.java61
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java611
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java10
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/DeleteScreenshotReceiverTest.java8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java30
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/SmartActionsReceiverTest.java6
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