Merge "Add screenshots logging" into rvc-dev am: 44cc1e274b

Change-Id: Ia8a3352ca08bfa9fdb6c67bc7450be5279f816b4
This commit is contained in:
Miranda Kephart
2020-04-21 17:06:35 +00:00
committed by Automerger Merge Worker
17 changed files with 527 additions and 196 deletions

View File

@@ -514,33 +514,24 @@ public interface WindowManager extends ViewManager {
int TAKE_SCREENSHOT_PROVIDED_IMAGE = 3; int TAKE_SCREENSHOT_PROVIDED_IMAGE = 3;
/** /**
* Parcel key for the screen shot bitmap sent with messages of type * Enum listing the possible sources from which a screenshot was originated. Used for logging.
* {@link #TAKE_SCREENSHOT_PROVIDED_IMAGE}, type {@link android.graphics.Bitmap} *
* @hide * @hide
*/ */
String PARCEL_KEY_SCREENSHOT_BITMAP = "screenshot_screen_bitmap"; @IntDef({ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS,
ScreenshotSource.SCREENSHOT_KEY_CHORD,
/** ScreenshotSource.SCREENSHOT_KEY_OTHER,
* Parcel key for the screen bounds of the image sent with messages of type ScreenshotSource.SCREENSHOT_OVERVIEW,
* [@link {@link #TAKE_SCREENSHOT_PROVIDED_IMAGE}], type {@link Rect} in screen coordinates. ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS,
* @hide ScreenshotSource.SCREENSHOT_OTHER})
*/ @interface ScreenshotSource {
String PARCEL_KEY_SCREENSHOT_BOUNDS = "screenshot_screen_bounds"; int SCREENSHOT_GLOBAL_ACTIONS = 0;
int SCREENSHOT_KEY_CHORD = 1;
/** int SCREENSHOT_KEY_OTHER = 2;
* Parcel key for the task id of the task that the screen shot was taken of, sent with messages int SCREENSHOT_OVERVIEW = 3;
* of type [@link {@link #TAKE_SCREENSHOT_PROVIDED_IMAGE}], type int. int SCREENSHOT_ACCESSIBILITY_ACTIONS = 4;
* @hide int SCREENSHOT_OTHER = 5;
*/ }
String PARCEL_KEY_SCREENSHOT_TASK_ID = "screenshot_task_id";
/**
* Parcel key for the visible insets of the image sent with messages of type
* [@link {@link #TAKE_SCREENSHOT_PROVIDED_IMAGE}], type {@link android.graphics.Insets} in
* screen coordinates.
* @hide
*/
String PARCEL_KEY_SCREENSHOT_INSETS = "screenshot_insets";
/** /**
* @hide * @hide

View File

@@ -1,5 +1,7 @@
package com.android.internal.util; package com.android.internal.util;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.content.ComponentName; import android.content.ComponentName;
@@ -10,11 +12,12 @@ import android.graphics.Bitmap;
import android.graphics.Insets; import android.graphics.Insets;
import android.graphics.Rect; import android.graphics.Rect;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.Message; import android.os.Message;
import android.os.Messenger; import android.os.Messenger;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.UserHandle; import android.os.UserHandle;
import android.util.Log; import android.util.Log;
@@ -23,6 +26,109 @@ import android.view.WindowManager;
import java.util.function.Consumer; import java.util.function.Consumer;
public class ScreenshotHelper { public class ScreenshotHelper {
/**
* Describes a screenshot request (to make it easier to pass data through to the handler).
*/
public static class ScreenshotRequest implements Parcelable {
private int mSource;
private boolean mHasStatusBar;
private boolean mHasNavBar;
private Bitmap mBitmap;
private Rect mBoundsInScreen;
private Insets mInsets;
private int mTaskId;
ScreenshotRequest(int source, boolean hasStatus, boolean hasNav) {
mSource = source;
mHasStatusBar = hasStatus;
mHasNavBar = hasNav;
}
ScreenshotRequest(
int source, Bitmap bitmap, Rect boundsInScreen, Insets insets, int taskId) {
mSource = source;
mBitmap = bitmap;
mBoundsInScreen = boundsInScreen;
mInsets = insets;
mTaskId = taskId;
}
ScreenshotRequest(Parcel in) {
mSource = in.readInt();
mHasStatusBar = in.readBoolean();
mHasNavBar = in.readBoolean();
if (in.readInt() == 1) {
mBitmap = in.readParcelable(Bitmap.class.getClassLoader());
mBoundsInScreen = in.readParcelable(Rect.class.getClassLoader());
mInsets = in.readParcelable(Insets.class.getClassLoader());
mTaskId = in.readInt();
}
}
public int getSource() {
return mSource;
}
public boolean getHasStatusBar() {
return mHasStatusBar;
}
public boolean getHasNavBar() {
return mHasNavBar;
}
public Bitmap getBitmap() {
return mBitmap;
}
public Rect getBoundsInScreen() {
return mBoundsInScreen;
}
public Insets getInsets() {
return mInsets;
}
public int getTaskId() {
return mTaskId;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mSource);
dest.writeBoolean(mHasStatusBar);
dest.writeBoolean(mHasNavBar);
if (mBitmap == null) {
dest.writeInt(0);
} else {
dest.writeInt(1);
dest.writeParcelable(mBitmap, 0);
dest.writeParcelable(mBoundsInScreen, 0);
dest.writeParcelable(mInsets, 0);
dest.writeInt(mTaskId);
}
}
public static final @NonNull Parcelable.Creator<ScreenshotRequest> CREATOR =
new Parcelable.Creator<ScreenshotRequest>() {
@Override
public ScreenshotRequest createFromParcel(Parcel source) {
return new ScreenshotRequest(source);
}
@Override
public ScreenshotRequest[] newArray(int size) {
return new ScreenshotRequest[size];
}
};
}
private static final String TAG = "ScreenshotHelper"; private static final String TAG = "ScreenshotHelper";
// Time until we give up on the screenshot & show an error instead. // Time until we give up on the screenshot & show an error instead.
@@ -36,8 +142,10 @@ public class ScreenshotHelper {
mContext = context; mContext = context;
} }
/** /**
* Request a screenshot be taken with a specific timeout. * Request a screenshot be taken.
* *
* Added to support reducing unit test duration; the method variant without a timeout argument * Added to support reducing unit test duration; the method variant without a timeout argument
* is recommended for general use. * is recommended for general use.
@@ -47,6 +155,32 @@ public class ScreenshotHelper {
* or * or
* {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION} * {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
* @param hasStatus {@code true} if the status bar is currently showing. {@code false} * @param hasStatus {@code true} if the status bar is currently showing. {@code false}
* if not.
* @param hasNav {@code true} if the navigation bar is currently showing. {@code
* false} if not.
* @param source The source of the screenshot request. One of
* {SCREENSHOT_GLOBAL_ACTIONS, SCREENSHOT_KEY_CHORD,
* SCREENSHOT_OVERVIEW, SCREENSHOT_OTHER}
* @param handler A handler used in case the screenshot times out
* @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
* screenshot was taken.
*/
public void takeScreenshot(final int screenshotType, final boolean hasStatus,
final boolean hasNav, int source, @NonNull Handler handler,
@Nullable Consumer<Uri> completionConsumer) {
ScreenshotRequest screenshotRequest = new ScreenshotRequest(source, hasStatus, hasNav);
takeScreenshot(screenshotType, SCREENSHOT_TIMEOUT_MS, handler, screenshotRequest,
completionConsumer);
}
/**
* Request a screenshot be taken, with provided reason.
*
* @param screenshotType The type of screenshot, for example either
* {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN}
* or
* {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
* @param hasStatus {@code true} if the status bar is currently showing. {@code false}
* if * if
* not. * not.
* @param hasNav {@code true} if the navigation bar is currently showing. {@code * @param hasNav {@code true} if the navigation bar is currently showing. {@code
@@ -64,7 +198,7 @@ public class ScreenshotHelper {
} }
/** /**
* Request a screenshot be taken. * Request a screenshot be taken with a specific timeout.
* *
* Added to support reducing unit test duration; the method variant without a timeout argument * Added to support reducing unit test duration; the method variant without a timeout argument
* is recommended for general use. * is recommended for general use.
@@ -89,9 +223,9 @@ public class ScreenshotHelper {
public void takeScreenshot(final int screenshotType, final boolean hasStatus, public void takeScreenshot(final int screenshotType, final boolean hasStatus,
final boolean hasNav, long timeoutMs, @NonNull Handler handler, final boolean hasNav, long timeoutMs, @NonNull Handler handler,
@Nullable Consumer<Uri> completionConsumer) { @Nullable Consumer<Uri> completionConsumer) {
takeScreenshot(screenshotType, hasStatus, hasNav, timeoutMs, handler, null, ScreenshotRequest screenshotRequest = new ScreenshotRequest(SCREENSHOT_OTHER, hasStatus,
completionConsumer hasNav);
); takeScreenshot(screenshotType, timeoutMs, handler, screenshotRequest, completionConsumer);
} }
/** /**
@@ -106,23 +240,16 @@ public class ScreenshotHelper {
* screenshot was taken. * screenshot was taken.
*/ */
public void provideScreenshot(@NonNull Bitmap screenshot, @NonNull Rect boundsInScreen, public void provideScreenshot(@NonNull Bitmap screenshot, @NonNull Rect boundsInScreen,
@NonNull Insets insets, int taskId, @NonNull Handler handler, @NonNull Insets insets, int taskId, int source,
@Nullable Consumer<Uri> completionConsumer) { @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) {
Bundle imageBundle = new Bundle(); ScreenshotRequest screenshotRequest =
imageBundle.putParcelable(WindowManager.PARCEL_KEY_SCREENSHOT_BITMAP, screenshot); new ScreenshotRequest(source, screenshot, boundsInScreen, insets, taskId);
imageBundle.putParcelable(WindowManager.PARCEL_KEY_SCREENSHOT_BOUNDS, boundsInScreen); takeScreenshot(WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_TIMEOUT_MS,
imageBundle.putParcelable(WindowManager.PARCEL_KEY_SCREENSHOT_INSETS, insets); handler, screenshotRequest, completionConsumer);
imageBundle.putInt(WindowManager.PARCEL_KEY_SCREENSHOT_TASK_ID, taskId);
takeScreenshot(
WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE,
false, false, // ignored when image bundle is set
SCREENSHOT_TIMEOUT_MS, handler, imageBundle, completionConsumer);
} }
private void takeScreenshot(final int screenshotType, final boolean hasStatus, private void takeScreenshot(final int screenshotType, long timeoutMs, @NonNull Handler handler,
final boolean hasNav, long timeoutMs, @NonNull Handler handler, ScreenshotRequest screenshotRequest, @Nullable Consumer<Uri> completionConsumer) {
@Nullable Bundle providedImage, @Nullable Consumer<Uri> completionConsumer) {
synchronized (mScreenshotLock) { synchronized (mScreenshotLock) {
if (mScreenshotConnection != null) { if (mScreenshotConnection != null) {
return; return;
@@ -157,7 +284,7 @@ public class ScreenshotHelper {
return; return;
} }
Messenger messenger = new Messenger(service); Messenger messenger = new Messenger(service);
Message msg = Message.obtain(null, screenshotType); Message msg = Message.obtain(null, screenshotType, screenshotRequest);
final ServiceConnection myConn = this; final ServiceConnection myConn = this;
Handler h = new Handler(handler.getLooper()) { Handler h = new Handler(handler.getLooper()) {
@Override @Override
@@ -175,12 +302,6 @@ public class ScreenshotHelper {
} }
}; };
msg.replyTo = new Messenger(h); msg.replyTo = new Messenger(h);
msg.arg1 = hasStatus ? 1 : 0;
msg.arg2 = hasNav ? 1 : 0;
if (screenshotType == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) {
msg.setData(providedImage);
}
try { try {
messenger.send(msg); messenger.send(msg);

View File

@@ -36,6 +36,7 @@ import android.graphics.Insets;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.view.WindowManager;
import androidx.test.runner.AndroidJUnit4; import androidx.test.runner.AndroidJUnit4;
@@ -91,7 +92,8 @@ public final class ScreenshotHelperTest {
public void testProvidedImageScreenshot() { public void testProvidedImageScreenshot() {
mScreenshotHelper.provideScreenshot( mScreenshotHelper.provideScreenshot(
Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888), new Rect(), Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888), new Rect(),
Insets.of(0, 0, 0, 0), 1, mHandler, null); Insets.of(0, 0, 0, 0), 1,
WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null);
} }
@Test @Test

View File

@@ -16,6 +16,8 @@
package com.android.systemui.accessibility; package com.android.systemui.accessibility;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS;
import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityService;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.RemoteAction; import android.app.RemoteAction;
@@ -282,8 +284,8 @@ public class SystemActions extends SystemUI {
private void handleTakeScreenshot() { private void handleTakeScreenshot() {
ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext); ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext);
screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN, screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN, true, true,
true, true, new Handler(Looper.getMainLooper()), null); SCREENSHOT_GLOBAL_ACTIONS, new Handler(Looper.getMainLooper()), null);
} }
private void handleAccessibilityMenu() { private void handleAccessibilityMenu() {

View File

@@ -16,6 +16,8 @@ package com.android.systemui.globalactions;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS;
import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
@@ -830,7 +832,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener,
mHandler.postDelayed(new Runnable() { mHandler.postDelayed(new Runnable() {
@Override @Override
public void run() { public void run() {
mScreenshotHelper.takeScreenshot(1, true, true, mHandler, null); mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, true, true,
SCREENSHOT_GLOBAL_ACTIONS, mHandler, null);
mMetricsLogger.action(MetricsEvent.ACTION_SCREENSHOT_POWER_MENU); mMetricsLogger.action(MetricsEvent.ACTION_SCREENSHOT_POWER_MENU);
mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS); mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS);
} }

View File

@@ -20,6 +20,7 @@ import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.ACTION_UP;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR;
@@ -380,7 +381,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
public void handleImageAsScreenshot(Bitmap screenImage, Rect locationInScreen, public void handleImageAsScreenshot(Bitmap screenImage, Rect locationInScreen,
Insets visibleInsets, int taskId) { Insets visibleInsets, int taskId) {
mScreenshotHelper.provideScreenshot(screenImage, locationInScreen, visibleInsets, mScreenshotHelper.provideScreenshot(screenImage, locationInScreen, visibleInsets,
taskId, mHandler, null); taskId, SCREENSHOT_OVERVIEW, mHandler, null);
} }
@Override @Override

View File

@@ -76,6 +76,7 @@ import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.Toast; import android.widget.Toast;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -104,7 +105,6 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
*/ */
static class SaveImageInBackgroundData { static class SaveImageInBackgroundData {
public Bitmap image; public Bitmap image;
public Uri imageUri;
public Consumer<Uri> finisher; public Consumer<Uri> finisher;
public GlobalScreenshot.ActionsReadyListener mActionsReadyListener; public GlobalScreenshot.ActionsReadyListener mActionsReadyListener;
public int errorMsgResId; public int errorMsgResId;
@@ -112,13 +112,33 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
void clearImage() { void clearImage() {
image = null; image = null;
imageUri = 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 static class ActionsReadyListener {
abstract void onActionsReady(Uri imageUri, List<Notification.Action> smartActions, abstract void onActionsReady(SavedImageData imageData);
List<Notification.Action> actions);
} }
// These strings are used for communicating the action invoked to // These strings are used for communicating the action invoked to
@@ -147,6 +167,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
private static final int MESSAGE_CORNER_TIMEOUT = 2; private static final int MESSAGE_CORNER_TIMEOUT = 2;
private final ScreenshotNotificationsController mNotificationsController; private final ScreenshotNotificationsController mNotificationsController;
private final UiEventLogger mUiEventLogger;
private final Context mContext; private final Context mContext;
private final WindowManager mWindowManager; private final WindowManager mWindowManager;
@@ -185,6 +206,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
switch (msg.what) { switch (msg.what) {
case MESSAGE_CORNER_TIMEOUT: case MESSAGE_CORNER_TIMEOUT:
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT);
GlobalScreenshot.this.clearScreenshot("timeout"); GlobalScreenshot.this.clearScreenshot("timeout");
break; break;
default: default:
@@ -199,9 +221,11 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
@Inject @Inject
public GlobalScreenshot( public GlobalScreenshot(
Context context, @Main Resources resources, LayoutInflater layoutInflater, Context context, @Main Resources resources, LayoutInflater layoutInflater,
ScreenshotNotificationsController screenshotNotificationsController) { ScreenshotNotificationsController screenshotNotificationsController,
UiEventLogger uiEventLogger) {
mContext = context; mContext = context;
mNotificationsController = screenshotNotificationsController; mNotificationsController = screenshotNotificationsController;
mUiEventLogger = uiEventLogger;
// Inflate the screenshot layout // Inflate the screenshot layout
mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null); mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null);
@@ -222,7 +246,10 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
mBackgroundProtection = mScreenshotLayout.findViewById( mBackgroundProtection = mScreenshotLayout.findViewById(
R.id.global_screenshot_actions_background); R.id.global_screenshot_actions_background);
mDismissButton = mScreenshotLayout.findViewById(R.id.global_screenshot_dismiss_button); mDismissButton = mScreenshotLayout.findViewById(R.id.global_screenshot_dismiss_button);
mDismissButton.setOnClickListener(view -> clearScreenshot("dismiss_button")); mDismissButton.setOnClickListener(view -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL);
clearScreenshot("dismiss_button");
});
mScreenshotFlash = mScreenshotLayout.findViewById(R.id.global_screenshot_flash); mScreenshotFlash = mScreenshotLayout.findViewById(R.id.global_screenshot_flash);
mScreenshotSelectorView = mScreenshotLayout.findViewById(R.id.global_screenshot_selector); mScreenshotSelectorView = mScreenshotLayout.findViewById(R.id.global_screenshot_selector);
@@ -445,12 +472,14 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() { saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() {
@Override @Override
void onActionsReady(Uri uri, List<Notification.Action> smartActions, void onActionsReady(SavedImageData imageData) {
List<Notification.Action> actions) { finisher.accept(imageData.uri);
if (uri == null) { if (imageData.uri == null) {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED);
mNotificationsController.notifyScreenshotError( mNotificationsController.notifyScreenshotError(
R.string.screenshot_failed_to_capture_text); R.string.screenshot_failed_to_capture_text);
} else { } else {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED);
mScreenshotHandler.post(() -> { mScreenshotHandler.post(() -> {
if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
mScreenshotAnimation.addListener( mScreenshotAnimation.addListener(
@@ -458,13 +487,12 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
@Override @Override
public void onAnimationEnd(Animator animation) { public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation); super.onAnimationEnd(animation);
createScreenshotActionsShadeAnimation( createScreenshotActionsShadeAnimation(imageData)
smartActions, actions).start(); .start();
} }
}); });
} else { } else {
createScreenshotActionsShadeAnimation(smartActions, createScreenshotActionsShadeAnimation(imageData).start();
actions).start();
} }
}); });
} }
@@ -567,8 +595,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
return dropInAnimation; return dropInAnimation;
} }
private ValueAnimator createScreenshotActionsShadeAnimation( private ValueAnimator createScreenshotActionsShadeAnimation(SavedImageData imageData) {
List<Notification.Action> smartActions, List<Notification.Action> actions) {
LayoutInflater inflater = LayoutInflater.from(mContext); LayoutInflater inflater = LayoutInflater.from(mContext);
mActionsView.removeAllViews(); mActionsView.removeAllViews();
mActionsContainer.setScrollX(0); mActionsContainer.setScrollX(0);
@@ -583,45 +610,63 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
} catch (RemoteException e) { } catch (RemoteException e) {
} }
for (Notification.Action smartAction : smartActions) { for (Notification.Action smartAction : imageData.smartActions) {
ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate( ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate(
R.layout.global_screenshot_action_chip, mActionsView, false); R.layout.global_screenshot_action_chip, mActionsView, false);
actionChip.setText(smartAction.title); actionChip.setText(smartAction.title);
actionChip.setIcon(smartAction.getIcon(), false); actionChip.setIcon(smartAction.getIcon(), false);
actionChip.setPendingIntent(smartAction.actionIntent, actionChip.setPendingIntent(smartAction.actionIntent,
() -> clearScreenshot("chip tapped")); () -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED);
clearScreenshot("chip tapped");
});
mActionsView.addView(actionChip); mActionsView.addView(actionChip);
} }
for (Notification.Action action : actions) { ScreenshotActionChip shareChip = (ScreenshotActionChip) inflater.inflate(
ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate(
R.layout.global_screenshot_action_chip, mActionsView, false); R.layout.global_screenshot_action_chip, mActionsView, false);
actionChip.setText(action.title); shareChip.setText(imageData.shareAction.title);
actionChip.setIcon(action.getIcon(), true); shareChip.setIcon(imageData.shareAction.getIcon(), true);
actionChip.setPendingIntent(action.actionIntent, () -> clearScreenshot("chip tapped")); shareChip.setPendingIntent(imageData.shareAction.actionIntent, () -> {
if (action.actionIntent.getIntent().getAction().equals(Intent.ACTION_EDIT)) { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED);
clearScreenshot("chip tapped");
});
mActionsView.addView(shareChip);
ScreenshotActionChip editChip = (ScreenshotActionChip) inflater.inflate(
R.layout.global_screenshot_action_chip, mActionsView, false);
editChip.setText(imageData.editAction.title);
editChip.setIcon(imageData.editAction.getIcon(), true);
editChip.setPendingIntent(imageData.editAction.actionIntent, () -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED);
clearScreenshot("chip tapped");
});
mActionsView.addView(editChip);
mScreenshotView.setOnClickListener(v -> { mScreenshotView.setOnClickListener(v -> {
try { try {
action.actionIntent.send(); imageData.editAction.actionIntent.send();
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED);
clearScreenshot("screenshot preview tapped"); clearScreenshot("screenshot preview tapped");
} catch (PendingIntent.CanceledException e) { } catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Intent cancelled", e); Log.e(TAG, "Intent cancelled", e);
} }
}); });
mScreenshotView.setContentDescription(action.title); mScreenshotView.setContentDescription(imageData.editAction.title);
}
mActionsView.addView(actionChip);
}
if (DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI, SCREENSHOT_SCROLLING_ENABLED, false)) { if (DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI, SCREENSHOT_SCROLLING_ENABLED, false)) {
ScreenshotActionChip scrollChip = (ScreenshotActionChip) inflater.inflate( ScreenshotActionChip scrollChip = (ScreenshotActionChip) inflater.inflate(
R.layout.global_screenshot_action_chip, mActionsView, false); R.layout.global_screenshot_action_chip, mActionsView, false);
Toast scrollNotImplemented = Toast.makeText( Toast scrollNotImplemented = Toast.makeText(
mContext, "Not implemented", Toast.LENGTH_SHORT); mContext, "Not implemented", Toast.LENGTH_SHORT);
scrollChip.setText("Extend"); // TODO (mkephart): add resource and translate scrollChip.setText("Extend"); // TODO : add resource and translate
scrollChip.setIcon( scrollChip.setIcon(
Icon.createWithResource(mContext, R.drawable.ic_arrow_downward), true); Icon.createWithResource(mContext, R.drawable.ic_arrow_downward), true);
scrollChip.setOnClickListener(v -> scrollNotImplemented.show()); scrollChip.setOnClickListener(v -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SCROLL_TAPPED);
scrollNotImplemented.show();
});
mActionsView.addView(scrollChip); mActionsView.addView(scrollChip);
} }

