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

Commit acc22852 authored by Eric Lin's avatar Eric Lin Committed by Android (Google) Code Review
Browse files

Merge "Support widget intents on connected displays." into main

parents c826f406 fa21ea62
Loading
Loading
Loading
Loading
+63 −2
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import static android.window.ConfigurationHelper.isDifferentDisplay;
import static android.window.ConfigurationHelper.shouldUpdateResources;

import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import static com.android.internal.os.SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL;

import android.annotation.NonNull;
@@ -116,6 +117,7 @@ import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.HardwareRenderer;
import android.graphics.Typeface;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerGlobal;
import android.media.MediaFrameworkInitializer;
import android.media.MediaFrameworkPlatformInitializer;
@@ -863,7 +865,8 @@ public final class ActivityThread extends ClientTransactionHandler
        }
    }

    static final class ReceiverData extends BroadcastReceiver.PendingResult {
    @VisibleForTesting(visibility = PACKAGE)
    public static final class ReceiverData extends BroadcastReceiver.PendingResult {
        public ReceiverData(Intent intent, int resultCode, String resultData, Bundle resultExtras,
                boolean ordered, boolean sticky, boolean assumeDelivered, IBinder token,
                int sendingUser, int sendingUid, String sendingPackage) {
@@ -871,6 +874,11 @@ public final class ActivityThread extends ClientTransactionHandler
                    assumeDelivered, token, sendingUser, intent.getFlags(), sendingUid,
                    sendingPackage);
            this.intent = intent;
            if (com.android.window.flags.Flags.supportWidgetIntentsOnConnectedDisplay()) {
                mOptions = ActivityOptions.fromBundle(resultExtras);
            } else {
                mOptions = null;
            }
        }

        @UnsupportedAppUsage
@@ -879,12 +887,16 @@ public final class ActivityThread extends ClientTransactionHandler
        ActivityInfo info;
        @UnsupportedAppUsage
        CompatibilityInfo compatInfo;
        @Nullable
        final ActivityOptions mOptions;

        public String toString() {
            return "ReceiverData{intent=" + intent + " packageName=" +
                    info.packageName + " resultCode=" + getResultCode()
                    + " resultData=" + getResultData() + " resultExtras="
                    + getResultExtras(false) + " sentFromUid="
                    + getSentFromUid() + " sentFromPackage=" + getSentFromPackage() + "}";
                    + getSentFromUid() + " sentFromPackage=" + getSentFromPackage()
                    + " mOptions=" + mOptions + "}";
        }
    }

@@ -4985,6 +4997,7 @@ public final class ActivityThread extends ClientTransactionHandler
                final String attributionTag = data.info.attributionTags[0];
                context = (ContextImpl) context.createAttributionContext(attributionTag);
            }
            context = (ContextImpl) createDisplayContextIfNeeded(context, data);
            java.lang.ClassLoader cl = context.getClassLoader();
            data.intent.setExtrasClassLoader(cl);
            data.intent.prepareToEnterProcess(
@@ -5033,6 +5046,54 @@ public final class ActivityThread extends ClientTransactionHandler
        }
    }

    /**
     * Creates a display context if the broadcast was initiated with a launch display ID.
     *
     * <p>When a broadcast initiates from a widget on a secondary display, the originating
     * display ID is included as an extra in the intent. This is accomplished by
     * {@link PendingIntentRecord#createSafeActivityOptionsBundle}, which transfers the launch
     * display ID from ActivityOptions into the intent's extras bundle. This method checks for
     * the presence of that extra and creates a display context associated with the initiated
     * display if it exists. This ensures that when the {@link BroadcastReceiver} invokes
     * {@link Context#startActivity(Intent)}, the activity is launched on the correct display.
     *
     * @param context The original context of the receiver.
     * @param data    The {@link ReceiverData} containing optional display information.
     * @return A display context if applicable; otherwise the original context.
     */
    @NonNull
    @VisibleForTesting(visibility = PRIVATE)
    public Context createDisplayContextIfNeeded(@NonNull Context context,
            @NonNull ReceiverData data) {
        if (!com.android.window.flags.Flags.supportWidgetIntentsOnConnectedDisplay()) {
            return context;
        }

        final ActivityOptions options = data.mOptions;
        if (options == null) {
            return context;
        }

        final int launchDisplayId = options.getLaunchDisplayId();
        if (launchDisplayId == INVALID_DISPLAY) {
            return context;
        }

        final DisplayManager dm = context.getSystemService(DisplayManager.class);
        if (dm == null) {
            return context;
        }

        final Display display = dm.getDisplay(launchDisplayId);
        if (display == null) {
            Slog.w(TAG, "Unable to create a display context for nonexistent display "
                    + launchDisplayId);
            return context;
        }

        return context.createDisplayContext(display);
    }

    // Instantiate a BackupAgent and tell it that it's alive
    private void handleCreateBackupAgent(CreateBackupAgentData data) {
        if (DEBUG_BACKUP) Slog.v(TAG, "handleCreateBackupAgent: " + data);
+11 −0
Original line number Diff line number Diff line
@@ -152,3 +152,14 @@ flag {
        purpose: PURPOSE_BUGFIX
    }
}

