Perform merging for A11Y_SHORTCUT B&R.

The restored value merges with the previous (pre-restore) value.

Also fixes some bugs:
- Prevents writing `null` to the setting during early SUW (by skipping
  writing when old==new). This was causing the value to be written as
  not-overwritable, which would cause B&R to fail for this setting
  altogether.
- Writes the value of the default service to the setting if the user
  manually triggers the shortcut and enables the default service during
  SUW. This gets the device to a healthy state where the setting value
  matches the observed behavior, rather than permanently relying on the
  confusing "null means default" behavior for this user.

Bug: 341374402
Flag: android.view.accessibility.restore_a11y_shortcut_target_service
Test: atest SettingsHelperRestoreTest
Test: atest AccessibilityManagerServiceTest
Test: atest AccessibilityShortcutControllerTest
Test: Trigger volume keys shortcut twice during SUW.
      Observe the setting contains the default service.
Test: Create a backup with Setting = A, then factory reset.
      - Keep S empty, then restore. Observe S = A.
      - Set S = A, then restore. Observe S = A.
      - Set S = B, then restore. Observe S = A:B.
Change-Id: I73a5fcf36cdb45dd9a66b5823c1cc8fd4c0cc446
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index da2bf9d..86c68fe 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -153,6 +153,16 @@
 }
 
 flag {
+    name: "restore_a11y_shortcut_target_service"
+    namespace: "accessibility"
+    description: "Perform merging and other bug fixes for SettingsProvider restore of ACCESSIBILITY_SHORTCUT_TARGET_SERVICES secure setting"
+    bug: "341374402"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "support_system_pinch_zoom_opt_out_apis"
     namespace: "accessibility"
     description: "Feature flag for declaring system pinch zoom opt-out apis"
diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
index 06ae11fee..33b4e4a 100644
--- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
+++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
@@ -28,6 +28,7 @@
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.annotation.IntDef;
 import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
 import android.app.ActivityManager;
 import android.app.ActivityThread;
 import android.app.AlertDialog;
@@ -240,6 +241,7 @@
     /**
      * Called when the accessibility shortcut is activated
      */
+    @SuppressLint("MissingPermission")
     public void performAccessibilityShortcut() {
         Slog.d(TAG, "Accessibility shortcut activated");
         final ContentResolver cr = mContext.getContentResolver();
@@ -274,6 +276,9 @@
                     cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, DialogStatus.SHOWN,
                     userId);
         } else {
+            if (Flags.restoreA11yShortcutTargetService()) {
+                enableDefaultHardwareShortcut(userId);
+            }
             playNotificationTone();
             if (mAlertDialog != null) {
                 mAlertDialog.dismiss();
@@ -349,34 +354,7 @@
                 .setMessage(getShortcutWarningMessage(targets))
                 .setCancelable(false)
                 .setNegativeButton(R.string.accessibility_shortcut_on,
-                        (DialogInterface d, int which) -> {
-                            String targetServices = Settings.Secure.getStringForUser(
-                                    mContext.getContentResolver(),
-                                    Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, userId);
-                            String defaultService = mContext.getString(
-                                    R.string.config_defaultAccessibilityService);
-                            // If the targetServices is null, means the user enables a
-                            // shortcut for the default service by triggering the volume keys
-                            // shortcut in the SUW instead of intentionally configuring the
-                            // shortcut on UI.
-                            if (targetServices == null && !TextUtils.isEmpty(defaultService)) {
-                                // The defaultService in the string resource could be a shorten
-                                // form like com.google.android.marvin.talkback/.TalkBackService.
-                                // Converts it to the componentName for consistency before saving
-                                // to the Settings.
-                                final ComponentName configDefaultService =
-                                        ComponentName.unflattenFromString(defaultService);
-                                if (Flags.migrateEnableShortcuts()) {
-                                    am.enableShortcutsForTargets(true, HARDWARE,
-                                            Set.of(configDefaultService.flattenToString()), userId);
-                                } else {
-                                    Settings.Secure.putStringForUser(mContext.getContentResolver(),
-                                            Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
-                                            configDefaultService.flattenToString(),
-                                            userId);
-                                }
-                            }
-                        })
+                        (DialogInterface d, int which) -> enableDefaultHardwareShortcut(userId))
                 .setPositiveButton(R.string.accessibility_shortcut_off,
                         (DialogInterface d, int which) -> {
                             Set<String> targetServices =
@@ -509,6 +487,47 @@
         }
     }
 
+    /**
+     * Writes {@link R.string#config_defaultAccessibilityService} to the
+     * {@link Settings.Secure#ACCESSIBILITY_SHORTCUT_TARGET_SERVICE} Setting if
+     * that Setting is currently {@code null}.
+     *
+     * <p>If {@code ACCESSIBILITY_SHORTCUT_TARGET_SERVICE} is {@code null} then the
+     * user triggered the shortcut during Setup Wizard <i>before</i> directly
+     * enabling the shortcut in the Settings UI of Setup Wizard.
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
+    private void enableDefaultHardwareShortcut(int userId) {
+        final AccessibilityManager accessibilityManager = mFrameworkObjectProvider
+                .getAccessibilityManagerInstance(mContext);
+        final String targetServices = Settings.Secure.getStringForUser(
+                mContext.getContentResolver(),
+                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, userId);
+        if (targetServices != null) {
+            // Do not write if the Setting was already configured.
+            return;
+        }
+        final String defaultService = mContext.getString(
+                R.string.config_defaultAccessibilityService);
+        // The defaultService in the string resource could be a shortened
+        // form: "com.android.accessibility.package/.MyService". Convert it to
+        // the component name form for consistency before writing to the Setting.
+        final ComponentName defaultServiceComponent = TextUtils.isEmpty(defaultService)
+                ? null : ComponentName.unflattenFromString(defaultService);
+        if (defaultServiceComponent == null) {
+            // Default service is invalid, so nothing we can do here.
+            return;
+        }
+        if (Flags.migrateEnableShortcuts()) {
+            accessibilityManager.enableShortcutsForTargets(true, HARDWARE,
+                    Set.of(defaultServiceComponent.flattenToString()), userId);
+        } else {
+            Settings.Secure.putStringForUser(mContext.getContentResolver(),
+                    Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
+                    defaultServiceComponent.flattenToString(), userId);
+        }
+    }
+
     private boolean performTtsPrompt(AlertDialog alertDialog) {
         final String serviceName = getShortcutFeatureDescription(false /* no summary */);
         final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