View File

@@ -24,7 +24,6 @@ import android.animation.AnimatorSet;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener; import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
@@ -53,7 +52,6 @@ import android.widget.Toast;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.Main;
import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import javax.inject.Inject; import javax.inject.Inject;
@@ -347,14 +345,13 @@ public class GlobalScreenshotLegacy {
// Save the screenshot once we have a bit of time now // Save the screenshot once we have a bit of time now
saveScreenshotInWorkerThread(finisher, new GlobalScreenshot.ActionsReadyListener() { saveScreenshotInWorkerThread(finisher, new GlobalScreenshot.ActionsReadyListener() {
@Override @Override
void onActionsReady(Uri uri, List<Notification.Action> smartActions, void onActionsReady(GlobalScreenshot.SavedImageData actionData) {
List<Notification.Action> actions) { if (actionData.uri == null) {
if (uri == null) {
mNotificationsController.notifyScreenshotError( mNotificationsController.notifyScreenshotError(
R.string.screenshot_failed_to_capture_text); R.string.screenshot_failed_to_capture_text);
} else { } else {
mNotificationsController mNotificationsController
.showScreenshotActionsNotification(uri, smartActions, actions); .showScreenshotActionsNotification(actionData);
} }
} }
}); });

View File

@@ -83,6 +83,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
private final Context mContext; private final Context mContext;
private final GlobalScreenshot.SaveImageInBackgroundData mParams; private final GlobalScreenshot.SaveImageInBackgroundData mParams;
private final GlobalScreenshot.SavedImageData mImageData;
private final String mImageFileName; private final String mImageFileName;
private final long mImageTime; private final long mImageTime;
private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider; private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
@@ -93,6 +94,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
SaveImageInBackgroundTask(Context context, GlobalScreenshot.SaveImageInBackgroundData data) { SaveImageInBackgroundTask(Context context, GlobalScreenshot.SaveImageInBackgroundData data) {
mContext = context; mContext = context;
mImageData = new GlobalScreenshot.SavedImageData();
// Prepare all the output metadata // Prepare all the output metadata
mParams = data; mParams = data;
@@ -145,6 +147,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
values.put(MediaColumns.IS_PENDING, 1); values.put(MediaColumns.IS_PENDING, 1);
final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
try { try {
// First, write the actual data for our screenshot // First, write the actual data for our screenshot
try (OutputStream out = resolver.openOutputStream(uri)) { try (OutputStream out = resolver.openOutputStream(uri)) {
@@ -192,8 +195,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
throw e; throw e;
} }
List<Notification.Action> actions =
populateNotificationActions(mContext, r, uri);
List<Notification.Action> smartActions = new ArrayList<>(); List<Notification.Action> smartActions = new ArrayList<>();
if (mSmartActionsEnabled) { if (mSmartActionsEnabled) {
int timeoutMs = DeviceConfig.getInt( int timeoutMs = DeviceConfig.getInt(
@@ -206,8 +207,14 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
mSmartActionsProvider), mSmartActionsProvider),
mContext)); mContext));
} }
mParams.mActionsReadyListener.onActionsReady(uri, smartActions, actions);
mParams.imageUri = uri; mImageData.uri = uri;
mImageData.smartActions = smartActions;
mImageData.shareAction = createShareAction(mContext, mContext.getResources(), uri);
mImageData.editAction = createEditAction(mContext, mContext.getResources(), uri);
mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri);
mParams.mActionsReadyListener.onActionsReady(mImageData);
mParams.image = null; mParams.image = null;
mParams.errorMsgResId = 0; mParams.errorMsgResId = 0;
} catch (Exception e) { } catch (Exception e) {
@@ -216,29 +223,26 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
Slog.e(TAG, "unable to save screenshot", e); Slog.e(TAG, "unable to save screenshot", e);
mParams.clearImage(); mParams.clearImage();
mParams.errorMsgResId = R.string.screenshot_failed_to_save_text; mParams.errorMsgResId = R.string.screenshot_failed_to_save_text;
mParams.mActionsReadyListener.onActionsReady(null, null, null); mImageData.reset();
mParams.mActionsReadyListener.onActionsReady(mImageData);
} }
return null; return null;
} }
@Override
protected void onPostExecute(Void params) {
mParams.finisher.accept(mParams.imageUri);
}
@Override @Override
protected void onCancelled(Void params) { protected void onCancelled(Void params) {
// If we are cancelled while the task is running in the background, we may get null // If we are cancelled while the task is running in the background, we may get null
// params. The finisher is expected to always be called back, so just use the baked-in // params. The finisher is expected to always be called back, so just use the baked-in
// params from the ctor in any case. // params from the ctor in any case.
mParams.mActionsReadyListener.onActionsReady(null, null, null); mImageData.reset();
mParams.mActionsReadyListener.onActionsReady(mImageData);
mParams.finisher.accept(null); mParams.finisher.accept(null);
mParams.clearImage(); mParams.clearImage();
} }
@VisibleForTesting @VisibleForTesting
List<Notification.Action> populateNotificationActions(Context context, Resources r, Uri uri) { Notification.Action createShareAction(Context context, Resources r, Uri uri) {
// Note: Both the share and edit actions are proxied through ActionProxyReceiver in // Note: Both the share and edit actions are proxied through ActionProxyReceiver in
// order to do some common work like dismissing the keyguard and sending // order to do some common work like dismissing the keyguard and sending
// closeSystemWindows // closeSystemWindows
@@ -263,8 +267,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
// by setting the (otherwise unused) request code to the current user id. // by setting the (otherwise unused) request code to the current user id.
int requestCode = context.getUserId(); int requestCode = context.getUserId();
ArrayList<Notification.Action> actions = new ArrayList<>();
PendingIntent chooserAction = PendingIntent.getBroadcast(context, requestCode, PendingIntent chooserAction = PendingIntent.getBroadcast(context, requestCode,
new Intent(context, GlobalScreenshot.TargetChosenReceiver.class), new Intent(context, GlobalScreenshot.TargetChosenReceiver.class),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
@@ -288,7 +290,15 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder( Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
Icon.createWithResource(r, R.drawable.ic_screenshot_share), Icon.createWithResource(r, R.drawable.ic_screenshot_share),
r.getString(com.android.internal.R.string.share), shareAction); r.getString(com.android.internal.R.string.share), shareAction);
actions.add(shareActionBuilder.build());
return shareActionBuilder.build();
}
@VisibleForTesting
Notification.Action createEditAction(Context context, Resources r, Uri uri) {
// Note: Both the share and edit actions are proxied through ActionProxyReceiver in
// order to do some common work like dismissing the keyguard and sending
// closeSystemWindows
// Create an edit intent, if a specific package is provided as the editor, then // Create an edit intent, if a specific package is provided as the editor, then
// launch that directly // launch that directly
@@ -302,6 +312,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Make sure pending intents for the system user are still unique across users
// by setting the (otherwise unused) request code to the current user id.
int requestCode = mContext.getUserId();
// Create a edit action // Create a edit action
PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode, PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode,
new Intent(context, GlobalScreenshot.ActionProxyReceiver.class) new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
@@ -317,9 +331,16 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
Notification.Action.Builder editActionBuilder = new Notification.Action.Builder( Notification.Action.Builder editActionBuilder = new Notification.Action.Builder(
Icon.createWithResource(r, R.drawable.ic_screenshot_edit), Icon.createWithResource(r, R.drawable.ic_screenshot_edit),
r.getString(com.android.internal.R.string.screenshot_edit), editAction); r.getString(com.android.internal.R.string.screenshot_edit), editAction);
actions.add(editActionBuilder.build());
if (mCreateDeleteAction) { return editActionBuilder.build();
}
@VisibleForTesting
Notification.Action createDeleteAction(Context context, Resources r, Uri uri) {
// Make sure pending intents for the system user are still unique across users
// by setting the (otherwise unused) request code to the current user id.
int requestCode = mContext.getUserId();
// Create a delete action for the notification // Create a delete action for the notification
PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode, PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode,
new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class) new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
@@ -332,9 +353,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder( Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
Icon.createWithResource(r, R.drawable.ic_screenshot_delete), Icon.createWithResource(r, R.drawable.ic_screenshot_delete),
r.getString(com.android.internal.R.string.delete), deleteAction); r.getString(com.android.internal.R.string.delete), deleteAction);
actions.add(deleteActionBuilder.build());
} return deleteActionBuilder.build();
return actions;
} }
private int getUserHandleOfForegroundApplication(Context context) { private int getUserHandleOfForegroundApplication(Context context) {

View File

@@ -0,0 +1,89 @@
/*
* 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.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
public enum ScreenshotEvent implements UiEventLogger.UiEventEnum {
@UiEvent(doc = "screenshot requested from global actions")
SCREENSHOT_REQUESTED_GLOBAL_ACTIONS(302),
@UiEvent(doc = "screenshot requested from key chord")
SCREENSHOT_REQUESTED_KEY_CHORD(303),
@UiEvent(doc = "screenshot requested from other key press (e.g. ctrl-s)")
SCREENSHOT_REQUESTED_KEY_OTHER(384),
@UiEvent(doc = "screenshot requested from overview")
SCREENSHOT_REQUESTED_OVERVIEW(304),
@UiEvent(doc = "screenshot requested from accessibility actions")
SCREENSHOT_REQUESTED_ACCESSIBILITY_ACTIONS(382),
@UiEvent(doc = "screenshot requested (other)")
SCREENSHOT_REQUESTED_OTHER(305),
@UiEvent(doc = "screenshot was saved")
SCREENSHOT_SAVED(306),
@UiEvent(doc = "screenshot failed to save")
SCREENSHOT_NOT_SAVED(336),
@UiEvent(doc = "screenshot preview tapped")
SCREENSHOT_PREVIEW_TAPPED(307),
@UiEvent(doc = "screenshot edit button tapped")
SCREENSHOT_EDIT_TAPPED(308),
@UiEvent(doc = "screenshot share button tapped")
SCREENSHOT_SHARE_TAPPED(309),
@UiEvent(doc = "screenshot smart action chip tapped")
SCREENSHOT_SMART_ACTION_TAPPED(374),
@UiEvent(doc = "screenshot scroll tapped")
SCREENSHOT_SCROLL_TAPPED(373),
@UiEvent(doc = "screenshot interaction timed out")
SCREENSHOT_INTERACTION_TIMEOUT(310),
@UiEvent(doc = "screenshot explicitly dismissed")
SCREENSHOT_EXPLICIT_DISMISSAL(311);
private final int mId;
ScreenshotEvent(int id) {
mId = id;
}
@Override
public int getId() {
return mId;
}
static ScreenshotEvent getScreenshotSource(int source) {
switch (source) {
case SCREENSHOT_GLOBAL_ACTIONS:
return ScreenshotEvent.SCREENSHOT_REQUESTED_GLOBAL_ACTIONS;
case SCREENSHOT_KEY_CHORD:
return ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_CHORD;
case SCREENSHOT_KEY_OTHER:
return ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER;
case SCREENSHOT_OVERVIEW:
return ScreenshotEvent.SCREENSHOT_REQUESTED_OVERVIEW;
case SCREENSHOT_ACCESSIBILITY_ACTIONS:
return ScreenshotEvent.SCREENSHOT_REQUESTED_ACCESSIBILITY_ACTIONS;
case SCREENSHOT_OTHER:
default:
return ScreenshotEvent.SCREENSHOT_REQUESTED_OTHER;
}
}
}

View File

@@ -32,7 +32,6 @@ import android.graphics.ColorMatrixColorFilter;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Picture; import android.graphics.Picture;
import android.net.Uri;
import android.os.UserHandle; import android.os.UserHandle;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.WindowManager; import android.view.WindowManager;
@@ -42,8 +41,6 @@ import com.android.systemui.R;
import com.android.systemui.SystemUI; import com.android.systemui.SystemUI;
import com.android.systemui.util.NotificationChannels; import com.android.systemui.util.NotificationChannels;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
/** /**
@@ -185,23 +182,20 @@ public class ScreenshotNotificationsController {
/** /**
* Shows a notification with the saved screenshot and actions that can be taken with it. * Shows a notification with the saved screenshot and actions that can be taken with it.
* *
* @param imageUri URI for the saved image * @param actionData SavedImageData struct with image URI and actions
* @param actions a list of notification actions which can be taken
*/ */
public void showScreenshotActionsNotification( public void showScreenshotActionsNotification(
Uri imageUri, GlobalScreenshot.SavedImageData actionData) {
List<Notification.Action> smartActions, mNotificationBuilder.addAction(actionData.shareAction);
List<Notification.Action> actions) { mNotificationBuilder.addAction(actionData.editAction);
for (Notification.Action action : actions) { mNotificationBuilder.addAction(actionData.deleteAction);
mNotificationBuilder.addAction(action); for (Notification.Action smartAction : actionData.smartActions) {
}
for (Notification.Action smartAction : smartActions) {
mNotificationBuilder.addAction(smartAction); mNotificationBuilder.addAction(smartAction);
} }
// Create the intent to show the screenshot in gallery // Create the intent to show the screenshot in gallery
Intent launchIntent = new Intent(Intent.ACTION_VIEW); Intent launchIntent = new Intent(Intent.ACTION_VIEW);
launchIntent.setDataAndType(imageUri, "image/png"); launchIntent.setDataAndType(actionData.uri, "image/png");
launchIntent.setFlags( launchIntent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION); Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);

View File

@@ -32,6 +32,9 @@ import android.os.UserManager;
import android.util.Log; import android.util.Log;
import android.view.WindowManager; import android.view.WindowManager;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.ScreenshotHelper;
import java.util.function.Consumer; import java.util.function.Consumer;
import javax.inject.Inject; import javax.inject.Inject;
@@ -42,6 +45,7 @@ public class TakeScreenshotService extends Service {
private final GlobalScreenshot mScreenshot; private final GlobalScreenshot mScreenshot;
private final GlobalScreenshotLegacy mScreenshotLegacy; private final GlobalScreenshotLegacy mScreenshotLegacy;
private final UserManager mUserManager; private final UserManager mUserManager;
private final UiEventLogger mUiEventLogger;
private Handler mHandler = new Handler(Looper.myLooper()) { private Handler mHandler = new Handler(Looper.myLooper()) {
@Override @Override
@@ -64,14 +68,22 @@ public class TakeScreenshotService extends Service {
return; return;
} }
// TODO (mkephart): clean up once notifications flow is fully deprecated // TODO: clean up once notifications flow is fully deprecated
boolean useCornerFlow = true; boolean useCornerFlow = true;
ScreenshotHelper.ScreenshotRequest screenshotRequest =
(ScreenshotHelper.ScreenshotRequest) msg.obj;
mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshotRequest.getSource()));
switch (msg.what) { switch (msg.what) {
case WindowManager.TAKE_SCREENSHOT_FULLSCREEN: case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
if (useCornerFlow) { if (useCornerFlow) {
mScreenshot.takeScreenshot(finisher); mScreenshot.takeScreenshot(finisher);
} else { } else {
mScreenshotLegacy.takeScreenshot(finisher, msg.arg1 > 0, msg.arg2 > 0); mScreenshotLegacy.takeScreenshot(
finisher, screenshotRequest.getHasStatusBar(),
screenshotRequest.getHasNavBar());
} }
break; break;
case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION: case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION:
@@ -79,17 +91,15 @@ public class TakeScreenshotService extends Service {
mScreenshot.takeScreenshotPartial(finisher); mScreenshot.takeScreenshotPartial(finisher);
} else { } else {
mScreenshotLegacy.takeScreenshotPartial( mScreenshotLegacy.takeScreenshotPartial(
finisher, msg.arg1 > 0, msg.arg2 > 0); finisher, screenshotRequest.getHasStatusBar(),
screenshotRequest.getHasNavBar());
} }
break; break;
case WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE: case WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE:
Bitmap screenshot = msg.getData().getParcelable( Bitmap screenshot = screenshotRequest.getBitmap();
WindowManager.PARCEL_KEY_SCREENSHOT_BITMAP); Rect screenBounds = screenshotRequest.getBoundsInScreen();
Rect screenBounds = msg.getData().getParcelable( Insets insets = screenshotRequest.getInsets();
WindowManager.PARCEL_KEY_SCREENSHOT_BOUNDS); int taskId = screenshotRequest.getTaskId();
Insets insets = msg.getData().getParcelable(
WindowManager.PARCEL_KEY_SCREENSHOT_INSETS);
int taskId = msg.getData().getInt(WindowManager.PARCEL_KEY_SCREENSHOT_TASK_ID);
if (useCornerFlow) { if (useCornerFlow) {
mScreenshot.handleImageAsScreenshot( mScreenshot.handleImageAsScreenshot(
screenshot, screenBounds, insets, taskId, finisher); screenshot, screenBounds, insets, taskId, finisher);
@@ -106,10 +116,12 @@ public class TakeScreenshotService extends Service {
@Inject @Inject
public TakeScreenshotService(GlobalScreenshot globalScreenshot, public TakeScreenshotService(GlobalScreenshot globalScreenshot,
GlobalScreenshotLegacy globalScreenshotLegacy, UserManager userManager) { GlobalScreenshotLegacy globalScreenshotLegacy, UserManager userManager,
UiEventLogger uiEventLogger) {
mScreenshot = globalScreenshot; mScreenshot = globalScreenshot;
mScreenshotLegacy = globalScreenshotLegacy; mScreenshotLegacy = globalScreenshotLegacy;
mUserManager = userManager; mUserManager = userManager;
mUiEventLogger = uiEventLogger;
} }
@Override @Override

View File

@@ -16,6 +16,10 @@
package com.android.systemui.screenshot; package com.android.systemui.screenshot;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
@@ -40,7 +44,6 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.SystemUIFactory; import com.android.systemui.SystemUIFactory;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -82,9 +85,9 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
CompletableFuture<List<Notification.Action>> smartActionsFuture = CompletableFuture<List<Notification.Action>> smartActionsFuture =
ScreenshotSmartActions.getSmartActionsFuture("", "", bitmap, ScreenshotSmartActions.getSmartActionsFuture("", "", bitmap,
smartActionsProvider, true, false); smartActionsProvider, true, false);
Assert.assertNotNull(smartActionsFuture); assertNotNull(smartActionsFuture);
List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS); List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS);
Assert.assertEquals(Collections.emptyList(), smartActions); assertEquals(Collections.emptyList(), smartActions);
} }
// Tests any exception thrown in waiting for smart actions future to complete does // Tests any exception thrown in waiting for smart actions future to complete does
@@ -99,7 +102,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
RuntimeException.class); RuntimeException.class);
List<Notification.Action> actions = ScreenshotSmartActions.getSmartActions( List<Notification.Action> actions = ScreenshotSmartActions.getSmartActions(
"", "", smartActionsFuture, timeoutMs, mSmartActionsProvider); "", "", smartActionsFuture, timeoutMs, mSmartActionsProvider);
Assert.assertEquals(Collections.emptyList(), actions); assertEquals(Collections.emptyList(), actions);
} }
// Tests any exception thrown in notifying feedback does not affect regular screenshot flow. // Tests any exception thrown in notifying feedback does not affect regular screenshot flow.
@@ -123,9 +126,9 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
mSmartActionsProvider, true, true); mSmartActionsProvider, true, true);
verify(mSmartActionsProvider, never()).getActions(any(), any(), any(), any(), verify(mSmartActionsProvider, never()).getActions(any(), any(), any(), any(),
eq(false)); eq(false));
Assert.assertNotNull(smartActionsFuture); assertNotNull(smartActionsFuture);
List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS); List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS);
Assert.assertEquals(Collections.emptyList(), smartActions); assertEquals(Collections.emptyList(), smartActions);
} }
// Tests for a hardware bitmap, ScreenshotNotificationSmartActionsProvider is invoked once. // Tests for a hardware bitmap, ScreenshotNotificationSmartActionsProvider is invoked once.
@@ -152,14 +155,14 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
ScreenshotSmartActions.getSmartActionsFuture("", "", bitmap, ScreenshotSmartActions.getSmartActionsFuture("", "", bitmap,
actionsProvider, actionsProvider,
true, true); true, true);
Assert.assertNotNull(smartActionsFuture); assertNotNull(smartActionsFuture);
List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS); List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS);
Assert.assertEquals(smartActions.size(), 0); assertEquals(smartActions.size(), 0);
} }
// Tests for notification action extras. // Tests for share action extras
@Test @Test
public void testNotificationActionExtras() { public void testShareActionExtras() {
if (Looper.myLooper() == null) { if (Looper.myLooper() == null) {
Looper.prepare(); Looper.prepare();
} }
@@ -169,35 +172,70 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
data.finisher = null; data.finisher = null;
data.mActionsReadyListener = null; data.mActionsReadyListener = null;
data.createDeleteAction = true;
SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data); SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data);
List<Notification.Action> actions = task.populateNotificationActions(
mContext, mContext.getResources(), Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(),
Uri.parse("Screenshot_123.png")); Uri.parse("Screenshot_123.png"));
Assert.assertEquals(actions.size(), 3); Intent intent = shareAction.actionIntent.getIntent();
boolean isShareFound = false; assertNotNull(intent);
boolean isEditFound = false;
boolean isDeleteFound = false;
for (Notification.Action action : actions) {
Intent intent = action.actionIntent.getIntent();
Assert.assertNotNull(intent);
Bundle bundle = intent.getExtras(); Bundle bundle = intent.getExtras();
Assert.assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID)); assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID));
Assert.assertTrue( assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED));
bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED)); assertEquals(GlobalScreenshot.ACTION_TYPE_SHARE, shareAction.title);
assertEquals(Intent.ACTION_SEND, intent.getAction());
if (action.title.equals(GlobalScreenshot.ACTION_TYPE_DELETE)) {
isDeleteFound = intent.getAction() == null;
} else if (action.title.equals(GlobalScreenshot.ACTION_TYPE_EDIT)) {
isEditFound = Intent.ACTION_EDIT.equals(intent.getAction());
} else if (action.title.equals(GlobalScreenshot.ACTION_TYPE_SHARE)) {
isShareFound = Intent.ACTION_SEND.equals(intent.getAction());
}
} }
Assert.assertTrue(isEditFound); // Tests for edit action extras
Assert.assertTrue(isDeleteFound); @Test
Assert.assertTrue(isShareFound); public void testEditActionExtras() {
if (Looper.myLooper() == null) {
Looper.prepare();
}
GlobalScreenshot.SaveImageInBackgroundData
data = new GlobalScreenshot.SaveImageInBackgroundData();
data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data);
Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(),
Uri.parse("Screenshot_123.png"));
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);
assertEquals(Intent.ACTION_EDIT, intent.getAction());
}
// Tests for share action extras
@Test
public void testDeleteActionExtras() {
if (Looper.myLooper() == null) {
Looper.prepare();
}
GlobalScreenshot.SaveImageInBackgroundData
data = new GlobalScreenshot.SaveImageInBackgroundData();
data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data);
Notification.Action deleteAction = task.createDeleteAction(mContext,
mContext.getResources(),
Uri.parse("Screenshot_123.png"));
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);
assertNull(intent.getAction());
} }
} }

