From: Benjamin Franz Date: Thu, 29 Jun 2017 14:06:13 +0000 (+0100) Subject: Add unit tests to ActivityStarterInterceptor X-Git-Tag: android-x86-9.0-r1~410^2~95^2 X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=563707b3870962b92d716d8611d4cae5a982a013;p=android-x86%2Fframeworks-base.git Add unit tests to ActivityStarterInterceptor Add some unit tests to the interceptor and also slightly change its behaviour to return whether interception occurred to make the code in ActivityStarter slightly more readable. Test: bit FrameworksServicesTests:com.android.server.am.ActivityStartInterceptorTest Test: go/wm-smoke Change-Id: I388727f2bbd96754ba67f9c777233adb46ede685 --- diff --git a/services/core/java/com/android/server/am/ActivityStartInterceptor.java b/services/core/java/com/android/server/am/ActivityStartInterceptor.java index b91c7b1726f3..6684f257fb2f 100644 --- a/services/core/java/com/android/server/am/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/am/ActivityStartInterceptor.java @@ -29,10 +29,10 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME; import static android.content.pm.ApplicationInfo.FLAG_SUSPENDED; -import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.KeyguardManager; import android.app.admin.DevicePolicyManagerInternal; +import android.content.Context; import android.content.IIntentSender; import android.content.Intent; import android.content.IntentSender; @@ -43,18 +43,26 @@ import android.os.Binder; import android.os.UserHandle; import android.os.UserManager; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.UnlaunchableAppActivity; import com.android.server.LocalServices; /** * A class that contains activity intercepting logic for {@link ActivityStarter#startActivityLocked} - * It's initialized + * It's initialized via setStates and interception occurs via the intercept method. + * + * Note that this class is instantiated when {@link ActivityManagerService} gets created so there + * is no guarantee that other system services are already present. */ class ActivityStartInterceptor { private final ActivityManagerService mService; - private UserManager mUserManager; private final ActivityStackSupervisor mSupervisor; + private final Context mServiceContext; + private final UserController mUserController; + + // UserManager cannot be final as it's not ready when this class is instantiated during boot + private UserManager mUserManager; /* * Per-intent states loaded from ActivityStarter than shouldn't be changed by any @@ -69,7 +77,8 @@ class ActivityStartInterceptor { /* * Per-intent states that were load from ActivityStarter and are subject to modifications * by the interception routines. After calling {@link #intercept} the caller should assign - * these values back to {@link ActivityStarter#startActivityLocked}'s local variables. + * these values back to {@link ActivityStarter#startActivityLocked}'s local variables if + * {@link #intercept} returns true. */ Intent mIntent; int mCallingPid; @@ -81,10 +90,22 @@ class ActivityStartInterceptor { ActivityOptions mActivityOptions; ActivityStartInterceptor(ActivityManagerService service, ActivityStackSupervisor supervisor) { + this(service, supervisor, service.mContext, service.mUserController); + } + + @VisibleForTesting + ActivityStartInterceptor(ActivityManagerService service, ActivityStackSupervisor supervisor, + Context context, UserController userController) { mService = service; mSupervisor = supervisor; + mServiceContext = context; + mUserController = userController; } + /** + * Effectively initialize the class before intercepting the start intent. The values set in this + * method should not be changed during intercept. + */ void setStates(int userId, int realCallingPid, int realCallingUid, int startFlags, String callingPackage) { mRealCallingPid = realCallingPid; @@ -94,9 +115,16 @@ class ActivityStartInterceptor { mCallingPackage = callingPackage; } - void intercept(Intent intent, ResolveInfo rInfo, ActivityInfo aInfo, String resolvedType, + /** + * Intercept the launch intent based on various signals. If an interception happened the + * internal variables get assigned and need to be read explicitly by the caller. + * + * @return true if an interception occurred + */ + boolean intercept(Intent intent, ResolveInfo rInfo, ActivityInfo aInfo, String resolvedType, TaskRecord inTask, int callingPid, int callingUid, ActivityOptions activityOptions) { - mUserManager = UserManager.get(mService.mContext); + mUserManager = UserManager.get(mServiceContext); + mIntent = intent; mCallingPid = callingPid; mCallingUid = callingUid; @@ -105,17 +133,18 @@ class ActivityStartInterceptor { mResolvedType = resolvedType; mInTask = inTask; mActivityOptions = activityOptions; + if (interceptSuspendPackageIfNeed()) { // Skip the rest of interceptions as the package is suspended by device admin so // no user action can undo this. - return; + return true; } if (interceptQuietProfileIfNeeded()) { // If work profile is turned off, skip the work challenge since the profile can only // be unlocked when profile's user is running. - return; + return true; } - interceptWorkProfileChallengeIfNeeded(); + return interceptWorkProfileChallengeIfNeeded(); } private boolean interceptQuietProfileIfNeeded() { @@ -146,8 +175,8 @@ class ActivityStartInterceptor { (mAInfo.applicationInfo.flags & FLAG_SUSPENDED) == 0) { return false; } - DevicePolicyManagerInternal devicePolicyManager = LocalServices.getService( - DevicePolicyManagerInternal.class); + DevicePolicyManagerInternal devicePolicyManager = LocalServices + .getService(DevicePolicyManagerInternal.class); if (devicePolicyManager == null) { return false; } @@ -207,7 +236,7 @@ class ActivityStartInterceptor { */ private Intent interceptWithConfirmCredentialsIfNeeded(Intent intent, String resolvedType, ActivityInfo aInfo, String callingPackage, int userId) { - if (!mService.mUserController.shouldConfirmCredentials(userId)) { + if (!mUserController.shouldConfirmCredentials(userId)) { return null; } // TODO(b/28935539): should allow certain activities to bypass work challenge @@ -216,7 +245,7 @@ class ActivityStartInterceptor { Binder.getCallingUid(), userId, null, null, 0, new Intent[]{ intent }, new String[]{ resolvedType }, FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT | FLAG_IMMUTABLE, null); - final KeyguardManager km = (KeyguardManager) mService.mContext + final KeyguardManager km = (KeyguardManager) mServiceContext .getSystemService(KEYGUARD_SERVICE); final Intent newIntent = km.createConfirmDeviceCredentialIntent(null, null, userId); if (newIntent == null) { diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java index f58c768300ac..4e682715c89a 100644 --- a/services/core/java/com/android/server/am/ActivityStarter.java +++ b/services/core/java/com/android/server/am/ActivityStarter.java @@ -144,7 +144,7 @@ class ActivityStarter { private final ActivityManagerService mService; private final ActivityStackSupervisor mSupervisor; - private ActivityStartInterceptor mInterceptor; + private final ActivityStartInterceptor mInterceptor; private WindowManagerService mWindowManager; final ArrayList mPendingActivityLaunches = new ArrayList<>(); @@ -446,16 +446,20 @@ class ActivityStarter { } mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags, callingPackage); - mInterceptor.intercept(intent, rInfo, aInfo, resolvedType, inTask, callingPid, callingUid, - options); - intent = mInterceptor.mIntent; - rInfo = mInterceptor.mRInfo; - aInfo = mInterceptor.mAInfo; - resolvedType = mInterceptor.mResolvedType; - inTask = mInterceptor.mInTask; - callingPid = mInterceptor.mCallingPid; - callingUid = mInterceptor.mCallingUid; - options = mInterceptor.mActivityOptions; + if (mInterceptor.intercept(intent, rInfo, aInfo, resolvedType, inTask, callingPid, + callingUid, options)) { + // 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; + rInfo = mInterceptor.mRInfo; + aInfo = mInterceptor.mAInfo; + resolvedType = mInterceptor.mResolvedType; + inTask = mInterceptor.mInTask; + callingPid = mInterceptor.mCallingPid; + callingUid = mInterceptor.mCallingUid; + options = mInterceptor.mActivityOptions; + } + if (abort) { if (resultRecord != null) { resultStack.sendActivityResultLocked(-1, resultRecord, resultWho, requestCode, diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 405ee323bbe1..89deb49a07b9 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -22,9 +22,7 @@ import static android.app.ActivityManager.USER_OP_ERROR_IS_SYSTEM; import static android.app.ActivityManager.USER_OP_ERROR_RELATED_USERS_CANNOT_STOP; import static android.app.ActivityManager.USER_OP_IS_CURRENT; import static android.app.ActivityManager.USER_OP_SUCCESS; -import static android.content.Context.KEYGUARD_SERVICE; import static android.os.Process.SYSTEM_UID; - import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -109,7 +107,7 @@ import java.util.concurrent.atomic.AtomicInteger; /** * Helper class for {@link ActivityManagerService} responsible for multi-user functionality. */ -final class UserController { +class UserController { private static final String TAG = TAG_WITH_CLASS_NAME ? "UserController" : TAG_AM; // Maximum number of users we allow to be running at a time. @@ -1602,7 +1600,7 @@ final class UserController { * Returns whether the given user requires credential entry at this time. This is used to * intercept activity launches for work apps when the Work Challenge is present. */ - boolean shouldConfirmCredentials(int userId) { + protected boolean shouldConfirmCredentials(int userId) { synchronized (mLock) { if (mStartedUsers.get(userId) == null) { return false; diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityStartInterceptorTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityStartInterceptorTest.java new file mode 100644 index 000000000000..194f4ae51a62 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/am/ActivityStartInterceptorTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2017, 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.content.pm.ApplicationInfo.FLAG_SUSPENDED; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.when; + +import android.app.KeyguardManager; +import android.app.admin.DevicePolicyManagerInternal; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.UserInfo; +import android.os.UserHandle; +import android.os.UserManager; +import android.support.test.filters.SmallTest; + +import com.android.internal.app.UnlaunchableAppActivity; +import com.android.server.LocalServices; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit tests for {@link ActivityStartInterceptorTest}. + * + * Build/Install/Run: + * bit FrameworksServicesTests:com.android.server.am.ActivityStartInterceptorTest + */ +@SmallTest +public class ActivityStartInterceptorTest { + private static final int TEST_USER_ID = 1; + private static final int TEST_REAL_CALLING_UID = 2; + private static final int TEST_REAL_CALLING_PID = 3; + private static final String TEST_CALLING_PACKAGE = "com.test.caller"; + private static final int TEST_START_FLAGS = 4; + private static final Intent ADMIN_SUPPORT_INTENT = + new Intent("com.test.ADMIN_SUPPORT"); + private static final Intent CONFIRM_CREDENTIALS_INTENT = + new Intent("com.test.CONFIRM_CREDENTIALS"); + private static final UserInfo PARENT_USER_INFO = new UserInfo(0 /* userId */, "parent", + 0 /* flags */); + private static final String TEST_PACKAGE_NAME = "com.test.package"; + + @Mock + private Context mContext; + @Mock + private ActivityManagerService mService; + @Mock + private ActivityStackSupervisor mSupervisor; + @Mock + private DevicePolicyManagerInternal mDevicePolicyManager; + @Mock + private UserManager mUserManager; + @Mock + private UserController mUserController; + @Mock + private KeyguardManager mKeyguardManager; + + private ActivityStartInterceptor mInterceptor; + private ActivityInfo mAInfo = new ActivityInfo(); + + @Before + public void setUp() { + // This property is used to allow mocking of package private classes with mockito + System.setProperty("dexmaker.share_classloader", "true"); + + MockitoAnnotations.initMocks(this); + mInterceptor = new ActivityStartInterceptor(mService, mSupervisor, mContext, + mUserController); + mInterceptor.setStates(TEST_USER_ID, TEST_REAL_CALLING_PID, TEST_REAL_CALLING_UID, + TEST_START_FLAGS, TEST_CALLING_PACKAGE); + + // Mock DevicePolicyManagerInternal + LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); + LocalServices.addService(DevicePolicyManagerInternal.class, + mDevicePolicyManager); + when(mDevicePolicyManager + .createShowAdminSupportIntent(TEST_USER_ID, true)) + .thenReturn(ADMIN_SUPPORT_INTENT); + + // Mock UserManager + when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); + when(mUserManager.getProfileParent(TEST_USER_ID)).thenReturn(PARENT_USER_INFO); + + // Mock KeyguardManager + when(mContext.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(mKeyguardManager); + when(mKeyguardManager.createConfirmDeviceCredentialIntent( + nullable(CharSequence.class), nullable(CharSequence.class), eq(TEST_USER_ID))). + thenReturn(CONFIRM_CREDENTIALS_INTENT); + + // Initialise activity info + mAInfo.packageName = TEST_PACKAGE_NAME; + mAInfo.applicationInfo = new ApplicationInfo(); + } + + @Test + public void testSuspendedPackage() { + // GIVEN the package we're about to launch is currently suspended + mAInfo.applicationInfo.flags = FLAG_SUSPENDED; + + // THEN calling intercept returns true + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null)); + + // THEN the returned intent is the admin support intent + assertEquals(ADMIN_SUPPORT_INTENT, mInterceptor.mIntent); + } + + @Test + public void testInterceptQuietProfile() { + // GIVEN that the user the activity is starting as is currently in quiet mode + when(mUserManager.isQuietModeEnabled(eq(UserHandle.of(TEST_USER_ID)))).thenReturn(true); + + // THEN calling intercept returns true + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null)); + + // THEN the returned intent is the quiet mode intent + assertTrue(UnlaunchableAppActivity.createInQuietModeDialogIntent(TEST_USER_ID) + .filterEquals(mInterceptor.mIntent)); + } + + @Test + public void testWorkChallenge() { + // GIVEN that the user the activity is starting as is currently locked + when(mUserController.shouldConfirmCredentials(TEST_USER_ID)).thenReturn(true); + + // THEN calling intercept returns true + mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null); + + // THEN the returned intent is the quiet mode intent + assertTrue(CONFIRM_CREDENTIALS_INTENT.filterEquals(mInterceptor.mIntent)); + } + + @Test + public void testNoInterception() { + // GIVEN that none of the interception conditions are met + + // THEN calling intercept returns false + assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null)); + } +}