Merge "[DO NOT MERGE] Update quickshare intent rather than recreating" into tm-qpr-dev

This commit is contained in:
Miranda Kephart
2023-06-16 13:16:15 +00:00
committed by Android (Google) Code Review
5 changed files with 355 additions and 52 deletions

View File

@@ -141,7 +141,12 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
// Since Quick Share target recommendation does not rely on image URL, it is
// queried and surfaced before image compress/export. Action intent would not be
// used, because it does not contain image URL.
queryQuickShareAction(image, user);
Notification.Action quickShare =
queryQuickShareAction(mScreenshotId, image, user, null);
if (quickShare != null) {
mQuickShareData.quickShareAction = quickShare;
mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
}
}
// Call synchronously here since already on a background thread.
@@ -180,9 +185,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
smartActionsEnabled);
mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri,
smartActionsEnabled);
mImageData.quickShareAction = createQuickShareAction(mContext,
mQuickShareData.quickShareAction, uri);
mImageData.subject = getSubjectString();
mImageData.quickShareAction = createQuickShareAction(
mQuickShareData.quickShareAction, mScreenshotId, uri, mImageTime, image,
user);
mImageData.subject = getSubjectString(mImageTime);
mParams.mActionsReadyListener.onActionsReady(mImageData);
if (DEBUG_CALLBACK) {
@@ -257,7 +263,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
new ClipData.Item(uri));
sharingIntent.setClipData(clipdata);
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, getSubjectString());
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, getSubjectString(mImageTime));
sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
@@ -423,60 +429,73 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
}
/**
* Populate image uri into intent of Quick Share action.
* Wrap the quickshare intent and populate the fillin intent with the URI
*/
@VisibleForTesting
private Notification.Action createQuickShareAction(Context context, Notification.Action action,
Uri uri) {
if (action == null) {
Notification.Action createQuickShareAction(
Notification.Action quickShare, String screenshotId, Uri uri, long imageTime,
Bitmap image, UserHandle user) {
if (quickShare == null) {
return null;
} else if (quickShare.actionIntent.isImmutable()) {
Notification.Action quickShareWithUri =
queryQuickShareAction(screenshotId, image, user, uri);
if (quickShareWithUri == null
|| !quickShareWithUri.title.toString().contentEquals(quickShare.title)) {
return null;
}
quickShare = quickShareWithUri;
}
// Populate image URI into Quick Share chip intent
Intent sharingIntent = action.actionIntent.getIntent();
sharingIntent.setType("image/png");
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
// Include URI in ClipData also, so that grantPermission picks it up.
// We don't use setData here because some apps interpret this as "to:".
ClipData clipdata = new ClipData(new ClipDescription("content",
new String[]{"image/png"}),
new ClipData.Item(uri));
sharingIntent.setClipData(clipdata);
sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
PendingIntent updatedPendingIntent = PendingIntent.getActivity(
context, 0, sharingIntent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
// Proxy smart actions through {@link SmartActionsReceiver} for logging smart actions.
Bundle extras = action.getExtras();
Intent wrappedIntent = new Intent(mContext, SmartActionsReceiver.class)
.putExtra(ScreenshotController.EXTRA_ACTION_INTENT, quickShare.actionIntent)
.putExtra(ScreenshotController.EXTRA_ACTION_INTENT_FILLIN,
createFillInIntent(uri, imageTime))
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
Bundle extras = quickShare.getExtras();
String actionType = extras.getString(
ScreenshotNotificationSmartActionsProvider.ACTION_TYPE,
ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE);
Intent intent = new Intent(context, SmartActionsReceiver.class)
.putExtra(ScreenshotController.EXTRA_ACTION_INTENT, updatedPendingIntent)
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
// We only query for quick share actions when smart actions are enabled, so we can assert
// that it's true here.
addIntentExtras(mScreenshotId, intent, actionType, true /* smartActionsEnabled */);
PendingIntent broadcastIntent = PendingIntent.getBroadcast(context,
mRandom.nextInt(),
intent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
return new Notification.Action.Builder(action.getIcon(), action.title,
broadcastIntent).setContextual(true).addExtras(extras).build();
addIntentExtras(screenshotId, wrappedIntent, actionType, true /* smartActionsEnabled */);
PendingIntent broadcastIntent =
PendingIntent.getBroadcast(mContext, mRandom.nextInt(), wrappedIntent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
return new Notification.Action.Builder(quickShare.getIcon(), quickShare.title,
broadcastIntent)
.setContextual(true)
.addExtras(extras)
.build();
}
private Intent createFillInIntent(Uri uri, long imageTime) {
Intent fillIn = new Intent();
fillIn.setType("image/png");
fillIn.putExtra(Intent.EXTRA_STREAM, uri);
fillIn.putExtra(Intent.EXTRA_SUBJECT, getSubjectString(imageTime));
// Include URI in ClipData also, so that grantPermission picks it up.
// We don't use setData here because some apps interpret this as "to:".
ClipData clipData = new ClipData(
new ClipDescription("content", new String[]{"image/png"}),
new ClipData.Item(uri));
fillIn.setClipData(clipData);
fillIn.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
return fillIn;
}
/**
* Query and surface Quick Share chip if it is available. Action intent would not be used,
* because it does not contain image URL which would be populated in {@link
* #createQuickShareAction(Context, Notification.Action, Uri)}
* #createQuickShareAction(Notification.Action, String, Uri, long, Bitmap, UserHandle)}
*/
private void queryQuickShareAction(Bitmap image, UserHandle user) {
@VisibleForTesting
Notification.Action queryQuickShareAction(
String screenshotId, Bitmap image, UserHandle user, Uri uri) {
CompletableFuture<List<Notification.Action>> quickShareActionsFuture =
mScreenshotSmartActions.getSmartActionsFuture(
mScreenshotId, null, image, mSmartActionsProvider,
screenshotId, uri, image, mSmartActionsProvider,
ScreenshotSmartActionType.QUICK_SHARE_ACTION,
true /* smartActionsEnabled */, user);
int timeoutMs = DeviceConfig.getInt(
@@ -485,17 +504,17 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
500);
List<Notification.Action> quickShareActions =
mScreenshotSmartActions.getSmartActions(
mScreenshotId, quickShareActionsFuture, timeoutMs,
screenshotId, quickShareActionsFuture, timeoutMs,
mSmartActionsProvider,
ScreenshotSmartActionType.QUICK_SHARE_ACTION);
if (!quickShareActions.isEmpty()) {
mQuickShareData.quickShareAction = quickShareActions.get(0);
mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
return quickShareActions.get(0);
}
return null;
}
private String getSubjectString() {
String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
private static String getSubjectString(long imageTime) {
String subjectDate = DateFormat.getDateTimeInstance().format(new Date(imageTime));
return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
}
}

View File

@@ -246,6 +246,7 @@ public class ScreenshotController {
static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled";
static final String EXTRA_OVERRIDE_TRANSITION = "android:screenshot_override_transition";
static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent";
static final String EXTRA_ACTION_INTENT_FILLIN = "android:screenshot_action_intent_fillin";
static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id";
static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification";

View File

@@ -30,7 +30,6 @@ import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -61,7 +60,6 @@ public class ScreenshotSmartActions {
screenshotNotificationSmartActionsProviderProvider;
}
@VisibleForTesting
CompletableFuture<List<Notification.Action>> getSmartActionsFuture(
String screenshotId, Uri screenshotUri, Bitmap image,
ScreenshotNotificationSmartActionsProvider smartActionsProvider,
@@ -83,7 +81,7 @@ public class ScreenshotSmartActions {
if (image.getConfig() != Bitmap.Config.HARDWARE) {
if (DEBUG_ACTIONS) {
Log.d(TAG, String.format("Bitmap expected: Hardware, Bitmap found: %s. "
+ "Returning empty list.", image.getConfig()));
+ "Returning empty list.", image.getConfig()));
}
return CompletableFuture.completedFuture(Collections.emptyList());
}
@@ -112,7 +110,6 @@ public class ScreenshotSmartActions {
return smartActionsFuture;
}
@VisibleForTesting
List<Notification.Action> getSmartActions(String screenshotId,
CompletableFuture<List<Notification.Action>> smartActionsFuture, int timeoutMs,
ScreenshotNotificationSmartActionsProvider smartActionsProvider,

View File

@@ -18,6 +18,7 @@ package com.android.systemui.screenshot;
import static com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS;
import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_INTENT;
import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_INTENT_FILLIN;
import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_TYPE;
import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID;
@@ -46,7 +47,9 @@ public class SmartActionsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
PendingIntent pendingIntent =
intent.getParcelableExtra(EXTRA_ACTION_INTENT, PendingIntent.class);
Intent fillIn = intent.getParcelableExtra(EXTRA_ACTION_INTENT_FILLIN, Intent.class);
String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE);
if (DEBUG_ACTIONS) {
Log.d(TAG, "Executing smart action [" + actionType + "]:" + pendingIntent.getIntent());
@@ -54,7 +57,7 @@ public class SmartActionsReceiver extends BroadcastReceiver {
ActivityOptions opts = ActivityOptions.makeBasic();
try {
pendingIntent.send(context, 0, null, null, null, null, opts.toBundle());
pendingIntent.send(context, 0, fillIn, null, null, null, opts.toBundle());
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Pending intent canceled", e);
}

View File

@@ -0,0 +1,283 @@
/*
* Copyright (C) 2023 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.app.Notification
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Icon
import android.net.Uri
import android.os.UserHandle
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.screenshot.ScreenshotController.SaveImageInBackgroundData
import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import java.util.concurrent.CompletableFuture
import java.util.function.Supplier
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
@SmallTest
class SaveImageInBackgroundTaskTest : SysuiTestCase() {
private val imageExporter = mock<ImageExporter>()
private val smartActions = mock<ScreenshotSmartActions>()
private val smartActionsProvider = mock<ScreenshotNotificationSmartActionsProvider>()
private val saveImageData = SaveImageInBackgroundData()
private val sharedTransitionSupplier =
mock<Supplier<ScreenshotController.SavedImageData.ActionTransition>>()
private val testScreenshotId: String = "testScreenshotId"
private val testBitmap = mock<Bitmap>()
private val testUser = UserHandle.getUserHandleForUid(0)
private val testIcon = mock<Icon>()
private val testImageTime = 1234.toLong()
private val flags = FakeFeatureFlags()
private val smartActionsUriFuture = mock<CompletableFuture<List<Notification.Action>>>()
private val smartActionsFuture = mock<CompletableFuture<List<Notification.Action>>>()
private val testUri: Uri = Uri.parse("testUri")
private val intent =
Intent(Intent.ACTION_SEND)
.setComponent(
ComponentName.unflattenFromString(
"com.google.android.test/com.google.android.test.TestActivity"
)
)
private val immutablePendingIntent =
PendingIntent.getBroadcast(
mContext,
0,
intent,
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
private val mutablePendingIntent =
PendingIntent.getBroadcast(
mContext,
0,
intent,
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE
)
private val saveImageTask =
SaveImageInBackgroundTask(
mContext,
flags,
imageExporter,
smartActions,
saveImageData,
sharedTransitionSupplier,
smartActionsProvider,
)
@Before
fun setup() {
whenever(
smartActions.getSmartActionsFuture(
eq(testScreenshotId),
any(Uri::class.java),
eq(testBitmap),
eq(smartActionsProvider),
any(ScreenshotSmartActionType::class.java),
any(Boolean::class.java),
eq(testUser)
)
)
.thenReturn(smartActionsUriFuture)
whenever(
smartActions.getSmartActionsFuture(
eq(testScreenshotId),
eq(null),
eq(testBitmap),
eq(smartActionsProvider),
any(ScreenshotSmartActionType::class.java),
any(Boolean::class.java),
eq(testUser)
)
)
.thenReturn(smartActionsFuture)
}
@Test
fun testQueryQuickShare_noAction() {
whenever(
smartActions.getSmartActions(
eq(testScreenshotId),
eq(smartActionsFuture),
any(Int::class.java),
eq(smartActionsProvider),
eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION)
)
)
.thenReturn(ArrayList<Notification.Action>())
val quickShareAction =
saveImageTask.queryQuickShareAction(testScreenshotId, testBitmap, testUser, testUri)
assertNull(quickShareAction)
}
@Test
fun testQueryQuickShare_withActions() {
val actions = ArrayList<Notification.Action>()
actions.add(constructAction("Action One", mutablePendingIntent))
actions.add(constructAction("Action Two", mutablePendingIntent))
whenever(
smartActions.getSmartActions(
eq(testScreenshotId),
eq(smartActionsUriFuture),
any(Int::class.java),
eq(smartActionsProvider),
eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION)
)
)
.thenReturn(actions)
val quickShareAction =
saveImageTask.queryQuickShareAction(testScreenshotId, testBitmap, testUser, testUri)!!
assertEquals("Action One", quickShareAction.title)
assertEquals(mutablePendingIntent, quickShareAction.actionIntent)
}
@Test
fun testCreateQuickShareAction_originalWasNull_returnsNull() {
val quickShareAction =
saveImageTask.createQuickShareAction(
null,
testScreenshotId,
testUri,
testImageTime,
testBitmap,
testUser
)
assertNull(quickShareAction)
}
@Test
fun testCreateQuickShareAction_immutableIntentDifferentAction_returnsNull() {
val actions = ArrayList<Notification.Action>()
actions.add(constructAction("New Test Action", immutablePendingIntent))
whenever(
smartActions.getSmartActions(
eq(testScreenshotId),
eq(smartActionsUriFuture),
any(Int::class.java),
eq(smartActionsProvider),
eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION)
)
)
.thenReturn(actions)
val origAction = constructAction("Old Test Action", immutablePendingIntent)
val quickShareAction =
saveImageTask.createQuickShareAction(
origAction,
testScreenshotId,
testUri,
testImageTime,
testBitmap,
testUser,
)
assertNull(quickShareAction)
}
@Test
fun testCreateQuickShareAction_mutableIntent_returnsSafeIntent() {
val actions = ArrayList<Notification.Action>()
val action = constructAction("Action One", mutablePendingIntent)
actions.add(action)
whenever(
smartActions.getSmartActions(
eq(testScreenshotId),
eq(smartActionsUriFuture),
any(Int::class.java),
eq(smartActionsProvider),
eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION)
)
)
.thenReturn(actions)
val quickShareAction =
saveImageTask.createQuickShareAction(
constructAction("Test Action", mutablePendingIntent),
testScreenshotId,
testUri,
testImageTime,
testBitmap,
testUser
)
val quickSharePendingIntent =
quickShareAction.actionIntent.intent.extras!!.getParcelable(
ScreenshotController.EXTRA_ACTION_INTENT,
PendingIntent::class.java
)
assertEquals("Test Action", quickShareAction.title)
assertEquals(mutablePendingIntent, quickSharePendingIntent)
}
@Test
fun testCreateQuickShareAction_immutableIntent_returnsSafeIntent() {
val actions = ArrayList<Notification.Action>()
val action = constructAction("Test Action", immutablePendingIntent)
actions.add(action)
whenever(
smartActions.getSmartActions(
eq(testScreenshotId),
eq(smartActionsUriFuture),
any(Int::class.java),
eq(smartActionsProvider),
eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION)
)
)
.thenReturn(actions)
val quickShareAction =
saveImageTask.createQuickShareAction(
constructAction("Test Action", immutablePendingIntent),
testScreenshotId,
testUri,
testImageTime,
testBitmap,
testUser,
)!!
assertEquals("Test Action", quickShareAction.title)
assertEquals(
immutablePendingIntent,
quickShareAction.actionIntent.intent.extras!!.getParcelable(
ScreenshotController.EXTRA_ACTION_INTENT,
PendingIntent::class.java
)
)
}
private fun constructAction(title: String, intent: PendingIntent): Notification.Action {
return Notification.Action.Builder(testIcon, title, intent).build()
}
}