View File

@@ -16,6 +16,8 @@
package com.android.server.accessibility; package com.android.server.accessibility;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
import android.accessibilityservice.AccessibilityService; import android.accessibilityservice.AccessibilityService;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.RemoteAction; import android.app.RemoteAction;
@@ -399,7 +401,8 @@ public class SystemActionPerformer {
ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null) ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null)
? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext); ? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext);
screenshotHelper.takeScreenshot(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN, screenshotHelper.takeScreenshot(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
true, true, new Handler(Looper.getMainLooper()), null); true, true, SCREENSHOT_ACCESSIBILITY_ACTIONS,
new Handler(Looper.getMainLooper()), null);
return true; return true;
} }
} }

View File

@@ -62,6 +62,8 @@ import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION; import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType; import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER;
import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN; import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
import static android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION; import static android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION;
import static android.view.WindowManagerGlobal.ADD_OKAY; import static android.view.WindowManagerGlobal.ADD_OKAY;
@@ -1328,6 +1330,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mScreenshotChordVolumeDownKeyConsumed = true; mScreenshotChordVolumeDownKeyConsumed = true;
cancelPendingPowerKeyAction(); cancelPendingPowerKeyAction();
mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN); mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_CHORD);
mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay()); mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay());
} }
} }
@@ -1411,14 +1414,19 @@ public class PhoneWindowManager implements WindowManagerPolicy {
private class ScreenshotRunnable implements Runnable { private class ScreenshotRunnable implements Runnable {
private int mScreenshotType = TAKE_SCREENSHOT_FULLSCREEN; private int mScreenshotType = TAKE_SCREENSHOT_FULLSCREEN;
private int mScreenshotSource = SCREENSHOT_KEY_OTHER;
public void setScreenshotType(int screenshotType) { public void setScreenshotType(int screenshotType) {
mScreenshotType = screenshotType; mScreenshotType = screenshotType;
} }
public void setScreenshotSource(int screenshotSource) {
mScreenshotSource = screenshotSource;
}
@Override @Override
public void run() { public void run() {
mDefaultDisplayPolicy.takeScreenshot(mScreenshotType); mDefaultDisplayPolicy.takeScreenshot(mScreenshotType, mScreenshotSource);
} }
} }
@@ -2693,6 +2701,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
int type = event.isShiftPressed() ? TAKE_SCREENSHOT_SELECTED_REGION int type = event.isShiftPressed() ? TAKE_SCREENSHOT_SELECTED_REGION
: TAKE_SCREENSHOT_FULLSCREEN; : TAKE_SCREENSHOT_FULLSCREEN;
mScreenshotRunnable.setScreenshotType(type); mScreenshotRunnable.setScreenshotType(type);
mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_OTHER);
mHandler.post(mScreenshotRunnable); mHandler.post(mScreenshotRunnable);
return -1; return -1;
} }
@@ -2709,6 +2718,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
} else if (keyCode == KeyEvent.KEYCODE_SYSRQ) { } else if (keyCode == KeyEvent.KEYCODE_SYSRQ) {
if (down && repeatCount == 0) { if (down && repeatCount == 0) {
mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN); mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_OTHER);
mHandler.post(mScreenshotRunnable); mHandler.post(mScreenshotRunnable);
} }
return -1; return -1;

