diff options
5 files changed, 356 insertions, 53 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index 3227ef47f733..bd1b7ca7916a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -137,7 +137,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, mParams.owner); + Notification.Action quickShare = + queryQuickShareAction(mScreenshotId, image, mParams.owner, null); + if (quickShare != null) { + mQuickShareData.quickShareAction = quickShare; + mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData); + } } // Call synchronously here since already on a background thread. @@ -176,9 +181,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, + mParams.owner); + mImageData.subject = getSubjectString(mImageTime); mParams.mActionsReadyListener.onActionsReady(mImageData); if (DEBUG_CALLBACK) { @@ -251,7 +257,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); @@ -417,60 +423,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( @@ -479,17 +498,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); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 77a65b22a7f4..b59106efb769 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -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"; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java index 68b46d2b7525..ca713feafe80 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java @@ -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, diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java index 45af1874e9db..9761f5931193 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java @@ -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); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt new file mode 100644 index 000000000000..fbb77cdc3049 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt @@ -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() + } +} |