Merge "Refactor GlobalScreenshot"

This commit is contained in:
Miranda Kephart
2020-10-21 18:36:55 +00:00
committed by Android (Google) Code Review
15 changed files with 1403 additions and 1287 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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,30 +51,28 @@ public class ScreenshotSelectorView extends View {
mPaintBackground.setAlpha(160);
mPaintSelection = new Paint(Color.TRANSPARENT);
mPaintSelection.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
public void startSelection(int x, int y) {
mStartPoint = new Point(x, y);
mSelectionRect = new Rect(x, y, x, y);
}
public void updateSelection(int x, int y) {
if (mSelectionRect != null) {
mSelectionRect.left = Math.min(mStartPoint.x, x);
mSelectionRect.right = Math.max(mStartPoint.x, x);
mSelectionRect.top = Math.min(mStartPoint.y, y);
mSelectionRect.bottom = Math.max(mStartPoint.y, y);
invalidate();
}
}
public Rect getSelectionRect() {
return mSelectionRect;
}
public void stopSelection() {
mStartPoint = null;
mSelectionRect = null;
setOnTouchListener((v, event) -> {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startSelection((int) event.getX(), (int) event.getY());
return true;
case MotionEvent.ACTION_MOVE:
updateSelection((int) event.getX(), (int) event.getY());
return true;
case MotionEvent.ACTION_UP:
setVisibility(View.GONE);
final Rect rect = getSelectionRect();
if (mOnScreenshotSelected != null
&& rect != null
&& rect.width() != 0 && rect.height() != 0) {
mOnScreenshotSelected.accept(rect);
}
stopSelection();
return true;
}
return false;
});
}
@Override
@@ -79,4 +82,38 @@ public class ScreenshotSelectorView extends View {
canvas.drawRect(mSelectionRect, mPaintSelection);
}
}
void setOnScreenshotSelected(Consumer<Rect> onScreenshotSelected) {
mOnScreenshotSelected = onScreenshotSelected;
}
void stop() {
if (getSelectionRect() != null) {
stopSelection();
}
}
private void startSelection(int x, int y) {
mStartPoint = new Point(x, y);
mSelectionRect = new Rect(x, y, x, y);
}
private void updateSelection(int x, int y) {
if (mSelectionRect != null) {
mSelectionRect.left = Math.min(mStartPoint.x, x);
mSelectionRect.right = Math.max(mStartPoint.x, x);
mSelectionRect.top = Math.min(mStartPoint.y, y);
mSelectionRect.bottom = Math.max(mStartPoint.y, y);
invalidate();
}
}
private Rect getSelectionRect() {
return mSelectionRect;
}
private void stopSelection() {
mStartPoint = null;
mSelectionRect = null;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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());
}
}

View File

@@ -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