index d560ef2..6b9dbba 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
@@ -634,6 +634,42 @@
     }
 
     @Test
+    @EnableFlags({
+            Flags.FLAG_MIGRATE_ENABLE_SHORTCUTS,
+            Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE})
+    public void testOnAccessibilityShortcut_settingNull_dialogShown_enablesDefaultShortcut()
+            throws Exception {
+        configureDefaultAccessibilityService();
+        Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN,
+                AccessibilityShortcutController.DialogStatus.SHOWN);
+        // Setting is only `null` during SUW.
+        Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, null);
+        getController().performAccessibilityShortcut();
+
+        verify(mAccessibilityManagerService).enableShortcutsForTargets(
+                eq(true), eq(HARDWARE), mListCaptor.capture(), anyInt());
+        assertThat(mListCaptor.getValue()).containsExactly(SERVICE_NAME_STRING);
+        verify(mAccessibilityManagerService).performAccessibilityShortcut(null);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE)
+    @DisableFlags(Flags.FLAG_MIGRATE_ENABLE_SHORTCUTS)
+    public void testOnAccessibilityShortcut_settingNull_dialogShown_writesDefaultSetting()
+            throws Exception {
+        configureDefaultAccessibilityService();
+        Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN,
+                AccessibilityShortcutController.DialogStatus.SHOWN);
+        // Setting is only `null` during SUW.
+        Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, null);
+        getController().performAccessibilityShortcut();
+
+        assertThat(Settings.Secure.getString(mContentResolver,
+                ACCESSIBILITY_SHORTCUT_TARGET_SERVICE)).isEqualTo(SERVICE_NAME_STRING);
+        verify(mAccessibilityManagerService).performAccessibilityShortcut(null);
+    }
+
+    @Test
     public void getFrameworkFeatureMap_shouldBeUnmodifiable() {
         final Map<ComponentName, AccessibilityShortcutController.FrameworkFeatureInfo>
                 frameworkFeatureMap =
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 2e9075c..7be15af 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -88,7 +88,7 @@
     private static final ArraySet<String> sBroadcastOnRestore;
     private static final ArraySet<String> sBroadcastOnRestoreSystemUI;
     static {
-        sBroadcastOnRestore = new ArraySet<String>(9);
+        sBroadcastOnRestore = new ArraySet<String>(12);
         sBroadcastOnRestore.add(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
         sBroadcastOnRestore.add(Settings.Secure.ENABLED_VR_LISTENERS);
         sBroadcastOnRestore.add(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
@@ -99,6 +99,7 @@
         sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED);
         sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
         sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_QS_TARGETS);
+        sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
         sBroadcastOnRestore.add(Settings.Secure.SCREEN_RESOLUTION_MODE);
         sBroadcastOnRestoreSystemUI = new ArraySet<String>(2);
         sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_TILES);
@@ -240,6 +241,11 @@
                 // Don't write it to setting. Let the broadcast receiver in
                 // AccessibilityManagerService handle restore/merging logic.
                 return;
+            } else if (android.view.accessibility.Flags.restoreA11yShortcutTargetService()
+                    && Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE.equals(name)) {
+                // Don't write it to setting. Let the broadcast receiver in
+                // AccessibilityManagerService handle restore/merging logic.
+                return;
             }
 
             // Default case: write the restored value to settings
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
index 2f8cf4b..f64f72a 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
@@ -26,6 +26,8 @@
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Build;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.provider.SettingsStringUtil;
 
