Move screenshot receivers and add tests

Moves the various broadcast receivers out of GlobalScreenshot and
adds unit tests for them, along with some other changes to make
them more easily testable:
    - ScreenshotSmartActions is now a stateless injectable class,
      instead of a collection of static methods
    - DeleteImageInBackgroundTask removed, in favor of just calling
      a background executor directly
    - remove the TargetChosenReceiver (used to remove notifications
      after a share target is chosen) since we're not using
      notifications anymore

Bug: 160325487
Test: atest SystemUITests, plus manually checked that screenshots
continue to function as expected

Change-Id: I1c054dddd76404f385e59f7ab7317beaafde1106
This commit is contained in:
Miranda Kephart
2020-07-16 13:19:15 -04:00
parent 256cbd1b51
commit c07f0a910f
13 changed files with 678 additions and 208 deletions

View File

@@ -395,19 +395,15 @@
<!-- Springboard for launching the share and edit activity. This needs to be in the main
system ui process since we need to notify the status bar to dismiss the keyguard -->
<receiver android:name=".screenshot.GlobalScreenshot$ActionProxyReceiver"
android:exported="false" />
<!-- Callback for dismissing screenshot notification after a share target is picked -->
<receiver android:name=".screenshot.GlobalScreenshot$TargetChosenReceiver"
<receiver android:name=".screenshot.ActionProxyReceiver"
android:exported="false" />
<!-- Callback for deleting screenshot notification -->
<receiver android:name=".screenshot.GlobalScreenshot$DeleteScreenshotReceiver"
<receiver android:name=".screenshot.DeleteScreenshotReceiver"
android:exported="false" />
<!-- Callback for invoking a smart action from the screenshot notification. -->
<receiver android:name=".screenshot.GlobalScreenshot$SmartActionsReceiver"
<receiver android:name=".screenshot.SmartActionsReceiver"
android:exported="false"/>
<!-- started from UsbDeviceSettingsManager -->

View File

@@ -18,7 +18,9 @@ package com.android.systemui.dagger;
import android.content.BroadcastReceiver;
import com.android.systemui.screenshot.GlobalScreenshot.ActionProxyReceiver;
import com.android.systemui.screenshot.ActionProxyReceiver;
import com.android.systemui.screenshot.DeleteScreenshotReceiver;
import com.android.systemui.screenshot.SmartActionsReceiver;
import dagger.Binds;
import dagger.Module;
@@ -30,10 +32,31 @@ import dagger.multibindings.IntoMap;
*/
@Module
public abstract class DefaultBroadcastReceiverBinder {
/** */
/**
*
*/
@Binds
@IntoMap
@ClassKey(ActionProxyReceiver.class)
public abstract BroadcastReceiver bindActionProxyReceiver(
ActionProxyReceiver broadcastReceiver);
/**
*
*/
@Binds
@IntoMap
@ClassKey(DeleteScreenshotReceiver.class)
public abstract BroadcastReceiver bindDeleteScreenshotReceiver(
DeleteScreenshotReceiver broadcastReceiver);
/**
*
*/
@Binds
@IntoMap
@ClassKey(SmartActionsReceiver.class)
public abstract BroadcastReceiver bindSmartActionsReceiver(
SmartActionsReceiver broadcastReceiver);
}

View File

