diff --git a/services/api/current.txt b/services/api/current.txt index da5b1fcaae99f..b5798d56f0c92 100644 --- a/services/api/current.txt +++ b/services/api/current.txt @@ -169,3 +169,52 @@ package com.android.server.wifi { } +package com.android.server.wm { + + public interface ActivityInterceptorCallback { + method public default void onActivityLaunched(@NonNull android.app.TaskInfo, @NonNull android.content.pm.ActivityInfo, @NonNull com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptorInfo); + method @Nullable public com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptResult onInterceptActivityLaunch(@NonNull com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptorInfo); + field public static final int MAINLINE_SDK_SANDBOX_ORDER_ID = 1001; // 0x3e9 + } + + public static final class ActivityInterceptorCallback.ActivityInterceptResult { + ctor public ActivityInterceptorCallback.ActivityInterceptResult(@NonNull android.content.Intent, @NonNull android.app.ActivityOptions, boolean); + method @NonNull public android.app.ActivityOptions getActivityOptions(); + method @NonNull public android.content.Intent getIntent(); + method public boolean isActivityResolved(); + } + + public static final class ActivityInterceptorCallback.ActivityInterceptorInfo { + method @NonNull public android.content.pm.ActivityInfo getActivityInfo(); + method @Nullable public String getCallingFeatureId(); + method @Nullable public String getCallingPackage(); + method public int getCallingPid(); + method public int getCallingUid(); + method @Nullable public android.app.ActivityOptions getCheckedOptions(); + method @Nullable public Runnable getClearOptionsAnimationRunnable(); + method @NonNull public android.content.Intent getIntent(); + method public int getRealCallingPid(); + method public int getRealCallingUid(); + method @NonNull public android.content.pm.ResolveInfo getResolveInfo(); + method @Nullable public String getResolvedType(); + method public int getUserId(); + } + + public static final class ActivityInterceptorCallback.ActivityInterceptorInfo.Builder { + ctor public ActivityInterceptorCallback.ActivityInterceptorInfo.Builder(int, int, int, int, int, @NonNull android.content.Intent, @NonNull android.content.pm.ResolveInfo, @NonNull android.content.pm.ActivityInfo); + method @NonNull public com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptorInfo build(); + method @NonNull public com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptorInfo.Builder setCallingFeatureId(@NonNull String); + method @NonNull public com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptorInfo.Builder setCallingPackage(@NonNull String); + method @NonNull public com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptorInfo.Builder setCheckedOptions(@NonNull android.app.ActivityOptions); + method @NonNull public com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptorInfo.Builder setClearOptionsAnimationRunnable(@NonNull Runnable); + method @NonNull public com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptorInfo.Builder setResolvedType(@NonNull String); + } + + public class ActivityInterceptorCallbackRegistry { + method @NonNull public static com.android.server.wm.ActivityInterceptorCallbackRegistry getInstance(); + method public void registerActivityInterceptorCallback(int, @NonNull com.android.server.wm.ActivityInterceptorCallback); + method public void unregisterActivityInterceptorCallback(int); + } + +} + diff --git a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java index c593fa3d91b97..ff1d44293dca8 100644 --- a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java +++ b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java @@ -19,6 +19,7 @@ package com.android.server.wm; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.app.ActivityOptions; import android.app.TaskInfo; import android.content.Intent; @@ -33,6 +34,7 @@ import java.lang.annotation.RetentionPolicy; * be called with the WindowManagerGlobalLock held. * @hide */ +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) public interface ActivityInterceptorCallback { /** * Called to allow intercepting activity launching based on the provided launch parameters and @@ -165,6 +167,7 @@ public interface ActivityInterceptorCallback { * Data class for storing the various arguments needed for activity interception. * @hide */ + @SystemApi(client = SystemApi.Client.SYSTEM_SERVER) final class ActivityInterceptorInfo { private final int mCallingUid; private final int mCallingPid; @@ -389,6 +392,7 @@ public interface ActivityInterceptorCallback { * Data class for storing the intercept result. * @hide */ + @SystemApi(client = SystemApi.Client.SYSTEM_SERVER) final class ActivityInterceptResult { @NonNull private final Intent mIntent; @@ -396,15 +400,35 @@ public interface ActivityInterceptorCallback { @NonNull private final ActivityOptions mActivityOptions; - /** Generates the result of intercepting launching the {@link android.app.Activity} + private final boolean mActivityResolved; + + /** + * This constructor should only be used if both {@link ActivityInfo} and {@link ResolveInfo} + * did not get resolved while interception. + * @hide + */ + public ActivityInterceptResult(@NonNull Intent intent, + @NonNull ActivityOptions activityOptions) { + this(intent, activityOptions, false /* activityResolved */); + } + + /** + * Generates the result of intercepting launching the {@link android.app.Activity} + * + *

Interceptor should return non-{@code null} result when {@link + * #onInterceptActivityLaunch(ActivityInterceptorInfo)} gets called as an indicator that + * interception has happened. * * @param intent is the modified {@link Intent} after interception. * @param activityOptions holds the {@link ActivityOptions} after interception. + * @param activityResolved should be {@code true} only if {@link ActivityInfo} or {@link + * ResolveInfo} gets resolved, otherwise should be {@code false}. */ - public ActivityInterceptResult( - @NonNull Intent intent, @NonNull ActivityOptions activityOptions) { + public ActivityInterceptResult(@NonNull Intent intent, + @NonNull ActivityOptions activityOptions, boolean activityResolved) { this.mIntent = intent; this.mActivityOptions = activityOptions; + this.mActivityResolved = activityResolved; } /** Returns the intercepted {@link Intent} */ @@ -419,5 +443,12 @@ public interface ActivityInterceptorCallback { public ActivityOptions getActivityOptions() { return mActivityOptions; } + + /** + * Returns if the {@link ActivityInfo} or {@link ResolveInfo} gets resolved. + */ + public boolean isActivityResolved() { + return mActivityResolved; + } } } diff --git a/services/core/java/com/android/server/wm/ActivityInterceptorCallbackRegistry.java b/services/core/java/com/android/server/wm/ActivityInterceptorCallbackRegistry.java new file mode 100644 index 0000000000000..bb9462f5b0341 --- /dev/null +++ b/services/core/java/com/android/server/wm/ActivityInterceptorCallbackRegistry.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2022 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.NonNull; +import android.annotation.SystemApi; +import android.os.Binder; +import android.os.Process; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.LocalServices; + +/** + * This class should be used by system services which are part of mainline modules to register + * {@link ActivityInterceptorCallback}. For other system services, this function should be used + * instead {@link ActivityTaskManagerInternal#registerActivityStartInterceptor( + * int, ActivityInterceptorCallback)}. + * @hide + */ +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) +public class ActivityInterceptorCallbackRegistry { + + private static final ActivityInterceptorCallbackRegistry sInstance = + new ActivityInterceptorCallbackRegistry(); + + private ActivityInterceptorCallbackRegistry() {} + + /** Returns an already initialised singleton instance of this class. */ + @NonNull + public static ActivityInterceptorCallbackRegistry getInstance() { + return sInstance; + } + + /** + * Registers a callback which can intercept activity launching flow. + * + *

Only system services which are part of mainline modules should call this function. + * + *

To avoid Activity launch delays, the callbacks must execute quickly and avoid acquiring + * other system process locks. + * + * @param mainlineOrderId has to be one of the following [{@link + * ActivityInterceptorCallback#MAINLINE_FIRST_ORDERED_ID}]. + * @param callback the {@link ActivityInterceptorCallback} to register. + * @throws IllegalArgumentException if duplicate ids are provided, the provided id is not the + * mainline module range or the provided {@code callback} is null. + */ + // ExecutorRegistration is suppressed as the callback is called synchronously in the system + // server. + @SuppressWarnings("ExecutorRegistration") + public void registerActivityInterceptorCallback( + @ActivityInterceptorCallback.OrderedId int mainlineOrderId, + @NonNull ActivityInterceptorCallback callback) { + if (getCallingUid() != Process.SYSTEM_UID) { + throw new SecurityException("Only system server can register " + + "ActivityInterceptorCallback"); + } + if (!ActivityInterceptorCallback.isValidMainlineOrderId(mainlineOrderId)) { + throw new IllegalArgumentException("id is not in the mainline modules range, please use" + + "ActivityTaskManagerInternal.registerActivityStartInterceptor(OrderedId, " + + "ActivityInterceptorCallback) instead."); + } + if (callback == null) { + throw new IllegalArgumentException("The passed ActivityInterceptorCallback can not be " + + "null"); + } + ActivityTaskManagerInternal activityTaskManagerInternal = + LocalServices.getService(ActivityTaskManagerInternal.class); + activityTaskManagerInternal.registerActivityStartInterceptor(mainlineOrderId, callback); + } + + /** + * Unregisters an already registered {@link ActivityInterceptorCallback}. + * + * @param mainlineOrderId the order id of the {@link ActivityInterceptorCallback} should be + * unregistered, this callback should be registered before by calling + * {@link #registerActivityInterceptorCallback(int, + * ActivityInterceptorCallback)} using the same order id. + * @throws IllegalArgumentException if the provided id is not the mainline module range or is + * not registered + */ + public void unregisterActivityInterceptorCallback( + @ActivityInterceptorCallback.OrderedId int mainlineOrderId) { + if (getCallingUid() != Process.SYSTEM_UID) { + throw new SecurityException("Only system server can register " + + "ActivityInterceptorCallback"); + } + if (!ActivityInterceptorCallback.isValidMainlineOrderId(mainlineOrderId)) { + throw new IllegalArgumentException("id is not in the mainline modules range, please use" + + "ActivityTaskManagerInternal.unregisterActivityStartInterceptor(OrderedId) " + + "instead."); + } + ActivityTaskManagerInternal activityTaskManagerInternal = + LocalServices.getService(ActivityTaskManagerInternal.class); + activityTaskManagerInternal.unregisterActivityStartInterceptor(mainlineOrderId); + } + + /** + * This hidden function is for unit tests as a way to behave like as if they are called from + * system server process uid by mocking it and returning {@link Process#SYSTEM_UID}. + * Do not make this {@code public} to apps as apps should not have a way to change the uid. + * @hide + */ + @VisibleForTesting + int getCallingUid() { + return Binder.getCallingUid(); + } +} diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java index 84100a7ab02d6..d4d0256d52566 100644 --- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java @@ -244,6 +244,9 @@ class ActivityStartInterceptor { mActivityOptions = interceptResult.getActivityOptions(); mCallingPid = mRealCallingPid; mCallingUid = mRealCallingUid; + if (interceptResult.isActivityResolved()) { + return true; + } mRInfo = mSupervisor.resolveIntent(mIntent, null, mUserId, 0, mRealCallingUid); mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, null /*profilerInfo*/); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index ec486437734b5..c63bd52900e8d 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -680,12 +680,23 @@ public abstract class ActivityTaskManagerInternal { /** * Registers a callback which can intercept activity starts. - * @throws IllegalArgumentException if duplicate ids are provided + * @throws IllegalArgumentException if duplicate ids are provided or the provided {@code + * callback} is null + * @see ActivityInterceptorCallbackRegistry + * #registerActivityInterceptorCallback(int, ActivityInterceptorCallback) */ public abstract void registerActivityStartInterceptor( @ActivityInterceptorCallback.OrderedId int id, ActivityInterceptorCallback callback); + /** + * Unregisters an {@link ActivityInterceptorCallback}. + * @throws IllegalArgumentException if id is not registered + * @see ActivityInterceptorCallbackRegistry#unregisterActivityInterceptorCallback(int) + */ + public abstract void unregisterActivityStartInterceptor( + @ActivityInterceptorCallback.OrderedId int id); + /** Get the most recent task excluding the first running task (the one on the front most). */ public abstract ActivityManager.RecentTaskInfo getMostRecentTaskFromBackground(); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 08c85748cb675..c7aeecba11532 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -6801,6 +6801,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { if (mActivityInterceptorCallbacks.contains(id)) { throw new IllegalArgumentException("Duplicate id provided: " + id); } + if (callback == null) { + throw new IllegalArgumentException("The passed ActivityInterceptorCallback " + + "can not be null"); + } if (!ActivityInterceptorCallback.isValidOrderId(id)) { throw new IllegalArgumentException( "Provided id " + id + " is not in range of valid ids for system " @@ -6813,6 +6817,18 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } } + @Override + public void unregisterActivityStartInterceptor( + @ActivityInterceptorCallback.OrderedId int id) { + synchronized (mGlobalLock) { + if (!mActivityInterceptorCallbacks.contains(id)) { + throw new IllegalArgumentException( + "ActivityInterceptorCallback with id (" + id + ") is not registered"); + } + mActivityInterceptorCallbacks.remove(id); + } + } + @Override public ActivityManager.RecentTaskInfo getMostRecentTaskFromBackground() { List runningTaskInfoList = getTasks(1); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityInterceptorCallbackRegistryTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityInterceptorCallbackRegistryTest.java new file mode 100644 index 0000000000000..3646f1a8f5c8a --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityInterceptorCallbackRegistryTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2022 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 static com.android.server.wm.ActivityInterceptorCallback.MAINLINE_FIRST_ORDERED_ID; +import static com.android.server.wm.ActivityInterceptorCallback.MAINLINE_LAST_ORDERED_ID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.spy; + +import android.os.Process; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.MediumTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +/** + * Tests for the {@link ActivityInterceptorCallbackRegistry} class. + */ +@Presubmit +@MediumTest +@RunWith(WindowTestRunner.class) +public final class ActivityInterceptorCallbackRegistryTest extends WindowTestsBase { + + private ActivityInterceptorCallbackRegistry mRegistry; + + @Before + public void setUp() { + mRegistry = spy(ActivityInterceptorCallbackRegistry.getInstance()); + Mockito.doReturn(Process.SYSTEM_UID).when(mRegistry).getCallingUid(); + } + + @Test + public void registerActivityInterceptorCallbackFailIfNotSystemId() { + // default registry with test app uid + ActivityInterceptorCallbackRegistry registry = spy( + ActivityInterceptorCallbackRegistry.getInstance()); + assertThrows( + SecurityException.class, + () -> registry.registerActivityInterceptorCallback(MAINLINE_LAST_ORDERED_ID + 1, + info -> null) + ); + } + + @Test + public void registerActivityInterceptorCallbackFailIfIdNotInRange() { + assertThrows( + IllegalArgumentException.class, + () -> mRegistry.registerActivityInterceptorCallback(MAINLINE_LAST_ORDERED_ID + 1, + info -> null) + ); + + assertThrows( + IllegalArgumentException.class, + () -> mRegistry.registerActivityInterceptorCallback(MAINLINE_FIRST_ORDERED_ID - 1, + info -> null) + ); + } + + @Test + public void registerActivityInterceptorCallbackFailIfCallbackIsNull() { + assertThrows( + IllegalArgumentException.class, + () -> mRegistry.registerActivityInterceptorCallback(MAINLINE_FIRST_ORDERED_ID, + null) + ); + } + + @Test + public void registerActivityInterceptorCallbackSuccessfully() { + int size = mAtm.getActivityInterceptorCallbacks().size(); + int orderId = MAINLINE_FIRST_ORDERED_ID; + mRegistry.registerActivityInterceptorCallback(orderId, + info -> null); + assertEquals(size + 1, mAtm.getActivityInterceptorCallbacks().size()); + assertTrue(mAtm.getActivityInterceptorCallbacks().contains(orderId)); + } + + @Test + public void unregisterActivityInterceptorCallbackFailIfNotSystemId() { + // default registry with test app uid + ActivityInterceptorCallbackRegistry registry = spy( + ActivityInterceptorCallbackRegistry.getInstance()); + assertThrows( + SecurityException.class, + () -> registry.unregisterActivityInterceptorCallback(MAINLINE_LAST_ORDERED_ID + 1) + ); + } + + @Test + public void unRegisterActivityInterceptorCallbackFailIfIdNotInRange() { + assertThrows( + IllegalArgumentException.class, + () -> mRegistry.unregisterActivityInterceptorCallback( + MAINLINE_LAST_ORDERED_ID + 1)); + } + + @Test + public void unregisterActivityInterceptorCallbackFailIfNotRegistered() { + assertThrows( + IllegalArgumentException.class, + () -> mRegistry.unregisterActivityInterceptorCallback(MAINLINE_FIRST_ORDERED_ID) + ); + } + + @Test + public void unregisterActivityInterceptorCallbackSuccessfully() { + int size = mAtm.getActivityInterceptorCallbacks().size(); + int orderId = MAINLINE_FIRST_ORDERED_ID; + mRegistry.registerActivityInterceptorCallback(orderId, + info -> null); + assertEquals(size + 1, mAtm.getActivityInterceptorCallbacks().size()); + assertTrue(mAtm.getActivityInterceptorCallbacks().contains(orderId)); + + mRegistry.unregisterActivityInterceptorCallback(orderId); + assertEquals(size, mAtm.getActivityInterceptorCallbacks().size()); + assertFalse(mAtm.getActivityInterceptorCallbacks().contains(orderId)); + + } +} 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 b0461c2276dde..7d16fb2e02554 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java @@ -30,6 +30,7 @@ import static com.android.server.wm.ActivityInterceptorCallback.MAINLINE_SDK_SAN import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; @@ -358,6 +359,12 @@ public class ActivityStartInterceptorTest { public void addMockInterceptorCallback( @Nullable Intent intent, @Nullable ActivityOptions activityOptions) { + addMockInterceptorCallback(intent, activityOptions, false); + } + + public void addMockInterceptorCallback( + @Nullable Intent intent, @Nullable ActivityOptions activityOptions, + boolean skipResolving) { int size = mActivityInterceptorCallbacks.size(); mActivityInterceptorCallbacks.put(size, new ActivityInterceptorCallback() { @Override @@ -368,7 +375,8 @@ public class ActivityStartInterceptorTest { } return new ActivityInterceptResult( intent != null ? intent : info.getIntent(), - activityOptions != null ? activityOptions : info.getCheckedOptions()); + activityOptions != null ? activityOptions : info.getCheckedOptions(), + skipResolving); } }); } @@ -400,6 +408,30 @@ public class ActivityStartInterceptorTest { assertEquals("android.test.second", mInterceptor.mIntent.getAction()); } + @Test + public void testInterceptionCallback_skipResolving() { + addMockInterceptorCallback( + new Intent("android.test.foo"), + ActivityOptions.makeBasic().setLaunchDisplayId(3), true); + ActivityInfo aInfo = mAInfo; + assertTrue(mInterceptor.intercept(null, null, aInfo, null, null, null, 0, 0, null)); + assertEquals("android.test.foo", mInterceptor.mIntent.getAction()); + assertEquals(3, mInterceptor.mActivityOptions.getLaunchDisplayId()); + assertEquals(aInfo, mInterceptor.mAInfo); // mAInfo should not be resolved + } + + @Test + public void testInterceptionCallback_NoSkipResolving() throws InterruptedException { + addMockInterceptorCallback( + new Intent("android.test.foo"), + ActivityOptions.makeBasic().setLaunchDisplayId(3)); + ActivityInfo aInfo = mAInfo; + assertTrue(mInterceptor.intercept(null, null, aInfo, null, null, null, 0, 0, null)); + assertEquals("android.test.foo", mInterceptor.mIntent.getAction()); + assertEquals(3, mInterceptor.mActivityOptions.getLaunchDisplayId()); + assertNotEquals(aInfo, mInterceptor.mAInfo); // mAInfo should be resolved after intercept + } + @Test public void testActivityLaunchedCallback_singleCallback() { addMockInterceptorCallback(null, null); 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 693d32eba3e8a..3dcae91f5c89a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java @@ -1027,4 +1027,25 @@ public class ActivityTaskManagerServiceTests extends WindowTestsBase { public void testSystemAndMainlineOrderIdsNotOverlapping() { assertTrue(MAINLINE_FIRST_ORDERED_ID - SYSTEM_LAST_ORDERED_ID > 1); } + + @Test + public void testUnregisterActivityStartInterceptor() { + int size = mAtm.getActivityInterceptorCallbacks().size(); + int orderId = SYSTEM_FIRST_ORDERED_ID; + + mAtm.mInternal.registerActivityStartInterceptor(orderId, + (ActivityInterceptorCallback) info -> null); + assertEquals(size + 1, mAtm.getActivityInterceptorCallbacks().size()); + assertTrue(mAtm.getActivityInterceptorCallbacks().contains(orderId)); + + mAtm.mInternal.unregisterActivityStartInterceptor(orderId); + assertEquals(size, mAtm.getActivityInterceptorCallbacks().size()); + assertFalse(mAtm.getActivityInterceptorCallbacks().contains(orderId)); + } + + @Test(expected = IllegalArgumentException.class) + public void testUnregisterActivityStartInterceptor_IdNotExist() { + assertEquals(0, mAtm.getActivityInterceptorCallbacks().size()); + mAtm.mInternal.unregisterActivityStartInterceptor(SYSTEM_FIRST_ORDERED_ID); + } }