@@ -35,6 +37,7 @@
 import com.android.internal.util.test.BroadcastInterceptingContext;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
@@ -48,6 +51,10 @@
  */
 @RunWith(AndroidJUnit4.class)
 public class SettingsHelperRestoreTest {
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private static final float FLOAT_TOLERANCE = 0.01f;
 
     private Context mContext;
@@ -202,4 +209,32 @@
                 Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, /* defaultValue= */ 0))
                 .isEqualTo(Build.VERSION.SDK_INT);
     }
+
+    @Test
+    @EnableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE)
+    public void restoreAccessibilityShortcutTargetService_broadcastSent()
+            throws ExecutionException, InterruptedException {
+        BroadcastInterceptingContext interceptingContext = new BroadcastInterceptingContext(
+                mContext);
+        final String settingName = Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
+        final String restoredValue = "com.android.a11y/Service";
+        BroadcastInterceptingContext.FutureIntent futureIntent =
+                interceptingContext.nextBroadcastIntent(Intent.ACTION_SETTING_RESTORED);
+
+        mSettingsHelper.restoreValue(
+                interceptingContext,
+                mContentResolver,
+                new ContentValues(2),
+                Settings.Secure.getUriFor(settingName),
+                settingName,
+                restoredValue,
+                Build.VERSION.SDK_INT);
+
+        Intent intentReceived = futureIntent.get();
+        assertThat(intentReceived.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE))
+                .isEqualTo(restoredValue);
+        assertThat(intentReceived.getIntExtra(
+                Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, /* defaultValue= */ 0))
+                .isEqualTo(Build.VERSION.SDK_INT);
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index a15d2ca..c55e9ea 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -65,6 +65,7 @@
 import android.annotation.Nullable;
 import android.annotation.PermissionManuallyEnforced;
 import android.annotation.RequiresNoPermission;
+import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.app.ActivityOptions;
 import android.app.AlertDialog;
@@ -857,6 +858,7 @@
         mPackageMonitor = monitor;
     }
 
+    @SuppressLint("MissingPermission")
     private void registerBroadcastReceivers() {
         // package changes
         mPackageMonitor = new ManagerPackageMonitor(this);
@@ -890,33 +892,47 @@
                     removeUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
                 } else if (Intent.ACTION_SETTING_RESTORED.equals(action)) {
                     final String which = intent.getStringExtra(Intent.EXTRA_SETTING_NAME);
-                    if (Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES.equals(which)) {
-                        synchronized (mLock) {
-                            restoreEnabledAccessibilityServicesLocked(
-                                    intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE),
-                                    intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE),
-                                    intent.getIntExtra(Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT,
-                                            0));
+                    if (which == null) {
+                        return;
+                    }
+                    final String previousValue =
+                            intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE);
+                    final String newValue =
+                            intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE);
+                    final int restoredFromSdk =
+                            intent.getIntExtra(Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, 0);
+                    switch (which) {
+                        case Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES -> {
+                            synchronized (mLock) {
+                                restoreEnabledAccessibilityServicesLocked(
+                                        previousValue, newValue, restoredFromSdk);
+                            }
                         }
-                    } else if (ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED.equals(which)) {
-                        synchronized (mLock) {
-                            restoreLegacyDisplayMagnificationNavBarIfNeededLocked(
-                                    intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE),
-                                    intent.getIntExtra(Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT,
-                                            0));
+                        case ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED -> {
+                            synchronized (mLock) {
+                                restoreLegacyDisplayMagnificationNavBarIfNeededLocked(
+                                        newValue, restoredFromSdk);
+                            }
                         }
-                    } else if (Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS.equals(which)) {
-                        synchronized (mLock) {
-                            restoreAccessibilityButtonTargetsLocked(
-                                    intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE),
-                                    intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
+                        case Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS -> {
+                            synchronized (mLock) {
+                                restoreAccessibilityButtonTargetsLocked(
+                                        previousValue, newValue);
+                            }
                         }
-                    } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(which)) {
-                        if (!android.view.accessibility.Flags.a11yQsShortcut()) {
-                            return;
+                        case Settings.Secure.ACCESSIBILITY_QS_TARGETS -> {
+                            if (!android.view.accessibility.Flags.a11yQsShortcut()) {
+                                return;
+                            }
+                            restoreAccessibilityQsTargets(newValue);
                         }
-                        restoreAccessibilityQsTargets(
-                                intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
+                        case Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE -> {
+                            if (!android.view.accessibility.Flags
+                                    .restoreA11yShortcutTargetService()) {
+                                return;
+                            }
+                            restoreAccessibilityShortcutTargetService(previousValue, newValue);
+                        }
                     }
                 }
             }
