From 4d8d32cb6af4b9ea2f8d320f4a52561413d146bd Mon Sep 17 00:00:00 2001 From: Jorim Jaggi Date: Fri, 19 Jan 2018 15:57:41 +0100 Subject: [PATCH] Fix issue with permission check for shortcuts and pendingIntent When launching a PendingIntent, we have two sets of activity options: One supplied from the caller that obtained the pending intent, and those supplied from the caller that is sending it. We need to perform the permission check depending on which caller the options are coming from. For that, we introduce SafeActivityOptions that records the callingPid/callingUid when obtaining it and checks for the permissions with the correct callingPid/callingUid, and also supports merging both activity options. In addition to that, fix shortcuts by not clearing caller identity in LauncherAppsService. Test: go/wm-smoke Test: Install custom launcher APK with animation for shortcuts enabled, make sure launch works properly. Bug: 64674361 Change-Id: I9a854d43c65c8fa69bf16ccfbed86e8e681a095b --- .../android/server/am/ActivityManagerService.java | 76 ++++--- .../android/server/am/ActivityStackSupervisor.java | 74 ++----- .../android/server/am/ActivityStartController.java | 37 ++-- .../com/android/server/am/ActivityStarter.java | 78 +++---- .../core/java/com/android/server/am/AppErrors.java | 8 +- .../java/com/android/server/am/AppTaskImpl.java | 9 +- .../com/android/server/am/PendingIntentRecord.java | 26 +-- .../com/android/server/am/SafeActivityOptions.java | 232 +++++++++++++++++++++ .../com/android/server/pm/LauncherAppsService.java | 3 - .../android/server/am/ActivityStarterTests.java | 6 +- .../android/server/am/SafeActivityOptionsTest.java | 46 ++++ 11 files changed, 426 insertions(+), 169 deletions(-) create mode 100644 services/core/java/com/android/server/am/SafeActivityOptions.java create mode 100644 services/tests/servicestests/src/com/android/server/am/SafeActivityOptionsTest.java diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 29d33ce777f5..b78b388ce424 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -4707,7 +4707,8 @@ public class ActivityManagerService extends IActivityManager.Stub .setRequestCode(requestCode) .setStartFlags(startFlags) .setProfilerInfo(profilerInfo) - .setMayWait(bOptions, userId) + .setActivityOptions(bOptions) + .setMayWait(userId) .execute(); } @@ -4778,7 +4779,8 @@ public class ActivityManagerService extends IActivityManager.Stub .setResultWho(resultWho) .setRequestCode(requestCode) .setStartFlags(startFlags) - .setMayWait(bOptions, userId) + .setActivityOptions(bOptions) + .setMayWait(userId) .setIgnoreTargetSecurity(ignoreTargetSecurity) .execute(); } catch (SecurityException e) { @@ -4814,7 +4816,8 @@ public class ActivityManagerService extends IActivityManager.Stub .setResultWho(resultWho) .setRequestCode(requestCode) .setStartFlags(startFlags) - .setMayWait(bOptions, userId) + .setActivityOptions(bOptions) + .setMayWait(userId) .setProfilerInfo(profilerInfo) .setWaitResult(res) .execute(); @@ -4838,7 +4841,8 @@ public class ActivityManagerService extends IActivityManager.Stub .setRequestCode(requestCode) .setStartFlags(startFlags) .setGlobalConfiguration(config) - .setMayWait(bOptions, userId) + .setActivityOptions(bOptions) + .setMayWait(userId) .execute(); } @@ -4893,7 +4897,8 @@ public class ActivityManagerService extends IActivityManager.Stub .setVoiceInteractor(interactor) .setStartFlags(startFlags) .setProfilerInfo(profilerInfo) - .setMayWait(bOptions, userId) + .setActivityOptions(bOptions) + .setMayWait(userId) .execute(); } @@ -4908,7 +4913,8 @@ public class ActivityManagerService extends IActivityManager.Stub .setCallingUid(callingUid) .setCallingPackage(callingPackage) .setResolvedType(resolvedType) - .setMayWait(bOptions, userId) + .setActivityOptions(bOptions) + .setMayWait(userId) .execute(); } @@ -4923,6 +4929,7 @@ public class ActivityManagerService extends IActivityManager.Stub throw new SecurityException(msg); } + final SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(options); final int recentsUid = mRecentTasks.getRecentsComponentUid(); final ComponentName recentsComponent = mRecentTasks.getRecentsComponent(); final String recentsPackage = recentsComponent.getPackageName(); @@ -4953,7 +4960,8 @@ public class ActivityManagerService extends IActivityManager.Stub return mActivityStartController.obtainStarter(intent, "startRecentsActivity") .setCallingUid(recentsUid) .setCallingPackage(recentsPackage) - .setMayWait(activityOptions, userId) + .setActivityOptions(safeOptions) + .setMayWait(userId) .execute(); } } finally { @@ -5041,17 +5049,17 @@ public class ActivityManagerService extends IActivityManager.Stub if (intent != null && intent.hasFileDescriptors() == true) { throw new IllegalArgumentException("File descriptors passed in Intent"); } - ActivityOptions options = ActivityOptions.fromBundle(bOptions); + SafeActivityOptions options = SafeActivityOptions.fromBundle(bOptions); synchronized (this) { final ActivityRecord r = ActivityRecord.isInStackLocked(callingActivity); if (r == null) { - ActivityOptions.abort(options); + SafeActivityOptions.abort(options); return false; } if (r.app == null || r.app.thread == null) { // The caller is not running... d'oh! - ActivityOptions.abort(options); + SafeActivityOptions.abort(options); return false; } intent = new Intent(intent); @@ -5096,7 +5104,7 @@ public class ActivityManagerService extends IActivityManager.Stub if (aInfo == null) { // Nobody who is next! - ActivityOptions.abort(options); + SafeActivityOptions.abort(options); if (debug) Slog.d(TAG, "Next matching activity: nothing found"); return false; } @@ -5158,10 +5166,13 @@ public class ActivityManagerService extends IActivityManager.Stub enforceCallerIsRecentsOrHasPermission(START_TASKS_FROM_RECENTS, "startActivityFromRecents()"); + final int callingPid = Binder.getCallingPid(); + final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); try { synchronized (this) { - return mStackSupervisor.startActivityFromRecents(taskId, bOptions); + return mStackSupervisor.startActivityFromRecents(callingPid, callingUid, taskId, + SafeActivityOptions.fromBundle(bOptions)); } } finally { Binder.restoreCallingIdentity(origId); @@ -5178,7 +5189,8 @@ public class ActivityManagerService extends IActivityManager.Stub userId, false, ALLOW_FULL_ONLY, reason, null); // TODO: Switch to user app stacks here. int ret = mActivityStartController.startActivities(caller, -1, callingPackage, - intents, resolvedTypes, resultTo, bOptions, userId, reason); + intents, resolvedTypes, resultTo, SafeActivityOptions.fromBundle(bOptions), userId, + reason); return ret; } @@ -7937,9 +7949,9 @@ public class ActivityManagerService extends IActivityManager.Stub flags &= ~(PendingIntent.FLAG_NO_CREATE|PendingIntent.FLAG_CANCEL_CURRENT |PendingIntent.FLAG_UPDATE_CURRENT); - PendingIntentRecord.Key key = new PendingIntentRecord.Key( - type, packageName, activity, resultWho, - requestCode, intents, resolvedTypes, flags, bOptions, userId); + PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName, activity, + resultWho, requestCode, intents, resolvedTypes, flags, + SafeActivityOptions.fromBundle(bOptions), userId); WeakReference ref; ref = mIntentSenderRecords.get(key); PendingIntentRecord rec = ref != null ? ref.get() : null; @@ -10422,9 +10434,13 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public void startInPlaceAnimationOnFrontMostApplication(Bundle opts) throws RemoteException { - final ActivityOptions activityOptions = ActivityOptions.fromBundle(opts); - if (activityOptions.getAnimationType() != ActivityOptions.ANIM_CUSTOM_IN_PLACE || - activityOptions.getCustomInPlaceResId() == 0) { + final SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(opts); + final ActivityOptions activityOptions = safeOptions != null + ? safeOptions.getOptions(mStackSupervisor) + : null; + if (activityOptions == null + || activityOptions.getAnimationType() != ActivityOptions.ANIM_CUSTOM_IN_PLACE + || activityOptions.getCustomInPlaceResId() == 0) { throw new IllegalArgumentException("Expected in-place ActivityOption " + "with valid animation"); } @@ -10527,16 +10543,17 @@ public class ActivityManagerService extends IActivityManager.Stub if (DEBUG_STACK) Slog.d(TAG_STACK, "moveTaskToFront: moving taskId=" + taskId); synchronized(this) { - moveTaskToFrontLocked(taskId, flags, bOptions, false /* fromRecents */); + moveTaskToFrontLocked(taskId, flags, SafeActivityOptions.fromBundle(bOptions), + false /* fromRecents */); } } - void moveTaskToFrontLocked(int taskId, int flags, Bundle bOptions, boolean fromRecents) { - ActivityOptions options = ActivityOptions.fromBundle(bOptions); + void moveTaskToFrontLocked(int taskId, int flags, SafeActivityOptions options, + boolean fromRecents) { if (!checkAppSwitchAllowedLocked(Binder.getCallingPid(), Binder.getCallingUid(), -1, -1, "Task to front")) { - ActivityOptions.abort(options); + SafeActivityOptions.abort(options); return; } final long origId = Binder.clearCallingIdentity(); @@ -10550,7 +10567,10 @@ public class ActivityManagerService extends IActivityManager.Stub Slog.e(TAG, "moveTaskToFront: Attempt to violate Lock Task Mode"); return; } - mStackSupervisor.findTaskToMoveToFront(task, flags, options, "moveTaskToFront", + ActivityOptions realOptions = options != null + ? options.getOptions(mStackSupervisor) + : null; + mStackSupervisor.findTaskToMoveToFront(task, flags, realOptions, "moveTaskToFront", false /* forceNonResizable */); final ActivityRecord topActivity = task.getTopActivity(); @@ -10564,7 +10584,7 @@ public class ActivityManagerService extends IActivityManager.Stub } finally { Binder.restoreCallingIdentity(origId); } - ActivityOptions.abort(options); + SafeActivityOptions.abort(options); } /** @@ -13522,6 +13542,7 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public boolean convertToTranslucent(IBinder token, Bundle options) { + SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(options); final long origId = Binder.clearCallingIdentity(); try { synchronized (this) { @@ -13533,7 +13554,7 @@ public class ActivityManagerService extends IActivityManager.Stub int index = task.mActivities.lastIndexOf(r); if (index > 0) { ActivityRecord under = task.mActivities.get(index - 1); - under.returningOptions = ActivityOptions.fromBundle(options); + under.returningOptions = safeOptions.getOptions(r); } final boolean translucentChanged = r.changeWindowTranslucency(false); if (translucentChanged) { @@ -24886,7 +24907,8 @@ public class ActivityManagerService extends IActivityManager.Stub synchronized (ActivityManagerService.this) { return mActivityStartController.startActivitiesInPackage(packageUid, packageName, - intents, resolvedTypes, /*resultTo*/ null, bOptions, userId); + intents, resolvedTypes, null /* resultTo */, + SafeActivityOptions.fromBundle(bOptions), userId); } } diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java index 8168cba213f5..9f56552d3a1e 100644 --- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java @@ -1591,7 +1591,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D boolean checkStartAnyActivityPermission(Intent intent, ActivityInfo aInfo, String resultWho, int requestCode, int callingPid, int callingUid, String callingPackage, boolean ignoreTargetSecurity, ProcessRecord callerApp, - ActivityRecord resultRecord, ActivityStack resultStack, ActivityOptions options) { + ActivityRecord resultRecord, ActivityStack resultStack) { final int startAnyPerm = mService.checkPermission(START_ANY_ACTIVITY, callingPid, callingUid); if (startAnyPerm == PERMISSION_GRANTED) { @@ -1645,57 +1645,6 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D Slog.w(TAG, message); return false; } - if (options != null) { - // If a launch task id is specified, then ensure that the caller is the recents - // component or has the START_TASKS_FROM_RECENTS permission - if (options.getLaunchTaskId() != INVALID_TASK_ID - && !mRecentTasks.isCallerRecents(callingUid)) { - final int startInTaskPerm = mService.checkPermission(START_TASKS_FROM_RECENTS, - callingPid, callingUid); - if (startInTaskPerm == PERMISSION_DENIED) { - final String msg = "Permission Denial: starting " + intent.toString() - + " from " + callerApp + " (pid=" + callingPid - + ", uid=" + callingUid + ") with launchTaskId=" - + options.getLaunchTaskId(); - Slog.w(TAG, msg); - throw new SecurityException(msg); - } - } - // Check if someone tries to launch an activity on a private display with a different - // owner. - final int launchDisplayId = options.getLaunchDisplayId(); - if (launchDisplayId != INVALID_DISPLAY && !isCallerAllowedToLaunchOnDisplay(callingPid, - callingUid, launchDisplayId, aInfo)) { - final String msg = "Permission Denial: starting " + intent.toString() - + " from " + callerApp + " (pid=" + callingPid - + ", uid=" + callingUid + ") with launchDisplayId=" - + launchDisplayId; - Slog.w(TAG, msg); - throw new SecurityException(msg); - } - // Check if someone tries to launch an unwhitelisted activity into LockTask mode. - final boolean lockTaskMode = options.getLockTaskMode(); - if (lockTaskMode && !mService.mLockTaskController.isPackageWhitelisted( - UserHandle.getUserId(callingUid), aInfo.packageName)) { - final String msg = "Permission Denial: starting " + intent.toString() - + " from " + callerApp + " (pid=" + callingPid - + ", uid=" + callingUid + ") with lockTaskMode=true"; - Slog.w(TAG, msg); - throw new SecurityException(msg); - } - - // Check permission for remote animations - final RemoteAnimationAdapter adapter = options.getRemoteAnimationAdapter(); - if (adapter != null && mService.checkPermission( - CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS, callingPid, callingUid) - != PERMISSION_GRANTED) { - final String msg = "Permission Denial: starting " + intent.toString() - + " from " + callerApp + " (pid=" + callingPid - + ", uid=" + callingUid + ") with remoteAnimationAdapter"; - Slog.w(TAG, msg); - throw new SecurityException(msg); - } - } return true; } @@ -2166,8 +2115,8 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D } } - void findTaskToMoveToFront(TaskRecord task, int flags, ActivityOptions options, - String reason, boolean forceNonResizeable) { + void findTaskToMoveToFront(TaskRecord task, int flags, ActivityOptions options, String reason, + boolean forceNonResizeable) { final ActivityStack currentStack = task.getStack(); if (currentStack == null) { Slog.e(TAG, "findTaskToMoveToFront: can't move task=" @@ -4538,16 +4487,17 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D task.setTaskDockedResizing(true); } - int startActivityFromRecents(int taskId, Bundle bOptions) { + int startActivityFromRecents(int callingPid, int callingUid, int taskId, + SafeActivityOptions options) { final TaskRecord task; - final int callingUid; final String callingPackage; final Intent intent; final int userId; int activityType = ACTIVITY_TYPE_UNDEFINED; int windowingMode = WINDOWING_MODE_UNDEFINED; - final ActivityOptions activityOptions = (bOptions != null) - ? new ActivityOptions(bOptions) : null; + final ActivityOptions activityOptions = options != null + ? options.getOptions(this) + : null; if (activityOptions != null) { activityType = activityOptions.getLaunchActivityType(); windowingMode = activityOptions.getLaunchWindowingMode(); @@ -4595,7 +4545,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D sendPowerHintForLaunchStartIfNeeded(true /* forceSend */, targetActivity); mActivityMetricsLogger.notifyActivityLaunching(); try { - mService.moveTaskToFrontLocked(task.taskId, 0, bOptions, + mService.moveTaskToFrontLocked(task.taskId, 0, options, true /* fromRecents */); } finally { mActivityMetricsLogger.notifyActivityLaunched(START_TASK_TO_FRONT, @@ -4614,13 +4564,13 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D task.getStack()); return ActivityManager.START_TASK_TO_FRONT; } - callingUid = task.mCallingUid; callingPackage = task.mCallingPackage; intent = task.intent; intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); userId = task.userId; - int result = mService.getActivityStartController().startActivityInPackage(callingUid, - callingPackage, intent, null, null, null, 0, 0, bOptions, userId, task, + int result = mService.getActivityStartController().startActivityInPackage( + task.mCallingUid, callingPid, callingUid, callingPackage, intent, null, null, + null, 0, 0, options, userId, task, "startActivityFromRecents"); if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) { setResizingDuringAnimation(task); diff --git a/services/core/java/com/android/server/am/ActivityStartController.java b/services/core/java/com/android/server/am/ActivityStartController.java index aed49e0037e1..f9932b20fb5b 100644 --- a/services/core/java/com/android/server/am/ActivityStartController.java +++ b/services/core/java/com/android/server/am/ActivityStartController.java @@ -220,43 +220,44 @@ public class ActivityStartController { } } - final int startActivityInPackage(int uid, String callingPackage, - Intent intent, String resolvedType, IBinder resultTo, - String resultWho, int requestCode, int startFlags, Bundle bOptions, int userId, - TaskRecord inTask, String reason) { + final int startActivityInPackage(int uid, int realCallingUid, int realCallingPid, + String callingPackage, Intent intent, String resolvedType, IBinder resultTo, + String resultWho, int requestCode, int startFlags, SafeActivityOptions options, + int userId, TaskRecord inTask, String reason) { - userId = mService.mUserController.handleIncomingUser(Binder.getCallingPid(), - Binder.getCallingUid(), userId, false, ALLOW_FULL_ONLY, "startActivityInPackage", - null); + userId = mService.mUserController.handleIncomingUser(realCallingPid, realCallingUid, userId, + false, ALLOW_FULL_ONLY, "startActivityInPackage", null); // TODO: Switch to user app stacks here. return obtainStarter(intent, reason) .setCallingUid(uid) + .setRealCallingPid(realCallingPid) + .setRealCallingUid(realCallingUid) .setCallingPackage(callingPackage) .setResolvedType(resolvedType) .setResultTo(resultTo) .setResultWho(resultWho) .setRequestCode(requestCode) .setStartFlags(startFlags) - .setMayWait(bOptions, userId) + .setActivityOptions(options) + .setMayWait(userId) .setInTask(inTask) .execute(); } final int startActivitiesInPackage(int uid, String callingPackage, Intent[] intents, - String[] resolvedTypes, IBinder resultTo, Bundle bOptions, int userId) { + String[] resolvedTypes, IBinder resultTo, SafeActivityOptions options, int userId) { final String reason = "startActivityInPackage"; userId = mService.mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, false, ALLOW_FULL_ONLY, reason, null); // TODO: Switch to user app stacks here. - int ret = startActivities(null, uid, callingPackage, intents, resolvedTypes, resultTo, - bOptions, userId, reason); - return ret; + return startActivities(null, uid, callingPackage, intents, resolvedTypes, resultTo, options, + userId, reason); } int startActivities(IApplicationThread caller, int callingUid, String callingPackage, - Intent[] intents, String[] resolvedTypes, IBinder resultTo, Bundle bOptions, int userId, - String reason) { + Intent[] intents, String[] resolvedTypes, IBinder resultTo, SafeActivityOptions options, + int userId, String reason) { if (intents == null) { throw new NullPointerException("intents is null"); } @@ -312,9 +313,9 @@ public class ActivityStartController { "FLAG_CANT_SAVE_STATE not supported here"); } - ActivityOptions options = ActivityOptions.fromBundle( - i == intents.length - 1 ? bOptions : null); - + final SafeActivityOptions checkedOptions = i == intents.length - 1 + ? options + : null; final int res = obtainStarter(intent, reason) .setCaller(caller) .setResolvedType(resolvedTypes[i]) @@ -326,7 +327,7 @@ public class ActivityStartController { .setCallingPackage(callingPackage) .setRealCallingPid(realCallingPid) .setRealCallingUid(realCallingUid) - .setActivityOptions(options) + .setActivityOptions(checkedOptions) .setComponentSpecified(componentSpecified) .setOutActivity(outActivity) .execute(); diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java index 3a13155d88e1..671375995859 100644 --- a/services/core/java/com/android/server/am/ActivityStarter.java +++ b/services/core/java/com/android/server/am/ActivityStarter.java @@ -74,6 +74,7 @@ import static com.android.server.am.TaskRecord.REPARENT_KEEP_STACK_AT_FRONT; import static com.android.server.am.TaskRecord.REPARENT_MOVE_STACK_TO_FRONT; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.IApplicationThread; @@ -298,7 +299,7 @@ class ActivityStarter { int realCallingPid; int realCallingUid; int startFlags; - ActivityOptions activityOptions; + SafeActivityOptions activityOptions; boolean ignoreTargetSecurity; boolean componentSpecified; ActivityRecord[] outActivity; @@ -306,13 +307,12 @@ class ActivityStarter { String reason; ProfilerInfo profilerInfo; Configuration globalConfig; - Bundle waitOptions; int userId; WaitResult waitResult; /** * Indicates that we should wait for the result of the start request. This flag is set when - * {@link ActivityStarter#setMayWait(Bundle, int)} is called. + * {@link ActivityStarter#setMayWait(int)} is called. * {@see ActivityStarter#startActivityMayWait}. */ boolean mayWait; @@ -353,7 +353,6 @@ class ActivityStarter { reason = null; profilerInfo = null; globalConfig = null; - waitOptions = null; userId = 0; waitResult = null; mayWait = false; @@ -388,7 +387,6 @@ class ActivityStarter { reason = request.reason; profilerInfo = request.profilerInfo; globalConfig = request.globalConfig; - waitOptions = request.waitOptions; userId = request.userId; waitResult = request.waitResult; mayWait = request.mayWait; @@ -473,7 +471,7 @@ class ActivityStarter { mRequest.voiceSession, mRequest.voiceInteractor, mRequest.resultTo, mRequest.resultWho, mRequest.requestCode, mRequest.startFlags, mRequest.profilerInfo, mRequest.waitResult, mRequest.globalConfig, - mRequest.waitOptions, mRequest.ignoreTargetSecurity, mRequest.userId, + mRequest.activityOptions, mRequest.ignoreTargetSecurity, mRequest.userId, mRequest.inTask, mRequest.reason); } else { return startActivity(mRequest.caller, mRequest.intent, mRequest.ephemeralIntent, @@ -513,7 +511,7 @@ class ActivityStarter { IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid, String callingPackage, int realCallingPid, int realCallingUid, int startFlags, - ActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified, + SafeActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity, TaskRecord inTask, String reason) { if (TextUtils.isEmpty(reason)) { @@ -555,8 +553,9 @@ class ActivityStarter { IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid, String callingPackage, int realCallingPid, int realCallingUid, int startFlags, - ActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified, - ActivityRecord[] outActivity, TaskRecord inTask) { + SafeActivityOptions options, + boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity, + TaskRecord inTask) { int err = ActivityManager.START_SUCCESS; // Pull the optional Ephemeral Installer-only bundle out of the options early. final Bundle verificationBundle @@ -603,7 +602,7 @@ class ActivityStarter { // Transfer the result target from the source activity to the new // one being started, including any failures. if (requestCode >= 0) { - ActivityOptions.abort(options); + SafeActivityOptions.abort(options); return ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT; } resultRecord = sourceRecord.resultTo; @@ -691,16 +690,20 @@ class ActivityStarter { resultStack.sendActivityResultLocked( -1, resultRecord, resultWho, requestCode, RESULT_CANCELED, null); } - ActivityOptions.abort(options); + SafeActivityOptions.abort(options); return err; } boolean abort = !mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho, - requestCode, callingPid, callingUid, callingPackage, ignoreTargetSecurity, callerApp, - resultRecord, resultStack, options); + requestCode, callingPid, callingUid, callingPackage, ignoreTargetSecurity, + callerApp, resultRecord, resultStack); abort |= !mService.mIntentFirewall.checkStartActivity(intent, callingUid, callingPid, resolvedType, aInfo.applicationInfo); + // Merge the two options bundles, while realCallerOptions takes precedence. + ActivityOptions checkedOptions = options != null + ? options.getOptions(intent, aInfo, callerApp, mSupervisor) + : null; if (mService.mController != null) { try { // The Intent we give to the watcher has the extra data @@ -715,7 +718,7 @@ class ActivityStarter { mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags, callingPackage); if (mInterceptor.intercept(intent, rInfo, aInfo, resolvedType, inTask, callingPid, - callingUid, options)) { + callingUid, checkedOptions)) { // activity start was intercepted, e.g. because the target user is currently in quiet // mode (turn off work) or the target application is suspended intent = mInterceptor.mIntent; @@ -725,7 +728,7 @@ class ActivityStarter { inTask = mInterceptor.mInTask; callingPid = mInterceptor.mCallingPid; callingUid = mInterceptor.mCallingUid; - options = mInterceptor.mActivityOptions; + checkedOptions = mInterceptor.mActivityOptions; } if (abort) { @@ -735,7 +738,7 @@ class ActivityStarter { } // We pretend to the caller that it was really started, but // they will just get a cancel result. - ActivityOptions.abort(options); + ActivityOptions.abort(checkedOptions); return START_ABORTED; } @@ -796,7 +799,7 @@ class ActivityStarter { ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid, callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(), resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null, - mSupervisor, options, sourceRecord); + mSupervisor, checkedOptions, sourceRecord); if (outActivity != null) { outActivity[0] = r; } @@ -808,13 +811,16 @@ class ActivityStarter { } final ActivityStack stack = mSupervisor.mFocusedStack; + + // If we are starting an activity that is not from the same uid as the currently resumed + // one, check whether app switches are allowed. if (voiceSession == null && (stack.mResumedActivity == null - || stack.mResumedActivity.info.applicationInfo.uid != callingUid)) { + || stack.mResumedActivity.info.applicationInfo.uid != realCallingUid)) { if (!mService.checkAppSwitchAllowedLocked(callingPid, callingUid, realCallingPid, realCallingUid, "Activity start")) { mController.addPendingActivityLaunch(new PendingActivityLaunch(r, sourceRecord, startFlags, stack, callerApp)); - ActivityOptions.abort(options); + ActivityOptions.abort(checkedOptions); return ActivityManager.START_SWITCHES_CANCELED; } } @@ -833,9 +839,10 @@ class ActivityStarter { mController.doPendingActivityLaunches(false); return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags, - true /* doResume */, options, inTask, outActivity); + true /* doResume */, checkedOptions, inTask, outActivity); } + /** * Creates a launch intent for the given auxiliary resolution data. */ @@ -900,8 +907,8 @@ class ActivityStarter { IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, IBinder resultTo, String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo, WaitResult outResult, - Configuration globalConfig, Bundle bOptions, boolean ignoreTargetSecurity, int userId, - TaskRecord inTask, String reason) { + Configuration globalConfig, SafeActivityOptions options, boolean ignoreTargetSecurity, + int userId, TaskRecord inTask, String reason) { // Refuse possible leaked file descriptors if (intent != null && intent.hasFileDescriptors()) { throw new IllegalArgumentException("File descriptors passed in Intent"); @@ -953,7 +960,6 @@ class ActivityStarter { // Collect information about the target of the Intent. ActivityInfo aInfo = mSupervisor.resolveActivity(intent, rInfo, startFlags, profilerInfo); - ActivityOptions options = ActivityOptions.fromBundle(bOptions); synchronized (mService) { final int realCallingPid = Binder.getCallingPid(); final int realCallingUid = Binder.getCallingUid(); @@ -993,7 +999,7 @@ class ActivityStarter { Slog.w(TAG, "Unable to find app for caller " + caller + " (pid=" + callingPid + ") when starting: " + intent.toString()); - ActivityOptions.abort(options); + SafeActivityOptions.abort(options); return ActivityManager.START_PERMISSION_DENIED; } } @@ -1039,12 +1045,10 @@ class ActivityStarter { } final ActivityRecord[] outRecord = new ActivityRecord[1]; - int res = startActivity(caller, intent, ephemeralIntent, resolvedType, - aInfo, rInfo, voiceSession, voiceInteractor, - resultTo, resultWho, requestCode, callingPid, - callingUid, callingPackage, realCallingPid, realCallingUid, startFlags, - options, ignoreTargetSecurity, componentSpecified, outRecord, inTask, - reason); + int res = startActivity(caller, intent, ephemeralIntent, resolvedType, aInfo, rInfo, + voiceSession, voiceInteractor, resultTo, resultWho, requestCode, callingPid, + callingUid, callingPackage, realCallingPid, realCallingUid, startFlags, options, + ignoreTargetSecurity, componentSpecified, outRecord, inTask, reason); Binder.restoreCallingIdentity(origId); @@ -2447,11 +2451,15 @@ class ActivityStarter { return this; } - ActivityStarter setActivityOptions(ActivityOptions options) { + ActivityStarter setActivityOptions(SafeActivityOptions options) { mRequest.activityOptions = options; return this; } + ActivityStarter setActivityOptions(Bundle bOptions) { + return setActivityOptions(SafeActivityOptions.fromBundle(bOptions)); + } + ActivityStarter setIgnoreTargetSecurity(boolean ignoreTargetSecurity) { mRequest.ignoreTargetSecurity = ignoreTargetSecurity; return this; @@ -2487,19 +2495,13 @@ class ActivityStarter { return this; } - ActivityStarter setWaitOptions(Bundle options) { - mRequest.waitOptions = options; - return this; - } - ActivityStarter setUserId(int userId) { mRequest.userId = userId; return this; } - ActivityStarter setMayWait(Bundle options, int userId) { + ActivityStarter setMayWait(int userId) { mRequest.mayWait = true; - mRequest.waitOptions = options; mRequest.userId = userId; return this; diff --git a/services/core/java/com/android/server/am/AppErrors.java b/services/core/java/com/android/server/am/AppErrors.java index 89102748796a..2e7e2577c8d2 100644 --- a/services/core/java/com/android/server/am/AppErrors.java +++ b/services/core/java/com/android/server/am/AppErrors.java @@ -408,9 +408,11 @@ class AppErrors { final Set cats = task.intent.getCategories(); if (cats != null && cats.contains(Intent.CATEGORY_LAUNCHER)) { mService.getActivityStartController().startActivityInPackage( - task.mCallingUid, task.mCallingPackage, task.intent, null, null, - null, 0, 0, ActivityOptions.makeBasic().toBundle(), task.userId, - null, "AppErrors"); + task.mCallingUid, callingPid, callingUid, task.mCallingPackage, + task.intent, null, null, null, 0, 0, + new SafeActivityOptions(ActivityOptions.makeBasic()), + task.userId, null, + "AppErrors"); } } } diff --git a/services/core/java/com/android/server/am/AppTaskImpl.java b/services/core/java/com/android/server/am/AppTaskImpl.java index f821f6bdb925..5f5a504bb014 100644 --- a/services/core/java/com/android/server/am/AppTaskImpl.java +++ b/services/core/java/com/android/server/am/AppTaskImpl.java @@ -20,6 +20,7 @@ import static com.android.server.am.ActivityStackSupervisor.MATCH_TASK_IN_STACKS import static com.android.server.am.ActivityStackSupervisor.REMOVE_FROM_RECENTS; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.app.IAppTask; import android.app.IApplicationThread; import android.content.Intent; @@ -93,10 +94,13 @@ class AppTaskImpl extends IAppTask.Stub { public void moveToFront() { checkCaller(); // Will bring task to front if it already has a root activity. + final int callingPid = Binder.getCallingPid(); + final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); try { synchronized (this) { - mService.mStackSupervisor.startActivityFromRecents(mTaskId, null); + mService.mStackSupervisor.startActivityFromRecents(callingPid, callingUid, mTaskId, + null); } } finally { Binder.restoreCallingIdentity(origId); @@ -127,7 +131,8 @@ class AppTaskImpl extends IAppTask.Stub { .setCaller(appThread) .setCallingPackage(callingPackage) .setResolvedType(resolvedType) - .setMayWait(bOptions, callingUser) + .setActivityOptions(bOptions) + .setMayWait(callingUser) .setInTask(tr) .execute(); } diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index c26e7703a1ca..26a08775af44 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -20,6 +20,7 @@ import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.content.IIntentSender; import android.content.IIntentReceiver; import android.app.PendingIntent; @@ -65,7 +66,7 @@ final class PendingIntentRecord extends IIntentSender.Stub { final int requestCode; final Intent requestIntent; final String requestResolvedType; - final Bundle options; + final SafeActivityOptions options; Intent[] allIntents; String[] allResolvedTypes; final int flags; @@ -75,7 +76,7 @@ final class PendingIntentRecord extends IIntentSender.Stub { private static final int ODD_PRIME_NUMBER = 37; Key(int _t, String _p, ActivityRecord _a, String _w, - int _r, Intent[] _i, String[] _it, int _f, Bundle _o, int _userId) { + int _r, Intent[] _i, String[] _it, int _f, SafeActivityOptions _o, int _userId) { type = _t; packageName = _p; activity = _a; @@ -313,14 +314,13 @@ final class PendingIntentRecord extends IIntentSender.Stub { int res = 0; switch (key.type) { case ActivityManager.INTENT_SENDER_ACTIVITY: - if (options == null) { - options = key.options; - } else if (key.options != null) { - Bundle opts = new Bundle(key.options); - opts.putAll(options); - options = opts; - } try { + SafeActivityOptions mergedOptions = key.options; + if (mergedOptions == null) { + mergedOptions = SafeActivityOptions.fromBundle(options); + } else { + mergedOptions.setCallerOptions(ActivityOptions.fromBundle(options)); + } if (key.allIntents != null && key.allIntents.length > 1) { Intent[] allIntents = new Intent[key.allIntents.length]; String[] allResolvedTypes = new String[key.allIntents.length]; @@ -334,12 +334,12 @@ final class PendingIntentRecord extends IIntentSender.Stub { allResolvedTypes[allResolvedTypes.length-1] = resolvedType; owner.getActivityStartController().startActivitiesInPackage(uid, key.packageName, allIntents, allResolvedTypes, resultTo, - options, userId); + mergedOptions, userId); } else { owner.getActivityStartController().startActivityInPackage(uid, - key.packageName, finalIntent, resolvedType, resultTo, - resultWho, requestCode, 0, options, userId, null, - "PendingIntentRecord"); + callingPid, callingUid, key.packageName, finalIntent, + resolvedType, resultTo, resultWho, requestCode, 0, + mergedOptions, userId, null, "PendingIntentRecord"); } } catch (RuntimeException e) { Slog.w(TAG, "Unable to send startActivity intent", e); diff --git a/services/core/java/com/android/server/am/SafeActivityOptions.java b/services/core/java/com/android/server/am/SafeActivityOptions.java new file mode 100644 index 000000000000..d08111ec0aa5 --- /dev/null +++ b/services/core/java/com/android/server/am/SafeActivityOptions.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2018 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.server.am; + +import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS; +import static android.Manifest.permission.START_TASKS_FROM_RECENTS; +import static android.content.pm.PackageManager.PERMISSION_DENIED; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.view.Display.INVALID_DISPLAY; +import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; +import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.am.TaskRecord.INVALID_TASK_ID; + +import android.annotation.Nullable; +import android.app.ActivityOptions; +import android.app.PendingIntent; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.os.Binder; +import android.os.Bundle; +import android.os.UserHandle; +import android.util.Slog; +import android.view.RemoteAnimationAdapter; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * Wraps {@link ActivityOptions}, records binder identity, and checks permission when retrieving + * the inner options. Also supports having two set of options: Once from the original caller, and + * once from the caller that is overriding it, which happens when sending a {@link PendingIntent}. + */ +class SafeActivityOptions { + + private static final String TAG = TAG_WITH_CLASS_NAME ? "SafeActivityOptions" : TAG_AM; + + private final int mOriginalCallingPid; + private final int mOriginalCallingUid; + private int mRealCallingPid; + private int mRealCallingUid; + private final @Nullable ActivityOptions mOriginalOptions; + private @Nullable ActivityOptions mCallerOptions; + + /** + * Constructs a new instance from a bundle and records {@link Binder#getCallingPid}/ + * {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when constructing + * this object. + * + * @param bOptions The {@link ActivityOptions} as {@link Bundle}. + */ + static SafeActivityOptions fromBundle(Bundle bOptions) { + return bOptions != null + ? new SafeActivityOptions(ActivityOptions.fromBundle(bOptions)) + : null; + } + + /** + * Constructs a new instance and records {@link Binder#getCallingPid}/ + * {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when constructing + * this object. + * + * @param options The options to wrap. + */ + SafeActivityOptions(@Nullable ActivityOptions options) { + mOriginalCallingPid = Binder.getCallingPid(); + mOriginalCallingUid = Binder.getCallingUid(); + mOriginalOptions = options; + } + + /** + * Overrides options with options from a caller and records {@link Binder#getCallingPid}/ + * {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when calling this + * method. + */ + void setCallerOptions(@Nullable ActivityOptions options) { + mRealCallingPid = Binder.getCallingPid(); + mRealCallingUid = Binder.getCallingUid(); + mCallerOptions = options; + } + + /** + * Performs permission check and retrieves the options. + * + * @param r The record of the being started activity. + */ + ActivityOptions getOptions(ActivityRecord r) throws SecurityException { + return getOptions(r.intent, r.info, r.app, r.mStackSupervisor); + } + + /** + * Performs permission check and retrieves the options when options are not being used to launch + * a specific activity (i.e. a task is moved to front). + */ + ActivityOptions getOptions(ActivityStackSupervisor supervisor) throws SecurityException { + return getOptions(null, null, null, supervisor); + } + + /** + * Performs permission check and retrieves the options. + * + * @param intent The intent that is being launched. + * @param aInfo The info of the activity being launched. + * @param callerApp The record of the caller. + */ + ActivityOptions getOptions(@Nullable Intent intent, @Nullable ActivityInfo aInfo, + @Nullable ProcessRecord callerApp, + ActivityStackSupervisor supervisor) throws SecurityException { + if (mOriginalOptions != null) { + checkPermissions(intent, aInfo, callerApp, supervisor, mOriginalOptions, + mOriginalCallingPid, mOriginalCallingUid); + } + if (mCallerOptions != null) { + checkPermissions(intent, aInfo, callerApp, supervisor, mCallerOptions, + mRealCallingPid, mRealCallingUid); + } + return mergeActivityOptions(mOriginalOptions, mCallerOptions); + } + + /** + * @see ActivityOptions#popAppVerificationBundle + */ + Bundle popAppVerificationBundle() { + return mOriginalOptions != null ? mOriginalOptions.popAppVerificationBundle() : null; + } + + private void abort() { + if (mOriginalOptions != null) { + ActivityOptions.abort(mOriginalOptions); + } + if (mCallerOptions != null) { + ActivityOptions.abort(mCallerOptions); + } + } + + static void abort(@Nullable SafeActivityOptions options) { + if (options != null) { + options.abort(); + } + } + + /** + * Merges two activity options into one, with {@code options2} taking precedence in case of a + * conflict. + */ + @VisibleForTesting + @Nullable ActivityOptions mergeActivityOptions(@Nullable ActivityOptions options1, + @Nullable ActivityOptions options2) { + if (options1 == null) { + return options2; + } + if (options2 == null) { + return options1; + } + final Bundle b1 = options1.toBundle(); + final Bundle b2 = options2.toBundle(); + b1.putAll(b2); + return ActivityOptions.fromBundle(b1); + } + + private void checkPermissions(@Nullable Intent intent, @Nullable ActivityInfo aInfo, + @Nullable ProcessRecord callerApp, ActivityStackSupervisor supervisor, + ActivityOptions options, int callingPid, int callingUid) { + // If a launch task id is specified, then ensure that the caller is the recents + // component or has the START_TASKS_FROM_RECENTS permission + if (options.getLaunchTaskId() != INVALID_TASK_ID + && !supervisor.mRecentTasks.isCallerRecents(callingUid)) { + final int startInTaskPerm = supervisor.mService.checkPermission( + START_TASKS_FROM_RECENTS, callingPid, callingUid); + if (startInTaskPerm == PERMISSION_DENIED) { + final String msg = "Permission Denial: starting " + getIntentString(intent) + + " from " + callerApp + " (pid=" + callingPid + + ", uid=" + callingUid + ") with launchTaskId=" + + options.getLaunchTaskId(); + Slog.w(TAG, msg); + throw new SecurityException(msg); + } + } + // Check if someone tries to launch an activity on a private display with a different + // owner. + final int launchDisplayId = options.getLaunchDisplayId(); + if (aInfo != null && launchDisplayId != INVALID_DISPLAY + && !supervisor.isCallerAllowedToLaunchOnDisplay(callingPid, callingUid, + launchDisplayId, aInfo)) { + final String msg = "Permission Denial: starting " + getIntentString(intent) + + " from " + callerApp + " (pid=" + callingPid + + ", uid=" + callingUid + ") with launchDisplayId=" + + launchDisplayId; + Slog.w(TAG, msg); + throw new SecurityException(msg); + } + // Check if someone tries to launch an unwhitelisted activity into LockTask mode. + final boolean lockTaskMode = options.getLockTaskMode(); + if (aInfo != null && lockTaskMode + && !supervisor.mService.mLockTaskController.isPackageWhitelisted( + UserHandle.getUserId(callingUid), aInfo.packageName)) { + final String msg = "Permission Denial: starting " + getIntentString(intent) + + " from " + callerApp + " (pid=" + callingPid + + ", uid=" + callingUid + ") with lockTaskMode=true"; + Slog.w(TAG, msg); + throw new SecurityException(msg); + } + + // Check permission for remote animations + final RemoteAnimationAdapter adapter = options.getRemoteAnimationAdapter(); + if (adapter != null && supervisor.mService.checkPermission( + CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS, callingPid, callingUid) + != PERMISSION_GRANTED) { + final String msg = "Permission Denial: starting " + getIntentString(intent) + + " from " + callerApp + " (pid=" + callingPid + + ", uid=" + callingUid + ") with remoteAnimationAdapter"; + Slog.w(TAG, msg); + throw new SecurityException(msg); + } + } + + private String getIntentString(Intent intent) { + return intent != null ? intent.toString() : "(no intent)"; + } +} diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 1717b3d1a5f7..14995b3827e1 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -560,7 +560,6 @@ public class LauncherAppsService extends SystemService { private boolean startShortcutIntentsAsPublisher(@NonNull Intent[] intents, @NonNull String publisherPackage, Bundle startActivityOptions, int userId) { final int code; - final long ident = injectClearCallingIdentity(); try { code = mActivityManagerInternal.startActivitiesAsPackage(publisherPackage, userId, intents, startActivityOptions); @@ -575,8 +574,6 @@ public class LauncherAppsService extends SystemService { Slog.d(TAG, "SecurityException while launching intent", e); } return false; - } finally { - injectRestoreCallingIdentity(ident); } } diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityStarterTests.java b/services/tests/servicestests/src/com/android/server/am/ActivityStarterTests.java index 589a89bcbddf..b58c7003f39e 100644 --- a/services/tests/servicestests/src/com/android/server/am/ActivityStarterTests.java +++ b/services/tests/servicestests/src/com/android/server/am/ActivityStarterTests.java @@ -228,8 +228,8 @@ public class ActivityStarterTests extends ActivityTestsBase { if (containsConditions(preconditions,PRECONDITION_CANNOT_START_ANY_ACTIVITY)) { doReturn(false).when(service.mStackSupervisor).checkStartAnyActivityPermission( - any(), any(), any(), anyInt(), anyInt(), anyInt(), any(), anyBoolean(), - any(), any(), any(), any()); + any(), any(), any(), anyInt(), anyInt(), anyInt(), any(), + anyBoolean(), any(), any(), any()); } try { @@ -278,7 +278,7 @@ public class ActivityStarterTests extends ActivityTestsBase { .setResultTo(resultTo) .setRequestCode(requestCode) .setReason("testLaunchActivityPermissionDenied") - .setActivityOptions(options) + .setActivityOptions(new SafeActivityOptions(options)) .execute(); verify(options, times(1)).abort(); } diff --git a/services/tests/servicestests/src/com/android/server/am/SafeActivityOptionsTest.java b/services/tests/servicestests/src/com/android/server/am/SafeActivityOptionsTest.java new file mode 100644 index 000000000000..168bc1782d56 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/am/SafeActivityOptionsTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 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.server.am; + +import static org.junit.Assert.assertEquals; + +import android.app.ActivityOptions; +import android.platform.test.annotations.Presubmit; +import android.support.test.filters.FlakyTest; +import android.support.test.filters.MediumTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@MediumTest +@Presubmit +@FlakyTest +@RunWith(AndroidJUnit4.class) +public class SafeActivityOptionsTest { + + @Test + public void testMerge() { + final ActivityOptions opts1 = ActivityOptions.makeBasic(); + opts1.setLaunchDisplayId(5); + final ActivityOptions opts2 = ActivityOptions.makeBasic(); + opts2.setLaunchDisplayId(6); + final SafeActivityOptions options = new SafeActivityOptions(opts1); + final ActivityOptions result = options.mergeActivityOptions(opts1, opts2); + assertEquals(6, result.getLaunchDisplayId()); + } +} -- 2.11.0