Add screenshots logging

Bug: 150710005
Test: manual
Change-Id: I54a37eb0a62234c6c53fc0f3c80e18e9ee269f12
This commit is contained in:
Miranda Kephart
2020-03-27 09:54:14 -04:00
parent e5eb16d8a8
commit 7b2c313da7
17 changed files with 528 additions and 197 deletions

View File

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

View File

@@ -1,5 +1,7 @@
package com.android.internal.util;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
@@ -10,11 +12,12 @@ import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
@@ -23,6 +26,109 @@ import android.view.WindowManager;
import java.util.function.Consumer;
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";
// Time until we give up on the screenshot & show an error instead.
@@ -36,8 +142,10 @@ public class ScreenshotHelper {
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
* is recommended for general use.
@@ -47,6 +155,32 @@ public class ScreenshotHelper {
* or
* {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
* @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
* not.
* @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
* is recommended for general use.
@@ -89,9 +223,9 @@ public class ScreenshotHelper {
public void takeScreenshot(final int screenshotType, final boolean hasStatus,
final boolean hasNav, long timeoutMs, @NonNull Handler handler,
@Nullable Consumer<Uri> completionConsumer) {
takeScreenshot(screenshotType, hasStatus, hasNav, timeoutMs, handler, null,
completionConsumer
);
ScreenshotRequest screenshotRequest = new ScreenshotRequest(SCREENSHOT_OTHER, hasStatus,
hasNav);
takeScreenshot(screenshotType, timeoutMs, handler, screenshotRequest, completionConsumer);
}
/**
@@ -106,23 +240,16 @@ public class ScreenshotHelper {
* screenshot was taken.
*/
public void provideScreenshot(@NonNull Bitmap screenshot, @NonNull Rect boundsInScreen,
@NonNull Insets insets, int taskId, @NonNull Handler handler,
@Nullable Consumer<Uri> completionConsumer) {
Bundle imageBundle = new Bundle();
imageBundle.putParcelable(WindowManager.PARCEL_KEY_SCREENSHOT_BITMAP, screenshot);
imageBundle.putParcelable(WindowManager.PARCEL_KEY_SCREENSHOT_BOUNDS, boundsInScreen);
imageBundle.putParcelable(WindowManager.PARCEL_KEY_SCREENSHOT_INSETS, insets);
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);
@NonNull Insets insets, int taskId, int source,
@NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) {
ScreenshotRequest screenshotRequest =
new ScreenshotRequest(source, screenshot, boundsInScreen, insets, taskId);
takeScreenshot(WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_TIMEOUT_MS,
handler, screenshotRequest, completionConsumer);
}
private void takeScreenshot(final int screenshotType, final boolean hasStatus,
final boolean hasNav, long timeoutMs, @NonNull Handler handler,
@Nullable Bundle providedImage, @Nullable Consumer<Uri> completionConsumer) {
private void takeScreenshot(final int screenshotType, long timeoutMs, @NonNull Handler handler,
ScreenshotRequest screenshotRequest, @Nullable Consumer<Uri> completionConsumer) {
synchronized (mScreenshotLock) {
if (mScreenshotConnection != null) {
return;
@@ -157,7 +284,7 @@ public class ScreenshotHelper {
return;
}
Messenger messenger = new Messenger(service);
Message msg = Message.obtain(null, screenshotType);
Message msg = Message.obtain(null, screenshotType, screenshotRequest);
final ServiceConnection myConn = this;
Handler h = new Handler(handler.getLooper()) {
@Override
@@ -175,12 +302,6 @@ public class ScreenshotHelper {
}
};
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 {
messenger.send(msg);

View File

@@ -36,6 +36,7 @@ import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.view.WindowManager;
import androidx.test.runner.AndroidJUnit4;
@@ -91,7 +92,8 @@ public final class ScreenshotHelperTest {
public void testProvidedImageScreenshot() {
mScreenshotHelper.provideScreenshot(
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

View File

@@ -16,6 +16,8 @@
package com.android.systemui.accessibility;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS;
import android.accessibilityservice.AccessibilityService;
import android.app.PendingIntent;
import android.app.RemoteAction;
@@ -282,8 +284,8 @@ public class SystemActions extends SystemUI {
private void handleTakeScreenshot() {
ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext);
screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
true, true, new Handler(Looper.getMainLooper()), null);
screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN, true, true,
SCREENSHOT_GLOBAL_ACTIONS, new Handler(Looper.getMainLooper()), null);
}
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.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.STRONG_AUTH_NOT_REQUIRED;
@@ -828,7 +830,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener,
mHandler.postDelayed(new Runnable() {
@Override
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);
mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS);
}
@@ -2331,4 +2334,4 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener,
// always use new controls layout
return true;
}
}
}

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_DOWN;
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 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,
Insets visibleInsets, int taskId) {
mScreenshotHelper.provideScreenshot(screenImage, locationInScreen, visibleInsets,
taskId, mHandler, null);
taskId, SCREENSHOT_OVERVIEW, mHandler, null);
}
@Override

View File

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

View File

@@ -24,7 +24,6 @@ import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -53,7 +52,6 @@ import android.widget.Toast;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import java.util.List;
import java.util.function.Consumer;
import javax.inject.Inject;
@@ -347,14 +345,13 @@ public class GlobalScreenshotLegacy {
// Save the screenshot once we have a bit of time now
saveScreenshotInWorkerThread(finisher, new GlobalScreenshot.ActionsReadyListener() {
@Override
void onActionsReady(Uri uri, List<Notification.Action> smartActions,
List<Notification.Action> actions) {
if (uri == null) {
void onActionsReady(GlobalScreenshot.SavedImageData actionData) {
if (actionData.uri == null) {
mNotificationsController.notifyScreenshotError(
R.string.screenshot_failed_to_capture_text);
} else {
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 GlobalScreenshot.SaveImageInBackgroundData mParams;
private final GlobalScreenshot.SavedImageData mImageData;
private final String mImageFileName;
private final long mImageTime;
private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
@@ -93,6 +94,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
SaveImageInBackgroundTask(Context context, GlobalScreenshot.SaveImageInBackgroundData data) {
mContext = context;
mImageData = new GlobalScreenshot.SavedImageData();
// Prepare all the output metadata
mParams = data;
@@ -145,6 +147,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
values.put(MediaColumns.IS_PENDING, 1);
final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
try {
// First, write the actual data for our screenshot
try (OutputStream out = resolver.openOutputStream(uri)) {
@@ -192,8 +195,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
throw e;
}
List<Notification.Action> actions =
populateNotificationActions(mContext, r, uri);
List<Notification.Action> smartActions = new ArrayList<>();
if (mSmartActionsEnabled) {
int timeoutMs = DeviceConfig.getInt(
@@ -206,8 +207,14 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
mSmartActionsProvider),
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.errorMsgResId = 0;
} catch (Exception e) {
@@ -216,29 +223,26 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
Slog.e(TAG, "unable to save screenshot", e);
mParams.clearImage();
mParams.errorMsgResId = R.string.screenshot_failed_to_save_text;
mParams.mActionsReadyListener.onActionsReady(null, null, null);
mImageData.reset();
mParams.mActionsReadyListener.onActionsReady(mImageData);
}
return null;
}
@Override
protected void onPostExecute(Void params) {
mParams.finisher.accept(mParams.imageUri);
}
@Override
protected void onCancelled(Void params) {
// 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 from the ctor in any case.
mParams.mActionsReadyListener.onActionsReady(null, null, null);
mImageData.reset();
mParams.mActionsReadyListener.onActionsReady(mImageData);
mParams.finisher.accept(null);
mParams.clearImage();
}
@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
// order to do some common work like dismissing the keyguard and sending
// closeSystemWindows
@@ -263,8 +267,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
// by setting the (otherwise unused) request code to the current user id.
int requestCode = context.getUserId();
ArrayList<Notification.Action> actions = new ArrayList<>();
PendingIntent chooserAction = PendingIntent.getBroadcast(context, requestCode,
new Intent(context, GlobalScreenshot.TargetChosenReceiver.class),
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(
Icon.createWithResource(r, R.drawable.ic_screenshot_share),
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
// 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_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
PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode,
new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
@@ -317,24 +331,30 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
Notification.Action.Builder editActionBuilder = new Notification.Action.Builder(
Icon.createWithResource(r, R.drawable.ic_screenshot_edit),
r.getString(com.android.internal.R.string.screenshot_edit), editAction);
actions.add(editActionBuilder.build());
if (mCreateDeleteAction) {
// Create a delete action for the notification
PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode,
new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
.putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString())
.putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId)
.putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED,
mSmartActionsEnabled)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
Icon.createWithResource(r, R.drawable.ic_screenshot_delete),
r.getString(com.android.internal.R.string.delete), deleteAction);
actions.add(deleteActionBuilder.build());
}
return actions;
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
PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode,
new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
.putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString())
.putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId)
.putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED,
mSmartActionsEnabled)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
Icon.createWithResource(r, R.drawable.ic_screenshot_delete),
r.getString(com.android.internal.R.string.delete), deleteAction);
return deleteActionBuilder.build();
}
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.Paint;
import android.graphics.Picture;
import android.net.Uri;
import android.os.UserHandle;
import android.util.DisplayMetrics;
import android.view.WindowManager;
@@ -42,8 +41,6 @@ import com.android.systemui.R;
import com.android.systemui.SystemUI;
import com.android.systemui.util.NotificationChannels;
import java.util.List;
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.
*
* @param imageUri URI for the saved image
* @param actions a list of notification actions which can be taken
* @param actionData SavedImageData struct with image URI and actions
*/
public void showScreenshotActionsNotification(
Uri imageUri,
List<Notification.Action> smartActions,
List<Notification.Action> actions) {
for (Notification.Action action : actions) {
mNotificationBuilder.addAction(action);
}
for (Notification.Action smartAction : smartActions) {
GlobalScreenshot.SavedImageData actionData) {
mNotificationBuilder.addAction(actionData.shareAction);
mNotificationBuilder.addAction(actionData.editAction);
mNotificationBuilder.addAction(actionData.deleteAction);
for (Notification.Action smartAction : actionData.smartActions) {
mNotificationBuilder.addAction(smartAction);
}
// Create the intent to show the screenshot in gallery
Intent launchIntent = new Intent(Intent.ACTION_VIEW);
launchIntent.setDataAndType(imageUri, "image/png");
launchIntent.setDataAndType(actionData.uri, "image/png");
launchIntent.setFlags(
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.view.WindowManager;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.ScreenshotHelper;
import java.util.function.Consumer;
import javax.inject.Inject;
@@ -42,6 +45,7 @@ public class TakeScreenshotService extends Service {
private final GlobalScreenshot mScreenshot;
private final GlobalScreenshotLegacy mScreenshotLegacy;
private final UserManager mUserManager;
private final UiEventLogger mUiEventLogger;
private Handler mHandler = new Handler(Looper.myLooper()) {
@Override
@@ -64,14 +68,22 @@ public class TakeScreenshotService extends Service {
return;
}
// TODO (mkephart): clean up once notifications flow is fully deprecated
// TODO: clean up once notifications flow is fully deprecated
boolean useCornerFlow = true;
ScreenshotHelper.ScreenshotRequest screenshotRequest =
(ScreenshotHelper.ScreenshotRequest) msg.obj;
mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshotRequest.getSource()));
switch (msg.what) {
case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
if (useCornerFlow) {
mScreenshot.takeScreenshot(finisher);
} else {
mScreenshotLegacy.takeScreenshot(finisher, msg.arg1 > 0, msg.arg2 > 0);
mScreenshotLegacy.takeScreenshot(
finisher, screenshotRequest.getHasStatusBar(),
screenshotRequest.getHasNavBar());
}
break;
case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION:
@@ -79,17 +91,15 @@ public class TakeScreenshotService extends Service {
mScreenshot.takeScreenshotPartial(finisher);
} else {
mScreenshotLegacy.takeScreenshotPartial(
finisher, msg.arg1 > 0, msg.arg2 > 0);
finisher, screenshotRequest.getHasStatusBar(),
screenshotRequest.getHasNavBar());
}
break;
case WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE:
Bitmap screenshot = msg.getData().getParcelable(
WindowManager.PARCEL_KEY_SCREENSHOT_BITMAP);
Rect screenBounds = msg.getData().getParcelable(
WindowManager.PARCEL_KEY_SCREENSHOT_BOUNDS);
Insets insets = msg.getData().getParcelable(
WindowManager.PARCEL_KEY_SCREENSHOT_INSETS);
int taskId = msg.getData().getInt(WindowManager.PARCEL_KEY_SCREENSHOT_TASK_ID);
Bitmap screenshot = screenshotRequest.getBitmap();
Rect screenBounds = screenshotRequest.getBoundsInScreen();
Insets insets = screenshotRequest.getInsets();
int taskId = screenshotRequest.getTaskId();
if (useCornerFlow) {
mScreenshot.handleImageAsScreenshot(
screenshot, screenBounds, insets, taskId, finisher);
@@ -106,10 +116,12 @@ public class TakeScreenshotService extends Service {
@Inject
public TakeScreenshotService(GlobalScreenshot globalScreenshot,
GlobalScreenshotLegacy globalScreenshotLegacy, UserManager userManager) {
GlobalScreenshotLegacy globalScreenshotLegacy, UserManager userManager,
UiEventLogger uiEventLogger) {
mScreenshot = globalScreenshot;
mScreenshotLegacy = globalScreenshotLegacy;
mUserManager = userManager;
mUiEventLogger = uiEventLogger;
}
@Override

View File

@@ -16,6 +16,10 @@
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.anyLong;
import static org.mockito.ArgumentMatchers.eq;
@@ -40,7 +44,6 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.SystemUIFactory;
import com.android.systemui.SysuiTestCase;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -82,9 +85,9 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
CompletableFuture<List<Notification.Action>> smartActionsFuture =
ScreenshotSmartActions.getSmartActionsFuture("", "", bitmap,
smartActionsProvider, true, false);
Assert.assertNotNull(smartActionsFuture);
assertNotNull(smartActionsFuture);
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
@@ -99,7 +102,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
RuntimeException.class);
List<Notification.Action> actions = ScreenshotSmartActions.getSmartActions(
"", "", 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.
@@ -123,9 +126,9 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
mSmartActionsProvider, true, true);
verify(mSmartActionsProvider, never()).getActions(any(), any(), any(), any(),
eq(false));
Assert.assertNotNull(smartActionsFuture);
assertNotNull(smartActionsFuture);
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.
@@ -152,14 +155,14 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
ScreenshotSmartActions.getSmartActionsFuture("", "", bitmap,
actionsProvider,
true, true);
Assert.assertNotNull(smartActionsFuture);
assertNotNull(smartActionsFuture);
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
public void testNotificationActionExtras() {
public void testShareActionExtras() {
if (Looper.myLooper() == null) {
Looper.prepare();
}
@@ -169,35 +172,70 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
data.finisher = null;
data.mActionsReadyListener = null;
data.createDeleteAction = true;
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"));
Assert.assertEquals(actions.size(), 3);
boolean isShareFound = false;
boolean isEditFound = false;
boolean isDeleteFound = false;
for (Notification.Action action : actions) {
Intent intent = action.actionIntent.getIntent();
Assert.assertNotNull(intent);
Bundle bundle = intent.getExtras();
Assert.assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID));
Assert.assertTrue(
bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED));
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);
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());
}
// Tests for edit action extras
@Test
public void testEditActionExtras() {
if (Looper.myLooper() == null) {
Looper.prepare();
}
Assert.assertTrue(isEditFound);
Assert.assertTrue(isDeleteFound);
Assert.assertTrue(isShareFound);
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;
import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
import android.accessibilityservice.AccessibilityService;
import android.app.PendingIntent;
import android.app.RemoteAction;
@@ -395,7 +397,8 @@ public class SystemActionPerformer {
ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null)
? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext);
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;
}
}

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_WALLPAPER;
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_SELECTED_REGION;
import static android.view.WindowManagerGlobal.ADD_OKAY;
@@ -1328,6 +1330,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mScreenshotChordVolumeDownKeyConsumed = true;
cancelPendingPowerKeyAction();
mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_CHORD);
mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay());
}
}
@@ -1411,14 +1414,19 @@ public class PhoneWindowManager implements WindowManagerPolicy {
private class ScreenshotRunnable implements Runnable {
private int mScreenshotType = TAKE_SCREENSHOT_FULLSCREEN;
private int mScreenshotSource = SCREENSHOT_KEY_OTHER;
public void setScreenshotType(int screenshotType) {
mScreenshotType = screenshotType;
}
public void setScreenshotSource(int screenshotSource) {
mScreenshotSource = screenshotSource;
}
@Override
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
: TAKE_SCREENSHOT_FULLSCREEN;
mScreenshotRunnable.setScreenshotType(type);
mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_OTHER);
mHandler.post(mScreenshotRunnable);
return -1;
}
@@ -2709,6 +2718,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
} else if (keyCode == KeyEvent.KEYCODE_SYSRQ) {
if (down && repeatCount == 0) {
mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_OTHER);
mHandler.post(mScreenshotRunnable);
}
return -1;

View File

@@ -3783,13 +3783,14 @@ public class DisplayPolicy {
* @param screenshotType The type of screenshot, for example either
* {@link WindowManager#TAKE_SCREENSHOT_FULLSCREEN} or
* {@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) {
mScreenshotHelper.takeScreenshot(screenshotType,
mStatusBar != null && mStatusBar.isVisibleLw(),
mNavigationBar != null && mNavigationBar.isVisibleLw(),
mHandler, null /* completionConsumer */);
source, mHandler, null /* completionConsumer */);
}
}

View File

@@ -16,6 +16,8 @@
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.is;
import static org.hamcrest.Matchers.not;
@@ -308,7 +310,7 @@ public class SystemActionPerformerTest {
AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT);
verify(mMockScreenshotHelper).takeScreenshot(
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