@@ -0,0 +1,105 @@
/*
* 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 com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_EDIT;
import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_SHARE;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_INTENT;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED;
import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.statusbar.phone.StatusBar;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.inject.Inject;
/**
* Receiver to proxy the share or edit intent, used to clean up the notification and send
* appropriate signals to the system (ie. to dismiss the keyguard if necessary).
*/
public class ActionProxyReceiver extends BroadcastReceiver {
private static final String TAG = "ActionProxyReceiver";
private static final int CLOSE_WINDOWS_TIMEOUT_MILLIS = 3000;
private final StatusBar mStatusBar;
private final ActivityManagerWrapper mActivityManagerWrapper;
private final ScreenshotSmartActions mScreenshotSmartActions;
@Inject
public ActionProxyReceiver(Optional<StatusBar> statusBar,
ActivityManagerWrapper activityManagerWrapper,
ScreenshotSmartActions screenshotSmartActions) {
mStatusBar = statusBar.orElse(null);
mActivityManagerWrapper = activityManagerWrapper;
mScreenshotSmartActions = screenshotSmartActions;
}
@Override
public void onReceive(Context context, final Intent intent) {
Runnable startActivityRunnable = () -> {
try {
mActivityManagerWrapper.closeSystemWindows(
SYSTEM_DIALOG_REASON_SCREENSHOT).get(
CLOSE_WINDOWS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
} catch (TimeoutException | InterruptedException | ExecutionException e) {
Log.e(TAG, "Unable to share screenshot", e);
return;
}
PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
ActivityOptions opts = ActivityOptions.makeBasic();
opts.setDisallowEnterPictureInPictureWhileLaunching(
intent.getBooleanExtra(EXTRA_DISALLOW_ENTER_PIP, false));
try {
actionIntent.send(context, 0, null, null, null, null, opts.toBundle());
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Pending intent canceled", e);
}
};
if (mStatusBar != null) {
mStatusBar.executeRunnableDismissingKeyguard(startActivityRunnable, null,
true /* dismissShade */, true /* afterKeyguardGone */,
true /* deferred */);
} else {
startActivityRunnable.run();
}
if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) {
String actionType = Intent.ACTION_EDIT.equals(intent.getAction())
? ACTION_TYPE_EDIT
: ACTION_TYPE_SHARE;
mScreenshotSmartActions.notifyScreenshotAction(
context, intent.getStringExtra(EXTRA_ID), actionType, false);
}
}
}

View File

@@ -1,43 +0,0 @@
/*
* Copyright (C) 2019 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 android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
/**
* An AsyncTask that deletes an image from the media store in the background.
*/
class DeleteImageInBackgroundTask extends AsyncTask<Uri, Void, Void> {
private Context mContext;
DeleteImageInBackgroundTask(Context context) {
mContext = context;
}
@Override
protected Void doInBackground(Uri... params) {
if (params.length != 1) return null;
Uri screenshotUri = params[0];
ContentResolver resolver = mContext.getContentResolver();
resolver.delete(screenshotUri, null, null);
return null;
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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 com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_DELETE;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED;
import static com.android.systemui.screenshot.GlobalScreenshot.SCREENSHOT_URI_ID;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import com.android.systemui.dagger.qualifiers.Background;
import java.util.concurrent.Executor;
import javax.inject.Inject;
/**
* Removes the file at a provided URI.
*/
public class DeleteScreenshotReceiver extends BroadcastReceiver {
private final ScreenshotSmartActions mScreenshotSmartActions;
private final Executor mBackgroundExecutor;
@Inject
public DeleteScreenshotReceiver(ScreenshotSmartActions screenshotSmartActions,
@Background Executor backgroundExecutor) {
mScreenshotSmartActions = screenshotSmartActions;
mBackgroundExecutor = backgroundExecutor;
}
@Override
public void onReceive(Context context, Intent intent) {
if (!intent.hasExtra(SCREENSHOT_URI_ID)) {
return;
}
// And delete the image from the media store
final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID));
mBackgroundExecutor.execute(() -> {
ContentResolver resolver = context.getContentResolver();
resolver.delete(uri, null, null);
});
if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) {
mScreenshotSmartActions.notifyScreenshotAction(
context, intent.getStringExtra(EXTRA_ID), ACTION_TYPE_DELETE, false);
}
}
}

View File

