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

Commit 26cceb87 authored by Louis Chang's avatar Louis Chang
Browse files

Adding unit tests for ActivityOptions

The test would fail whenever adding a new type of options in order to
make a notice that if any permission should be added to prevent
malicious uses.

Also adding tests to verify the existing safe options.

Bug: 240978635
Test: atest ActivityOptionsTest SafeActivityOptionsTest
Change-Id: Ib3363d874a283674afefbfec570ecc271a64afbd
parent 15d68f41
Loading
Loading
Loading
Loading
+27 −20
Original line number Diff line number Diff line
@@ -293,26 +293,7 @@ public class SafeActivityOptions {
            throw new SecurityException(msg);
        }
        // Check if the caller is allowed to launch on the specified display area.
        final WindowContainerToken daToken = options.getLaunchTaskDisplayArea();
        TaskDisplayArea taskDisplayArea = daToken != null
                ? (TaskDisplayArea) WindowContainer.fromBinder(daToken.asBinder()) : null;

        // If we do not have a task display area token, check if the launch task display area
        // feature id is specified.
        if (taskDisplayArea == null) {
            final int launchTaskDisplayAreaFeatureId = options.getLaunchTaskDisplayAreaFeatureId();
            if (launchTaskDisplayAreaFeatureId != FEATURE_UNDEFINED) {
                final int launchDisplayId = options.getLaunchDisplayId() == INVALID_DISPLAY
                        ? DEFAULT_DISPLAY : options.getLaunchDisplayId();
                final DisplayContent dc = supervisor.mRootWindowContainer
                        .getDisplayContent(launchDisplayId);
                if (dc != null) {
                    taskDisplayArea = dc.getItemFromTaskDisplayAreas(tda ->
                            tda.mFeatureId == launchTaskDisplayAreaFeatureId ? tda : null);
                }
            }
        }

        final TaskDisplayArea taskDisplayArea = getLaunchTaskDisplayArea(options, supervisor);
        if (aInfo != null && taskDisplayArea != null
                && !supervisor.isCallerAllowedToLaunchOnTaskDisplayArea(callingPid, callingUid,
                taskDisplayArea, aInfo)) {
@@ -428,6 +409,32 @@ public class SafeActivityOptions {
        }
    }

    @VisibleForTesting
    TaskDisplayArea getLaunchTaskDisplayArea(ActivityOptions options,
            ActivityTaskSupervisor supervisor) {
        final WindowContainerToken daToken = options.getLaunchTaskDisplayArea();
        TaskDisplayArea taskDisplayArea = daToken != null
                ? (TaskDisplayArea) WindowContainer.fromBinder(daToken.asBinder()) : null;
        if (taskDisplayArea != null) {
            return taskDisplayArea;
        }

        // If we do not have a task display area token, check if the launch task display area
        // feature id is specified.
        final int launchTaskDisplayAreaFeatureId = options.getLaunchTaskDisplayAreaFeatureId();
        if (launchTaskDisplayAreaFeatureId != FEATURE_UNDEFINED) {
            final int launchDisplayId = options.getLaunchDisplayId() == INVALID_DISPLAY
                    ? DEFAULT_DISPLAY : options.getLaunchDisplayId();
            final DisplayContent dc = supervisor.mRootWindowContainer
                    .getDisplayContent(launchDisplayId);
            if (dc != null) {
                taskDisplayArea = dc.getItemFromTaskDisplayAreas(tda ->
                        tda.mFeatureId == launchTaskDisplayAreaFeatureId ? tda : null);
            }
        }
        return taskDisplayArea;
    }

    private boolean isAssistant(ActivityTaskManagerService atmService, int callingUid) {
        if (atmService.mActiveVoiceInteractionServiceComponent == null) {
            return false;
+126 −0
Original line number Diff line number Diff line
@@ -22,11 +22,17 @@ import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;

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.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;

import android.app.Activity;
import android.app.ActivityManager.RunningTaskInfo;
@@ -41,6 +47,7 @@ import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.util.Log;
import android.util.Rational;
import android.view.SurfaceControl;
import android.window.TaskOrganizer;
@@ -48,7 +55,10 @@ import android.window.TaskOrganizer;
import androidx.test.filters.MediumTest;

import org.junit.Test;
import org.mockito.ArgumentCaptor;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@@ -166,6 +176,122 @@ public class ActivityOptionsTest {
        }
    }

    /**
     * Tests if any unknown key is being used in the ActivityOptions bundle. If so, please review
     * if the newly added bundle should be protected with permissions to avoid malicious attacks.
     *
     * @see SafeActivityOptionsTest#test_getOptions
     */
    @Test
    public void testActivityOptionsFromBundle() {
        // Spy on a bundle that is generated from a basic ActivityOptions.
        final ActivityOptions options = ActivityOptions.makeBasic();
        Bundle bundle = options.toBundle();
        spyOn(bundle);

        // Create a new ActivityOptions from the bundle
        new ActivityOptions(bundle);

        // Verify the keys that are being used.
        final ArgumentCaptor<String> stringCaptor =  ArgumentCaptor.forClass(String.class);
        verify(bundle, atLeastOnce()).getString(stringCaptor.capture());
        verify(bundle, atLeastOnce()).getBoolean(stringCaptor.capture());
        verify(bundle, atLeastOnce()).getParcelable(stringCaptor.capture(), any());
        verify(bundle, atLeastOnce()).getInt(stringCaptor.capture(), anyInt());
        verify(bundle, atLeastOnce()).getBinder(stringCaptor.capture());
        verify(bundle, atLeastOnce()).getBundle(stringCaptor.capture());
        final List<String> keys = stringCaptor.getAllValues();
        final List<String> unknownKeys = new ArrayList<>();
        for (String key : keys) {
            switch (key) {
                case ActivityOptions.KEY_PACKAGE_NAME:
                case ActivityOptions.KEY_LAUNCH_BOUNDS:
                case ActivityOptions.KEY_ANIM_TYPE:
                case ActivityOptions.KEY_ANIM_ENTER_RES_ID:
                case ActivityOptions.KEY_ANIM_EXIT_RES_ID:
                case ActivityOptions.KEY_ANIM_IN_PLACE_RES_ID:
                case ActivityOptions.KEY_ANIM_BACKGROUND_COLOR:
                case ActivityOptions.KEY_ANIM_THUMBNAIL:
                case ActivityOptions.KEY_ANIM_START_X:
                case ActivityOptions.KEY_ANIM_START_Y:
                case ActivityOptions.KEY_ANIM_WIDTH:
                case ActivityOptions.KEY_ANIM_HEIGHT:
                case ActivityOptions.KEY_ANIM_START_LISTENER:
                case ActivityOptions.KEY_SPLASH_SCREEN_THEME:
                case ActivityOptions.KEY_LEGACY_PERMISSION_PROMPT_ELIGIBLE:
                case ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN:
                case ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN:
                case ActivityOptions.KEY_TRANSIENT_LAUNCH:
                case "android:activity.animationFinishedListener":
                    // KEY_ANIMATION_FINISHED_LISTENER
                case "android:activity.animSpecs": // KEY_ANIM_SPECS
                case "android:activity.lockTaskMode": // KEY_LOCK_TASK_MODE
                case "android:activity.shareIdentity": // KEY_SHARE_IDENTITY
                case "android.activity.launchDisplayId": // KEY_LAUNCH_DISPLAY_ID
                case "android.activity.callerDisplayId": // KEY_CALLER_DISPLAY_ID
                case "android.activity.launchTaskDisplayAreaToken":
                    // KEY_LAUNCH_TASK_DISPLAY_AREA_TOKEN
                case "android.activity.launchTaskDisplayAreaFeatureId":
                    // KEY_LAUNCH_TASK_DISPLAY_AREA_FEATURE_ID
                case "android.activity.windowingMode": // KEY_LAUNCH_WINDOWING_MODE
                case "android.activity.activityType": // KEY_LAUNCH_ACTIVITY_TYPE
                case "android.activity.launchTaskId": // KEY_LAUNCH_TASK_ID
                case "android.activity.disableStarting": // KEY_DISABLE_STARTING_WINDOW
                case "android.activity.pendingIntentLaunchFlags":
                    // KEY_PENDING_INTENT_LAUNCH_FLAGS
                case "android.activity.alwaysOnTop": // KEY_TASK_ALWAYS_ON_TOP
                case "android.activity.taskOverlay": // KEY_TASK_OVERLAY
                case "android.activity.taskOverlayCanResume": // KEY_TASK_OVERLAY_CAN_RESUME
                case "android.activity.avoidMoveToFront": // KEY_AVOID_MOVE_TO_FRONT
                case "android.activity.freezeRecentTasksReordering":
                    // KEY_FREEZE_RECENT_TASKS_REORDERING
                case "android:activity.disallowEnterPictureInPictureWhileLaunching":
                    // KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING
                case "android:activity.applyActivityFlagsForBubbles":
                    // KEY_APPLY_ACTIVITY_FLAGS_FOR_BUBBLES
                case "android:activity.applyMultipleTaskFlagForShortcut":
                    // KEY_APPLY_MULTIPLE_TASK_FLAG_FOR_SHORTCUT
                case "android:activity.applyNoUserActionFlagForShortcut":
                    // KEY_APPLY_NO_USER_ACTION_FLAG_FOR_SHORTCUT
                case "android:activity.transitionCompleteListener":
                    // KEY_TRANSITION_COMPLETE_LISTENER
                case "android:activity.transitionIsReturning": // KEY_TRANSITION_IS_RETURNING
                case "android:activity.sharedElementNames": // KEY_TRANSITION_SHARED_ELEMENTS
                case "android:activity.resultData": // KEY_RESULT_DATA
                case "android:activity.resultCode": // KEY_RESULT_CODE
                case "android:activity.exitCoordinatorIndex": // KEY_EXIT_COORDINATOR_INDEX
                case "android.activity.sourceInfo": // KEY_SOURCE_INFO
                case "android:activity.usageTimeReport": // KEY_USAGE_TIME_REPORT
                case "android:activity.rotationAnimationHint": // KEY_ROTATION_ANIMATION_HINT
                case "android:instantapps.installerbundle": // KEY_INSTANT_APP_VERIFICATION_BUNDLE
                case "android:activity.specsFuture": // KEY_SPECS_FUTURE
                case "android:activity.remoteAnimationAdapter": // KEY_REMOTE_ANIMATION_ADAPTER
                case "android:activity.remoteTransition": // KEY_REMOTE_TRANSITION
                case "android:activity.overrideTaskTransition": // KEY_OVERRIDE_TASK_TRANSITION
                case "android.activity.removeWithTaskOrganizer": // KEY_REMOVE_WITH_TASK_ORGANIZER
                case "android.activity.launchTypeBubble": // KEY_LAUNCHED_FROM_BUBBLE
                case "android.activity.splashScreenStyle": // KEY_SPLASH_SCREEN_STYLE
                case "android.activity.launchIntoPipParams": // KEY_LAUNCH_INTO_PIP_PARAMS
                case "android.activity.dismissKeyguard": // KEY_DISMISS_KEYGUARD
                case "android.activity.pendingIntentCreatorBackgroundActivityStartMode":
                    // KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE
                case "android.activity.launchCookie": // KEY_LAUNCH_COOKIE
                    // Existing keys
                    break;
                default:
                    unknownKeys.add(key);
                    break;
            }
        }

        // Report if any unknown key exists.
        for (String key : unknownKeys) {
            Log.e("ActivityOptionsTests", "Unknown key " + key + " is found. "
                    + "Please review if the given bundle should be protected with permissions.");
        }
        assertTrue(unknownKeys.isEmpty());
    }

    public static class TrampolineActivity extends Activity {
        static int sTaskId;

+134 −0
Original line number Diff line number Diff line
@@ -16,17 +16,36 @@

package com.android.server.wm;

import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.view.Display.DEFAULT_DISPLAY;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;

import android.app.ActivityOptions;
import android.content.pm.ActivityInfo;
import android.os.Looper;
import android.platform.test.annotations.Presubmit;
import android.view.RemoteAnimationAdapter;
import android.window.RemoteTransition;
import android.window.WindowContainerToken;

import androidx.test.filters.MediumTest;

import org.junit.Test;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;

/**
 * Build/Install/Run:
@@ -73,4 +92,119 @@ public class SafeActivityOptionsTest {

        assertSame(clone.getOriginalOptions().getLaunchRootTask(), token);
    }

    @Test
    public void test_getOptions() {
        // Mock everything necessary
        MockitoSession mockingSession = mockitoSession()
                .mockStatic(ActivityTaskManagerService.class)
                .strictness(Strictness.LENIENT)
                .startMocking();
        doReturn(PERMISSION_DENIED).when(() -> ActivityTaskManagerService.checkPermission(
                any(), anyInt(), anyInt()));

        final LockTaskController lockTaskController = mock(LockTaskController.class);
        doReturn(false).when(lockTaskController).isPackageAllowlisted(anyInt(), any());

        final ActivityTaskManagerService atm = mock(ActivityTaskManagerService.class);
        doReturn(lockTaskController).when(atm).getLockTaskController();

        final ActivityTaskSupervisor taskSupervisor =
                new ActivityTaskSupervisor(atm, mock(Looper.class));
        spyOn(taskSupervisor);
        doReturn(false).when(taskSupervisor).isCallerAllowedToLaunchOnDisplay(anyInt(),
                anyInt(), anyInt(), any());
        doReturn(false).when(taskSupervisor).isCallerAllowedToLaunchOnTaskDisplayArea(anyInt(),
                anyInt(), any(), any());

        taskSupervisor.mRecentTasks = mock(RecentTasks.class);
        doReturn(false).when(taskSupervisor.mRecentTasks).isCallerRecents(anyInt());

        // Ensure exceptions are thrown when lack of permissions.
        ActivityOptions activityOptions = ActivityOptions.makeBasic();
        try {
            activityOptions.setLaunchTaskId(100);
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            activityOptions = ActivityOptions.makeBasic();
            activityOptions.setDisableStartingWindow(true);
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            activityOptions = ActivityOptions.makeBasic();
            activityOptions.setTransientLaunch();
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            activityOptions = ActivityOptions.makeBasic();
            activityOptions.setDismissKeyguard();
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            activityOptions = ActivityOptions.makeBasic();
            activityOptions.setLaunchActivityType(ACTIVITY_TYPE_STANDARD);
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            activityOptions = ActivityOptions.makeBasic();
            activityOptions.setLaunchedFromBubble(true);
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            activityOptions = ActivityOptions.makeBasic();
            activityOptions.setLaunchDisplayId(DEFAULT_DISPLAY);
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            activityOptions = ActivityOptions.makeBasic();
            activityOptions.setLockTaskEnabled(true);
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            activityOptions = ActivityOptions.makeCustomTaskAnimation(
                    getInstrumentation().getContext(), 0, 0, null, null, null);
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            RemoteAnimationAdapter remoteAnimationAdapter = mock(RemoteAnimationAdapter.class);
            RemoteTransition remoteTransition = mock(RemoteTransition.class);
            activityOptions = ActivityOptions.makeRemoteAnimation(remoteAnimationAdapter);
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            activityOptions = ActivityOptions.makeRemoteAnimation(remoteAnimationAdapter,
                    remoteTransition);
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            activityOptions = ActivityOptions.makeBasic();
            activityOptions.setRemoteAnimationAdapter(remoteAnimationAdapter);
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            activityOptions = ActivityOptions.makeRemoteTransition(remoteTransition);
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            activityOptions = ActivityOptions.makeBasic();
            activityOptions.setRemoteTransition(remoteTransition);
            verifySecureExceptionThrown(activityOptions, taskSupervisor);

            verifySecureExceptionThrown(activityOptions, taskSupervisor,
                    mock(TaskDisplayArea.class));
        } finally {
            mockingSession.finishMocking();
        }
    }

    private void verifySecureExceptionThrown(ActivityOptions activityOptions,
            ActivityTaskSupervisor taskSupervisor) {
        verifySecureExceptionThrown(activityOptions, taskSupervisor, null /* mockTda */);
    }

    private void verifySecureExceptionThrown(ActivityOptions activityOptions,
            ActivityTaskSupervisor taskSupervisor, TaskDisplayArea mockTda) {
        SafeActivityOptions safeActivityOptions = new SafeActivityOptions(activityOptions);
        if (mockTda != null) {
            spyOn(safeActivityOptions);
            doReturn(mockTda).when(safeActivityOptions).getLaunchTaskDisplayArea(any(), any());
        }

        boolean isExceptionThrow = false;
        final ActivityInfo aInfo = mock(ActivityInfo.class);
        try {
            safeActivityOptions.getOptions(null, aInfo, null, taskSupervisor);
        } catch (SecurityException ex) {
            isExceptionThrow = true;
        }
        assertTrue(isExceptionThrow);
    }
}