Merge "Refactor GlobalScreenshot"
This commit is contained in:
committed by
Android (Google) Code Review
commit
42e40336cc
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user