@@ -21,8 +21,6 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -30,13 +28,10 @@ import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -63,7 +58,6 @@ import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.MathUtils;
import android.util.Slog;
import android.view.Display;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -88,23 +82,15 @@ 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;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.phone.StatusBar;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import javax.inject.Inject;
import javax.inject.Singleton;
import dagger.Lazy;
/**
* Class for handling device screen shots
*/
@@ -193,6 +179,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
private final UiEventLogger mUiEventLogger;
private final Context mContext;
private final ScreenshotSmartActions mScreenshotSmartActions;
private final WindowManager mWindowManager;
private final WindowManager.LayoutParams mWindowLayoutParams;
private final Display mDisplay;
@@ -248,9 +235,11 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
@Inject
public GlobalScreenshot(
Context context, @Main Resources resources,
ScreenshotSmartActions screenshotSmartActions,
ScreenshotNotificationsController screenshotNotificationsController,
UiEventLogger uiEventLogger) {
mContext = context;
mScreenshotSmartActions = screenshotSmartActions;
mNotificationsController = screenshotNotificationsController;
mUiEventLogger = uiEventLogger;
@@ -713,7 +702,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
});
}
mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data);
mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data);
mSaveInBgTask.execute();
}
@@ -1125,119 +1114,4 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
return insetDrawable;
}
}
/**
* Receiver to proxy the share or edit intent, used to clean up the notification and send
* appropriate signals to the system (ie. to dismiss the keyguard if necessary).
*/
public static class ActionProxyReceiver extends BroadcastReceiver {
static final int CLOSE_WINDOWS_TIMEOUT_MILLIS = 3000;
private final StatusBar mStatusBar;
@Inject
public ActionProxyReceiver(Optional<Lazy<StatusBar>> statusBarLazy) {
Lazy<StatusBar> statusBar = statusBarLazy.orElse(null);
mStatusBar = statusBar != null ? statusBar.get() : null;
}
@Override
public void onReceive(Context context, final Intent intent) {
Runnable startActivityRunnable = () -> {
try {
ActivityManagerWrapper.getInstance().closeSystemWindows(
SYSTEM_DIALOG_REASON_SCREENSHOT).get(
CLOSE_WINDOWS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
} catch (TimeoutException | InterruptedException | ExecutionException e) {
Slog.e(TAG, "Unable to share screenshot", e);
return;
}
PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
if (intent.getBooleanExtra(EXTRA_CANCEL_NOTIFICATION, false)) {
ScreenshotNotificationsController.cancelScreenshotNotification(context);
}
ActivityOptions opts = ActivityOptions.makeBasic();
opts.setDisallowEnterPictureInPictureWhileLaunching(
intent.getBooleanExtra(EXTRA_DISALLOW_ENTER_PIP, false));
try {
actionIntent.send(context, 0, null, null, null, null, opts.toBundle());
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Pending intent canceled", e);
}
};
if (mStatusBar != null) {
mStatusBar.executeRunnableDismissingKeyguard(startActivityRunnable, null,
true /* dismissShade */, true /* afterKeyguardGone */,
true /* deferred */);
} else {
startActivityRunnable.run();
}
if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) {
String actionType = Intent.ACTION_EDIT.equals(intent.getAction())
? ACTION_TYPE_EDIT
: ACTION_TYPE_SHARE;
ScreenshotSmartActions.notifyScreenshotAction(
context, intent.getStringExtra(EXTRA_ID), actionType, false);
}
}
}
/**
* Removes the notification for a screenshot after a share target is chosen.
*/
public static class TargetChosenReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// Clear the notification only after the user has chosen a share action
ScreenshotNotificationsController.cancelScreenshotNotification(context);
}
}
/**
* Removes the last screenshot.
*/
public static class DeleteScreenshotReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!intent.hasExtra(SCREENSHOT_URI_ID)) {
return;
}
// Clear the notification when the image is deleted
ScreenshotNotificationsController.cancelScreenshotNotification(context);
// And delete the image from the media store
final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID));
new DeleteImageInBackgroundTask(context).execute(uri);
if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) {
ScreenshotSmartActions.notifyScreenshotAction(
context, intent.getStringExtra(EXTRA_ID), ACTION_TYPE_DELETE, false);
}
}
}
/**
* Executes the smart action tapped by the user in the notification.
*/
public static class SmartActionsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE);
Slog.d(TAG, "Executing smart action [" + actionType + "]:" + pendingIntent.getIntent());
ActivityOptions opts = ActivityOptions.makeBasic();
try {
pendingIntent.send(context, 0, null, null, null, null, opts.toBundle());
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Pending intent canceled", e);
}
ScreenshotSmartActions.notifyScreenshotAction(
context, intent.getStringExtra(EXTRA_ID), actionType, true);
}
}
}

