diff --git a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java new file mode 100644 index 0000000000000..1c2333a6ffa42 --- /dev/null +++ b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2021 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.wm; + +import android.annotation.IntDef; +import android.annotation.Nullable; +import android.app.ActivityOptions; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ResolveInfo; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Callback to intercept activity starts and possibly block/redirect them. + */ +public abstract class ActivityInterceptorCallback { + /** + * Intercept the launch intent based on various signals. If an interception happened, returns + * a new/existing non-null {@link Intent} which may redirect to another activity. + * + * @return null if no interception occurred, or a non-null intent which replaces the + * existing intent. + */ + public abstract @Nullable Intent intercept(ActivityInterceptorInfo info); + + /** + * The unique id of each interceptor which determines the order it will execute in. + */ + @IntDef(suffix = { "_ORDERED_ID" }, value = { + FIRST_ORDERED_ID, + LAST_ORDERED_ID // Update this when adding new ids + }) + @Retention(RetentionPolicy.SOURCE) + public @interface OrderedId {} + + /** + * The first id, used by the framework to determine the valid range of ids. + */ + static final int FIRST_ORDERED_ID = 0; + + /** + * The final id, used by the framework to determine the valid range of ids. Update this when + * adding new ids. + */ + static final int LAST_ORDERED_ID = FIRST_ORDERED_ID; + + /** + * Data class for storing the various arguments needed for activity interception. + */ + public static final class ActivityInterceptorInfo { + public final int realCallingUid; + public final int realCallingPid; + public final int userId; + public final String callingPackage; + public final String callingFeatureId; + public final Intent intent; + public final ResolveInfo rInfo; + public final ActivityInfo aInfo; + public final String resolvedType; + public final int callingPid; + public final int callingUid; + public final ActivityOptions checkedOptions; + + public ActivityInterceptorInfo(int realCallingUid, int realCallingPid, int userId, + String callingPackage, String callingFeatureId, Intent intent, + ResolveInfo rInfo, ActivityInfo aInfo, String resolvedType, int callingPid, + int callingUid, ActivityOptions checkedOptions) { + this.realCallingUid = realCallingUid; + this.realCallingPid = realCallingPid; + this.userId = userId; + this.callingPackage = callingPackage; + this.callingFeatureId = callingFeatureId; + this.intent = intent; + this.rInfo = rInfo; + this.aInfo = aInfo; + this.resolvedType = resolvedType; + this.callingPid = callingPid; + this.callingUid = callingUid; + this.checkedOptions = checkedOptions; + } + } +} diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java index 979cea9b15699..223f0be9bbeaf 100644 --- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java @@ -51,6 +51,7 @@ import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.BlockedAppActivity; @@ -178,7 +179,33 @@ class ActivityStartInterceptor { // before issuing the work challenge. return true; } - return interceptLockedManagedProfileIfNeeded(); + if (interceptLockedManagedProfileIfNeeded()) { + return true; + } + + final SparseArray callbacks = + mService.getActivityInterceptorCallbacks(); + final ActivityInterceptorCallback.ActivityInterceptorInfo interceptorInfo = + new ActivityInterceptorCallback.ActivityInterceptorInfo(mRealCallingUid, + mRealCallingPid, mUserId, mCallingPackage, mCallingFeatureId, mIntent, + mRInfo, mAInfo, mResolvedType, mCallingPid, mCallingUid, + mActivityOptions); + + for (int i = 0; i < callbacks.size(); i++) { + final ActivityInterceptorCallback callback = callbacks.valueAt(i); + final Intent newIntent = callback.intercept(interceptorInfo); + if (newIntent == null) { + continue; + } + mIntent = newIntent; + mCallingPid = mRealCallingPid; + mCallingUid = mRealCallingUid; + mRInfo = mSupervisor.resolveIntent(mIntent, null, mUserId, 0, mRealCallingUid); + mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, + null /*profilerInfo*/); + return true; + } + return false; } private boolean hasCrossProfileAnimation() { diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 9db13ba825f48..3150ccd86d8ce 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -678,4 +678,12 @@ public abstract class ActivityTaskManagerInternal { /** Called when the device is waking up */ public abstract void notifyWakingUp(); + + /** + * Registers a callback which can intercept activity starts. + * @throws IllegalArgumentException if duplicate ids are provided + */ + public abstract void registerActivityStartInterceptor( + @ActivityInterceptorCallback.OrderedId int id, + ActivityInterceptorCallback callback); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 89f7d92b2087b..7f3f1f86103fb 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -92,6 +92,8 @@ import static com.android.server.am.ActivityManagerServiceDumpProcessesProto.Scr import static com.android.server.am.ActivityManagerServiceDumpProcessesProto.ScreenCompatPackage.PACKAGE; import static com.android.server.am.EventLogTags.writeBootProgressEnableScreen; import static com.android.server.am.EventLogTags.writeConfigurationChanged; +import static com.android.server.wm.ActivityInterceptorCallback.FIRST_ORDERED_ID; +import static com.android.server.wm.ActivityInterceptorCallback.LAST_ORDERED_ID; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ALL; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_ROOT_TASK; import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_SWITCH; @@ -458,6 +460,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { /** The controller for all operations related to locktask. */ private LockTaskController mLockTaskController; private ActivityStartController mActivityStartController; + private SparseArray mActivityInterceptorCallbacks = + new SparseArray<>(); PackageConfigPersister mPackageConfigPersister; boolean mSuppressResizeConfigChanges; @@ -1115,6 +1119,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return mBackgroundActivityStartCallback; } + SparseArray getActivityInterceptorCallbacks() { + return mActivityInterceptorCallbacks; + } + private void start() { LocalServices.addService(ActivityTaskManagerInternal.class, mInternal); } @@ -4738,7 +4746,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mContext.getText(R.string.heavy_weight_notification_detail)) .setContentIntent(PendingIntent.getActivityAsUser(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT - | PendingIntent.FLAG_IMMUTABLE, null, + | PendingIntent.FLAG_IMMUTABLE, null, new UserHandle(userId))) .build(); try { @@ -6583,5 +6591,22 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { getTransitionController().requestTransitionIfNeeded(TRANSIT_WAKE, 0 /* flags */, null /* trigger */, mRootWindowContainer.getDefaultDisplay()); } + + @Override + public void registerActivityStartInterceptor( + @ActivityInterceptorCallback.OrderedId int id, + ActivityInterceptorCallback callback) { + synchronized (mGlobalLock) { + if (mActivityInterceptorCallbacks.contains(id)) { + throw new IllegalArgumentException("Duplicate id provided: " + id); + } + if (id > LAST_ORDERED_ID || id < FIRST_ORDERED_ID) { + throw new IllegalArgumentException( + "Provided id " + id + " is not in range of valid ids [" + + FIRST_ORDERED_ID + "," + LAST_ORDERED_ID + "]"); + } + mActivityInterceptorCallbacks.put(id, callback); + } + } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java index a3ad09a50b8cf..c103bc6fb9a13 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java @@ -28,6 +28,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; +import android.annotation.Nullable; import android.app.ActivityManagerInternal; import android.app.KeyguardManager; import android.app.admin.DevicePolicyManagerInternal; @@ -42,6 +43,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.testing.DexmakerShareClassLoaderRule; +import android.util.SparseArray; import androidx.test.filters.SmallTest; @@ -64,7 +66,7 @@ import org.mockito.MockitoAnnotations; * Unit tests for {@link ActivityStartInterceptorTest}. * * Build/Install/Run: - * atest WmTests:ActivityStartInterceptorTest + * atest WmTests:ActivityStartInterceptorTest */ @SmallTest @Presubmit @@ -114,6 +116,9 @@ public class ActivityStartInterceptorTest { private ActivityStartInterceptor mInterceptor; private ActivityInfo mAInfo = new ActivityInfo(); + private SparseArray mActivityInterceptorCallbacks = + new SparseArray<>(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -157,6 +162,9 @@ public class ActivityStartInterceptorTest { TEST_USER_ID, TEST_PACKAGE_NAME, LOCK_TASK_LAUNCH_MODE_DEFAULT)) .thenReturn(true); + // Mock the activity start callbacks + when(mService.getActivityInterceptorCallbacks()).thenReturn(mActivityInterceptorCallbacks); + // Initialise activity info mAInfo.applicationInfo = new ApplicationInfo(); mAInfo.packageName = mAInfo.applicationInfo.packageName = TEST_PACKAGE_NAME; @@ -285,4 +293,38 @@ public class ActivityStartInterceptorTest { // THEN calling intercept returns false assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null)); } + + public void addMockInterceptorCallback(@Nullable Intent intent) { + int size = mActivityInterceptorCallbacks.size(); + mActivityInterceptorCallbacks.put(size, new ActivityInterceptorCallback() { + @Override + public Intent intercept(ActivityInterceptorInfo info) { + return intent; + } + }); + } + + @Test + public void testInterceptionCallback_singleCallback() { + addMockInterceptorCallback(new Intent("android.test.foo")); + + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null)); + assertEquals("android.test.foo", mInterceptor.mIntent.getAction()); + } + + @Test + public void testInterceptionCallback_singleCallbackReturnsNull() { + addMockInterceptorCallback(null); + + assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null)); + } + + @Test + public void testInterceptionCallback_fallbackToSecondCallback() { + addMockInterceptorCallback(null); + addMockInterceptorCallback(new Intent("android.test.second")); + + assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null)); + assertEquals("android.test.second", mInterceptor.mIntent.getAction()); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java index 5de4fcbb5aa76..5d1a06825a754 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java @@ -26,6 +26,8 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.wm.ActivityInterceptorCallback.FIRST_ORDERED_ID; +import static com.android.server.wm.ActivityInterceptorCallback.LAST_ORDERED_ID; import static com.android.server.wm.ActivityRecord.State.PAUSED; import static com.android.server.wm.ActivityRecord.State.PAUSING; import static com.android.server.wm.ActivityRecord.State.RESUMED; @@ -42,12 +44,14 @@ import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; +import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityManager; import android.app.IApplicationThread; import android.app.PictureInPictureParams; import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.EnterPipRequestedItem; +import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.res.Configuration; @@ -846,7 +850,64 @@ public class ActivityTaskManagerServiceTests extends WindowTestsBase { return wpc; } + @Test(expected = IllegalArgumentException.class) + public void testRegisterActivityStartInterceptor_IndexTooSmall() { + mAtm.mInternal.registerActivityStartInterceptor(FIRST_ORDERED_ID - 1, + new ActivityInterceptorCallback() { + @Nullable + @Override + public Intent intercept(ActivityInterceptorInfo info) { + return null; + } + }); + } + + @Test(expected = IllegalArgumentException.class) + public void testRegisterActivityStartInterceptor_IndexTooLarge() { + mAtm.mInternal.registerActivityStartInterceptor(LAST_ORDERED_ID + 1, + new ActivityInterceptorCallback() { + @Nullable + @Override + public Intent intercept(ActivityInterceptorInfo info) { + return null; + } + }); + } + + @Test(expected = IllegalArgumentException.class) + public void testRegisterActivityStartInterceptor_DuplicateId() { + mAtm.mInternal.registerActivityStartInterceptor(FIRST_ORDERED_ID, + new ActivityInterceptorCallback() { + @Nullable + @Override + public Intent intercept(ActivityInterceptorInfo info) { + return null; + } + }); + mAtm.mInternal.registerActivityStartInterceptor(FIRST_ORDERED_ID, + new ActivityInterceptorCallback() { + @Nullable + @Override + public Intent intercept(ActivityInterceptorInfo info) { + return null; + } + }); + } + + @Test + public void testRegisterActivityStartInterceptor() { + assertEquals(0, mAtm.getActivityInterceptorCallbacks().size()); + + mAtm.mInternal.registerActivityStartInterceptor(FIRST_ORDERED_ID, + new ActivityInterceptorCallback() { + @Nullable + @Override + public Intent intercept(ActivityInterceptorInfo info) { + return null; + } + }); + + assertEquals(1, mAtm.getActivityInterceptorCallbacks().size()); + assertTrue(mAtm.getActivityInterceptorCallbacks().contains(FIRST_ORDERED_ID)); + } } - - -