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
This commit is contained in:
Louis Chang
2023-07-06 10:36:40 +00:00
parent 15d68f4163
commit 26cceb8762
3 changed files with 287 additions and 20 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}