View File

@@ -81,6 +81,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
private final Context mContext;
private final ScreenshotSmartActions mScreenshotSmartActions;
private final GlobalScreenshot.SaveImageInBackgroundData mParams;
private final GlobalScreenshot.SavedImageData mImageData;
private final String mImageFileName;
@@ -90,8 +91,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
private final boolean mSmartActionsEnabled;
private final Random mRandom = new Random();
SaveImageInBackgroundTask(Context context, GlobalScreenshot.SaveImageInBackgroundData data) {
SaveImageInBackgroundTask(Context context, ScreenshotSmartActions screenshotSmartActions,
GlobalScreenshot.SaveImageInBackgroundData data) {
mContext = context;
mScreenshotSmartActions = screenshotSmartActions;
mImageData = new GlobalScreenshot.SavedImageData();
// Prepare all the output metadata
@@ -141,7 +144,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
CompletableFuture<List<Notification.Action>> smartActionsFuture =
ScreenshotSmartActions.getSmartActionsFuture(
mScreenshotSmartActions.getSmartActionsFuture(
mScreenshotId, uri, image, mSmartActionsProvider,
mSmartActionsEnabled, getUserHandle(mContext));
@@ -199,7 +202,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS,
1000);
smartActions.addAll(buildSmartActions(
ScreenshotSmartActions.getSmartActions(
mScreenshotSmartActions.getSmartActions(
mScreenshotId, smartActionsFuture, timeoutMs,
mSmartActionsProvider),
mContext));
@@ -274,11 +277,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
// by setting the (otherwise unused) request code to the current user id.
int requestCode = context.getUserId();
PendingIntent chooserAction = PendingIntent.getBroadcast(context, requestCode,
new Intent(context, GlobalScreenshot.TargetChosenReceiver.class),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
Intent sharingChooserIntent =
Intent.createChooser(sharingIntent, null, chooserAction.getIntentSender())
Intent.createChooser(sharingIntent, null)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@@ -288,7 +288,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
// Create a share action for the notification
PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode,
new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
new Intent(context, ActionProxyReceiver.class)
.putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, pendingIntent)
.putExtra(GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP, true)
.putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId)
@@ -333,10 +333,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
// Create a edit action
PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode,
new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
new Intent(context, ActionProxyReceiver.class)
.putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, pendingIntent)
.putExtra(GlobalScreenshot.EXTRA_CANCEL_NOTIFICATION,
editIntent.getComponent() != null)
.putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId)
.putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED,
mSmartActionsEnabled)
@@ -358,7 +356,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
// Create a delete action for the notification
PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode,
new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
new Intent(context, DeleteScreenshotReceiver.class)
.putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString())
.putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId)
.putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED,
@@ -398,7 +396,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
String actionType = extras.getString(
ScreenshotNotificationSmartActionsProvider.ACTION_TYPE,
ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE);
Intent intent = new Intent(context, GlobalScreenshot.SmartActionsReceiver.class)
Intent intent = new Intent(context, SmartActionsReceiver.class)
.putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, action.actionIntent)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
addIntentExtras(mScreenshotId, intent, actionType, mSmartActionsEnabled);

View File

