Merge "[Panlingual] Dynamic locales"
This commit is contained in:
@@ -5874,10 +5874,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
|
||||
@@ -5888,8 +5893,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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
19
core/java/android/app/LocaleConfig.aidl
Normal file
19
core/java/android/app/LocaleConfig.aidl
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3627,6 +3627,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"
|
||||
|
||||
@@ -515,6 +515,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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -137,7 +137,7 @@ public class SystemAppUpdateTrackerTest {
|
||||
|
||||
AppUpdateTracker appUpdateTracker = mock(AppUpdateTracker.class);
|
||||
mPackageMonitor = new LocaleManagerServicePackageMonitor(mockLocaleManagerBackupHelper,
|
||||
mSystemAppUpdateTracker, appUpdateTracker);
|
||||
mSystemAppUpdateTracker, appUpdateTracker, mLocaleManagerService);
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
Reference in New Issue
Block a user