[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
This commit is contained in:
Josh Hou
2022-12-06 15:04:40 +08:00
parent dca85cd154
commit dea37e1a18
12 changed files with 638 additions and 28 deletions

View File

@@ -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<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 @@ 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 {

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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 <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 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}.
*
* <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
*/
@@ -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<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;
}
}

View File

@@ -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.
*
* <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 @@ 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.
*
* <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();
}
}
}

View File

@@ -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"

View File

@@ -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" />

View File

@@ -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.
*
* <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 @@ 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<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);
}
}

View File

@@ -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

View File

@@ -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 <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 @@ 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) {

View File

@@ -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<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 @@ public class LocaleManagerBackupRestoreTest {
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(

View File

@@ -137,7 +137,7 @@ public class SystemAppUpdateTrackerTest {
AppUpdateTracker appUpdateTracker = mock(AppUpdateTracker.class);
mPackageMonitor = new LocaleManagerServicePackageMonitor(mockLocaleManagerBackupHelper,
mSystemAppUpdateTracker, appUpdateTracker);
mSystemAppUpdateTracker, appUpdateTracker, mLocaleManagerService);
}
@After