@@ -39,14 +39,21 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Collects the static functions for retrieving and acting on smart actions.
*/
@Singleton
public class ScreenshotSmartActions {
private static final String TAG = "ScreenshotSmartActions";
@Inject
public ScreenshotSmartActions() {}
@VisibleForTesting
static CompletableFuture<List<Notification.Action>> getSmartActionsFuture(
CompletableFuture<List<Notification.Action>> getSmartActionsFuture(
String screenshotId, Uri screenshotUri, Bitmap image,
ScreenshotNotificationSmartActionsProvider smartActionsProvider,
boolean smartActionsEnabled, UserHandle userHandle) {
@@ -86,7 +93,7 @@ public class ScreenshotSmartActions {
}
@VisibleForTesting
static List<Notification.Action> getSmartActions(String screenshotId,
List<Notification.Action> getSmartActions(String screenshotId,
CompletableFuture<List<Notification.Action>> smartActionsFuture, int timeoutMs,
ScreenshotNotificationSmartActionsProvider smartActionsProvider) {
long startTimeMs = SystemClock.uptimeMillis();
@@ -116,7 +123,7 @@ public class ScreenshotSmartActions {
}
}
static void notifyScreenshotOp(String screenshotId,
void notifyScreenshotOp(String screenshotId,
ScreenshotNotificationSmartActionsProvider smartActionsProvider,
ScreenshotNotificationSmartActionsProvider.ScreenshotOp op,
ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus status, long durationMs) {
@@ -127,7 +134,7 @@ public class ScreenshotSmartActions {
}
}
static void notifyScreenshotAction(Context context, String screenshotId, String action,
void notifyScreenshotAction(Context context, String screenshotId, String action,
boolean isSmartAction) {
try {
ScreenshotNotificationSmartActionsProvider provider =

View File

@@ -0,0 +1,63 @@
/*
* 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 com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_INTENT;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_TYPE;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.util.Slog;
import javax.inject.Inject;
/**
* Executes the smart action tapped by the user in the notification.
*/
public class SmartActionsReceiver extends BroadcastReceiver {
private static final String TAG = "SmartActionsReceiver";
private final ScreenshotSmartActions mScreenshotSmartActions;
@Inject
SmartActionsReceiver(ScreenshotSmartActions screenshotSmartActions) {
mScreenshotSmartActions = screenshotSmartActions;
}
@Override
public void onReceive(Context context, Intent intent) {
PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE);
Slog.d(TAG, "Executing smart action [" + actionType + "]:" + pendingIntent.getIntent());
ActivityOptions opts = ActivityOptions.makeBasic();
try {
pendingIntent.send(context, 0, null, null, null, null, opts.toBundle());
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Pending intent canceled", e);
}
mScreenshotSmartActions.notifyScreenshotAction(
context, intent.getStringExtra(EXTRA_ID), actionType, true);
}
}

View File

@@ -0,0 +1,153 @@
/*
* 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 com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_SHARE;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED;
import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.statusbar.phone.StatusBar;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@RunWith(AndroidTestingRunner.class)
@SmallTest
public class ActionProxyReceiverTest extends SysuiTestCase {
@Mock
private StatusBar mMockStatusBar;
@Mock
private ActivityManagerWrapper mMockActivityManagerWrapper;
@Mock
private Future mMockFuture;
@Mock
private ScreenshotSmartActions mMockScreenshotSmartActions;
@Mock
private PendingIntent mMockPendingIntent;
private Intent mIntent;
@Before
public void setup() throws InterruptedException, ExecutionException, TimeoutException {
MockitoAnnotations.initMocks(this);
mIntent = new Intent(mContext, ActionProxyReceiver.class)
.putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, mMockPendingIntent);
when(mMockActivityManagerWrapper.closeSystemWindows(anyString())).thenReturn(mMockFuture);
when(mMockFuture.get(anyLong(), any(TimeUnit.class))).thenReturn(null);
}
@Test
public void testPendingIntentSentWithoutStatusBar() throws PendingIntent.CanceledException {
ActionProxyReceiver actionProxyReceiver = constructActionProxyReceiver(false);
actionProxyReceiver.onReceive(mContext, mIntent);
verify(mMockActivityManagerWrapper).closeSystemWindows(SYSTEM_DIALOG_REASON_SCREENSHOT);
verify(mMockStatusBar, never()).executeRunnableDismissingKeyguard(
any(Runnable.class), any(Runnable.class), anyBoolean(), anyBoolean(), anyBoolean());
verify(mMockPendingIntent).send(
eq(mContext), anyInt(), isNull(), isNull(), isNull(), isNull(), any(Bundle.class));
}
@Test
public void testPendingIntentSentWithStatusBar() throws PendingIntent.CanceledException {
ActionProxyReceiver actionProxyReceiver = constructActionProxyReceiver(true);
// ensure that the pending intent call is passed through
doAnswer((Answer<Object>) invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
}).when(mMockStatusBar).executeRunnableDismissingKeyguard(
any(Runnable.class), isNull(), anyBoolean(), anyBoolean(), anyBoolean());
actionProxyReceiver.onReceive(mContext, mIntent);
verify(mMockActivityManagerWrapper).closeSystemWindows(SYSTEM_DIALOG_REASON_SCREENSHOT);
verify(mMockStatusBar).executeRunnableDismissingKeyguard(
any(Runnable.class), isNull(), eq(true), eq(true), eq(true));
verify(mMockPendingIntent).send(
eq(mContext), anyInt(), isNull(), isNull(), isNull(), isNull(), any(Bundle.class));
}
@Test
public void testSmartActionsNotNotifiedByDefault() {
ActionProxyReceiver actionProxyReceiver = constructActionProxyReceiver(true);
actionProxyReceiver.onReceive(mContext, mIntent);
verify(mMockScreenshotSmartActions, never())
.notifyScreenshotAction(any(Context.class), anyString(), anyString(), anyBoolean());
}
@Test
public void testSmartActionsNotifiedIfEnabled() {
ActionProxyReceiver actionProxyReceiver = constructActionProxyReceiver(true);
mIntent.putExtra(EXTRA_SMART_ACTIONS_ENABLED, true);
String testId = "testID";
mIntent.putExtra(EXTRA_ID, testId);
actionProxyReceiver.onReceive(mContext, mIntent);
verify(mMockScreenshotSmartActions).notifyScreenshotAction(
mContext, testId, ACTION_TYPE_SHARE, false);
}
private ActionProxyReceiver constructActionProxyReceiver(boolean withStatusBar) {
if (withStatusBar) {
return new ActionProxyReceiver(
Optional.of(mMockStatusBar), mMockActivityManagerWrapper,
mMockScreenshotSmartActions);
} else {
return new ActionProxyReceiver(
Optional.empty(), mMockActivityManagerWrapper, mMockScreenshotSmartActions);
}
}
}

View File

@@ -0,0 +1,145 @@
/*
* 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 com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_DELETE;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED;
import static com.android.systemui.screenshot.GlobalScreenshot.SCREENSHOT_URI_ID;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.File;
import java.util.concurrent.Executor;
@RunWith(AndroidTestingRunner.class)
@SmallTest
public class DeleteScreenshotReceiverTest extends SysuiTestCase {
@Mock
private ScreenshotSmartActions mMockScreenshotSmartActions;
@Mock
private Executor mMockExecutor;
private DeleteScreenshotReceiver mDeleteScreenshotReceiver;
private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mDeleteScreenshotReceiver =
new DeleteScreenshotReceiver(mMockScreenshotSmartActions, mMockExecutor);
}
@Test
public void testNoUriProvided() {
Intent intent = new Intent(mContext, DeleteScreenshotReceiver.class);
mDeleteScreenshotReceiver.onReceive(mContext, intent);
verify(mMockExecutor, never()).execute(any(Runnable.class));
verify(mMockScreenshotSmartActions, never()).notifyScreenshotAction(
any(Context.class), any(String.class), any(String.class), anyBoolean());
}
@Test
public void testFileDeleted() {
DeleteScreenshotReceiver deleteScreenshotReceiver =
new DeleteScreenshotReceiver(mMockScreenshotSmartActions, mFakeExecutor);
ContentResolver contentResolver = mContext.getContentResolver();
final Uri testUri = contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, getFakeContentValues());
assertNotNull(testUri);
try {
Cursor cursor =
contentResolver.query(testUri, null, null, null, null);
assertEquals(1, cursor.getCount());
Intent intent = new Intent(mContext, DeleteScreenshotReceiver.class)
.putExtra(SCREENSHOT_URI_ID, testUri.toString());
deleteScreenshotReceiver.onReceive(mContext, intent);
int runCount = mFakeExecutor.runAllReady();
assertEquals(1, runCount);
cursor =
contentResolver.query(testUri, null, null, null, null);
assertEquals(0, cursor.getCount());
} finally {
contentResolver.delete(testUri, null, null);
}
// ensure smart actions not called by default
verify(mMockScreenshotSmartActions, never()).notifyScreenshotAction(
any(Context.class), any(String.class), any(String.class), anyBoolean());
}
@Test
public void testNotifyScreenshotAction() {
Intent intent = new Intent(mContext, DeleteScreenshotReceiver.class);
String uriString = "testUri";
String testId = "testID";
intent.putExtra(SCREENSHOT_URI_ID, uriString);
intent.putExtra(EXTRA_ID, testId);
intent.putExtra(EXTRA_SMART_ACTIONS_ENABLED, true);
mDeleteScreenshotReceiver.onReceive(mContext, intent);
verify(mMockExecutor).execute(any(Runnable.class));
verify(mMockScreenshotSmartActions).notifyScreenshotAction(
mContext, testId, ACTION_TYPE_DELETE, false);
}
private static ContentValues getFakeContentValues() {
final ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES
+ File.separator + Environment.DIRECTORY_SCREENSHOTS);
values.put(MediaStore.MediaColumns.DISPLAY_NAME, "test_screenshot");
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png");
values.put(MediaStore.MediaColumns.DATE_ADDED, 0);
values.put(MediaStore.MediaColumns.DATE_MODIFIED, 0);
return values;
}
}

View File

@@ -61,12 +61,14 @@ import java.util.concurrent.TimeUnit;
*/
public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
private ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
private ScreenshotSmartActions mScreenshotSmartActions;
private Handler mHandler;
@Before
public void setup() {
mSmartActionsProvider = mock(
ScreenshotNotificationSmartActionsProvider.class);
mScreenshotSmartActions = new ScreenshotSmartActions();
mHandler = mock(Handler.class);
}
@@ -82,7 +84,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
when(smartActionsProvider.getActions(any(), any(), any(), any(), any()))
.thenThrow(RuntimeException.class);
CompletableFuture<List<Notification.Action>> smartActionsFuture =
ScreenshotSmartActions.getSmartActionsFuture(
mScreenshotSmartActions.getSmartActionsFuture(
"", Uri.parse("content://authority/data"), bitmap, smartActionsProvider,
true, UserHandle.getUserHandleForUid(UserHandle.myUserId()));
assertNotNull(smartActionsFuture);
@@ -100,7 +102,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
int timeoutMs = 1000;
when(smartActionsFuture.get(timeoutMs, TimeUnit.MILLISECONDS)).thenThrow(
RuntimeException.class);
List<Notification.Action> actions = ScreenshotSmartActions.getSmartActions(
List<Notification.Action> actions = mScreenshotSmartActions.getSmartActions(
"", smartActionsFuture, timeoutMs, mSmartActionsProvider);
assertEquals(Collections.emptyList(), actions);
}
@@ -111,7 +113,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
throws Exception {
doThrow(RuntimeException.class).when(mSmartActionsProvider).notifyOp(any(), any(), any(),
anyLong());
ScreenshotSmartActions.notifyScreenshotOp(null, mSmartActionsProvider, null, null, -1);
mScreenshotSmartActions.notifyScreenshotOp(null, mSmartActionsProvider, null, null, -1);
}
// Tests for a non-hardware bitmap, ScreenshotNotificationSmartActionsProvider is never invoked
@@ -122,7 +124,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
Bitmap bitmap = mock(Bitmap.class);
when(bitmap.getConfig()).thenReturn(Bitmap.Config.RGB_565);
CompletableFuture<List<Notification.Action>> smartActionsFuture =
ScreenshotSmartActions.getSmartActionsFuture(
mScreenshotSmartActions.getSmartActionsFuture(
"", Uri.parse("content://autority/data"), bitmap, mSmartActionsProvider,
true, UserHandle.getUserHandleForUid(UserHandle.myUserId()));
verify(mSmartActionsProvider, never()).getActions(any(), any(), any(), any(), any());
@@ -136,7 +138,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
public void testScreenshotNotificationSmartActionsProviderInvokedOnce() {
Bitmap bitmap = mock(Bitmap.class);
when(bitmap.getConfig()).thenReturn(Bitmap.Config.HARDWARE);
ScreenshotSmartActions.getSmartActionsFuture(
mScreenshotSmartActions.getSmartActionsFuture(
"", Uri.parse("content://autority/data"), bitmap, mSmartActionsProvider, true,
UserHandle.getUserHandleForUid(UserHandle.myUserId()));
verify(mSmartActionsProvider, times(1)).getActions(any(), any(), any(), any(), any());
@@ -152,7 +154,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
SystemUIFactory.getInstance().createScreenshotNotificationSmartActionsProvider(
mContext, null, mHandler);
CompletableFuture<List<Notification.Action>> smartActionsFuture =
ScreenshotSmartActions.getSmartActionsFuture("", null, bitmap,
mScreenshotSmartActions.getSmartActionsFuture("", null, bitmap,
actionsProvider,
true, UserHandle.getUserHandleForUid(UserHandle.myUserId()));
assertNotNull(smartActionsFuture);
@@ -172,7 +174,8 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data);
SaveImageInBackgroundTask task =
new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data);
Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(),
Uri.parse("Screenshot_123.png"));
@@ -198,7 +201,8 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data);
SaveImageInBackgroundTask task =
new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data);
Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(),
Uri.parse("Screenshot_123.png"));
@@ -224,7 +228,8 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data);
SaveImageInBackgroundTask task =
new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data);
Notification.Action deleteAction = task.createDeleteAction(mContext,
mContext.getResources(),

View File

@@ -0,0 +1,76 @@
/*
* 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 com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_TYPE;
import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.verify;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
@SmallTest
public class SmartActionsReceiverTest extends SysuiTestCase {
@Mock
private ScreenshotSmartActions mMockScreenshotSmartActions;
@Mock
private PendingIntent mMockPendingIntent;
private SmartActionsReceiver mSmartActionsReceiver;
private Intent mIntent;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mSmartActionsReceiver = new SmartActionsReceiver(mMockScreenshotSmartActions);
mIntent = new Intent(mContext, SmartActionsReceiver.class)
.putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, mMockPendingIntent);
}
@Test
public void testSmartActionIntent() throws PendingIntent.CanceledException {
String testId = "testID";
String testActionType = "testActionType";
mIntent.putExtra(EXTRA_ID, testId);
mIntent.putExtra(EXTRA_ACTION_TYPE, testActionType);
mSmartActionsReceiver.onReceive(mContext, mIntent);
verify(mMockPendingIntent).send(
eq(mContext), eq(0), isNull(), isNull(), isNull(), isNull(), any(Bundle.class));
verify(mMockScreenshotSmartActions).notifyScreenshotAction(
mContext, testId, testActionType, true);
}
}