Merge "Merge "Move screenshot receivers and add tests" into rvc-qpr-dev am: 08a2a77e65" into rvc-qpr-dev-plus-aosp

This commit is contained in:
Automerger Merge Worker
2020-07-22 17:05:51 +00:00
committed by Android (Google) Code Review
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;
@@ -62,7 +57,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;
@@ -87,23 +81,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
*/
@@ -192,6 +178,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;
@@ -247,9 +234,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;
@@ -704,7 +693,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset
});
}
mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data);
mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data);
mSaveInBgTask.execute();
}
@@ -1116,119 +1105,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);
}
}