OSDN Git Service

Add unit tests to ActivityStarterInterceptor
authorBenjamin Franz <bfranz@google.com>
Thu, 29 Jun 2017 14:06:13 +0000 (15:06 +0100)
committerBenjamin Franz <bfranz@google.com>
Tue, 1 Aug 2017 15:20:10 +0000 (16:20 +0100)
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

services/core/java/com/android/server/am/ActivityStartInterceptor.java
services/core/java/com/android/server/am/ActivityStarter.java
services/core/java/com/android/server/am/UserController.java
services/tests/servicestests/src/com/android/server/am/ActivityStartInterceptorTest.java [new file with mode: 0644]

index b91c7b1..6684f25 100644 (file)
@@ -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) {
index f58c768..4e68271 100644 (file)
@@ -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<PendingActivityLaunch> 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,
index 405ee32..89deb49 100644 (file)
@@ -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 (file)
index 0000000..194f4ae
--- /dev/null
@@ -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));
+    }
+}