@@ -2079,6 +2095,31 @@
         }
     }
 
+    /**
+     * Merges the old and restored value of
+     * {@link Settings.Secure#ACCESSIBILITY_SHORTCUT_TARGET_SERVICE}.
+     */
+    private void restoreAccessibilityShortcutTargetService(
+            String oldValue, String restoredValue) {
+        final Set<String> targetsFromSetting = new ArraySet<>();
+        readColonDelimitedStringToSet(oldValue, str -> str,
+                targetsFromSetting, /*doMerge=*/false);
+        readColonDelimitedStringToSet(restoredValue, str -> str,
+                targetsFromSetting, /*doMerge=*/true);
+        synchronized (mLock) {
+            final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
+            final Set<String> shortcutTargets =
+                    userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
+            shortcutTargets.clear();
+            shortcutTargets.addAll(targetsFromSetting);
+            persistColonDelimitedSetToSettingLocked(
+                    Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
+                    UserHandle.USER_SYSTEM, targetsFromSetting, str -> str);
+            scheduleNotifyClientsOfServicesStateChangeLocked(userState);
+            onUserStateChangedLocked(userState);
+        }
+    }
+
     private int getClientStateLocked(AccessibilityUserState userState) {
         return userState.getClientStateLocked(
             mUiAutomationManager.canIntrospect(),
@@ -2605,12 +2646,35 @@
             }
             builder.append(str);
         }
+        final String builderValue = builder.toString();
+        final String settingValue = TextUtils.isEmpty(builderValue)
+                ? defaultEmptyString : builderValue;
+        if (android.view.accessibility.Flags.restoreA11yShortcutTargetService()) {
+            final String currentValue = Settings.Secure.getStringForUser(
+                    mContext.getContentResolver(), settingName, userId);
+            if (Objects.equals(settingValue, currentValue)) {
+                // This logic exists to fix a bug where AccessibilityManagerService was writing
+                // `null` to the ACCESSIBILITY_SHORTCUT_TARGET_SERVICE setting during early boot
+                // during setup, due to a race condition in package scanning making A11yMS think
+                // that the default service was not installed.
+                //
+                // Writing `null` was implicitly causing that Setting to have the default
+                // `DEFAULT_OVERRIDEABLE_BY_RESTORE` property, which was preventing B&R for that
+                // Setting altogether.
+                //
+                // The "quick fix" here is to not write `null` if the existing value is already
+                // `null`. The ideal fix would be use the Settings.Secure#putStringForUser overload
+                // that allows override-by-restore, but the full repercussions of using that here
+                // have not yet been evaluated.
+                // TODO: b/333457719 - Evaluate and fix AccessibilityManagerService's usage of
+                //  "overridable by restore" when writing secure settings.
+                return;
+            }
+        }
         final long identity = Binder.clearCallingIdentity();
         try {
-            final String settingValue = builder.toString();
             Settings.Secure.putStringForUser(mContext.getContentResolver(),
-                    settingName,
-                    TextUtils.isEmpty(settingValue) ? defaultEmptyString : settingValue, userId);
+                    settingName, settingValue, userId);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index ca15aa2..78cd2c1 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -1590,12 +1590,10 @@
         userState.updateA11yQsTargetLocked(Set.of(daltonizerTile));
         mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
 
