Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 65114fdd authored by Miranda Kephart's avatar Miranda Kephart Committed by Android (Google) Code Review
Browse files

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

parents 7a18989c 1d364395
Loading
Loading
Loading
Loading
+66 −47
Original line number Diff line number Diff line
@@ -89,7 +89,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
    SaveImageInBackgroundTask(Context context, ImageExporter exporter,
            ScreenshotSmartActions screenshotSmartActions,
            ScreenshotController.SaveImageInBackgroundData data,
            Supplier<ActionTransition> sharedElementTransition) {
            Supplier<ActionTransition> sharedElementTransition,
            boolean smartActionsEnabled) {
        mContext = context;
        mScreenshotSmartActions = screenshotSmartActions;
        mImageData = new ScreenshotController.SavedImageData();
@@ -101,8 +102,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
        mParams = data;

        // Initialize screenshot notification smart actions provider.
        mSmartActionsEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
                SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, true);
        mSmartActionsEnabled = smartActionsEnabled;
        if (mSmartActionsEnabled) {
            mSmartActionsProvider =
                    SystemUIFactory.getInstance()
@@ -135,7 +135,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.
@@ -168,8 +173,9 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
            mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri);
            mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri);
            mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri);
            mImageData.quickShareAction = createQuickShareAction(mContext,
                    mQuickShareData.quickShareAction, uri);
            mImageData.quickShareAction = createQuickShareAction(
                    mQuickShareData.quickShareAction, mScreenshotId, uri, mImageTime, image,
                    user);

            mParams.mActionsReadyListener.onActionsReady(mImageData);
            if (DEBUG_CALLBACK) {
@@ -407,60 +413,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;
            }
        // 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);
            quickShare = quickShareWithUri;
        }

        // Proxy smart actions through {@link GlobalScreenshot.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);
        addIntentExtras(mScreenshotId, intent, actionType, mSmartActionsEnabled);
        PendingIntent broadcastIntent = PendingIntent.getBroadcast(context,
                mRandom.nextInt(),
                intent,
        addIntentExtras(screenshotId, wrappedIntent, actionType, mSmartActionsEnabled);
        PendingIntent broadcastIntent =
                PendingIntent.getBroadcast(mContext, mRandom.nextInt(), wrappedIntent,
                        PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
        return new Notification.Action.Builder(action.getIcon(), action.title,
                broadcastIntent).setContextual(true).addExtras(extras).build();
        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);
        String subjectDate = DateFormat.getDateTimeInstance().format(new Date(imageTime));
        String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
        fillIn.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));
        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,
                        QUICK_SHARE_ACTION,
                        screenshotId, uri, image, mSmartActionsProvider, QUICK_SHARE_ACTION,
                        mSmartActionsEnabled, user);
        int timeoutMs = DeviceConfig.getInt(
                DeviceConfig.NAMESPACE_SYSTEMUI,
@@ -468,11 +487,11 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
                500);
        List<Notification.Action> quickShareActions =
                mScreenshotSmartActions.getSmartActions(
                        mScreenshotId, quickShareActionsFuture, timeoutMs,
                        screenshotId, quickShareActionsFuture, timeoutMs,
                        mSmartActionsProvider, QUICK_SHARE_ACTION);
        if (!quickShareActions.isEmpty()) {
            mQuickShareData.quickShareAction = quickShareActions.get(0);
            mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData);
            return quickShareActions.get(0);
        }
        return null;
    }
}
+7 −1
Original line number Diff line number Diff line
@@ -57,6 +57,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -83,6 +84,7 @@ import android.widget.Toast;
import android.window.WindowContext;

import com.android.internal.app.ChooserActivity;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.policy.PhoneWindow;
import com.android.settingslib.applications.InterestingConfigChanges;
@@ -227,6 +229,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";
@@ -867,8 +870,11 @@ public class ScreenshotController {
            mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
        }

        boolean smartActionsEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
                SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, true);

        mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mImageExporter,
                mScreenshotSmartActions, data, getActionTransitionSupplier());
                mScreenshotSmartActions, data, getActionTransitionSupplier(), smartActionsEnabled);
        mSaveInBgTask.execute();
    }

+3 −1
Original line number Diff line number Diff line
@@ -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;

@@ -47,6 +48,7 @@ public class SmartActionsReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
        Intent fillIn = intent.getParcelableExtra(EXTRA_ACTION_INTENT_FILLIN);
        String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE);
        if (DEBUG_ACTIONS) {
            Log.d(TAG, "Executing smart action [" + actionType + "]:" + pendingIntent.getIntent());
@@ -54,7 +56,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);
        }
