[Panlingual] Dynamic locales

Allows applications to  update their list of supported locales while running, without an update of the app’s software.

Bug: 248446474
Test: atest LocaleManagerTests
      atest LocaleConfigTest
      Use the shell command to set/get the override LocaleConfig
Change-Id: I1bfd5431302d4d31b0f8891dd5a7419641657416
diff --git a/core/api/current.txt b/core/api/current.txt
index 434b60d..101726b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -5863,10 +5863,15 @@
     method @Deprecated public android.view.Window startActivity(String, android.content.Intent);
   }
 
-  public class LocaleConfig {
+  public class LocaleConfig implements android.os.Parcelable {
     ctor public LocaleConfig(@NonNull android.content.Context);
+    ctor public LocaleConfig(@NonNull android.os.LocaleList);
+    method public int describeContents();
+    method @NonNull public static android.app.LocaleConfig fromResources(@NonNull android.content.Context);
     method public int getStatus();
     method @Nullable public android.os.LocaleList getSupportedLocales();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.LocaleConfig> CREATOR;
     field public static final int STATUS_NOT_SPECIFIED = 1; // 0x1
     field public static final int STATUS_PARSING_FAILED = 2; // 0x2
     field public static final int STATUS_SUCCESS = 0; // 0x0
@@ -5877,8 +5882,10 @@
   public class LocaleManager {
     method @NonNull public android.os.LocaleList getApplicationLocales();
     method @NonNull @RequiresPermission(value="android.permission.READ_APP_SPECIFIC_LOCALES", conditional=true) public android.os.LocaleList getApplicationLocales(@NonNull String);
+    method @Nullable public android.app.LocaleConfig getOverrideLocaleConfig();
     method @NonNull public android.os.LocaleList getSystemLocales();
     method public void setApplicationLocales(@NonNull android.os.LocaleList);
+    method public void setOverrideLocaleConfig(@Nullable android.app.LocaleConfig);
   }
 
   public class MediaRouteActionProvider extends android.view.ActionProvider {
diff --git a/core/java/android/app/ILocaleManager.aidl b/core/java/android/app/ILocaleManager.aidl
index c38b64f..e6d40f2 100644
--- a/core/java/android/app/ILocaleManager.aidl
+++ b/core/java/android/app/ILocaleManager.aidl
@@ -17,6 +17,7 @@
 
 package android.app;
 
+import android.app.LocaleConfig;
 import android.os.LocaleList;
 
 /**
@@ -29,7 +30,6 @@
  * @hide
  */
  interface ILocaleManager {
-
      /**
       * Sets a specified app’s app-specific UI locales.
       */
@@ -45,4 +45,7 @@
        */
      LocaleList getSystemLocales();
 
+     void setOverrideLocaleConfig(String packageName, int userId, in LocaleConfig localeConfig);
+
+     LocaleConfig getOverrideLocaleConfig(String packageName, int userId);
  }
diff --git a/core/java/android/app/LocaleConfig.aidl b/core/java/android/app/LocaleConfig.aidl
new file mode 100644
index 0000000..37e0847
--- /dev/null
+++ b/core/java/android/app/LocaleConfig.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.app;
+
+parcelable LocaleConfig;
\ No newline at end of file
diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java
index bbe3ce3..5d50d29 100644
--- a/core/java/android/app/LocaleConfig.java
+++ b/core/java/android/app/LocaleConfig.java
@@ -25,6 +25,8 @@
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.util.Slog;
 import android.util.Xml;
@@ -37,23 +39,27 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.HashSet;
+import java.util.Locale;
 import java.util.Set;
 
 /**
  * The LocaleConfig of an application.
- * Defined in an XML resource file with an {@code <locale-config>} element and
- * referenced in the manifest via {@code android:localeConfig} on
- * {@code <application>}.
+ * There are two sources. One is from an XML resource file with an {@code <locale-config>} element
+ * and referenced in the manifest via {@code android:localeConfig} on {@code <application>}. The
+ * other is that the application dynamically provides an override version which is persisted in
+ * {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)}.
  *
- * <p>For more information, see
+ * <p>For more information about the LocaleConfig from an XML resource file, see
  * <a href="https://developer.android.com/about/versions/13/features/app-languages#use-localeconfig">
  * the section on per-app language preferences</a>.
  *
  * @attr ref android.R.styleable#LocaleConfig_Locale_name
  * @attr ref android.R.styleable#AndroidManifestApplication_localeConfig
+ *
+ * <p>For more information about the LocaleConfig overridden by the application, see
+ * TODO(b/261528306): add link to guide
  */
-public class LocaleConfig {
-
+public class LocaleConfig implements Parcelable {
     private static final String TAG = "LocaleConfig";
     public static final String TAG_LOCALE_CONFIG = "locale-config";
     public static final String TAG_LOCALE = "locale";
@@ -83,13 +89,46 @@
     public @interface Status{}
 
     /**
-     * Returns the LocaleConfig for the provided application context
+     * Returns an override LocaleConfig if it has been set via
+     * {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)}. Otherwise, returns the
+     * LocaleConfig from the application resources.
      *
-     * @param context the context of the application
+     * @param context the context of the application.
      *
      * @see Context#createPackageContext(String, int).
      */
     public LocaleConfig(@NonNull Context context) {
+        this(context, true);
+    }
+
+    /**
+     * Returns a LocaleConfig from the application resources regardless of whether any LocaleConfig
+     * is overridden via {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)}.
+     *
+     * @param context the context of the application.
+     *
+     * @see Context#createPackageContext(String, int).
+     */
+    @NonNull
+    public static LocaleConfig fromResources(@NonNull Context context) {
+        return new LocaleConfig(context, false);
+    }
+
+    private LocaleConfig(@NonNull Context context, boolean allowOverride) {
+        if (allowOverride) {
+            LocaleManager localeManager = context.getSystemService(LocaleManager.class);
+            if (localeManager == null) {
+                Slog.w(TAG, "LocaleManager is null, cannot get the override LocaleConfig");
+                return;
+            }
+            LocaleConfig localeConfig = localeManager.getOverrideLocaleConfig();
+            if (localeConfig != null) {
+                Slog.d(TAG, "Has the override LocaleConfig");
+                mStatus = localeConfig.getStatus();
+                mLocales = localeConfig.getSupportedLocales();
+                return;
+            }
+        }
         int resId = 0;
         Resources res = context.getResources();
         try {
@@ -109,6 +148,38 @@
     }
 
     /**
+     * Return the LocaleConfig with any sequence of locales combined into a {@link LocaleList}.
+     *
+     * <p><b>Note:</b> Applications seeking to create an override LocaleConfig via
+     * {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)} should use this constructor to
+     * first create the LocaleConfig they intend the system to see as the override.
+     *
+     * <p><b>Note:</b> The creation of this LocaleConfig does not automatically mean it will
+     * become the override config for an application. Any LocaleConfig desired to be the override
+     * must be passed into the {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)},
+     * otherwise it will not persist or affect the system’s understanding of app-supported
+     * resources.
+     *
+     * @param locales the desired locales for a specified application
+     */
+    public LocaleConfig(@NonNull LocaleList locales) {
+        mStatus = STATUS_SUCCESS;
+        mLocales = locales;
+    }
+
+    /**
+     * Instantiate a new LocaleConfig from the data in a Parcel that was
+     * previously written with {@link #writeToParcel(Parcel, int)}.
+     *
+     * @param in The Parcel containing the previously written LocaleConfig,
+     * positioned at the location in the buffer where it was written.
+     */
+    private LocaleConfig(@NonNull Parcel in) {
+        mStatus = in.readInt();
+        mLocales = in.readTypedObject(LocaleList.CREATOR);
+    }
+
+    /**
      * Parse the XML content and get the locales supported by the application
      */
     private void parseLocaleConfig(XmlResourceParser parser, Resources res)
@@ -165,4 +236,52 @@
     public @Status int getStatus() {
         return mStatus;
     }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mStatus);
+        dest.writeTypedObject(mLocales, flags);
+    }
+
+    public static final @NonNull Parcelable.Creator<LocaleConfig> CREATOR =
+            new Parcelable.Creator<LocaleConfig>() {
+                @Override
+                public LocaleConfig createFromParcel(Parcel source) {
+                    return new LocaleConfig(source);
+                }
+
+                @Override
+                public LocaleConfig[] newArray(int size) {
+                    return new LocaleConfig[size];
+                }
+            };
+
+    /**
+     * Compare whether the locale is existed in the {@code mLocales} of the LocaleConfig.
+     *
+     * @param locale The {@link Locale} to compare for.
+     *
+     * @return true if the locale is existed in the {@code mLocales} of the LocaleConfig, false
+     * otherwise.
+     *
+     * @hide
+     */
+    public boolean containsLocale(Locale locale) {
+        if (mLocales == null) {
+            return false;
+        }
+
+        for (int i = 0; i < mLocales.size(); i++) {
+            if (LocaleList.matchesLanguageAndScript(mLocales.get(i), locale)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
diff --git a/core/java/android/app/LocaleManager.java b/core/java/android/app/LocaleManager.java
index 70c014f..b62f766 100644
--- a/core/java/android/app/LocaleManager.java
+++ b/core/java/android/app/LocaleManager.java
@@ -18,6 +18,7 @@
 
 import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
@@ -29,8 +30,9 @@
 import android.os.RemoteException;
 
 /**
- * This class gives access to system locale services. These services allow applications to control
- * granular locale settings (such as per-app locales).
+ * This class gives access to system locale services. These services allow applications to
+ * control granular locale settings (such as per-app locales) or override their list of supported
+ * locales while running.
  *
  * <p> Third party applications should treat this as a write-side surface, and continue reading
  * locales via their in-process {@link LocaleList}s.
@@ -106,8 +108,8 @@
     private void setApplicationLocales(@NonNull String appPackageName, @NonNull LocaleList locales,
             boolean fromDelegate) {
         try {
-            mService.setApplicationLocales(appPackageName, mContext.getUser().getIdentifier(),
-                    locales, fromDelegate);
+            mService.setApplicationLocales(appPackageName, mContext.getUserId(), locales,
+                    fromDelegate);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -144,8 +146,7 @@
     @NonNull
     public LocaleList getApplicationLocales(@NonNull String appPackageName) {
         try {
-            return mService.getApplicationLocales(appPackageName, mContext.getUser()
-                    .getIdentifier());
+            return mService.getApplicationLocales(appPackageName, mContext.getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -187,4 +188,49 @@
         }
     }
 
+    /**
+     * Sets the override LocaleConfig for the calling app.
+     *
+     * <p><b>Note:</b> Only the app itself with the same user can override its own LocaleConfig.
+     *
+     * <p><b>Note:</b> This function takes in a {@link LocaleConfig} which is intended to
+     * override the original config in the application’s resources. This LocaleConfig will become
+     * the override config, and stored in a system file for future access.
+     *
+     * <p><b>Note:</b> Using this function, applications can update their list of supported
+     * locales while running, without an update of the application’s software. For more
+     * information, see TODO(b/261528306): add link to guide.
+     *
+     * <p>Applications can remove the override LocaleConfig with a {@code null} object.
+     *
+     * @param localeConfig the desired {@link LocaleConfig} for the calling app.
+     */
+    @UserHandleAware
+    public void setOverrideLocaleConfig(@Nullable LocaleConfig localeConfig) {
+        try {
+            // The permission android.Manifest.permission#SET_APP_SPECIFIC_LOCALECONFIG is
+            // required to set an override LocaleConfig of another packages
+            mService.setOverrideLocaleConfig(mContext.getPackageName(), mContext.getUserId(),
+                    localeConfig);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the override LocaleConfig for the calling app.
+     *
+     * @return the override LocaleConfig, or {@code null} if the LocaleConfig isn't overridden.
+     */
+    @Nullable
+    @UserHandleAware
+    public LocaleConfig getOverrideLocaleConfig() {
+        try {
+            return mService.getOverrideLocaleConfig(mContext.getPackageName(),
+                    mContext.getUserId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 71a02dd..a6186d3 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3607,6 +3607,11 @@
     <permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
                 android:protectionLevel="signature|installer" />
 
+    <!-- @hide Allows applications to set an application-specific {@link LocaleConfig}.
+    <p>Not for use by third-party applications. -->
+    <permission android:name="android.permission.SET_APP_SPECIFIC_LOCALECONFIG"
+        android:protectionLevel="signature" />
+
     <!-- @hide Allows an application to monitor {@link android.provider.Settings.Config} access.
     <p>Not for use by third-party applications. -->
     <permission android:name="android.permission.MONITOR_DEVICE_CONFIG_ACCESS"
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index d3ba5e6..a8a30209 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -509,6 +509,9 @@
     <!-- Permissions needed for CTS test - CtsLocaleManagerTestCases -->
     <uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
 
+    <!-- Permissions needed for CTS test - CtsLocaleManagerTestCases -->
+    <uses-permission android:name="android.permission.SET_APP_SPECIFIC_LOCALECONFIG" />
+
     <!-- Permissions used for manual testing of time detection behavior. -->
     <uses-permission android:name="android.permission.SUGGEST_MANUAL_TIME" />
     <uses-permission android:name="android.permission.SUGGEST_TELEPHONY_TIME" />
diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java
index 783a6ae..66c01dc 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerService.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerService.java
@@ -25,6 +25,7 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.ILocaleManager;
+import android.app.LocaleConfig;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -32,6 +33,7 @@
 import android.content.pm.PackageManager.PackageInfoFlags;
 import android.content.res.Configuration;
 import android.os.Binder;
+import android.os.Environment;
 import android.os.HandlerThread;
 import android.os.LocaleList;
 import android.os.Process;
@@ -42,21 +44,40 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
+import android.util.AtomicFile;
 import android.util.Slog;
+import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
 
 /**
  * The implementation of ILocaleManager.aidl.
  *
- * <p>This service is API entry point for storing app-specific UI locales
+ * <p>This service is API entry point for storing app-specific UI locales and an override
+ * {@link LocaleConfig} for a specified app.
  */
 public class LocaleManagerService extends SystemService {
     private static final String TAG = "LocaleManagerService";
@@ -64,6 +85,13 @@
     // app.
     private static final String PROP_ALLOW_IME_QUERY_APP_LOCALE =
             "i18n.feature.allow_ime_query_app_locale";
+    // The feature flag control that the application can dynamically override the LocaleConfig.
+    private static final String PROP_DYNAMIC_LOCALES_CHANGE =
+            "i18n.feature.dynamic_locales_change";
+    private static final String LOCALE_CONFIGS = "locale_configs";
+    private static final String SUFFIX_FILE_NAME = ".xml";
+    private static final String ATTR_NAME = "name";
+
     final Context mContext;
     private final LocaleManagerService.LocaleManagerBinderService mBinderService;
     private ActivityTaskManagerInternal mActivityTaskManagerInternal;
@@ -74,6 +102,8 @@
 
     private final PackageMonitor mPackageMonitor;
 
+    private final Object mWriteLock = new Object();
+
     public static final boolean DEBUG = false;
 
     public LocaleManagerService(Context context) {
@@ -103,7 +133,7 @@
                 new AppUpdateTracker(mContext, this, mBackupHelper);
 
         mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper,
-                systemAppUpdateTracker, appUpdateTracker);
+                systemAppUpdateTracker, appUpdateTracker, this);
         mPackageMonitor.register(context, broadcastHandlerThread.getLooper(),
                 UserHandle.ALL,
                 true);
@@ -173,6 +203,19 @@
         }
 
         @Override
+        public void setOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId,
+                @Nullable LocaleConfig localeConfig) throws RemoteException {
+            LocaleManagerService.this.setOverrideLocaleConfig(appPackageName, userId, localeConfig);
+        }
+
+        @Override
+        @Nullable
+        public LocaleConfig getOverrideLocaleConfig(@NonNull String appPackageName,
+                @UserIdInt int userId) {
+            return LocaleManagerService.this.getOverrideLocaleConfig(appPackageName, userId);
+        }
+
+        @Override
         public void onShellCommand(FileDescriptor in, FileDescriptor out,
                 FileDescriptor err, String[] args, ShellCallback callback,
                 ResultReceiver resultReceiver) {
@@ -211,7 +254,6 @@
             if (!isCallerOwner) {
                 enforceChangeConfigurationPermission(atomRecordForMetrics);
             }
-
             mBackupHelper.persistLocalesModificationInfo(userId, appPackageName, fromDelegate,
                     locales.isEmpty());
             final long token = Binder.clearCallingIdentity();
@@ -516,4 +558,242 @@
                 atomRecordForMetrics.mPrevLocales,
                 atomRecordForMetrics.mStatus);
     }
+
+    /**
+     * Storing an override {@link LocaleConfig} for a specified app.
+     */
+    public void setOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId,
+            @Nullable LocaleConfig localeConfig) throws IllegalArgumentException {
+        // TODO(b/262713398): Remove when stable
+        if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) {
+            return;
+        }
+
+        requireNonNull(appPackageName);
+
+        //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user.
+        userId = mActivityManagerInternal.handleIncomingUser(
+                Binder.getCallingPid(), Binder.getCallingUid(), userId,
+                false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
+                "setOverrideLocaleConfig", /* callerPackage= */ null);
+
+        // This function handles two types of set operations:
+        // 1.) A normal, an app overrides its own LocaleConfig.
+        // 2.) A privileged system application or service is granted the necessary permission to
+        // override a LocaleConfig of another package.
+        if (!isPackageOwnedByCaller(appPackageName, userId)) {
+            enforceSetAppSpecificLocaleConfigPermission();
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            setOverrideLocaleConfigUnchecked(appPackageName, userId, localeConfig);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+
+        //TODO: Add metrics to monitor the usage by applications
+    }
+    private void setOverrideLocaleConfigUnchecked(@NonNull String appPackageName,
+            @UserIdInt int userId, @Nullable LocaleConfig overridelocaleConfig) {
+        synchronized (mWriteLock) {
+            if (DEBUG) {
+                Slog.d(TAG,
+                        "set the override LocaleConfig for package " + appPackageName + " and user "
+                                + userId);
+            }
+            final File file = getXmlFileNameForUser(appPackageName, userId);
+
+            if (overridelocaleConfig == null) {
+                if (file.exists()) {
+                    Slog.d(TAG, "remove the override LocaleConfig");
+                    file.delete();
+                }
+                return;
+            } else {
+                LocaleList localeList = overridelocaleConfig.getSupportedLocales();
+                // Normally the LocaleList object should not be null. However we reassign it as the
+                // empty list in case it happens.
+                if (localeList == null) {
+                    localeList = LocaleList.getEmptyLocaleList();
+                }
+                if (DEBUG) {
+                    Slog.d(TAG,
+                            "setOverrideLocaleConfig, localeList: " + localeList.toLanguageTags());
+                }
+
+                // Store the override LocaleConfig to the file storage.
+                final AtomicFile atomicFile = new AtomicFile(file);
+                FileOutputStream stream = null;
+                try {
+                    stream = atomicFile.startWrite();
+                    stream.write(toXmlByteArray(localeList));
+                } catch (Exception e) {
+                    Slog.e(TAG, "Failed to write file " + atomicFile, e);
+                    if (stream != null) {
+                        atomicFile.failWrite(stream);
+                    }
+                    return;
+                }
+                atomicFile.finishWrite(stream);
+                // Clear per-app locales if they are not in the override LocaleConfig.
+                removeUnsupportedAppLocales(appPackageName, userId, overridelocaleConfig);
+                if (DEBUG) {
+                    Slog.i(TAG, "Successfully written to " + atomicFile);
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks if the per-app locales are in the new override LocaleConfig. Per-app locales
+     * missing from the new LocaleConfig will be removed.
+     */
+    private void removeUnsupportedAppLocales(String appPackageName, int userId,
+            LocaleConfig localeConfig) {
+        LocaleList appLocales = getApplicationLocalesUnchecked(appPackageName, userId);
+        // Remove the app locale from the locale list if it doesn't exist in the override
+        // LocaleConfig.
+        boolean resetAppLocales = false;
+        List<Locale> newAppLocales = new ArrayList<Locale>();
+        for (int i = 0; i < appLocales.size(); i++) {
+            if (!localeConfig.containsLocale(appLocales.get(i))) {
+                Slog.i(TAG, "reset the app locales");
+                resetAppLocales = true;
+                continue;
+            }
+            newAppLocales.add(appLocales.get(i));
+        }
+
+        if (resetAppLocales) {
+            // Reset the app locales
+            Locale[] locales = new Locale[newAppLocales.size()];
+            try {
+                setApplicationLocales(appPackageName, userId,
+                        new LocaleList(newAppLocales.toArray(locales)), false);
+            } catch (RemoteException | IllegalArgumentException e) {
+                Slog.e(TAG, "Could not set locales for " + appPackageName, e);
+            }
+        }
+    }
+
+    private void enforceSetAppSpecificLocaleConfigPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.SET_APP_SPECIFIC_LOCALECONFIG,
+                "setOverrideLocaleConfig");
+    }
+
+    /**
+     * Returns the override LocaleConfig for a specified app.
+     */
+    @Nullable
+    public LocaleConfig getOverrideLocaleConfig(@NonNull String appPackageName,
+            @UserIdInt int userId) {
+        // TODO(b/262713398): Remove when stable
+        if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) {
+            return null;
+        }
+
+        requireNonNull(appPackageName);
+
+        // Allow apps with INTERACT_ACROSS_USERS permission to query the override LocaleConfig for
+        // different user.
+        userId = mActivityManagerInternal.handleIncomingUser(
+                Binder.getCallingPid(), Binder.getCallingUid(), userId,
+                false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
+                "getOverrideLocaleConfig", /* callerPackage= */ null);
+
+        final File file = getXmlFileNameForUser(appPackageName, userId);
+        if (!file.exists()) {
+            if (DEBUG) {
+                Slog.i(TAG, "getOverrideLocaleConfig, the file is not existed.");
+            }
+            return null;
+        }
+
+        try (InputStream in = new FileInputStream(file)) {
+            final TypedXmlPullParser parser = Xml.resolvePullParser(in);
+            List<String> overrideLocales = loadFromXml(parser);
+            if (DEBUG) {
+                Slog.i(TAG, "getOverrideLocaleConfig, Loaded locales: " + overrideLocales);
+            }
+            LocaleConfig storedLocaleConfig = new LocaleConfig(
+                    LocaleList.forLanguageTags(String.join(",", overrideLocales)));
+
+            return storedLocaleConfig;
+        } catch (IOException | XmlPullParserException e) {
+            Slog.e(TAG, "Failed to parse XML configuration from " + file, e);
+        }
+
+        return null;
+    }
+
+    /**
+     * Delete an override {@link LocaleConfig} for a specified app from the file storage.
+     *
+     * <p>Clear the override LocaleConfig from the storage when the app is uninstalled.
+     */
+    void deleteOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId) {
+        final File file = getXmlFileNameForUser(appPackageName, userId);
+
+        if (file.exists()) {
+            Slog.d(TAG, "Delete the override LocaleConfig.");
+            file.delete();
+        }
+    }
+
+    private byte[] toXmlByteArray(LocaleList localeList) {
+        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+            TypedXmlSerializer out = Xml.newFastSerializer();
+            out.setOutput(os, StandardCharsets.UTF_8.name());
+            out.startDocument(/* encoding= */ null, /* standalone= */ true);
+            out.startTag(/* namespace= */ null, LocaleConfig.TAG_LOCALE_CONFIG);
+
+            List<String> locales = new ArrayList<String>(
+                    Arrays.asList(localeList.toLanguageTags().split(",")));
+            for (String locale : locales) {
+                out.startTag(null, LocaleConfig.TAG_LOCALE);
+                out.attribute(null, ATTR_NAME, locale);
+                out.endTag(null, LocaleConfig.TAG_LOCALE);
+            }
+
+            out.endTag(/* namespace= */ null, LocaleConfig.TAG_LOCALE_CONFIG);
+            out.endDocument();
+
+            if (DEBUG) {
+                Slog.d(TAG, "setOverrideLocaleConfig toXmlByteArray, output: " + os.toString());
+            }
+            return os.toByteArray();
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    @NonNull
+    private List<String> loadFromXml(TypedXmlPullParser parser)
+            throws IOException, XmlPullParserException {
+        List<String> localeList = new ArrayList<>();
+
+        XmlUtils.beginDocument(parser, LocaleConfig.TAG_LOCALE_CONFIG);
+        int depth = parser.getDepth();
+        while (XmlUtils.nextElementWithin(parser, depth)) {
+            final String tagName = parser.getName();
+            if (LocaleConfig.TAG_LOCALE.equals(tagName)) {
+                String locale = parser.getAttributeValue(/* namespace= */ null, ATTR_NAME);
+                localeList.add(locale);
+            } else {
+                Slog.w(TAG, "Unexpected tag name: " + tagName);
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+
+        return localeList;
+    }
+
+    @NonNull
+    private File getXmlFileNameForUser(@NonNull String appPackageName, @UserIdInt int userId) {
+        // TODO(b/262752965): use per-package data directory
+        final File dir = new File(Environment.getDataSystemDeDirectory(userId), LOCALE_CONFIGS);
+        return new File(dir, appPackageName + SUFFIX_FILE_NAME);
+    }
 }
diff --git a/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java b/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
index 1a38f0c..771e1b0 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
@@ -16,6 +16,9 @@
 
 package com.android.server.locales;
 
+import android.annotation.NonNull;
+import android.os.UserHandle;
+
 import com.android.internal.content.PackageMonitor;
 
 /**
@@ -35,12 +38,16 @@
     private LocaleManagerBackupHelper mBackupHelper;
     private SystemAppUpdateTracker mSystemAppUpdateTracker;
     private AppUpdateTracker mAppUpdateTracker;
+    private LocaleManagerService mLocaleManagerService;
 
-    LocaleManagerServicePackageMonitor(LocaleManagerBackupHelper localeManagerBackupHelper,
-            SystemAppUpdateTracker systemAppUpdateTracker, AppUpdateTracker appUpdateTracker) {
+    LocaleManagerServicePackageMonitor(@NonNull LocaleManagerBackupHelper localeManagerBackupHelper,
+            @NonNull SystemAppUpdateTracker systemAppUpdateTracker,
+            @NonNull AppUpdateTracker appUpdateTracker,
+            @NonNull LocaleManagerService localeManagerService) {
         mBackupHelper = localeManagerBackupHelper;
         mSystemAppUpdateTracker = systemAppUpdateTracker;
         mAppUpdateTracker = appUpdateTracker;
+        mLocaleManagerService = localeManagerService;
     }
 
     @Override
@@ -56,6 +63,7 @@
     @Override
     public void onPackageRemoved(String packageName, int uid) {
         mBackupHelper.onPackageRemoved(packageName, uid);
+        mLocaleManagerService.deleteOverrideLocaleConfig(packageName, UserHandle.getUserId(uid));
     }
 
     @Override
diff --git a/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java b/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
index c5069e5..09f2ffa 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
@@ -18,6 +18,7 @@
 
 import android.app.ActivityManager;
 import android.app.ILocaleManager;
+import android.app.LocaleConfig;
 import android.os.LocaleList;
 import android.os.RemoteException;
 import android.os.ShellCommand;
@@ -44,6 +45,10 @@
                 return runSetAppLocales();
             case "get-app-locales":
                 return runGetAppLocales();
+            case "set-app-localeconfig":
+                return runSetAppOverrideLocaleConfig();
+            case "get-app-localeconfig":
+                return runGetAppOverrideLocaleConfig();
             default: {
                 return handleDefaultCommands(cmd);
             }
@@ -62,15 +67,30 @@
         pw.println("      --user <USER_ID>: apply for the given user, "
                 + "the current user is used when unspecified.");
         pw.println("      --locales <LOCALE_INFO>: The language tags of locale to be included "
-                + "as a single String separated by commas");
-        pw.println("                 Empty locale list is used when unspecified.");
+                + "as a single String separated by commas.");
         pw.println("                 eg. en,en-US,hi ");
+        pw.println("                 Empty locale list is used when unspecified.");
         pw.println("      --delegate <FROM_DELEGATE>: The locales are set from a delegate, "
                 + "the value could be true or false. false is the default when unspecified.");
         pw.println("  get-app-locales <PACKAGE_NAME> [--user <USER_ID>]");
         pw.println("      Get the locales for the specified app.");
         pw.println("      --user <USER_ID>: get for the given user, "
                 + "the current user is used when unspecified.");
+        pw.println(
+                "  set-app-localeconfig <PACKAGE_NAME> [--user <USER_ID>] [--locales "
+                        + "<LOCALE_INFO>]");
+        pw.println("      Set the override LocaleConfig for the specified app.");
+        pw.println("      --user <USER_ID>: apply for the given user, "
+                + "the current user is used when unspecified.");
+        pw.println("      --locales <LOCALE_INFO>: The language tags of locale to be included "
+                + "as a single String separated by commas.");
+        pw.println("                 eg. en,en-US,hi ");
+        pw.println("                 Empty locale list is used when typing a 'empty' word");
+        pw.println("                 NULL is used when unspecified.");
+        pw.println("  get-app-localeconfig <PACKAGE_NAME> [--user <USER_ID>]");
+        pw.println("      Get the locales within the override LocaleConfig for the specified app.");
+        pw.println("      --user <USER_ID>: get for the given user, "
+                + "the current user is used when unspecified.");
     }
 
     private int runSetAppLocales() {
@@ -155,6 +175,106 @@
         return 0;
     }
 
+    private int runSetAppOverrideLocaleConfig() {
+        String packageName = getNextArg();
+
+        if (packageName != null) {
+            int userId = ActivityManager.getCurrentUser();
+            LocaleList locales = null;
+            do {
+                String option = getNextOption();
+                if (option == null) {
+                    break;
+                }
+                switch (option) {
+                    case "--user": {
+                        userId = UserHandle.parseUserArg(getNextArgRequired());
+                        break;
+                    }
+                    case "--locales": {
+                        locales = parseOverrideLocales();
+                        break;
+                    }
+                    default: {
+                        throw new IllegalArgumentException("Unknown option: " + option);
+                    }
+                }
+            } while (true);
+
+            try {
+                LocaleConfig localeConfig = locales == null ? null : new LocaleConfig(locales);
+                mBinderService.setOverrideLocaleConfig(packageName, userId, localeConfig);
+            } catch (RemoteException e) {
+                getOutPrintWriter().println("Remote Exception: " + e);
+            }
+        } else {
+            final PrintWriter err = getErrPrintWriter();
+            err.println("Error: no package specified");
+            return -1;
+        }
+        return 0;
+    }
+
+    private int runGetAppOverrideLocaleConfig() {
+        String packageName = getNextArg();
+
+        if (packageName != null) {
+            int userId = ActivityManager.getCurrentUser();
+            do {
+                String option = getNextOption();
+                if (option == null) {
+                    break;
+                }
+                if ("--user".equals(option)) {
+                    userId = UserHandle.parseUserArg(getNextArgRequired());
+                    break;
+                } else {
+                    throw new IllegalArgumentException("Unknown option: " + option);
+                }
+            } while (true);
+            try {
+                LocaleConfig localeConfig = mBinderService.getOverrideLocaleConfig(packageName,
+                        userId);
+                if (localeConfig == null) {
+                    getOutPrintWriter().println("LocaleConfig for " + packageName
+                            + " for user " + userId + " is null");
+                } else {
+                    LocaleList locales = localeConfig.getSupportedLocales();
+                    if (locales == null) {
+                        getOutPrintWriter().println(
+                                "Locales within the LocaleConfig for " + packageName + " for user "
+                                        + userId + " are null");
+                    } else {
+                        getOutPrintWriter().println(
+                                "Locales within the LocaleConfig for " + packageName + " for user "
+                                        + userId + " are [" + locales.toLanguageTags() + "]");
+                    }
+                }
+            } catch (RemoteException e) {
+                getOutPrintWriter().println("Remote Exception: " + e);
+            }
+        } else {
+            final PrintWriter err = getErrPrintWriter();
+            err.println("Error: no package specified");
+            return -1;
+        }
+        return 0;
+    }
+
+    private LocaleList parseOverrideLocales() {
+        String locales = getNextArg();
+        if (locales == null) {
+            return null;
+        } else if (locales.equals("empty")) {
+            return LocaleList.getEmptyLocaleList();
+        } else {
+            if (locales.startsWith("-")) {
+                throw new IllegalArgumentException("Unknown locales: " + locales);
+            }
+            return LocaleList.forLanguageTags(locales);
+        }
+    }
+
     private LocaleList parseLocales() {
         String locales = getNextArg();
         if (locales == null) {
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
index 4d42afa..1b8958b 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
@@ -158,7 +158,7 @@
 
         mUserMonitor = mBackupHelper.getUserMonitor();
         mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper,
-            systemAppUpdateTracker, appUpdateTracker);
+            systemAppUpdateTracker, appUpdateTracker, mMockLocaleManagerService);
         setCurrentTimeMillis(DEFAULT_CREATION_TIME_MILLIS);
     }
 
@@ -667,7 +667,7 @@
         String pkgNameB = "com.android.myAppB";
         setUpPackageNamesForSp(new ArraySet<>(Arrays.asList(pkgNameA, pkgNameB)));
 
-        mPackageMonitor.onPackageRemoved(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        mBackupHelper.onPackageRemoved(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
 
         verify(mMockSpEditor, times(0)).putStringSet(anyString(), any());
     }
@@ -679,7 +679,7 @@
         Set<String> pkgNames = new ArraySet<>(Arrays.asList(pkgNameA, pkgNameB));
         setUpPackageNamesForSp(pkgNames);
 
-        mPackageMonitor.onPackageRemoved(pkgNameA, DEFAULT_UID);
+        mBackupHelper.onPackageRemoved(pkgNameA, DEFAULT_UID);
         pkgNames.remove(pkgNameA);
 
         verify(mMockSpEditor, times(1)).putStringSet(
@@ -693,7 +693,7 @@
         Set<String> pkgNames = new ArraySet<>(Arrays.asList(pkgNameA, pkgNameB));
         setUpPackageNamesForSp(pkgNames);
 
-        mPackageMonitor.onPackageDataCleared(pkgNameB, DEFAULT_UID);
+        mBackupHelper.onPackageDataCleared(pkgNameB, DEFAULT_UID);
         pkgNames.remove(pkgNameB);
 
         verify(mMockSpEditor, times(1)).putStringSet(
diff --git a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
index dc0740a..cbf555e 100644
--- a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
@@ -137,7 +137,7 @@
 
         AppUpdateTracker appUpdateTracker = mock(AppUpdateTracker.class);
         mPackageMonitor = new LocaleManagerServicePackageMonitor(mockLocaleManagerBackupHelper,
-                mSystemAppUpdateTracker, appUpdateTracker);
+                mSystemAppUpdateTracker, appUpdateTracker, mLocaleManagerService);
     }
 
     @After