-        Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
-                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
-                .putExtra(Intent.EXTRA_SETTING_NAME, Settings.Secure.ACCESSIBILITY_QS_TARGETS)
-                .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, colorInversionTile);
-        sendBroadcastToAccessibilityManagerService(intent);
-        mTestableLooper.processAllMessages();
+        broadcastSettingRestored(
+                Settings.Secure.ACCESSIBILITY_QS_TARGETS,
+                /*previousValue=*/null,
+                /*newValue=*/colorInversionTile);
 
         assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM).getA11yQsTargets())
                 .containsExactlyElementsIn(Set.of(daltonizerTile, colorInversionTile));
@@ -1613,12 +1611,10 @@
         userState.updateA11yQsTargetLocked(Set.of(daltonizerTile));
         mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
 
-        Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
-                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
-                .putExtra(Intent.EXTRA_SETTING_NAME, Settings.Secure.ACCESSIBILITY_QS_TARGETS)
-                .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, colorInversionTile);
-        sendBroadcastToAccessibilityManagerService(intent);
-        mTestableLooper.processAllMessages();
+        broadcastSettingRestored(
+                Settings.Secure.ACCESSIBILITY_QS_TARGETS,
+                /*previousValue=*/null,
+                /*newValue=*/colorInversionTile);
 
         assertThat(userState.getA11yQsTargets())
                 .containsExactlyElementsIn(Set.of(daltonizerTile));
@@ -1679,12 +1675,55 @@
 
         assertFalse(mA11yms.getPackageMonitor().onHandleForceStop(
                 new Intent(),
-                new String[]{ "FOO", "BAR"},
+                new String[]{"FOO", "BAR"},
                 UserHandle.USER_SYSTEM,
                 false
         ));
     }
 
+    @Test
+    @EnableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE)
+    public void restoreA11yShortcutTargetService_targetsMerged() {
+        final String servicePrevious = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
+        final String otherPrevious = TARGET_MAGNIFICATION;
+        final String combinedPrevious = String.join(":", servicePrevious, otherPrevious);
+        final String serviceRestored = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
+        final AccessibilityUserState userState = new AccessibilityUserState(
+                UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
+        mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
+        setupShortcutTargetServices(userState);
+
+        broadcastSettingRestored(
+                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
+                /*previousValue=*/combinedPrevious,
+                /*newValue=*/serviceRestored);
+
+        final Set<String> expected = Set.of(servicePrevious, otherPrevious, serviceRestored);
+        assertThat(readStringsFromSetting(
+                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE))
+                .containsExactlyElementsIn(expected);
+        assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM)
+                .getShortcutTargetsLocked(UserShortcutType.HARDWARE))
+                .containsExactlyElementsIn(expected);
+    }
+
+    private Set<String> readStringsFromSetting(String setting) {
+        final Set<String> result = new ArraySet<>();
+        mA11yms.readColonDelimitedSettingToSet(
+                setting, UserHandle.USER_SYSTEM, str -> str, result);
+        return result;
+    }
+
+    private void broadcastSettingRestored(String setting, String previousValue, String newValue) {
+        Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
+                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
+                .putExtra(Intent.EXTRA_SETTING_NAME, setting)
+                .putExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE, previousValue)
+                .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, newValue);
+        sendBroadcastToAccessibilityManagerService(intent);
+        mTestableLooper.processAllMessages();
+    }
+
     private static AccessibilityServiceInfo mockAccessibilityServiceInfo(
             ComponentName componentName) {
         return mockAccessibilityServiceInfo(
@@ -1738,6 +1777,10 @@
     }
 
     private void setupShortcutTargetServices() {
+        setupShortcutTargetServices(mA11yms.getCurrentUserState());
+    }
+
+    private void setupShortcutTargetServices(AccessibilityUserState userState) {
         AccessibilityServiceInfo alwaysOnServiceInfo = mockAccessibilityServiceInfo(
                 TARGET_ALWAYS_ON_A11Y_SERVICE,
                 /* isSystemApp= */ false,
@@ -1748,9 +1791,9 @@
                 TARGET_STANDARD_A11Y_SERVICE,
                 /* isSystemApp= */ false,
                 /* isAlwaysOnService= */ false);
-        mA11yms.getCurrentUserState().mInstalledServices.addAll(
+        userState.mInstalledServices.addAll(
                 List.of(alwaysOnServiceInfo, standardServiceInfo));
-        mA11yms.getCurrentUserState().updateTileServiceMapForAccessibilityServiceLocked();
+        userState.updateTileServiceMapForAccessibilityServiceLocked();
     }
 
     private void sendBroadcastToAccessibilityManagerService(Intent intent) {