+274 −0
Original line number Diff line number Diff line
/*
 * 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.screenshot.ScreenshotController.SaveImageInBackgroundData
import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType
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
import org.mockito.Mockito

@SmallTest
class SaveImageInBackgroundTaskTest : SysuiTestCase() {
    private val imageExporter = mock<ImageExporter>()
    private val smartActions = mock<ScreenshotSmartActions>()
    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 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,
            imageExporter,
            smartActions,
            saveImageData,
            sharedTransitionSupplier,
            false, // forces a no-op implementation; we're mocking out the behavior anyway
        )

    @Before
    fun setup() {
        Mockito.`when`(
                smartActions.getSmartActionsFuture(
                    Mockito.eq(testScreenshotId),
                    Mockito.any(Uri::class.java),
                    Mockito.eq(testBitmap),
                    Mockito.any(ScreenshotNotificationSmartActionsProvider::class.java),
                    Mockito.any(ScreenshotSmartActionType::class.java),
                    Mockito.any(Boolean::class.java),
                    Mockito.eq(testUser)
                )
            )
            .thenReturn(smartActionsUriFuture)
        Mockito.`when`(
                smartActions.getSmartActionsFuture(
                    Mockito.eq(testScreenshotId),
                    Mockito.eq(null),
                    Mockito.eq(testBitmap),
                    Mockito.any(ScreenshotNotificationSmartActionsProvider::class.java),
                    Mockito.any(ScreenshotSmartActionType::class.java),
                    Mockito.any(Boolean::class.java),
                    Mockito.eq(testUser)
                )
            )
            .thenReturn(smartActionsFuture)
    }

    @Test
    fun testQueryQuickShare_noAction() {
        Mockito.`when`(
                smartActions.getSmartActions(
                    Mockito.eq(testScreenshotId),
                    Mockito.eq(smartActionsFuture),
                    Mockito.any(Int::class.java),
                    Mockito.any(ScreenshotNotificationSmartActionsProvider::class.java),
                    Mockito.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))
        Mockito.`when`(
                smartActions.getSmartActions(
                    Mockito.eq(testScreenshotId),
                    Mockito.eq(smartActionsUriFuture),
                    Mockito.any(Int::class.java),
                    Mockito.any(ScreenshotNotificationSmartActionsProvider::class.java),
                    Mockito.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))
        Mockito.`when`(
                smartActions.getSmartActions(
                    Mockito.eq(testScreenshotId),
                    Mockito.eq(smartActionsUriFuture),
                    Mockito.any(Int::class.java),
                    Mockito.any(ScreenshotNotificationSmartActionsProvider::class.java),
                    Mockito.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)
        Mockito.`when`(
                smartActions.getSmartActions(
                    Mockito.eq(testScreenshotId),
                    Mockito.eq(smartActionsUriFuture),
                    Mockito.any(Int::class.java),
                    Mockito.any(ScreenshotNotificationSmartActionsProvider::class.java),
                    Mockito.eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION)
                )
            )
            .thenReturn(actions)

        val quickShareAction =
            saveImageTask.createQuickShareAction(
                constructAction("Test Action", mutablePendingIntent),
                testScreenshotId,
                testUri,
                testImageTime,
                testBitmap,
                testUser
            )
        val quickSharePendingIntent : PendingIntent =
            quickShareAction.actionIntent.intent.extras!!.getParcelable(
                ScreenshotController.EXTRA_ACTION_INTENT)!!

        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)
        Mockito.`when`(
                smartActions.getSmartActions(
                    Mockito.eq(testScreenshotId),
                    Mockito.eq(smartActionsUriFuture),
                    Mockito.any(Int::class.java),
                    Mockito.any(ScreenshotNotificationSmartActionsProvider::class.java),
                    Mockito.eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION)
                )
            )
            .thenReturn(actions)

        val quickShareAction =
            saveImageTask.createQuickShareAction(
                constructAction("Test Action", immutablePendingIntent),
                testScreenshotId,
                testUri,
                testImageTime,
                testBitmap,
                testUser,
            )!!
        val quickSharePendingIntent : PendingIntent =
            quickShareAction.actionIntent.intent.extras!!.getParcelable(
                ScreenshotController.EXTRA_ACTION_INTENT)!!

        assertEquals("Test Action", quickShareAction.title)
        assertEquals(immutablePendingIntent, quickSharePendingIntent)
    }

    private fun constructAction(title: String, intent: PendingIntent): Notification.Action {
        return Notification.Action.Builder(testIcon, title, intent).build()
    }

    inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T =
        Mockito.mock(T::class.java).apply(apply)
}
+3 −3
Original line number Diff line number Diff line
@@ -183,7 +183,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
        data.mActionsReadyListener = null;
        SaveImageInBackgroundTask task =
                new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
                        ActionTransition::new);
                        ActionTransition::new, false);

        Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(),
                Uri.parse("Screenshot_123.png")).get().action;
@@ -211,7 +211,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
        data.mActionsReadyListener = null;
        SaveImageInBackgroundTask task =
                new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
                        ActionTransition::new);
                        ActionTransition::new, false);

        Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(),
                Uri.parse("Screenshot_123.png")).get().action;
@@ -239,7 +239,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
        data.mActionsReadyListener = null;
        SaveImageInBackgroundTask task =
                new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
                        ActionTransition::new);
                        ActionTransition::new, false);

        Notification.Action deleteAction = task.createDeleteAction(mContext,
                mContext.getResources(),