• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.locales;
18 
19 import static java.util.Objects.requireNonNull;
20 
21 import android.Manifest;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.UserIdInt;
25 import android.app.ActivityManager;
26 import android.app.ActivityManagerInternal;
27 import android.app.ILocaleManager;
28 import android.app.LocaleConfig;
29 import android.content.ComponentName;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.pm.PackageManager;
33 import android.content.pm.PackageManager.PackageInfoFlags;
34 import android.content.res.Configuration;
35 import android.os.Binder;
36 import android.os.Environment;
37 import android.os.HandlerThread;
38 import android.os.LocaleList;
39 import android.os.Process;
40 import android.os.RemoteException;
41 import android.os.ResultReceiver;
42 import android.os.ShellCallback;
43 import android.os.SystemProperties;
44 import android.os.UserHandle;
45 import android.provider.Settings;
46 import android.text.TextUtils;
47 import android.util.AtomicFile;
48 import android.util.Slog;
49 import android.util.Xml;
50 
51 import com.android.internal.annotations.VisibleForTesting;
52 import com.android.internal.content.PackageMonitor;
53 import com.android.internal.util.FrameworkStatsLog;
54 import com.android.internal.util.XmlUtils;
55 import com.android.modules.utils.TypedXmlPullParser;
56 import com.android.modules.utils.TypedXmlSerializer;
57 import com.android.server.LocalServices;
58 import com.android.server.SystemService;
59 import com.android.server.wm.ActivityTaskManagerInternal;
60 
61 import org.xmlpull.v1.XmlPullParserException;
62 
63 import java.io.ByteArrayOutputStream;
64 import java.io.File;
65 import java.io.FileDescriptor;
66 import java.io.FileInputStream;
67 import java.io.FileOutputStream;
68 import java.io.IOException;
69 import java.io.InputStream;
70 import java.nio.charset.StandardCharsets;
71 import java.util.ArrayList;
72 import java.util.Arrays;
73 import java.util.List;
74 import java.util.Locale;
75 
76 /**
77  * The implementation of ILocaleManager.aidl.
78  *
79  * <p>This service is API entry point for storing app-specific UI locales and an override
80  * {@link LocaleConfig} for a specified app.
81  */
82 public class LocaleManagerService extends SystemService {
83     private static final String TAG = "LocaleManagerService";
84     // The feature flag control that allows the active IME to query the locales of the foreground
85     // app.
86     private static final String PROP_ALLOW_IME_QUERY_APP_LOCALE =
87             "i18n.feature.allow_ime_query_app_locale";
88     // The feature flag control that the application can dynamically override the LocaleConfig.
89     private static final String PROP_DYNAMIC_LOCALES_CHANGE =
90             "i18n.feature.dynamic_locales_change";
91     private static final String LOCALE_CONFIGS = "locale_configs";
92     private static final String SUFFIX_FILE_NAME = ".xml";
93     private static final String ATTR_NAME = "name";
94 
95     final Context mContext;
96     private final LocaleManagerService.LocaleManagerBinderService mBinderService;
97     private ActivityTaskManagerInternal mActivityTaskManagerInternal;
98     private ActivityManagerInternal mActivityManagerInternal;
99     private PackageManager mPackageManager;
100 
101     private LocaleManagerBackupHelper mBackupHelper;
102 
103     private final PackageMonitor mPackageMonitor;
104 
105     private final Object mWriteLock = new Object();
106 
107     public static final boolean DEBUG = false;
108 
LocaleManagerService(Context context)109     public LocaleManagerService(Context context) {
110         super(context);
111         mContext = context;
112         mBinderService = new LocaleManagerBinderService();
113         mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
114         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
115         mPackageManager = mContext.getPackageManager();
116 
117         HandlerThread broadcastHandlerThread = new HandlerThread(TAG,
118                 Process.THREAD_PRIORITY_BACKGROUND);
119         broadcastHandlerThread.start();
120 
121         SystemAppUpdateTracker systemAppUpdateTracker =
122                 new SystemAppUpdateTracker(this);
123         broadcastHandlerThread.getThreadHandler().postAtFrontOfQueue(new Runnable() {
124             @Override
125             public void run() {
126                 systemAppUpdateTracker.init();
127             }
128         });
129 
130         mBackupHelper = new LocaleManagerBackupHelper(this,
131                 mPackageManager, broadcastHandlerThread);
132         mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper,
133                 systemAppUpdateTracker, this);
134         mPackageMonitor.register(context, broadcastHandlerThread.getLooper(),
135                 UserHandle.ALL,
136                 true);
137     }
138 
139     @VisibleForTesting
LocaleManagerService(Context context, ActivityTaskManagerInternal activityTaskManagerInternal, ActivityManagerInternal activityManagerInternal, PackageManager packageManager, LocaleManagerBackupHelper localeManagerBackupHelper, PackageMonitor packageMonitor)140     LocaleManagerService(Context context, ActivityTaskManagerInternal activityTaskManagerInternal,
141             ActivityManagerInternal activityManagerInternal,
142             PackageManager packageManager,
143             LocaleManagerBackupHelper localeManagerBackupHelper,
144             PackageMonitor packageMonitor) {
145         super(context);
146         mContext = context;
147         mBinderService = new LocaleManagerBinderService();
148         mActivityTaskManagerInternal = activityTaskManagerInternal;
149         mActivityManagerInternal = activityManagerInternal;
150         mPackageManager = packageManager;
151         mBackupHelper = localeManagerBackupHelper;
152         mPackageMonitor = packageMonitor;
153     }
154 
155     @Override
onStart()156     public void onStart() {
157         publishBinderService(Context.LOCALE_SERVICE, mBinderService);
158         LocalServices.addService(LocaleManagerInternal.class, new LocaleManagerInternalImpl());
159     }
160 
161     private final class LocaleManagerInternalImpl extends LocaleManagerInternal {
162 
163         @Override
getBackupPayload(int userId)164         public @Nullable byte[] getBackupPayload(int userId) {
165             checkCallerIsSystem();
166             return mBackupHelper.getBackupPayload(userId);
167         }
168 
169         @Override
stageAndApplyRestoredPayload(byte[] payload, int userId)170         public void stageAndApplyRestoredPayload(byte[] payload, int userId) {
171             mBackupHelper.stageAndApplyRestoredPayload(payload, userId);
172         }
173 
checkCallerIsSystem()174         private void checkCallerIsSystem() {
175             if (Binder.getCallingUid() != Process.SYSTEM_UID) {
176                 throw new SecurityException("Caller is not system.");
177             }
178         }
179     }
180 
181     private final class LocaleManagerBinderService extends ILocaleManager.Stub {
182         @Override
setApplicationLocales(@onNull String appPackageName, @UserIdInt int userId, @NonNull LocaleList locales, boolean fromDelegate)183         public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId,
184                 @NonNull LocaleList locales, boolean fromDelegate) throws RemoteException {
185             int caller = fromDelegate
186                     ? FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DELEGATE
187                     : FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS;
188             LocaleManagerService.this.setApplicationLocales(appPackageName, userId, locales,
189                     fromDelegate, caller);
190         }
191 
192         @Override
193         @NonNull
getApplicationLocales(@onNull String appPackageName, @UserIdInt int userId)194         public LocaleList getApplicationLocales(@NonNull String appPackageName,
195                 @UserIdInt int userId) throws RemoteException {
196             return LocaleManagerService.this.getApplicationLocales(appPackageName, userId);
197         }
198 
199         @Override
200         @NonNull
getSystemLocales()201         public LocaleList getSystemLocales() throws RemoteException {
202             return LocaleManagerService.this.getSystemLocales();
203         }
204 
205         @Override
setOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId, @Nullable LocaleConfig localeConfig)206         public void setOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId,
207                 @Nullable LocaleConfig localeConfig) throws RemoteException {
208             LocaleManagerService.this.setOverrideLocaleConfig(appPackageName, userId, localeConfig);
209         }
210 
211         @Override
212         @Nullable
getOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId)213         public LocaleConfig getOverrideLocaleConfig(@NonNull String appPackageName,
214                 @UserIdInt int userId) {
215             return LocaleManagerService.this.getOverrideLocaleConfig(appPackageName, userId);
216         }
217 
218         @Override
onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver)219         public void onShellCommand(FileDescriptor in, FileDescriptor out,
220                 FileDescriptor err, String[] args, ShellCallback callback,
221                 ResultReceiver resultReceiver) {
222             (new LocaleManagerShellCommand(mBinderService))
223                     .exec(this, in, out, err, args, callback, resultReceiver);
224         }
225 
226     }
227 
228     /**
229      * Sets the current UI locales for a specified app.
230      */
setApplicationLocales(@onNull String appPackageName, @UserIdInt int userId, @NonNull LocaleList locales, boolean fromDelegate, int caller)231     public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId,
232             @NonNull LocaleList locales, boolean fromDelegate, int caller)
233             throws RemoteException, IllegalArgumentException {
234         AppLocaleChangedAtomRecord atomRecordForMetrics = new
235                 AppLocaleChangedAtomRecord(Binder.getCallingUid());
236         try {
237             requireNonNull(appPackageName);
238             requireNonNull(locales);
239             atomRecordForMetrics.setCaller(caller);
240             atomRecordForMetrics.setNewLocales(locales.toLanguageTags());
241             //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user.
242             userId = mActivityManagerInternal.handleIncomingUser(
243                     Binder.getCallingPid(), Binder.getCallingUid(), userId,
244                     false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
245                     "setApplicationLocales", /* callerPackage= */ null);
246 
247             // This function handles two types of set operations:
248             // 1.) A normal, non-privileged app setting its own locale.
249             // 2.) A privileged system service setting locales of another package.
250             // The least privileged case is a normal app performing a set, so check that first and
251             // set locales if the package name is owned by the app. Next, check if the caller has
252             // the necessary permission and set locales.
253             boolean isCallerOwner = isPackageOwnedByCaller(appPackageName, userId,
254                     atomRecordForMetrics, null);
255             if (!isCallerOwner) {
256                 enforceChangeConfigurationPermission(atomRecordForMetrics);
257             }
258             mBackupHelper.persistLocalesModificationInfo(userId, appPackageName, fromDelegate,
259                     locales.isEmpty());
260             final long token = Binder.clearCallingIdentity();
261             try {
262                 setApplicationLocalesUnchecked(appPackageName, userId, locales,
263                         atomRecordForMetrics);
264             } finally {
265                 Binder.restoreCallingIdentity(token);
266             }
267         } finally {
268             logAppLocalesMetric(atomRecordForMetrics);
269         }
270     }
271 
setApplicationLocalesUnchecked(@onNull String appPackageName, @UserIdInt int userId, @NonNull LocaleList locales, @NonNull AppLocaleChangedAtomRecord atomRecordForMetrics)272     private void setApplicationLocalesUnchecked(@NonNull String appPackageName,
273             @UserIdInt int userId, @NonNull LocaleList locales,
274             @NonNull AppLocaleChangedAtomRecord atomRecordForMetrics) {
275         if (DEBUG) {
276             Slog.d(TAG, "setApplicationLocales: setting locales for package " + appPackageName
277                     + " and user " + userId);
278         }
279 
280         atomRecordForMetrics.setPrevLocales(
281                 getApplicationLocalesUnchecked(appPackageName, userId).toLanguageTags());
282         final ActivityTaskManagerInternal.PackageConfigurationUpdater updater =
283                 mActivityTaskManagerInternal.createPackageConfigurationUpdater(appPackageName,
284                         userId);
285         boolean isConfigChanged = updater.setLocales(locales).commit();
286 
287         //We want to send the broadcasts only if config was actually updated on commit.
288         if (isConfigChanged) {
289             notifyAppWhoseLocaleChanged(appPackageName, userId, locales);
290             notifyInstallerOfAppWhoseLocaleChanged(appPackageName, userId, locales);
291             notifyRegisteredReceivers(appPackageName, userId, locales);
292 
293             mBackupHelper.notifyBackupManager();
294             atomRecordForMetrics.setStatus(
295                     FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__STATUS__CONFIG_COMMITTED);
296         } else {
297             atomRecordForMetrics.setStatus(FrameworkStatsLog
298                     .APPLICATION_LOCALES_CHANGED__STATUS__CONFIG_UNCOMMITTED);
299         }
300     }
301 
302     /**
303      * Sends an implicit broadcast with action
304      * {@link android.content.Intent#ACTION_APPLICATION_LOCALE_CHANGED}
305      * to receivers with {@link android.Manifest.permission#READ_APP_SPECIFIC_LOCALES}.
306      */
notifyRegisteredReceivers(String appPackageName, int userId, LocaleList locales)307     private void notifyRegisteredReceivers(String appPackageName, int userId,
308             LocaleList locales) {
309         Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED,
310                 appPackageName, locales);
311         mContext.sendBroadcastAsUser(intent, UserHandle.of(userId),
312                 Manifest.permission.READ_APP_SPECIFIC_LOCALES);
313     }
314 
315     /**
316      * Sends an explicit broadcast with action
317      * {@link android.content.Intent#ACTION_APPLICATION_LOCALE_CHANGED} to
318      * the installer (as per {@link android.content.pm.InstallSourceInfo#getInstallingPackageName})
319      * of app whose locale has changed.
320      *
321      * <p><b>Note:</b> This is can be used by installers to deal with cases such as
322      * language-based APK Splits.
323      */
notifyInstallerOfAppWhoseLocaleChanged(String appPackageName, int userId, LocaleList locales)324     void notifyInstallerOfAppWhoseLocaleChanged(String appPackageName, int userId,
325             LocaleList locales) {
326         String installingPackageName = getInstallingPackageName(appPackageName, userId);
327         if (installingPackageName != null) {
328             Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED,
329                     appPackageName, locales);
330             //Set package name to ensure that only installer of the app receives this intent.
331             intent.setPackage(installingPackageName);
332             mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
333         }
334     }
335 
336     /**
337      * Sends an explicit broadcast with action {@link android.content.Intent#ACTION_LOCALE_CHANGED}
338      * to the app whose locale has changed.
339      */
notifyAppWhoseLocaleChanged(String appPackageName, int userId, LocaleList locales)340     private void notifyAppWhoseLocaleChanged(String appPackageName, int userId,
341             LocaleList locales) {
342         Intent intent = createBaseIntent(Intent.ACTION_LOCALE_CHANGED, appPackageName, locales);
343         //Set package name to ensure that only the app whose locale changed receives this intent.
344         intent.setPackage(appPackageName);
345         intent.addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
346         mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
347     }
348 
createBaseIntent(String intentAction, String appPackageName, LocaleList locales)349     static Intent createBaseIntent(String intentAction, String appPackageName,
350             LocaleList locales) {
351         return new Intent(intentAction)
352                 .putExtra(Intent.EXTRA_PACKAGE_NAME, appPackageName)
353                 .putExtra(Intent.EXTRA_LOCALE_LIST, locales)
354                 .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
355                         | Intent.FLAG_RECEIVER_FOREGROUND);
356     }
357 
358     /**
359      * Checks if the package is owned by the calling app or not for the given user id.
360      *
361      * @throws IllegalArgumentException if package not found for given userid
362      */
isPackageOwnedByCaller(String appPackageName, int userId, @Nullable AppLocaleChangedAtomRecord atomRecordForMetrics, @Nullable AppSupportedLocalesChangedAtomRecord appSupportedLocalesChangedAtomRecord)363     private boolean isPackageOwnedByCaller(String appPackageName, int userId,
364             @Nullable AppLocaleChangedAtomRecord atomRecordForMetrics,
365             @Nullable AppSupportedLocalesChangedAtomRecord appSupportedLocalesChangedAtomRecord) {
366         final int uid = getPackageUid(appPackageName, userId);
367         if (uid < 0) {
368             Slog.w(TAG, "Unknown package " + appPackageName + " for user " + userId);
369             if (atomRecordForMetrics != null) {
370                 atomRecordForMetrics.setStatus(FrameworkStatsLog
371                         .APPLICATION_LOCALES_CHANGED__STATUS__FAILURE_INVALID_TARGET_PACKAGE);
372             } else if (appSupportedLocalesChangedAtomRecord != null) {
373                 appSupportedLocalesChangedAtomRecord.setStatus(FrameworkStatsLog
374                         .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_INVALID_TARGET_PACKAGE);
375             }
376             throw new IllegalArgumentException("Unknown package: " + appPackageName
377                     + " for user " + userId);
378         }
379         if (atomRecordForMetrics != null) {
380             atomRecordForMetrics.setTargetUid(uid);
381         } else if (appSupportedLocalesChangedAtomRecord != null) {
382             appSupportedLocalesChangedAtomRecord.setTargetUid(uid);
383         }
384         //Once valid package found, ignore the userId part for validating package ownership
385         //as apps with INTERACT_ACROSS_USERS permission could be changing locale for different user.
386         return UserHandle.isSameApp(Binder.getCallingUid(), uid);
387     }
388 
enforceChangeConfigurationPermission(@onNull AppLocaleChangedAtomRecord atomRecordForMetrics)389     private void enforceChangeConfigurationPermission(@NonNull AppLocaleChangedAtomRecord
390             atomRecordForMetrics) {
391         try {
392             mContext.enforceCallingOrSelfPermission(
393                     android.Manifest.permission.CHANGE_CONFIGURATION, "setApplicationLocales");
394         } catch (SecurityException e) {
395             atomRecordForMetrics.setStatus(FrameworkStatsLog
396                     .APPLICATION_LOCALES_CHANGED__STATUS__FAILURE_PERMISSION_ABSENT);
397             throw e;
398         }
399     }
400 
401     /**
402      * Returns the current UI locales for the specified app.
403      */
404     @NonNull
getApplicationLocales(@onNull String appPackageName, @UserIdInt int userId)405     public LocaleList getApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId)
406             throws RemoteException, IllegalArgumentException {
407         requireNonNull(appPackageName);
408 
409         //Allow apps with INTERACT_ACROSS_USERS permission to query locales for different user.
410         userId = mActivityManagerInternal.handleIncomingUser(
411                 Binder.getCallingPid(), Binder.getCallingUid(), userId,
412                 false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
413                 "getApplicationLocales", /* callerPackage= */ null);
414 
415         // This function handles four types of query operations:
416         // 1.) A normal, non-privileged app querying its own locale.
417         // 2.) The installer of the given app querying locales of a package installed by said
418         // installer.
419         // 3.) The current input method querying locales of the current foreground app.
420         // 4.) A privileged system service querying locales of another package.
421         // The least privileged case is a normal app performing a query, so check that first and get
422         // locales if the package name is owned by the app. Next check if the calling app is the
423         // installer of the given app and get locales. Finally check if the calling app is the
424         // current input method, and that app is querying locales of the current foreground app. If
425         // neither conditions matched, check if the caller has the necessary permission and fetch
426         // locales.
427         if (!isPackageOwnedByCaller(appPackageName, userId, null, null)
428                 && !isCallerInstaller(appPackageName, userId)
429                 && !(isCallerFromCurrentInputMethod(userId)
430                     && mActivityManagerInternal.isAppForeground(
431                             getPackageUid(appPackageName, userId)))) {
432             enforceReadAppSpecificLocalesPermission();
433         }
434         final long token = Binder.clearCallingIdentity();
435         try {
436             return getApplicationLocalesUnchecked(appPackageName, userId);
437         } finally {
438             Binder.restoreCallingIdentity(token);
439         }
440     }
441 
442     @NonNull
getApplicationLocalesUnchecked(@onNull String appPackageName, @UserIdInt int userId)443     private LocaleList getApplicationLocalesUnchecked(@NonNull String appPackageName,
444             @UserIdInt int userId) {
445         if (DEBUG) {
446             Slog.d(TAG, "getApplicationLocales: fetching locales for package " + appPackageName
447                     + " and user " + userId);
448         }
449 
450         final ActivityTaskManagerInternal.PackageConfig appConfig =
451                 mActivityTaskManagerInternal.getApplicationConfig(appPackageName, userId);
452         if (appConfig == null) {
453             if (DEBUG) {
454                 Slog.d(TAG, "getApplicationLocales: application config not found for "
455                         + appPackageName + " and user id " + userId);
456             }
457             return LocaleList.getEmptyLocaleList();
458         }
459         LocaleList locales = appConfig.mLocales;
460         return locales != null ? locales : LocaleList.getEmptyLocaleList();
461     }
462 
463     /**
464      * Checks if the calling app is the installer of the app whose locale changed.
465      */
isCallerInstaller(String appPackageName, int userId)466     private boolean isCallerInstaller(String appPackageName, int userId) {
467         String installingPackageName = getInstallingPackageName(appPackageName, userId);
468         if (installingPackageName != null) {
469             // Get the uid of installer-on-record to compare with the calling uid.
470             int installerUid = getPackageUid(installingPackageName, userId);
471             return installerUid >= 0 && UserHandle.isSameApp(Binder.getCallingUid(), installerUid);
472         }
473         return false;
474     }
475 
476     /**
477      * Checks if the calling app is the current input method.
478      */
isCallerFromCurrentInputMethod(int userId)479     private boolean isCallerFromCurrentInputMethod(int userId) {
480         if (!SystemProperties.getBoolean(PROP_ALLOW_IME_QUERY_APP_LOCALE, true)) {
481             return false;
482         }
483 
484         String currentInputMethod = Settings.Secure.getStringForUser(
485                 mContext.getContentResolver(),
486                 Settings.Secure.DEFAULT_INPUT_METHOD,
487                 userId);
488         if (!TextUtils.isEmpty(currentInputMethod)) {
489             String inputMethodPkgName = ComponentName
490                     .unflattenFromString(currentInputMethod)
491                     .getPackageName();
492             int inputMethodUid = getPackageUid(inputMethodPkgName, userId);
493             return inputMethodUid >= 0 && UserHandle.isSameApp(Binder.getCallingUid(),
494                     inputMethodUid);
495         }
496 
497         return false;
498     }
499 
enforceReadAppSpecificLocalesPermission()500     private void enforceReadAppSpecificLocalesPermission() {
501         mContext.enforceCallingOrSelfPermission(
502                 android.Manifest.permission.READ_APP_SPECIFIC_LOCALES,
503                 "getApplicationLocales");
504     }
505 
getPackageUid(String appPackageName, int userId)506     private int getPackageUid(String appPackageName, int userId) {
507         try {
508             return mPackageManager
509                     .getPackageUidAsUser(appPackageName, PackageInfoFlags.of(0), userId);
510         } catch (PackageManager.NameNotFoundException e) {
511             return Process.INVALID_UID;
512         }
513     }
514 
515     @Nullable
getInstallingPackageName(String packageName, int userId)516     String getInstallingPackageName(String packageName, int userId) {
517         try {
518             return mContext.createContextAsUser(UserHandle.of(userId), /* flags= */
519                     0).getPackageManager().getInstallSourceInfo(
520                     packageName).getInstallingPackageName();
521         } catch (PackageManager.NameNotFoundException e) {
522             Slog.w(TAG, "Package not found " + packageName);
523         }
524         return null;
525     }
526 
527     /**
528      * Returns the current system locales.
529      */
530     @NonNull
getSystemLocales()531     public LocaleList getSystemLocales() throws RemoteException {
532         final long token = Binder.clearCallingIdentity();
533         try {
534             return getSystemLocalesUnchecked();
535         } finally {
536             Binder.restoreCallingIdentity(token);
537         }
538     }
539 
540     @NonNull
getSystemLocalesUnchecked()541     private LocaleList getSystemLocalesUnchecked() throws RemoteException {
542         LocaleList systemLocales = null;
543         Configuration conf = ActivityManager.getService().getConfiguration();
544         if (conf != null) {
545             systemLocales = conf.getLocales();
546         }
547         if (systemLocales == null) {
548             systemLocales = LocaleList.getEmptyLocaleList();
549         }
550         return systemLocales;
551     }
552 
logAppLocalesMetric(@onNull AppLocaleChangedAtomRecord atomRecordForMetrics)553     private void logAppLocalesMetric(@NonNull AppLocaleChangedAtomRecord atomRecordForMetrics) {
554         FrameworkStatsLog.write(FrameworkStatsLog.APPLICATION_LOCALES_CHANGED,
555                 atomRecordForMetrics.mCallingUid,
556                 atomRecordForMetrics.mTargetUid,
557                 atomRecordForMetrics.mNewLocales,
558                 atomRecordForMetrics.mPrevLocales,
559                 atomRecordForMetrics.mStatus,
560                 atomRecordForMetrics.mCaller);
561     }
562 
563     /**
564      * Storing an override {@link LocaleConfig} for a specified app.
565      */
setOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId, @Nullable LocaleConfig localeConfig)566     public void setOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId,
567             @Nullable LocaleConfig localeConfig) throws IllegalArgumentException {
568         if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) {
569             return;
570         }
571 
572         AppSupportedLocalesChangedAtomRecord atomRecord = new AppSupportedLocalesChangedAtomRecord(
573                 Binder.getCallingUid());
574         try {
575             requireNonNull(appPackageName);
576 
577             //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user.
578             userId = mActivityManagerInternal.handleIncomingUser(
579                     Binder.getCallingPid(), Binder.getCallingUid(), userId,
580                     false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
581                     "setOverrideLocaleConfig", /* callerPackage= */ null);
582 
583             // This function handles two types of set operations:
584             // 1.) A normal, an app overrides its own LocaleConfig.
585             // 2.) A privileged system application or service is granted the necessary permission to
586             // override a LocaleConfig of another package.
587             if (!isPackageOwnedByCaller(appPackageName, userId, null, atomRecord)) {
588                 enforceSetAppSpecificLocaleConfigPermission(atomRecord);
589             }
590 
591             final long token = Binder.clearCallingIdentity();
592             try {
593                 setOverrideLocaleConfigUnchecked(appPackageName, userId, localeConfig, atomRecord);
594             } finally {
595                 Binder.restoreCallingIdentity(token);
596             }
597         } finally {
598             logAppSupportedLocalesChangedMetric(atomRecord);
599         }
600     }
601 
setOverrideLocaleConfigUnchecked(@onNull String appPackageName, @UserIdInt int userId, @Nullable LocaleConfig overrideLocaleConfig, @NonNull AppSupportedLocalesChangedAtomRecord atomRecord)602     private void setOverrideLocaleConfigUnchecked(@NonNull String appPackageName,
603             @UserIdInt int userId, @Nullable LocaleConfig overrideLocaleConfig,
604             @NonNull AppSupportedLocalesChangedAtomRecord atomRecord) {
605         synchronized (mWriteLock) {
606             if (DEBUG) {
607                 Slog.d(TAG,
608                         "set the override LocaleConfig for package " + appPackageName + " and user "
609                                 + userId);
610             }
611             LocaleConfig resLocaleConfig = null;
612             try {
613                 resLocaleConfig = LocaleConfig.fromContextIgnoringOverride(
614                         mContext.createPackageContext(appPackageName, 0));
615             } catch (PackageManager.NameNotFoundException e) {
616                 Slog.e(TAG, "Unknown package name " + appPackageName);
617                 return;
618             }
619             final File file = getXmlFileNameForUser(appPackageName, userId);
620 
621             if (overrideLocaleConfig == null) {
622                 if (file.exists()) {
623                     Slog.d(TAG, "remove the override LocaleConfig");
624                     file.delete();
625                 }
626                 removeUnsupportedAppLocales(appPackageName, userId, resLocaleConfig,
627                         FrameworkStatsLog
628                                 .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE
629                 );
630                 atomRecord.setOverrideRemoved(true);
631                 atomRecord.setStatus(FrameworkStatsLog
632                         .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS);
633                 return;
634             } else {
635                 if (overrideLocaleConfig.isSameLocaleConfig(
636                         getOverrideLocaleConfig(appPackageName, userId))) {
637                     Slog.d(TAG, "the same override, ignore it");
638                     atomRecord.setSameAsPrevConfig(true);
639                     return;
640                 }
641 
642                 LocaleList localeList = overrideLocaleConfig.getSupportedLocales();
643                 // Normally the LocaleList object should not be null. However we reassign it as the
644                 // empty list in case it happens.
645                 if (localeList == null) {
646                     localeList = LocaleList.getEmptyLocaleList();
647                 }
648                 if (DEBUG) {
649                     Slog.d(TAG,
650                             "setOverrideLocaleConfig, localeList: " + localeList.toLanguageTags());
651                 }
652                 atomRecord.setNumLocales(localeList.size());
653 
654                 // Store the override LocaleConfig to the file storage.
655                 final AtomicFile atomicFile = new AtomicFile(file);
656                 FileOutputStream stream = null;
657                 try {
658                     stream = atomicFile.startWrite();
659                     stream.write(toXmlByteArray(localeList));
660                 } catch (Exception e) {
661                     Slog.e(TAG, "Failed to write file " + atomicFile, e);
662                     if (stream != null) {
663                         atomicFile.failWrite(stream);
664                     }
665                     atomRecord.setStatus(FrameworkStatsLog
666                             .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_WRITE_TO_STORAGE);
667                     return;
668                 }
669                 atomicFile.finishWrite(stream);
670                 // Clear per-app locales if they are not in the override LocaleConfig.
671                 removeUnsupportedAppLocales(appPackageName, userId, overrideLocaleConfig,
672                         FrameworkStatsLog
673                                 .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE
674                 );
675                 if (overrideLocaleConfig.isSameLocaleConfig(resLocaleConfig)) {
676                     Slog.d(TAG, "setOverrideLocaleConfig, same as the app's LocaleConfig");
677                     atomRecord.setSameAsResConfig(true);
678                 }
679                 atomRecord.setStatus(FrameworkStatsLog
680                         .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS);
681                 if (DEBUG) {
682                     Slog.i(TAG, "Successfully written to " + atomicFile);
683                 }
684             }
685         }
686     }
687 
688     /**
689      * Checks if the per-app locales are in the LocaleConfig. Per-app locales missing from the
690      * LocaleConfig will be removed.
691      *
692      * <p><b>Note:</b> Check whether to remove the per-app locales when the app is upgraded or
693      * the LocaleConfig is overridden.
694      */
removeUnsupportedAppLocales(String appPackageName, int userId, LocaleConfig localeConfig, int caller)695     void removeUnsupportedAppLocales(String appPackageName, int userId,
696             LocaleConfig localeConfig, int caller) {
697         LocaleList appLocales = getApplicationLocalesUnchecked(appPackageName, userId);
698         // Remove the per-app locales from the locale list if they don't exist in the LocaleConfig.
699         boolean resetAppLocales = false;
700         List<Locale> newAppLocales = new ArrayList<Locale>();
701 
702         if (localeConfig == null) {
703             //Reset the app locales to the system default
704             Slog.i(TAG, "There is no LocaleConfig, reset app locales");
705             resetAppLocales = true;
706         } else {
707             for (int i = 0; i < appLocales.size(); i++) {
708                 if (!localeConfig.containsLocale(appLocales.get(i))) {
709                     Slog.i(TAG, "Missing from the LocaleConfig, reset app locales");
710                     resetAppLocales = true;
711                     continue;
712                 }
713                 newAppLocales.add(appLocales.get(i));
714             }
715         }
716 
717         if (resetAppLocales) {
718             // Reset the app locales
719             Locale[] locales = new Locale[newAppLocales.size()];
720             try {
721                 setApplicationLocales(appPackageName, userId,
722                         new LocaleList(newAppLocales.toArray(locales)),
723                         mBackupHelper.areLocalesSetFromDelegate(userId, appPackageName), caller);
724             } catch (RemoteException | IllegalArgumentException e) {
725                 Slog.e(TAG, "Could not set locales for " + appPackageName, e);
726             }
727         }
728     }
729 
enforceSetAppSpecificLocaleConfigPermission( AppSupportedLocalesChangedAtomRecord atomRecord)730     private void enforceSetAppSpecificLocaleConfigPermission(
731             AppSupportedLocalesChangedAtomRecord atomRecord) {
732         try {
733             mContext.enforceCallingOrSelfPermission(
734                     android.Manifest.permission.SET_APP_SPECIFIC_LOCALECONFIG,
735                     "setOverrideLocaleConfig");
736         } catch (SecurityException e) {
737             atomRecord.setStatus(FrameworkStatsLog
738                     .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_PERMISSION_ABSENT);
739             throw e;
740         }
741     }
742 
743     /**
744      * Returns the override LocaleConfig for a specified app.
745      */
746     @Nullable
getOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId)747     public LocaleConfig getOverrideLocaleConfig(@NonNull String appPackageName,
748             @UserIdInt int userId) {
749         if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) {
750             return null;
751         }
752 
753         requireNonNull(appPackageName);
754 
755         // Allow apps with INTERACT_ACROSS_USERS permission to query the override LocaleConfig for
756         // different user.
757         userId = mActivityManagerInternal.handleIncomingUser(
758                 Binder.getCallingPid(), Binder.getCallingUid(), userId,
759                 false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
760                 "getOverrideLocaleConfig", /* callerPackage= */ null);
761 
762         final File file = getXmlFileNameForUser(appPackageName, userId);
763         if (!file.exists()) {
764             if (DEBUG) {
765                 Slog.i(TAG, "getOverrideLocaleConfig, the file is not existed.");
766             }
767             return null;
768         }
769 
770         try (InputStream in = new FileInputStream(file)) {
771             final TypedXmlPullParser parser = Xml.resolvePullParser(in);
772             List<String> overrideLocales = loadFromXml(parser);
773             if (DEBUG) {
774                 Slog.i(TAG, "getOverrideLocaleConfig, Loaded locales: " + overrideLocales);
775             }
776             LocaleConfig storedLocaleConfig = new LocaleConfig(
777                     LocaleList.forLanguageTags(String.join(",", overrideLocales)));
778 
779             return storedLocaleConfig;
780         } catch (IOException | XmlPullParserException e) {
781             Slog.e(TAG, "Failed to parse XML configuration from " + file, e);
782         }
783 
784         return null;
785     }
786 
787     /**
788      * Delete an override {@link LocaleConfig} for a specified app from the file storage.
789      *
790      * <p>Clear the override LocaleConfig from the storage when the app is uninstalled.
791      */
deleteOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId)792     void deleteOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId) {
793         final File file = getXmlFileNameForUser(appPackageName, userId);
794 
795         if (file.exists()) {
796             Slog.d(TAG, "Delete the override LocaleConfig.");
797             file.delete();
798         }
799     }
800 
toXmlByteArray(LocaleList localeList)801     private byte[] toXmlByteArray(LocaleList localeList) {
802         try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
803             TypedXmlSerializer out = Xml.newFastSerializer();
804             out.setOutput(os, StandardCharsets.UTF_8.name());
805             out.startDocument(/* encoding= */ null, /* standalone= */ true);
806             out.startTag(/* namespace= */ null, LocaleConfig.TAG_LOCALE_CONFIG);
807 
808             List<String> locales = new ArrayList<String>(
809                     Arrays.asList(localeList.toLanguageTags().split(",")));
810             for (String locale : locales) {
811                 out.startTag(null, LocaleConfig.TAG_LOCALE);
812                 out.attribute(null, ATTR_NAME, locale);
813                 out.endTag(null, LocaleConfig.TAG_LOCALE);
814             }
815 
816             out.endTag(/* namespace= */ null, LocaleConfig.TAG_LOCALE_CONFIG);
817             out.endDocument();
818 
819             if (DEBUG) {
820                 Slog.d(TAG, "setOverrideLocaleConfig toXmlByteArray, output: " + os.toString());
821             }
822             return os.toByteArray();
823         } catch (IOException e) {
824             return null;
825         }
826     }
827 
828     @NonNull
loadFromXml(TypedXmlPullParser parser)829     private List<String> loadFromXml(TypedXmlPullParser parser)
830             throws IOException, XmlPullParserException {
831         List<String> localeList = new ArrayList<>();
832 
833         XmlUtils.beginDocument(parser, LocaleConfig.TAG_LOCALE_CONFIG);
834         int depth = parser.getDepth();
835         while (XmlUtils.nextElementWithin(parser, depth)) {
836             final String tagName = parser.getName();
837             if (LocaleConfig.TAG_LOCALE.equals(tagName)) {
838                 String locale = parser.getAttributeValue(/* namespace= */ null, ATTR_NAME);
839                 localeList.add(locale);
840             } else {
841                 Slog.w(TAG, "Unexpected tag name: " + tagName);
842                 XmlUtils.skipCurrentTag(parser);
843             }
844         }
845 
846         return localeList;
847     }
848 
849     @NonNull
getXmlFileNameForUser(@onNull String appPackageName, @UserIdInt int userId)850     private File getXmlFileNameForUser(@NonNull String appPackageName, @UserIdInt int userId) {
851         final File dir = new File(Environment.getDataSystemCeDirectory(userId), LOCALE_CONFIGS);
852         return new File(dir, appPackageName + SUFFIX_FILE_NAME);
853     }
854 
logAppSupportedLocalesChangedMetric( @onNull AppSupportedLocalesChangedAtomRecord atomRecord)855     private void logAppSupportedLocalesChangedMetric(
856             @NonNull AppSupportedLocalesChangedAtomRecord atomRecord) {
857         FrameworkStatsLog.write(FrameworkStatsLog.APP_SUPPORTED_LOCALES_CHANGED,
858                 atomRecord.mCallingUid,
859                 atomRecord.mTargetUid,
860                 atomRecord.mNumLocales,
861                 atomRecord.mOverrideRemoved,
862                 atomRecord.mSameAsResConfig,
863                 atomRecord.mSameAsPrevConfig,
864                 atomRecord.mStatus);
865     }
866 }
867