| /* |
| * Copyright (C) 2021 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 com.android.server.locales; |
| |
| import static java.util.Objects.requireNonNull; |
| |
| import android.Manifest; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| 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; |
| 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; |
| import android.os.RemoteException; |
| import android.os.ResultReceiver; |
| import android.os.ShellCallback; |
| 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 and an override |
| * {@link LocaleConfig} for a specified app. |
| */ |
| public class LocaleManagerService extends SystemService { |
| private static final String TAG = "LocaleManagerService"; |
| // The feature flag control that allows the active IME to query the locales of the foreground |
| // 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; |
| private ActivityManagerInternal mActivityManagerInternal; |
| private PackageManager mPackageManager; |
| |
| private LocaleManagerBackupHelper mBackupHelper; |
| |
| private final PackageMonitor mPackageMonitor; |
| |
| private final Object mWriteLock = new Object(); |
| |
| public static final boolean DEBUG = false; |
| |
| public LocaleManagerService(Context context) { |
| super(context); |
| mContext = context; |
| mBinderService = new LocaleManagerBinderService(); |
| mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); |
| mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); |
| mPackageManager = mContext.getPackageManager(); |
| |
| HandlerThread broadcastHandlerThread = new HandlerThread(TAG, |
| Process.THREAD_PRIORITY_BACKGROUND); |
| broadcastHandlerThread.start(); |
| |
| SystemAppUpdateTracker systemAppUpdateTracker = |
| new SystemAppUpdateTracker(this); |
| broadcastHandlerThread.getThreadHandler().postAtFrontOfQueue(new Runnable() { |
| @Override |
| public void run() { |
| systemAppUpdateTracker.init(); |
| } |
| }); |
| |
| mBackupHelper = new LocaleManagerBackupHelper(this, |
| mPackageManager, broadcastHandlerThread); |
| mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper, |
| systemAppUpdateTracker, this); |
| mPackageMonitor.register(context, broadcastHandlerThread.getLooper(), |
| UserHandle.ALL, |
| true); |
| } |
| |
| @VisibleForTesting |
| LocaleManagerService(Context context, ActivityTaskManagerInternal activityTaskManagerInternal, |
| ActivityManagerInternal activityManagerInternal, |
| PackageManager packageManager, |
| LocaleManagerBackupHelper localeManagerBackupHelper, |
| PackageMonitor packageMonitor) { |
| super(context); |
| mContext = context; |
| mBinderService = new LocaleManagerBinderService(); |
| mActivityTaskManagerInternal = activityTaskManagerInternal; |
| mActivityManagerInternal = activityManagerInternal; |
| mPackageManager = packageManager; |
| mBackupHelper = localeManagerBackupHelper; |
| mPackageMonitor = packageMonitor; |
| } |
| |
| @Override |
| public void onStart() { |
| publishBinderService(Context.LOCALE_SERVICE, mBinderService); |
| LocalServices.addService(LocaleManagerInternal.class, new LocaleManagerInternalImpl()); |
| } |
| |
| private final class LocaleManagerInternalImpl extends LocaleManagerInternal { |
| |
| @Override |
| public @Nullable byte[] getBackupPayload(int userId) { |
| checkCallerIsSystem(); |
| return mBackupHelper.getBackupPayload(userId); |
| } |
| |
| @Override |
| public void stageAndApplyRestoredPayload(byte[] payload, int userId) { |
| mBackupHelper.stageAndApplyRestoredPayload(payload, userId); |
| } |
| |
| private void checkCallerIsSystem() { |
| if (Binder.getCallingUid() != Process.SYSTEM_UID) { |
| throw new SecurityException("Caller is not system."); |
| } |
| } |
| } |
| |
| private final class LocaleManagerBinderService extends ILocaleManager.Stub { |
| @Override |
| public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId, |
| @NonNull LocaleList locales, boolean fromDelegate) throws RemoteException { |
| int caller = fromDelegate |
| ? FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DELEGATE |
| : FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS; |
| LocaleManagerService.this.setApplicationLocales(appPackageName, userId, locales, |
| fromDelegate, caller); |
| } |
| |
| @Override |
| @NonNull |
| public LocaleList getApplicationLocales(@NonNull String appPackageName, |
| @UserIdInt int userId) throws RemoteException { |
| return LocaleManagerService.this.getApplicationLocales(appPackageName, userId); |
| } |
| |
| @Override |
| @NonNull |
| public LocaleList getSystemLocales() throws RemoteException { |
| 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, |
| ResultReceiver resultReceiver) { |
| (new LocaleManagerShellCommand(mBinderService)) |
| .exec(this, in, out, err, args, callback, resultReceiver); |
| } |
| |
| } |
| |
| /** |
| * Sets the current UI locales for a specified app. |
| */ |
| public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId, |
| @NonNull LocaleList locales, boolean fromDelegate, int caller) |
| throws RemoteException, IllegalArgumentException { |
| AppLocaleChangedAtomRecord atomRecordForMetrics = new |
| AppLocaleChangedAtomRecord(Binder.getCallingUid()); |
| try { |
| requireNonNull(appPackageName); |
| requireNonNull(locales); |
| atomRecordForMetrics.setCaller(caller); |
| atomRecordForMetrics.setNewLocales(locales.toLanguageTags()); |
| //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, |
| "setApplicationLocales", /* callerPackage= */ null); |
| |
| // This function handles two types of set operations: |
| // 1.) A normal, non-privileged app setting its own locale. |
| // 2.) A privileged system service setting locales of another package. |
| // The least privileged case is a normal app performing a set, so check that first and |
| // set locales if the package name is owned by the app. Next, check if the caller has |
| // the necessary permission and set locales. |
| boolean isCallerOwner = isPackageOwnedByCaller(appPackageName, userId, |
| atomRecordForMetrics, null); |
| if (!isCallerOwner) { |
| enforceChangeConfigurationPermission(atomRecordForMetrics); |
| } |
| mBackupHelper.persistLocalesModificationInfo(userId, appPackageName, fromDelegate, |
| locales.isEmpty()); |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| setApplicationLocalesUnchecked(appPackageName, userId, locales, |
| atomRecordForMetrics); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } finally { |
| logAppLocalesMetric(atomRecordForMetrics); |
| } |
| } |
| |
| private void setApplicationLocalesUnchecked(@NonNull String appPackageName, |
| @UserIdInt int userId, @NonNull LocaleList locales, |
| @NonNull AppLocaleChangedAtomRecord atomRecordForMetrics) { |
| if (DEBUG) { |
| Slog.d(TAG, "setApplicationLocales: setting locales for package " + appPackageName |
| + " and user " + userId); |
| } |
| |
| atomRecordForMetrics.setPrevLocales( |
| getApplicationLocalesUnchecked(appPackageName, userId).toLanguageTags()); |
| final ActivityTaskManagerInternal.PackageConfigurationUpdater updater = |
| mActivityTaskManagerInternal.createPackageConfigurationUpdater(appPackageName, |
| userId); |
| boolean isConfigChanged = updater.setLocales(locales).commit(); |
| |
| //We want to send the broadcasts only if config was actually updated on commit. |
| if (isConfigChanged) { |
| notifyAppWhoseLocaleChanged(appPackageName, userId, locales); |
| notifyInstallerOfAppWhoseLocaleChanged(appPackageName, userId, locales); |
| notifyRegisteredReceivers(appPackageName, userId, locales); |
| |
| mBackupHelper.notifyBackupManager(); |
| atomRecordForMetrics.setStatus( |
| FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__STATUS__CONFIG_COMMITTED); |
| } else { |
| atomRecordForMetrics.setStatus(FrameworkStatsLog |
| .APPLICATION_LOCALES_CHANGED__STATUS__CONFIG_UNCOMMITTED); |
| } |
| } |
| |
| /** |
| * Sends an implicit broadcast with action |
| * {@link android.content.Intent#ACTION_APPLICATION_LOCALE_CHANGED} |
| * to receivers with {@link android.Manifest.permission#READ_APP_SPECIFIC_LOCALES}. |
| */ |
| private void notifyRegisteredReceivers(String appPackageName, int userId, |
| LocaleList locales) { |
| Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED, |
| appPackageName, locales); |
| mContext.sendBroadcastAsUser(intent, UserHandle.of(userId), |
| Manifest.permission.READ_APP_SPECIFIC_LOCALES); |
| } |
| |
| /** |
| * Sends an explicit broadcast with action |
| * {@link android.content.Intent#ACTION_APPLICATION_LOCALE_CHANGED} to |
| * the installer (as per {@link android.content.pm.InstallSourceInfo#getInstallingPackageName}) |
| * of app whose locale has changed. |
| * |
| * <p><b>Note:</b> This is can be used by installers to deal with cases such as |
| * language-based APK Splits. |
| */ |
| void notifyInstallerOfAppWhoseLocaleChanged(String appPackageName, int userId, |
| LocaleList locales) { |
| String installingPackageName = getInstallingPackageName(appPackageName, userId); |
| if (installingPackageName != null) { |
| Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED, |
| appPackageName, locales); |
| //Set package name to ensure that only installer of the app receives this intent. |
| intent.setPackage(installingPackageName); |
| mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); |
| } |
| } |
| |
| /** |
| * Sends an explicit broadcast with action {@link android.content.Intent#ACTION_LOCALE_CHANGED} |
| * to the app whose locale has changed. |
| */ |
| private void notifyAppWhoseLocaleChanged(String appPackageName, int userId, |
| LocaleList locales) { |
| Intent intent = createBaseIntent(Intent.ACTION_LOCALE_CHANGED, appPackageName, locales); |
| //Set package name to ensure that only the app whose locale changed receives this intent. |
| intent.setPackage(appPackageName); |
| intent.addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS); |
| mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); |
| } |
| |
| static Intent createBaseIntent(String intentAction, String appPackageName, |
| LocaleList locales) { |
| return new Intent(intentAction) |
| .putExtra(Intent.EXTRA_PACKAGE_NAME, appPackageName) |
| .putExtra(Intent.EXTRA_LOCALE_LIST, locales) |
| .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND |
| | Intent.FLAG_RECEIVER_FOREGROUND); |
| } |
| |
| /** |
| * Checks if the package is owned by the calling app or not for the given user id. |
| * |
| * @throws IllegalArgumentException if package not found for given userid |
| */ |
| private boolean isPackageOwnedByCaller(String appPackageName, int userId, |
| @Nullable AppLocaleChangedAtomRecord atomRecordForMetrics, |
| @Nullable AppSupportedLocalesChangedAtomRecord appSupportedLocalesChangedAtomRecord) { |
| final int uid = getPackageUid(appPackageName, userId); |
| if (uid < 0) { |
| Slog.w(TAG, "Unknown package " + appPackageName + " for user " + userId); |
| if (atomRecordForMetrics != null) { |
| atomRecordForMetrics.setStatus(FrameworkStatsLog |
| .APPLICATION_LOCALES_CHANGED__STATUS__FAILURE_INVALID_TARGET_PACKAGE); |
| } else if (appSupportedLocalesChangedAtomRecord != null) { |
| appSupportedLocalesChangedAtomRecord.setStatus(FrameworkStatsLog |
| .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_INVALID_TARGET_PACKAGE); |
| } |
| throw new IllegalArgumentException("Unknown package: " + appPackageName |
| + " for user " + userId); |
| } |
| if (atomRecordForMetrics != null) { |
| atomRecordForMetrics.setTargetUid(uid); |
| } else if (appSupportedLocalesChangedAtomRecord != null) { |
| appSupportedLocalesChangedAtomRecord.setTargetUid(uid); |
| } |
| //Once valid package found, ignore the userId part for validating package ownership |
| //as apps with INTERACT_ACROSS_USERS permission could be changing locale for different user. |
| return UserHandle.isSameApp(Binder.getCallingUid(), uid); |
| } |
| |
| private void enforceChangeConfigurationPermission(@NonNull AppLocaleChangedAtomRecord |
| atomRecordForMetrics) { |
| try { |
| mContext.enforceCallingOrSelfPermission( |
| android.Manifest.permission.CHANGE_CONFIGURATION, "setApplicationLocales"); |
| } catch (SecurityException e) { |
| atomRecordForMetrics.setStatus(FrameworkStatsLog |
| .APPLICATION_LOCALES_CHANGED__STATUS__FAILURE_PERMISSION_ABSENT); |
| throw e; |
| } |
| } |
| |
| /** |
| * Returns the current UI locales for the specified app. |
| */ |
| @NonNull |
| public LocaleList getApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId) |
| throws RemoteException, IllegalArgumentException { |
| requireNonNull(appPackageName); |
| |
| //Allow apps with INTERACT_ACROSS_USERS permission to query locales for different user. |
| userId = mActivityManagerInternal.handleIncomingUser( |
| Binder.getCallingPid(), Binder.getCallingUid(), userId, |
| false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL, |
| "getApplicationLocales", /* callerPackage= */ null); |
| |
| // This function handles four types of query operations: |
| // 1.) A normal, non-privileged app querying its own locale. |
| // 2.) The installer of the given app querying locales of a package installed by said |
| // installer. |
| // 3.) The current input method querying locales of the current foreground app. |
| // 4.) A privileged system service querying locales of another package. |
| // The least privileged case is a normal app performing a query, so check that first and get |
| // locales if the package name is owned by the app. Next check if the calling app is the |
| // installer of the given app and get locales. Finally check if the calling app is the |
| // current input method, and that app is querying locales of the current foreground app. If |
| // neither conditions matched, check if the caller has the necessary permission and fetch |
| // locales. |
| if (!isPackageOwnedByCaller(appPackageName, userId, null, null) |
| && !isCallerInstaller(appPackageName, userId) |
| && !(isCallerFromCurrentInputMethod(userId) |
| && mActivityManagerInternal.isAppForeground( |
| getPackageUid(appPackageName, userId)))) { |
| enforceReadAppSpecificLocalesPermission(); |
| } |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| return getApplicationLocalesUnchecked(appPackageName, userId); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| @NonNull |
| private LocaleList getApplicationLocalesUnchecked(@NonNull String appPackageName, |
| @UserIdInt int userId) { |
| if (DEBUG) { |
| Slog.d(TAG, "getApplicationLocales: fetching locales for package " + appPackageName |
| + " and user " + userId); |
| } |
| |
| final ActivityTaskManagerInternal.PackageConfig appConfig = |
| mActivityTaskManagerInternal.getApplicationConfig(appPackageName, userId); |
| if (appConfig == null) { |
| if (DEBUG) { |
| Slog.d(TAG, "getApplicationLocales: application config not found for " |
| + appPackageName + " and user id " + userId); |
| } |
| return LocaleList.getEmptyLocaleList(); |
| } |
| LocaleList locales = appConfig.mLocales; |
| return locales != null ? locales : LocaleList.getEmptyLocaleList(); |
| } |
| |
| /** |
| * Checks if the calling app is the installer of the app whose locale changed. |
| */ |
| private boolean isCallerInstaller(String appPackageName, int userId) { |
| String installingPackageName = getInstallingPackageName(appPackageName, userId); |
| if (installingPackageName != null) { |
| // Get the uid of installer-on-record to compare with the calling uid. |
| int installerUid = getPackageUid(installingPackageName, userId); |
| return installerUid >= 0 && UserHandle.isSameApp(Binder.getCallingUid(), installerUid); |
| } |
| return false; |
| } |
| |
| /** |
| * Checks if the calling app is the current input method. |
| */ |
| private boolean isCallerFromCurrentInputMethod(int userId) { |
| if (!SystemProperties.getBoolean(PROP_ALLOW_IME_QUERY_APP_LOCALE, true)) { |
| return false; |
| } |
| |
| String currentInputMethod = Settings.Secure.getStringForUser( |
| mContext.getContentResolver(), |
| Settings.Secure.DEFAULT_INPUT_METHOD, |
| userId); |
| if (!TextUtils.isEmpty(currentInputMethod)) { |
| String inputMethodPkgName = ComponentName |
| .unflattenFromString(currentInputMethod) |
| .getPackageName(); |
| int inputMethodUid = getPackageUid(inputMethodPkgName, userId); |
| return inputMethodUid >= 0 && UserHandle.isSameApp(Binder.getCallingUid(), |
| inputMethodUid); |
| } |
| |
| return false; |
| } |
| |
| private void enforceReadAppSpecificLocalesPermission() { |
| mContext.enforceCallingOrSelfPermission( |
| android.Manifest.permission.READ_APP_SPECIFIC_LOCALES, |
| "getApplicationLocales"); |
| } |
| |
| private int getPackageUid(String appPackageName, int userId) { |
| try { |
| return mPackageManager |
| .getPackageUidAsUser(appPackageName, PackageInfoFlags.of(0), userId); |
| } catch (PackageManager.NameNotFoundException e) { |
| return Process.INVALID_UID; |
| } |
| } |
| |
| @Nullable |
| String getInstallingPackageName(String packageName, int userId) { |
| try { |
| return mContext.createContextAsUser(UserHandle.of(userId), /* flags= */ |
| 0).getPackageManager().getInstallSourceInfo( |
| packageName).getInstallingPackageName(); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.w(TAG, "Package not found " + packageName); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the current system locales. |
| */ |
| @NonNull |
| public LocaleList getSystemLocales() throws RemoteException { |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| return getSystemLocalesUnchecked(); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| @NonNull |
| private LocaleList getSystemLocalesUnchecked() throws RemoteException { |
| LocaleList systemLocales = null; |
| Configuration conf = ActivityManager.getService().getConfiguration(); |
| if (conf != null) { |
| systemLocales = conf.getLocales(); |
| } |
| if (systemLocales == null) { |
| systemLocales = LocaleList.getEmptyLocaleList(); |
| } |
| return systemLocales; |
| } |
| |
| private void logAppLocalesMetric(@NonNull AppLocaleChangedAtomRecord atomRecordForMetrics) { |
| FrameworkStatsLog.write(FrameworkStatsLog.APPLICATION_LOCALES_CHANGED, |
| atomRecordForMetrics.mCallingUid, |
| atomRecordForMetrics.mTargetUid, |
| atomRecordForMetrics.mNewLocales, |
| atomRecordForMetrics.mPrevLocales, |
| atomRecordForMetrics.mStatus, |
| atomRecordForMetrics.mCaller); |
| } |
| |
| /** |
| * Storing an override {@link LocaleConfig} for a specified app. |
| */ |
| public void setOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId, |
| @Nullable LocaleConfig localeConfig) throws IllegalArgumentException { |
| if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) { |
| return; |
| } |
| |
| AppSupportedLocalesChangedAtomRecord atomRecord = new AppSupportedLocalesChangedAtomRecord( |
| Binder.getCallingUid()); |
| try { |
| 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, null, atomRecord)) { |
| enforceSetAppSpecificLocaleConfigPermission(atomRecord); |
| } |
| |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| setOverrideLocaleConfigUnchecked(appPackageName, userId, localeConfig, atomRecord); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } finally { |
| logAppSupportedLocalesChangedMetric(atomRecord); |
| } |
| } |
| |
| private void setOverrideLocaleConfigUnchecked(@NonNull String appPackageName, |
| @UserIdInt int userId, @Nullable LocaleConfig overrideLocaleConfig, |
| @NonNull AppSupportedLocalesChangedAtomRecord atomRecord) { |
| synchronized (mWriteLock) { |
| if (DEBUG) { |
| Slog.d(TAG, |
| "set the override LocaleConfig for package " + appPackageName + " and user " |
| + userId); |
| } |
| LocaleConfig resLocaleConfig = null; |
| try { |
| resLocaleConfig = LocaleConfig.fromContextIgnoringOverride( |
| mContext.createPackageContext(appPackageName, 0)); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.e(TAG, "Unknown package name " + appPackageName); |
| return; |
| } |
| final File file = getXmlFileNameForUser(appPackageName, userId); |
| |
| if (overrideLocaleConfig == null) { |
| if (file.exists()) { |
| Slog.d(TAG, "remove the override LocaleConfig"); |
| file.delete(); |
| } |
| removeUnsupportedAppLocales(appPackageName, userId, resLocaleConfig, |
| FrameworkStatsLog |
| .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE |
| ); |
| atomRecord.setOverrideRemoved(true); |
| atomRecord.setStatus(FrameworkStatsLog |
| .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS); |
| return; |
| } else { |
| if (overrideLocaleConfig.isSameLocaleConfig( |
| getOverrideLocaleConfig(appPackageName, userId))) { |
| Slog.d(TAG, "the same override, ignore it"); |
| atomRecord.setSameAsPrevConfig(true); |
| return; |
| } |
| |
| 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()); |
| } |
| atomRecord.setNumLocales(localeList.size()); |
| |
| // 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); |
| } |
| atomRecord.setStatus(FrameworkStatsLog |
| .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_WRITE_TO_STORAGE); |
| return; |
| } |
| atomicFile.finishWrite(stream); |
| // Clear per-app locales if they are not in the override LocaleConfig. |
| removeUnsupportedAppLocales(appPackageName, userId, overrideLocaleConfig, |
| FrameworkStatsLog |
| .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE |
| ); |
| if (overrideLocaleConfig.isSameLocaleConfig(resLocaleConfig)) { |
| Slog.d(TAG, "setOverrideLocaleConfig, same as the app's LocaleConfig"); |
| atomRecord.setSameAsResConfig(true); |
| } |
| atomRecord.setStatus(FrameworkStatsLog |
| .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS); |
| if (DEBUG) { |
| Slog.i(TAG, "Successfully written to " + atomicFile); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Checks if the per-app locales are in the LocaleConfig. Per-app locales missing from the |
| * LocaleConfig will be removed. |
| * |
| * <p><b>Note:</b> Check whether to remove the per-app locales when the app is upgraded or |
| * the LocaleConfig is overridden. |
| */ |
| void removeUnsupportedAppLocales(String appPackageName, int userId, |
| LocaleConfig localeConfig, int caller) { |
| LocaleList appLocales = getApplicationLocalesUnchecked(appPackageName, userId); |
| // Remove the per-app locales from the locale list if they don't exist in the LocaleConfig. |
| boolean resetAppLocales = false; |
| List<Locale> newAppLocales = new ArrayList<Locale>(); |
| |
| if (localeConfig == null) { |
| //Reset the app locales to the system default |
| Slog.i(TAG, "There is no LocaleConfig, reset app locales"); |
| resetAppLocales = true; |
| } else { |
| for (int i = 0; i < appLocales.size(); i++) { |
| if (!localeConfig.containsLocale(appLocales.get(i))) { |
| Slog.i(TAG, "Missing from the LocaleConfig, reset 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)), |
| mBackupHelper.areLocalesSetFromDelegate(userId, appPackageName), caller); |
| } catch (RemoteException | IllegalArgumentException e) { |
| Slog.e(TAG, "Could not set locales for " + appPackageName, e); |
| } |
| } |
| } |
| |
| private void enforceSetAppSpecificLocaleConfigPermission( |
| AppSupportedLocalesChangedAtomRecord atomRecord) { |
| try { |
| mContext.enforceCallingOrSelfPermission( |
| android.Manifest.permission.SET_APP_SPECIFIC_LOCALECONFIG, |
| "setOverrideLocaleConfig"); |
| } catch (SecurityException e) { |
| atomRecord.setStatus(FrameworkStatsLog |
| .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_PERMISSION_ABSENT); |
| throw e; |
| } |
| } |
| |
| /** |
| * Returns the override LocaleConfig for a specified app. |
| */ |
| @Nullable |
| public LocaleConfig getOverrideLocaleConfig(@NonNull String appPackageName, |
| @UserIdInt int userId) { |
| 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) { |
| final File dir = new File(Environment.getDataSystemCeDirectory(userId), LOCALE_CONFIGS); |
| return new File(dir, appPackageName + SUFFIX_FILE_NAME); |
| } |
| |
| private void logAppSupportedLocalesChangedMetric( |
| @NonNull AppSupportedLocalesChangedAtomRecord atomRecord) { |
| FrameworkStatsLog.write(FrameworkStatsLog.APP_SUPPORTED_LOCALES_CHANGED, |
| atomRecord.mCallingUid, |
| atomRecord.mTargetUid, |
| atomRecord.mNumLocales, |
| atomRecord.mOverrideRemoved, |
| atomRecord.mSameAsResConfig, |
| atomRecord.mSameAsPrevConfig, |
| atomRecord.mStatus); |
| } |
| } |