View File

@@ -3783,13 +3783,14 @@ public class DisplayPolicy {
* @param screenshotType The type of screenshot, for example either * @param screenshotType The type of screenshot, for example either
* {@link WindowManager#TAKE_SCREENSHOT_FULLSCREEN} or * {@link WindowManager#TAKE_SCREENSHOT_FULLSCREEN} or
* {@link WindowManager#TAKE_SCREENSHOT_SELECTED_REGION} * {@link WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
* @param source Where the screenshot originated from (see WindowManager.ScreenshotSource)
*/ */
public void takeScreenshot(int screenshotType) { public void takeScreenshot(int screenshotType, int source) {
if (mScreenshotHelper != null) { if (mScreenshotHelper != null) {
mScreenshotHelper.takeScreenshot(screenshotType, mScreenshotHelper.takeScreenshot(screenshotType,
mStatusBar != null && mStatusBar.isVisibleLw(), mStatusBar != null && mStatusBar.isVisibleLw(),
mNavigationBar != null && mNavigationBar.isVisibleLw(), mNavigationBar != null && mNavigationBar.isVisibleLw(),
mHandler, null /* completionConsumer */); source, mHandler, null /* completionConsumer */);
} }
} }

View File

@@ -16,6 +16,8 @@
package com.android.server.accessibility; package com.android.server.accessibility;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
@@ -308,7 +310,7 @@ public class SystemActionPerformerTest {
AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT); AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT);
verify(mMockScreenshotHelper).takeScreenshot( verify(mMockScreenshotHelper).takeScreenshot(
eq(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN), anyBoolean(), eq(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN), anyBoolean(),
anyBoolean(), any(Handler.class), any()); anyBoolean(), eq(SCREENSHOT_ACCESSIBILITY_ACTIONS), any(Handler.class), any());
} }
// PendingIntent is a final class and cannot be mocked. So we are using this // PendingIntent is a final class and cannot be mocked. So we are using this