flag {
    namespace: "windowing_sdk"
    name: "support_widget_intents_on_connected_display"
    description: "Launch widget intents on originating display"
    bug: "358368849"
    is_fixed_read_only: true
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}
+100 −4
Original line number Diff line number Diff line
@@ -25,6 +25,8 @@ import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;

import static com.android.window.flags.Flags.FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

@@ -39,11 +41,15 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.ActivityThread;
import android.app.ActivityThread.ActivityClientRecord;
import android.app.ActivityThread.ReceiverData;
import android.app.Application;
import android.app.IApplicationThread;
import android.app.PictureInPictureParams;
@@ -158,10 +164,7 @@ public class ActivityThreadTest {

    @After
    public void tearDown() {
        if (mCreatedVirtualDisplays != null) {
            mCreatedVirtualDisplays.forEach(VirtualDisplay::release);
            mCreatedVirtualDisplays = null;
        }
        tearDownVirtualDisplays();
        WindowTokenClientController.overrideForTesting(mOriginalWindowTokenClientController);
        ClientTransactionListenerController.getInstance()
                .unregisterActivityWindowInfoChangedListener(mActivityWindowInfoListener);
@@ -1007,6 +1010,92 @@ public class ActivityThreadTest {
                .that(systemContext.getApplicationInfo()).isSameInstanceAs(newAppInfo);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY)
    public void tesScheduleReceiver_withLaunchDisplayId_receivesDisplayContext() {
        final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
        final Display virtualDisplay = createVirtualDisplay(context, 100 /* w */, 100 /* h */);
        final int virtualDisplayId = virtualDisplay.getDisplayId();
        final ActivityOptions activityOptions =
                ActivityOptions.makeBasic().setLaunchDisplayId(virtualDisplayId);
        final ActivityThread activityThread = ActivityThread.currentActivityThread();

        final ReceiverData data = createReceiverData(activityOptions.toBundle());
        final Context resultContext =
                activityThread.createDisplayContextIfNeeded(context, data);

        final Display resultDisplay = resultContext.getDisplayNoVerify();
        assertThat(resultDisplay).isNotNull();
        assertThat(resultDisplay.getDisplayId()).isEqualTo(virtualDisplayId);
        assertThat(resultContext.getAssociatedDisplayId()).isEqualTo(virtualDisplayId);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY)
    public void tesScheduleReceiver_withNotExistDisplayId_receivesNoneUiContext() {
        final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
        final Display virtualDisplay = createVirtualDisplay(context, 100 /* w */, 100 /* h */);
        final int virtualDisplayId = virtualDisplay.getDisplayId();
        final ActivityOptions activityOptions =
                ActivityOptions.makeBasic().setLaunchDisplayId(virtualDisplayId);
        final ActivityThread activityThread = ActivityThread.currentActivityThread();
        tearDownVirtualDisplays();

        final ReceiverData data = createReceiverData(activityOptions.toBundle());
        final Context resultContext = activityThread.createDisplayContextIfNeeded(context, data);

        assertThat(resultContext).isEqualTo(context);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY)
    public void tesScheduleReceiver_withInvalidDisplay_receivesNoneUiContext() {
        final Context context = mock(Context.class);
        final ActivityOptions activityOptions =
                ActivityOptions.makeBasic().setLaunchDisplayId(INVALID_DISPLAY);
        final ActivityThread activityThread = ActivityThread.currentActivityThread();

        final ReceiverData data = createReceiverData(activityOptions.toBundle());
        final Context resultContext = activityThread.createDisplayContextIfNeeded(context, data);

        verify(context, never()).createDisplayContext(any());
        assertThat(resultContext).isEqualTo(context);
    }

    @Test
    @RequiresFlagsEnabled(FLAG_SUPPORT_WIDGET_INTENTS_ON_CONNECTED_DISPLAY)
    public void tesScheduleReceiver_withoutDisplayManagerService_receivesNoneUiContext() {
        final Context context = mock(Context.class);
        when(context.getSystemService(DisplayManager.class)).thenReturn(null);
        final ActivityThread activityThread = ActivityThread.currentActivityThread();

        final ReceiverData data = createReceiverData(null /* resultExtras */);
        final Context resultContext = activityThread.createDisplayContextIfNeeded(context, data);

        verify(context, never()).createDisplayContext(any());
        assertThat(resultContext).isEqualTo(context);
    }

    @Test
    public void tesScheduleReceiver_withoutActivityOptions_receivesNoneUiContext() {
        final Context context = mock(Context.class);
        final ActivityThread activityThread = ActivityThread.currentActivityThread();

        final ReceiverData data = createReceiverData(null /* resultExtras */);
        final Context resultContext = activityThread.createDisplayContextIfNeeded(context, data);

        verify(context, never()).createDisplayContext(any());
        assertThat(resultContext).isEqualTo(context);
    }

    @NonNull
    private ReceiverData createReceiverData(@Nullable Bundle resultExtras) {
        return new ReceiverData(new Intent("test.action.WIDGET_ITEM_CLICK"),
                0 /* resultCode */, null /* resultData */, resultExtras, false /* ordered */,
                false /* sticky */, false /* assumeDelivered */, null /* token */,
                0 /* sendingUser */, -1 /* sendingUid */, null /* sendingPackage */);
    }

    /**
     * Calls {@link ActivityThread#handleActivityConfigurationChanged(ActivityClientRecord,
     * Configuration, int, ActivityWindowInfo)} to try to push activity configuration to the
@@ -1056,6 +1145,13 @@ public class ActivityThreadTest {
        return virtualDisplay.getDisplay();
    }

    private void tearDownVirtualDisplays() {
        if (mCreatedVirtualDisplays != null) {
            mCreatedVirtualDisplays.forEach(VirtualDisplay::release);
            mCreatedVirtualDisplays = null;
        }
    }

    private static ActivityClientRecord getActivityClientRecord(Activity activity) {
        final ActivityThread thread = activity.getActivityThread();
        final IBinder token = activity.getActivityToken();
+28 −1
Original line number Diff line number Diff line
@@ -668,12 +668,13 @@ public final class PendingIntentRecord extends IIntentSender.Stub {
                                getBackgroundStartPrivilegesForActivitySender(
                                        mAllowBgActivityStartsForBroadcastSender, allowlistToken,
                                        options, callingUid);
                        final Bundle extras = createSafeActivityOptionsBundle(options);
                        // If a completion callback has been requested, require
                        // that the broadcast be delivered synchronously
                        int sent = controller.mAmInternal.broadcastIntentInPackage(key.packageName,
                                key.featureId, uid, callingUid, callingPid, finalIntent,
                                resolvedType, finishedReceiverThread, finishedReceiver, code, null,
                                null, requiredPermission, options, (finishedReceiver != null),
                                extras, requiredPermission, options, (finishedReceiver != null),
                                false, userId, backgroundStartPrivileges,
                                null /* broadcastAllowList */);
                        if (sent == ActivityManager.BROADCAST_SUCCESS) {
@@ -716,6 +717,32 @@ public final class PendingIntentRecord extends IIntentSender.Stub {
        return res;
    }

    /**
     * Creates a safe ActivityOptions bundle with only the launchDisplayId set.
     *
     * <p>This prevents unintended data from being sent to the app process. The resulting bundle
     * is then used by {@link ActivityThread#createDisplayContextIfNeeded} to create a display
     * context for the {@link BroadcastReceiver}, ensuring that activities launched from the
     * receiver's context are started on the correct display.
     *
     * @param optionsBundle The original ActivityOptions bundle.
     * @return A new bundle containing only the launchDisplayId from the original options, or null
     * if the original bundle is null.
     */
    @Nullable
    private Bundle createSafeActivityOptionsBundle(@Nullable Bundle optionsBundle) {
        if (!com.android.window.flags.Flags.supportWidgetIntentsOnConnectedDisplay()) {
            return null;
        }
        if (optionsBundle == null) {
            return null;
        }
        final ActivityOptions options = ActivityOptions.fromBundle(optionsBundle);
        return ActivityOptions.makeBasic()
                .setLaunchDisplayId(options.getLaunchDisplayId())
                .toBundle();
    }

    @VisibleForTesting BackgroundStartPrivileges getBackgroundStartPrivilegesForActivitySender(
            IBinder allowlistToken) {
        return mAllowBgActivityStartsForActivitySender.contains(allowlistToken)
+62 −2
Original line number Diff line number Diff line
@@ -34,25 +34,32 @@ import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER;
import static com.android.server.am.PendingIntentRecord.cancelReasonToString;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
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.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityOptions;
import android.app.AppGlobals;
import android.app.BackgroundStartPrivileges;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.IPackageManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.Looper;
import android.os.UserHandle;

import androidx.test.runner.AndroidJUnit4;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import com.android.server.AlarmManagerInternal;
import com.android.server.LocalServices;
@@ -108,7 +115,8 @@ public class PendingIntentControllerTest {
        mPendingIntentController.onActivityManagerInternalAdded();
    }

    private PendingIntentRecord createPendingIntentRecord(int flags) {
    @NonNull
    private PendingIntentRecord createPendingIntentRecord(@PendingIntent.Flags int flags) {
        return mPendingIntentController.getIntentSender(ActivityManager.INTENT_SENDER_BROADCAST,
                TEST_PACKAGE_NAME, TEST_FEATURE_ID, TEST_CALLING_UID, TEST_USER_ID, null, null, 0,
                TEST_INTENTS, null, flags, null);
@@ -219,6 +227,58 @@ public class PendingIntentControllerTest {
                allowlistDurationLocked.type);
    }

    @Test
    public void testSendWithBundleExtras() {
        final PendingIntentRecord pir = createPendingIntentRecord(0);
        final ActivityOptions activityOptions = ActivityOptions.makeBasic();
        activityOptions.setLaunchDisplayId(2);
        activityOptions.setLaunchTaskId(123);
        final Bundle options = activityOptions.toBundle();
        options.putString("testKey", "testValue");

        pir.send(0, null, null, null, null, null, options);

        final ArgumentCaptor<Bundle> resultExtrasCaptor = ArgumentCaptor.forClass(Bundle.class);
        verify(mActivityManagerInternal).broadcastIntentInPackage(
                eq(TEST_PACKAGE_NAME),
                eq(TEST_FEATURE_ID),
                eq(TEST_CALLING_UID),
                eq(TEST_CALLING_UID), // realCallingUid
                anyInt(), // realCallingPid
                any(), // intent
                any(), // resolvedType
                any(), // resultToThread
                any(), // resultTo
                anyInt(), // resultCode
                any(), // resultData
                resultExtrasCaptor.capture(), // resultExtras
                any(), // requiredPermission
                eq(options), // bOptions
                anyBoolean(), // serialized
                anyBoolean(), // sticky
                anyInt(), // userId
                any(),  // backgroundStartPrivileges
                any() // broadcastAllowList
        );
        final Bundle result = resultExtrasCaptor.getValue();
        if (com.android.window.flags.Flags.supportWidgetIntentsOnConnectedDisplay()) {
            // Check that only launchDisplayId in ActivityOptions is passed via resultExtras.
            final ActivityOptions expected = ActivityOptions.makeBasic().setLaunchDisplayId(2);
            assertBundleEquals(expected.toBundle(), result);
            // Check that launchTaskId is dropped in resultExtras.
            assertNotEquals(123, ActivityOptions.fromBundle(result).getLaunchTaskId());
        } else {
            assertNull(result);
        }
    }

    private void assertBundleEquals(@NonNull Bundle expected, @NonNull Bundle observed) {
        assertEquals(expected.size(), observed.size());
        for (String key : expected.keySet()) {
            assertEquals(expected.get(key), observed.get(key));
        }
    }

    private void assertCancelReason(int expectedReason, int actualReason) {
        final String errMsg = "Expected: " + cancelReasonToString(expectedReason)
                + "; Actual: " + cancelReasonToString(actualReason);