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;
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
/*
* 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;
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;
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;
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() {
(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;
}
*/
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
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) {
private final ActivityManagerService mService;
private final ActivityStackSupervisor mSupervisor;
- private ActivityStartInterceptor mInterceptor;
+ private final ActivityStartInterceptor mInterceptor;
private WindowManagerService mWindowManager;
final ArrayList<PendingActivityLaunch> mPendingActivityLaunches = new ArrayList<>();
}
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,
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;
/**
* 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.
* 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;
--- /dev/null
+/*
+ * 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));
+ }
+}