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..cb66a39376a2f --- /dev/null +++ b/services/core/java/com/android/server/wm/ActivityInterceptorCallbackRegistry.java @@ -0,0 +1,120 @@ +/* + * 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.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 + */ +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/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 c2d4bfdab4e5f..9c41a2bca0eca 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -6813,6 +6813,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 " @@ -6825,6 +6829,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/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); + } }