From dea37e1a18d361dcf57ca84788639d7df3679388 Mon Sep 17 00:00:00 2001 From: Josh Hou Date: Tue, 6 Dec 2022 15:04:40 +0800 Subject: [PATCH] [Panlingual] Dynamic locales MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- core/api/current.txt | 9 +- core/java/android/app/ILocaleManager.aidl | 5 +- core/java/android/app/LocaleConfig.aidl | 19 ++ core/java/android/app/LocaleConfig.java | 135 ++++++++- core/java/android/app/LocaleManager.java | 58 +++- core/res/AndroidManifest.xml | 5 + packages/Shell/AndroidManifest.xml | 3 + .../server/locales/LocaleManagerService.java | 286 +++++++++++++++++- .../LocaleManagerServicePackageMonitor.java | 12 +- .../locales/LocaleManagerShellCommand.java | 124 +++++++- .../LocaleManagerBackupRestoreTest.java | 8 +- .../locales/SystemAppUpdateTrackerTest.java | 2 +- 12 files changed, 638 insertions(+), 28 deletions(-) create mode 100644 core/java/android/app/LocaleConfig.aidl diff --git a/core/api/current.txt b/core/api/current.txt index 434b60dd144a7..101726bcc74ef 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -5863,10 +5863,15 @@ package android.app { 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 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 @@ package android.app { 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 c38b64fd27a81..e6d40f2c68b38 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 @@ import android.os.LocaleList; * @hide */ interface ILocaleManager { - /** * Sets a specified app’s app-specific UI locales. */ @@ -45,4 +45,7 @@ import android.os.LocaleList; */ 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 0000000000000..37e0847d46b7e --- /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 bbe3ce3c1cdbf..5d50d29b46ae3 100644 --- a/core/java/android/app/LocaleConfig.java +++ b/core/java/android/app/LocaleConfig.java @@ -25,6 +25,8 @@ import android.content.res.Resources; 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.io.IOException; 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 } element and - * referenced in the manifest via {@code android:localeConfig} on - * {@code }. + * There are two sources. One is from an XML resource file with an {@code } element + * and referenced in the manifest via {@code android:localeConfig} on {@code }. The + * other is that the application dynamically provides an override version which is persisted in + * {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)}. * - *

For more information, see + *

For more information about the LocaleConfig from an XML resource file, see * * the section on per-app language preferences. * * @attr ref android.R.styleable#LocaleConfig_Locale_name * @attr ref android.R.styleable#AndroidManifestApplication_localeConfig + * + *

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 class LocaleConfig { 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 { @@ -108,6 +147,38 @@ public class LocaleConfig { } } + /** + * Return the LocaleConfig with any sequence of locales combined into a {@link LocaleList}. + * + *

Note: 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. + * + *

Note: 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 */ @@ -165,4 +236,52 @@ public class LocaleConfig { 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 CREATOR = + new Parcelable.Creator() { + @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 70c014f136deb..b62f7664c760f 100644 --- a/core/java/android/app/LocaleManager.java +++ b/core/java/android/app/LocaleManager.java @@ -18,6 +18,7 @@ package android.app; 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.LocaleList; 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. * *

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 @@ public class LocaleManager { 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 @@ public class LocaleManager { @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 @@ public class LocaleManager { } } + /** + * Sets the override LocaleConfig for the calling app. + * + *

Note: Only the app itself with the same user can override its own LocaleConfig. + * + *

Note: 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. + * + *

Note: 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. + * + *

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 71a02dd64b9c5..a6186d3ea1501 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3607,6 +3607,11 @@ + + + + + + diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java index 783a6ae0e4950..66c01dc4af7e5 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.annotation.UserIdInt; 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; 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.SystemProperties; 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. * - *

This service is API entry point for storing app-specific UI locales + *

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 @@ public class LocaleManagerService extends SystemService { // 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 @@ public class LocaleManagerService extends SystemService { private final PackageMonitor mPackageMonitor; + private final Object mWriteLock = new Object(); + public static final boolean DEBUG = false; public LocaleManagerService(Context context) { @@ -103,7 +133,7 @@ public class LocaleManagerService extends SystemService { new AppUpdateTracker(mContext, this, mBackupHelper); mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper, - systemAppUpdateTracker, appUpdateTracker); + systemAppUpdateTracker, appUpdateTracker, this); mPackageMonitor.register(context, broadcastHandlerThread.getLooper(), UserHandle.ALL, true); @@ -172,6 +202,19 @@ public class LocaleManagerService extends SystemService { return LocaleManagerService.this.getSystemLocales(); } + @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, @@ -211,7 +254,6 @@ public class LocaleManagerService extends SystemService { if (!isCallerOwner) { enforceChangeConfigurationPermission(atomRecordForMetrics); } - mBackupHelper.persistLocalesModificationInfo(userId, appPackageName, fromDelegate, locales.isEmpty()); final long token = Binder.clearCallingIdentity(); @@ -516,4 +558,242 @@ public class LocaleManagerService extends SystemService { 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 newAppLocales = new ArrayList(); + 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 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. + * + *

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 locales = new ArrayList( + 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 loadFromXml(TypedXmlPullParser parser) + throws IOException, XmlPullParserException { + List 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 1a38f0cb2d427..771e1b0423a17 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 @@ final class LocaleManagerServicePackageMonitor extends PackageMonitor { 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 @@ final class LocaleManagerServicePackageMonitor extends PackageMonitor { @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 c5069e5c84808..09f2ffa35da59 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 @@ package com.android.server.locales; 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 @@ public class LocaleManagerShellCommand extends ShellCommand { 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 @@ public class LocaleManagerShellCommand extends ShellCommand { pw.println(" --user : apply for the given user, " + "the current user is used when unspecified."); pw.println(" --locales : 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 : 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 [--user ]"); pw.println(" Get the locales for the specified app."); pw.println(" --user : get for the given user, " + "the current user is used when unspecified."); + pw.println( + " set-app-localeconfig [--user ] [--locales " + + "]"); + pw.println(" Set the override LocaleConfig for the specified app."); + pw.println(" --user : apply for the given user, " + + "the current user is used when unspecified."); + pw.println(" --locales : 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 [--user ]"); + pw.println(" Get the locales within the override LocaleConfig for the specified app."); + pw.println(" --user : get for the given user, " + + "the current user is used when unspecified."); } private int runSetAppLocales() { @@ -155,6 +175,106 @@ public class LocaleManagerShellCommand extends ShellCommand { 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 4d42afaa6c69f..1b8958bcadb8d 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 @@ public class LocaleManagerBackupRestoreTest { mUserMonitor = mBackupHelper.getUserMonitor(); mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper, - systemAppUpdateTracker, appUpdateTracker); + systemAppUpdateTracker, appUpdateTracker, mMockLocaleManagerService); setCurrentTimeMillis(DEFAULT_CREATION_TIME_MILLIS); } @@ -667,7 +667,7 @@ public class LocaleManagerBackupRestoreTest { 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 @@ public class LocaleManagerBackupRestoreTest { Set 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 @@ public class LocaleManagerBackupRestoreTest { Set 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 dc0740ab61e0c..cbf555ef4b5ab 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 @@ public class SystemAppUpdateTrackerTest { AppUpdateTracker appUpdateTracker = mock(AppUpdateTracker.class); mPackageMonitor = new LocaleManagerServicePackageMonitor(mockLocaleManagerBackupHelper, - mSystemAppUpdateTracker, appUpdateTracker); + mSystemAppUpdateTracker, appUpdateTracker, mLocaleManagerService); } @After