SystemActionPerformer implementation for new API.

Implmentation details of Accessibility framework SystemActionPerformer.
Will be accessed by AccessibilityManagerService to provide API
implementation support for system action registration and system action
access.

Bug: 136286274
Test: atest SystemActionPerformerTest

Change-Id: Ic0adbb6fc21d262fb2949f616ca28940fd6f1f53
diff --git a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
index 19ac0d3..1754926 100644
--- a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
+++ b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
@@ -17,6 +17,8 @@
 package com.android.server.accessibility;
 
 import android.accessibilityservice.AccessibilityService;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
 import android.app.StatusBarManager;
 import android.content.Context;
 import android.hardware.input.InputManager;
@@ -25,81 +27,272 @@
 import android.os.Looper;
 import android.os.PowerManager;
 import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.util.Slog;
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ScreenshotHelper;
 import com.android.server.LocalServices;
 import com.android.server.statusbar.StatusBarManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
 import java.util.function.Supplier;
 
 /**
- * Handle the back-end of AccessibilityService#performGlobalAction
+ * Handle the back-end of system AccessibilityAction.
+ *
+ * This class should support three use cases with combined usage of new API and legacy API:
+ *
+ * Use case 1: SystemUI doesn't use the new system action registration API. Accessibility
+ *             service doesn't use the new system action API to obtain action list. Accessibility
+ *             service uses legacy global action id to perform predefined system actions.
+ * Use case 2: SystemUI uses the new system action registration API to register available system
+ *             actions. Accessibility service doesn't use the new system action API to obtain action
+ *             list. Accessibility service uses legacy global action id to trigger the system
+ *             actions registered by SystemUI.
+ * Use case 3: SystemUI doesn't use the new system action registration API.Accessibility service
+ *             obtains the available system actions using new AccessibilityService API and trigger
+ *             the predefined system actions.
  */
 public class SystemActionPerformer {
+    private static final String TAG = "SystemActionPerformer";
+
+    interface SystemActionsChangedListener {
+        void onSystemActionsChanged();
+    }
+    private final SystemActionsChangedListener mListener;
+
+    private final Object mSystemActionLock = new Object();
+    // Resource id based ActionId -> RemoteAction
+    @GuardedBy("mSystemActionLock")
+    private final Map<Integer, RemoteAction> mRegisteredSystemActions = new ArrayMap<>();
+
+    // Legacy system actions.
+    private final AccessibilityAction mLegacyHomeAction;
+    private final AccessibilityAction mLegacyBackAction;
+    private final AccessibilityAction mLegacyRecentsAction;
+    private final AccessibilityAction mLegacyNotificationsAction;
+    private final AccessibilityAction mLegacyQuickSettingsAction;
+    private final AccessibilityAction mLegacyPowerDialogAction;
+    private final AccessibilityAction mLegacyToggleSplitScreenAction;
+    private final AccessibilityAction mLegacyLockScreenAction;
+    private final AccessibilityAction mLegacyTakeScreenshotAction;
+
     private final WindowManagerInternal mWindowManagerService;
     private final Context mContext;
     private Supplier<ScreenshotHelper> mScreenshotHelperSupplier;
 
-    public SystemActionPerformer(Context context, WindowManagerInternal windowManagerInternal) {
-        mContext = context;
-        mWindowManagerService = windowManagerInternal;
-        mScreenshotHelperSupplier = null;
+    public SystemActionPerformer(
+            Context context,
+            WindowManagerInternal windowManagerInternal) {
+      this(context, windowManagerInternal, null, null);
     }
 
     // Used to mock ScreenshotHelper
     @VisibleForTesting
-    public SystemActionPerformer(Context context, WindowManagerInternal windowManagerInternal,
+    public SystemActionPerformer(
+            Context context,
+            WindowManagerInternal windowManagerInternal,
             Supplier<ScreenshotHelper> screenshotHelperSupplier) {
-        this(context, windowManagerInternal);
+        this(context, windowManagerInternal, screenshotHelperSupplier, null);
+    }
+
+    public SystemActionPerformer(
+            Context context,
+            WindowManagerInternal windowManagerInternal,
+            Supplier<ScreenshotHelper> screenshotHelperSupplier,
+            SystemActionsChangedListener listener) {
+        mContext = context;
+        mWindowManagerService = windowManagerInternal;
+        mListener = listener;
         mScreenshotHelperSupplier = screenshotHelperSupplier;
+
+        mLegacyHomeAction = new AccessibilityAction(
+                AccessibilityService.GLOBAL_ACTION_HOME,
+                mContext.getResources().getString(
+                        R.string.accessibility_system_action_home_label));
+        mLegacyBackAction = new AccessibilityAction(
+                AccessibilityService.GLOBAL_ACTION_BACK,
+                mContext.getResources().getString(
+                        R.string.accessibility_system_action_back_label));
+        mLegacyRecentsAction = new AccessibilityAction(
+                AccessibilityService.GLOBAL_ACTION_RECENTS,
+                mContext.getResources().getString(
+                        R.string.accessibility_system_action_recents_label));
+        mLegacyNotificationsAction = new AccessibilityAction(
+                AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS,
+                mContext.getResources().getString(
+                        R.string.accessibility_system_action_notifications_label));
+        mLegacyQuickSettingsAction = new AccessibilityAction(
+                AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS,
+                mContext.getResources().getString(
+                        R.string.accessibility_system_action_quick_settings_label));
+        mLegacyPowerDialogAction = new AccessibilityAction(
+                AccessibilityService.GLOBAL_ACTION_POWER_DIALOG,
+                mContext.getResources().getString(
+                        R.string.accessibility_system_action_power_dialog_label));
+        mLegacyToggleSplitScreenAction = new AccessibilityAction(
+                AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN,
+                mContext.getResources().getString(
+                        R.string.accessibility_system_action_toggle_split_screen_label));
+        mLegacyLockScreenAction = new AccessibilityAction(
+                AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN,
+                mContext.getResources().getString(
+                        R.string.accessibility_system_action_lock_screen_label));
+        mLegacyTakeScreenshotAction = new AccessibilityAction(
+                AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT,
+                mContext.getResources().getString(
+                        R.string.accessibility_system_action_screenshot_label));
     }
 
     /**
-     * Performe the system action matching the given action id.
+     * This method is called to register a system action. If a system action is already registered
+     * with the given id, the existing system action will be overwritten.
      */
-    public boolean performSystemAction(int action) {
+    void registerSystemAction(int id, RemoteAction action) {
+        synchronized (mSystemActionLock) {
+            mRegisteredSystemActions.put(id, action);
+        }
+        if (mListener != null) {
+            mListener.onSystemActionsChanged();
+        }
+    }
+
+    /**
+     * This method is called to unregister a system action previously registered through
+     * registerSystemAction.
+     */
+    void unregisterSystemAction(int id) {
+        synchronized (mSystemActionLock) {
+            mRegisteredSystemActions.remove(id);
+        }
+        if (mListener != null) {
+            mListener.onSystemActionsChanged();
+        }
+    }
+
+    /**
+     * This method returns the list of available system actions.
+     */
+    List<AccessibilityAction> getSystemActions() {
+        List<AccessibilityAction> systemActions = new ArrayList<>();
+        synchronized (mSystemActionLock) {
+            for (Map.Entry<Integer, RemoteAction> entry : mRegisteredSystemActions.entrySet()) {
+                AccessibilityAction systemAction = new AccessibilityAction(
+                        entry.getKey(),
+                        entry.getValue().getTitle());
+                systemActions.add(systemAction);
+            }
+
+            // add AccessibilitySystemAction entry for legacy system actions if not overwritten
+            addLegacySystemActions(systemActions);
+        }
+        return systemActions;
+    }
+
+    private void addLegacySystemActions(List<AccessibilityAction> systemActions) {
+        if (!mRegisteredSystemActions.containsKey(AccessibilityService.GLOBAL_ACTION_BACK)) {
+            systemActions.add(mLegacyBackAction);
+        }
+        if (!mRegisteredSystemActions.containsKey(AccessibilityService.GLOBAL_ACTION_HOME)) {
+            systemActions.add(mLegacyHomeAction);
+        }
+        if (!mRegisteredSystemActions.containsKey(AccessibilityService.GLOBAL_ACTION_RECENTS)) {
+            systemActions.add(mLegacyRecentsAction);
+        }
+        if (!mRegisteredSystemActions.containsKey(
+                AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS)) {
+            systemActions.add(mLegacyNotificationsAction);
+        }
+        if (!mRegisteredSystemActions.containsKey(
+                AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS)) {
+            systemActions.add(mLegacyQuickSettingsAction);
+        }
+        if (!mRegisteredSystemActions.containsKey(
+                AccessibilityService.GLOBAL_ACTION_POWER_DIALOG)) {
+            systemActions.add(mLegacyPowerDialogAction);
+        }
+        if (!mRegisteredSystemActions.containsKey(
+                AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN)) {
+            systemActions.add(mLegacyToggleSplitScreenAction);
+        }
+        if (!mRegisteredSystemActions.containsKey(
+                AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN)) {
+            systemActions.add(mLegacyLockScreenAction);
+        }
+        if (!mRegisteredSystemActions.containsKey(
+                AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT)) {
+            systemActions.add(mLegacyTakeScreenshotAction);
+        }
+    }
+
+    /**
+     * Trigger the registered action by the matching action id.
+     */
+    public boolean performSystemAction(int actionId) {
         final long identity = Binder.clearCallingIdentity();
         try {
-            switch (action) {
-                case AccessibilityService.GLOBAL_ACTION_BACK: {
-                    sendDownAndUpKeyEvents(KeyEvent.KEYCODE_BACK);
-                }
-                return true;
-                case AccessibilityService.GLOBAL_ACTION_HOME: {
-                    sendDownAndUpKeyEvents(KeyEvent.KEYCODE_HOME);
-                }
-                return true;
-                case AccessibilityService.GLOBAL_ACTION_RECENTS: {
-                    return openRecents();
-                }
-                case AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS: {
-                    expandNotifications();
-                }
-                return true;
-                case AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS: {
-                    expandQuickSettings();
-                }
-                return true;
-                case AccessibilityService.GLOBAL_ACTION_POWER_DIALOG: {
-                    showGlobalActions();
-                }
-                return true;
-                case AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN: {
-                    return toggleSplitScreen();
-                }
-                case AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN: {
-                    return lockScreen();
-                }
-                case AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT: {
-                    return takeScreenshot();
+            synchronized (mSystemActionLock) {
+                // If a system action is registered with the given actionId, call the corresponding
+                // RemoteAction.
+                RemoteAction registeredAction = mRegisteredSystemActions.get(actionId);
+                if (registeredAction != null) {
+                    try {
+                        registeredAction.getActionIntent().send();
+                        return true;
+                    } catch (PendingIntent.CanceledException ex) {
+                        Slog.e(TAG,
+                                "canceled PendingIntent for global action "
+                                        + registeredAction.getTitle(),
+                                ex);
+                    }
+                    return false;
                 }
             }
-            return false;
+
+            // No RemoteAction registered with the given actionId, try the default legacy system
+            // actions.
+            switch (actionId) {
+                case AccessibilityService.GLOBAL_ACTION_BACK: {
+                    sendDownAndUpKeyEvents(KeyEvent.KEYCODE_BACK);
+                    return true;
+                }
+                case AccessibilityService.GLOBAL_ACTION_HOME: {
+                    sendDownAndUpKeyEvents(KeyEvent.KEYCODE_HOME);
+                    return true;
+                }
+                case AccessibilityService.GLOBAL_ACTION_RECENTS:
+                    return openRecents();
+                case AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS: {
+                    expandNotifications();
+                    return true;
+                }
+                case AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS: {
+                    expandQuickSettings();
+                    return true;
+                }
+                case AccessibilityService.GLOBAL_ACTION_POWER_DIALOG: {
+                    showGlobalActions();
+                    return true;
+                }
+                case AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN:
+                    return toggleSplitScreen();
+                case AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN:
+                    return lockScreen();
+                case AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT:
+                    return takeScreenshot();
+                default:
+                    return false;
+            }
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
index 37f5b87..3352177 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
@@ -16,18 +16,39 @@
 
 package com.android.server.accessibility;
 
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.accessibilityservice.AccessibilityService;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.app.RemoteAction;
 import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.drawable.Icon;
 import android.os.Handler;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import androidx.test.InstrumentationRegistry;
 
 import com.android.internal.util.ScreenshotHelper;
+import com.android.server.LocalServices;
+import com.android.server.statusbar.StatusBarManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
 
 import org.junit.Before;
@@ -35,55 +56,290 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 /**
  * Tests for SystemActionPerformer
  */
 public class SystemActionPerformerTest {
-    SystemActionPerformer mSystemActionPerformer;
+    private static final int LATCH_TIMEOUT_MS = 500;
+    private static final int LEGACY_SYSTEM_ACTION_COUNT = 9;
+    private static final int NEW_ACTION_ID = 20;
+    private static final String LABEL_1 = "label1";
+    private static final String LABEL_2 = "label2";
+    private static final String INTENT_ACTION1 = "TESTACTION1";
+    private static final String INTENT_ACTION2 = "TESTACTION2";
+    private static final String DESCRIPTION1 = "description1";
+    private static final String DESCRIPTION2 = "description2";
+    private static final PendingIntent TEST_PENDING_INTENT_1 = PendingIntent.getBroadcast(
+            InstrumentationRegistry.getTargetContext(), 0, new Intent(INTENT_ACTION1), 0);
+    private static final RemoteAction NEW_TEST_ACTION_1 = new RemoteAction(
+            Icon.createWithContentUri("content://test"),
+            LABEL_1,
+            DESCRIPTION1,
+            TEST_PENDING_INTENT_1);
+    private static final PendingIntent TEST_PENDING_INTENT_2 = PendingIntent.getBroadcast(
+            InstrumentationRegistry.getTargetContext(), 0, new Intent(INTENT_ACTION2), 0);
+    private static final RemoteAction NEW_TEST_ACTION_2 = new RemoteAction(
+            Icon.createWithContentUri("content://test"),
+            LABEL_2,
+            DESCRIPTION2,
+            TEST_PENDING_INTENT_2);
 
-    @Mock Context mMockContext;
-    @Mock WindowManagerInternal mMockWindowManagerInternal;
-    @Mock StatusBarManager mMockStatusBarManager;
-    @Mock ScreenshotHelper mMockScreenshotHelper;
+    private static final AccessibilityAction NEW_ACCESSIBILITY_ACTION =
+            new AccessibilityAction(NEW_ACTION_ID, LABEL_1);
+    private static final AccessibilityAction LEGACY_NOTIFICATIONS_ACCESSIBILITY_ACTION =
+            new AccessibilityAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS, LABEL_1);
+    private static final AccessibilityAction LEGACY_HOME_ACCESSIBILITY_ACTION =
+            new AccessibilityAction(AccessibilityService.GLOBAL_ACTION_HOME, LABEL_2);
+
+    private SystemActionPerformer mSystemActionPerformer;
+
+    @Mock private Context mMockContext;
+    @Mock private StatusBarManagerInternal mMockStatusBarManagerInternal;
+    @Mock private WindowManagerInternal mMockWindowManagerInternal;
+    @Mock private StatusBarManager mMockStatusBarManager;
+    @Mock private ScreenshotHelper mMockScreenshotHelper;
+    @Mock private SystemActionPerformer.SystemActionsChangedListener mMockListener;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        LocalServices.removeServiceForTest(StatusBarManagerInternal.class);
+        LocalServices.addService(StatusBarManagerInternal.class, mMockStatusBarManagerInternal);
+    }
 
-        when(mMockContext.getSystemService(android.app.Service.STATUS_BAR_SERVICE))
-                .thenReturn(mMockStatusBarManager);
+    private void setupWithMockContext() {
+        doReturn(mMockStatusBarManager).when(
+                mMockContext).getSystemService(android.app.Service.STATUS_BAR_SERVICE);
+        doReturn(InstrumentationRegistry.getContext().getResources()).when(
+                mMockContext).getResources();
+        mSystemActionPerformer = new SystemActionPerformer(
+                mMockContext,
+                mMockWindowManagerInternal,
+                () -> mMockScreenshotHelper,
+                mMockListener);
+    }
 
-        mSystemActionPerformer =
-                new SystemActionPerformer(mMockContext, mMockWindowManagerInternal,
-                        () -> mMockScreenshotHelper);
+    private void setupWithRealContext() {
+        mSystemActionPerformer = new SystemActionPerformer(
+                InstrumentationRegistry.getContext(),
+                mMockWindowManagerInternal,
+                () -> mMockScreenshotHelper,
+                mMockListener);
+    }
+
+    // We need below two help functions because AccessbilityAction.equals function only compares
+    // action ids. To verify the test result here, we are also looking at action labels.
+    private void assertHasLegacyAccessibilityAction(
+            List<AccessibilityAction> actions, AccessibilityAction action) {
+        boolean foundAction = false;
+        for (AccessibilityAction a : actions) {
+            if ((a.getId() == action.getId()) && (a.getLabel().equals(action.getLabel()))) {
+                foundAction = true;
+                break;
+            }
+        }
+        assertTrue(foundAction);
+    }
+
+    private void assertHasNoLegacyAccessibilityAction(
+            List<AccessibilityAction> actions, AccessibilityAction action) {
+        boolean foundAction = false;
+        for (AccessibilityAction a : actions) {
+            if ((a.getId() == action.getId()) && (a.getLabel().equals(action.getLabel()))) {
+                foundAction = true;
+                break;
+            }
+        }
+        assertFalse(foundAction);
     }
 
     @Test
-    public void testNotifications_expandsNotificationPanel() {
+    public void testRegisterSystemAction_addedIntoAvailableSystemActions() {
+        setupWithRealContext();
+        // Before any new system action is registered, getSystemActions returns all legacy actions
+        List<AccessibilityAction> actions = mSystemActionPerformer.getSystemActions();
+        assertEquals(LEGACY_SYSTEM_ACTION_COUNT, actions.size());
+        // Register a new system action
+        mSystemActionPerformer.registerSystemAction(NEW_ACTION_ID, NEW_TEST_ACTION_1);
+        actions = mSystemActionPerformer.getSystemActions();
+        assertEquals(LEGACY_SYSTEM_ACTION_COUNT + 1, actions.size());
+        assertThat(actions, hasItem(NEW_ACCESSIBILITY_ACTION));
+    }
+
+    @Test
+    public void testRegisterSystemAction_overrideLegacyAction() {
+        setupWithRealContext();
+        // Before any new system action is registered, getSystemActions returns all legacy actions
+        List<AccessibilityAction> actions = mSystemActionPerformer.getSystemActions();
+        assertEquals(LEGACY_SYSTEM_ACTION_COUNT, actions.size());
+        // Overriding a legacy system action using legacy notification action id
+        mSystemActionPerformer.registerSystemAction(
+                AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS, NEW_TEST_ACTION_1);
+        actions = mSystemActionPerformer.getSystemActions();
+        assertEquals(LEGACY_SYSTEM_ACTION_COUNT, actions.size());
+        assertHasLegacyAccessibilityAction(actions, LEGACY_NOTIFICATIONS_ACCESSIBILITY_ACTION);
+    }
+
+    @Test
+    public void testUnregisterSystemAction_removeFromAvailableSystemActions() {
+        setupWithRealContext();
+        // Before any new system action is registered, getSystemActions returns all legacy actions
+        List<AccessibilityAction> actions = mSystemActionPerformer.getSystemActions();
+        assertEquals(LEGACY_SYSTEM_ACTION_COUNT, actions.size());
+        // Register a new system action
+        mSystemActionPerformer.registerSystemAction(NEW_ACTION_ID, NEW_TEST_ACTION_1);
+        actions = mSystemActionPerformer.getSystemActions();
+        assertEquals(LEGACY_SYSTEM_ACTION_COUNT + 1, actions.size());
+
+        mSystemActionPerformer.unregisterSystemAction(NEW_ACTION_ID);
+        actions = mSystemActionPerformer.getSystemActions();
+        assertEquals(LEGACY_SYSTEM_ACTION_COUNT, actions.size());
+        assertThat(actions, is(not(hasItem(NEW_ACCESSIBILITY_ACTION))));
+    }
+
+    @Test
+    public void testUnregisterSystemAction_removeOverrideForLegacyAction() {
+        setupWithRealContext();
+
+        // Overriding a legacy system action
+        mSystemActionPerformer.registerSystemAction(
+                AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS, NEW_TEST_ACTION_1);
+        List<AccessibilityAction> actions = mSystemActionPerformer.getSystemActions();
+        assertEquals(LEGACY_SYSTEM_ACTION_COUNT, actions.size());
+        assertHasLegacyAccessibilityAction(actions, LEGACY_NOTIFICATIONS_ACCESSIBILITY_ACTION);
+
+        // Remove the overriding action using legacy action id
+        mSystemActionPerformer.unregisterSystemAction(
+                AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS);
+        actions = mSystemActionPerformer.getSystemActions();
+        assertEquals(LEGACY_SYSTEM_ACTION_COUNT, actions.size());
+        assertHasNoLegacyAccessibilityAction(actions, LEGACY_NOTIFICATIONS_ACCESSIBILITY_ACTION);
+    }
+
+    @Test
+    public void testPerformSystemActionNewAction() throws CanceledException {
+        setupWithRealContext();
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        mSystemActionPerformer.registerSystemAction(NEW_ACTION_ID, NEW_TEST_ACTION_1);
+        TestBroadcastReceiver br = new TestBroadcastReceiver(latch);
+        br.register(InstrumentationRegistry.getTargetContext());
+        mSystemActionPerformer.performSystemAction(NEW_ACTION_ID);
+        try {
+            latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            fail("RemoteAction should be triggered.");
+        } finally {
+            br.unregister(InstrumentationRegistry.getTargetContext());
+        }
+    }
+
+    @Test
+    public void testPerformSystemActionOverrideLegacyActionUsingLegacyActionId()
+            throws CanceledException {
+        setupWithRealContext();
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        mSystemActionPerformer.registerSystemAction(
+                AccessibilityService.GLOBAL_ACTION_RECENTS, NEW_TEST_ACTION_1);
+        TestBroadcastReceiver br = new TestBroadcastReceiver(latch);
+        br.register(InstrumentationRegistry.getTargetContext());
+        mSystemActionPerformer.performSystemAction(AccessibilityService.GLOBAL_ACTION_RECENTS);
+        try {
+            latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            fail("RemoteAction should be triggered.");
+        } finally {
+            br.unregister(InstrumentationRegistry.getTargetContext());
+        }
+        verify(mMockStatusBarManagerInternal, never()).toggleRecentApps();
+        // Now revert to legacy action
+        mSystemActionPerformer.unregisterSystemAction(AccessibilityService.GLOBAL_ACTION_RECENTS);
+        mSystemActionPerformer.performSystemAction(AccessibilityService.GLOBAL_ACTION_RECENTS);
+        verify(mMockStatusBarManagerInternal).toggleRecentApps();
+    }
+
+    @Test
+    public void testNotifications_expandsNotificationPanel_legacy() {
+        setupWithMockContext();
         mSystemActionPerformer
                 .performSystemAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS);
         verify(mMockStatusBarManager).expandNotificationsPanel();
     }
 
     @Test
-    public void testQuickSettings_requestsQuickSettingsPanel() {
+    public void testQuickSettings_requestsQuickSettingsPanel_legacy() {
+        setupWithMockContext();
         mSystemActionPerformer
                 .performSystemAction(AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS);
         verify(mMockStatusBarManager).expandSettingsPanel();
     }
 
     @Test
-    public void testPowerDialog_requestsFromWindowManager() {
+    public void testRecentApps_legacy() {
+        setupWithRealContext();
+        mSystemActionPerformer.performSystemAction(AccessibilityService.GLOBAL_ACTION_RECENTS);
+        verify(mMockStatusBarManagerInternal).toggleRecentApps();
+    }
+
+    @Test
+    public void testPowerDialog_requestsFromWindowManager_legacy() {
+        setupWithMockContext();
         mSystemActionPerformer.performSystemAction(AccessibilityService.GLOBAL_ACTION_POWER_DIALOG);
         verify(mMockWindowManagerInternal).showGlobalActions();
     }
 
     @Test
-    public void testScreenshot_requestsFromScreenshotHelper() {
+    public void testToggleSplitScreen_legacy() {
+        setupWithRealContext();
+        mSystemActionPerformer.performSystemAction(
+                AccessibilityService.GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN);
+        verify(mMockStatusBarManagerInternal).toggleSplitScreen();
+    }
+
+    @Test
+    public void testScreenshot_requestsFromScreenshotHelper_legacy() {
+        setupWithMockContext();
         mSystemActionPerformer.performSystemAction(
                 AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT);
         verify(mMockScreenshotHelper).takeScreenshot(
                 eq(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN), anyBoolean(),
                 anyBoolean(), any(Handler.class), any());
     }
+
+    // PendingIntent is a final class and cannot be mocked. So we are using this
+    // Broadcast receiver to verify the registered remote action is called correctly.
+    private static final class TestBroadcastReceiver extends BroadcastReceiver {
+        private CountDownLatch mLatch;
+        private boolean mRegistered;
+        private final IntentFilter mFilter;
+
+        TestBroadcastReceiver(CountDownLatch latch) {
+            mLatch = latch;
+            mRegistered = false;
+            mFilter = new IntentFilter(INTENT_ACTION1);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mLatch.countDown();
+        }
+
+        void register(Context context) {
+            if (!mRegistered) {
+                context.registerReceiver(this, mFilter);
+                mRegistered = true;
+            }
+        }
+
+        void unregister(Context context) {
+            if (mRegistered) {
+                context.unregisterReceiver(this);
+            }
+        }
+    }
 }