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             ComponentName componentName = ComponentName.unflattenFromString(currentInputMethod);
490             if (componentName == null) {
491                 Slog.d(TAG, "inValid input method");
492                 return false;
493             }
494             String inputMethodPkgName = componentName.getPackageName();
495             int inputMethodUid = getPackageUid(inputMethodPkgName, userId);
496             return inputMethodUid >= 0 && UserHandle.isSameApp(Binder.getCallingUid(),
497                     inputMethodUid);
498         }
499 
500         return false;
501     }
502 
enforceReadAppSpecificLocalesPermission()503     private void enforceReadAppSpecificLocalesPermission() {
504         mContext.enforceCallingOrSelfPermission(
505                 android.Manifest.permission.READ_APP_SPECIFIC_LOCALES,
506                 "getApplicationLocales");
507     }
508 
getPackageUid(String appPackageName, int userId)509     private int getPackageUid(String appPackageName, int userId) {
510         try {
511             return mPackageManager
512                     .getPackageUidAsUser(appPackageName, PackageInfoFlags.of(0), userId);
513         } catch (PackageManager.NameNotFoundException e) {
514             return Process.INVALID_UID;
515         }
516     }
517 
518     @Nullable
getInstallingPackageName(String packageName, int userId)519     String getInstallingPackageName(String packageName, int userId) {
520         try {
521             return mContext.createContextAsUser(UserHandle.of(userId), /* flags= */
522                     0).getPackageManager().getInstallSourceInfo(
523                     packageName).getInstallingPackageName();
524         } catch (PackageManager.NameNotFoundException e) {
525             Slog.w(TAG, "Package not found " + packageName);
526         }
527         return null;
528     }
529 
530     /**
531      * Returns the current system locales.
532      */
533     @NonNull
getSystemLocales()534     public LocaleList getSystemLocales() throws RemoteException {
535         final long token = Binder.clearCallingIdentity();
536         try {
537             return getSystemLocalesUnchecked();
538         } finally {
539             Binder.restoreCallingIdentity(token);
540         }
541     }
542 
543     @NonNull
getSystemLocalesUnchecked()544     private LocaleList getSystemLocalesUnchecked() throws RemoteException {
545         LocaleList systemLocales = null;
546         Configuration conf = ActivityManager.getService().getConfiguration();
547         if (conf != null) {
548             systemLocales = conf.getLocales();
549         }
550         if (systemLocales == null) {
551             systemLocales = LocaleList.getEmptyLocaleList();
552         }
553         return systemLocales;
554     }
555 
logAppLocalesMetric(@onNull AppLocaleChangedAtomRecord atomRecordForMetrics)556     private void logAppLocalesMetric(@NonNull AppLocaleChangedAtomRecord atomRecordForMetrics) {
557         FrameworkStatsLog.write(FrameworkStatsLog.APPLICATION_LOCALES_CHANGED,
558                 atomRecordForMetrics.mCallingUid,
559                 atomRecordForMetrics.mTargetUid,
560                 atomRecordForMetrics.mNewLocales,
561                 atomRecordForMetrics.mPrevLocales,
562                 atomRecordForMetrics.mStatus,
563                 atomRecordForMetrics.mCaller);
564     }
565 
566     /**
567      * Storing an override {@link LocaleConfig} for a specified app.
568      */
setOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId, @Nullable LocaleConfig localeConfig)569     public void setOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId,
570             @Nullable LocaleConfig localeConfig) throws IllegalArgumentException {
571         if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) {
572             return;
573         }
574 
575         AppSupportedLocalesChangedAtomRecord atomRecord = new AppSupportedLocalesChangedAtomRecord(
576                 Binder.getCallingUid());
577         try {
578             requireNonNull(appPackageName);
579 
580             //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user.
581             userId = mActivityManagerInternal.handleIncomingUser(
582                     Binder.getCallingPid(), Binder.getCallingUid(), userId,
583                     false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
584                     "setOverrideLocaleConfig", /* callerPackage= */ null);
585 
586             // This function handles two types of set operations:
587             // 1.) A normal, an app overrides its own LocaleConfig.
588             // 2.) A privileged system application or service is granted the necessary permission to
589             // override a LocaleConfig of another package.
590             if (!isPackageOwnedByCaller(appPackageName, userId, null, atomRecord)) {
591                 enforceSetAppSpecificLocaleConfigPermission(atomRecord);
592             }
593 
594             final long token = Binder.clearCallingIdentity();
595             try {
596                 setOverrideLocaleConfigUnchecked(appPackageName, userId, localeConfig, atomRecord);
597             } finally {
598                 Binder.restoreCallingIdentity(token);
599             }
600         } finally {
601             logAppSupportedLocalesChangedMetric(atomRecord);
602         }
603     }
604 
setOverrideLocaleConfigUnchecked(@onNull String appPackageName, @UserIdInt int userId, @Nullable LocaleConfig overrideLocaleConfig, @NonNull AppSupportedLocalesChangedAtomRecord atomRecord)605     private void setOverrideLocaleConfigUnchecked(@NonNull String appPackageName,
606             @UserIdInt int userId, @Nullable LocaleConfig overrideLocaleConfig,
607             @NonNull AppSupportedLocalesChangedAtomRecord atomRecord) {
608         synchronized (mWriteLock) {
609             if (DEBUG) {
610                 Slog.d(TAG,
611                         "set the override LocaleConfig for package " + appPackageName + " and user "
612                                 + userId);
613             }
614             LocaleConfig resLocaleConfig = null;
615             try {
616                 resLocaleConfig = LocaleConfig.fromContextIgnoringOverride(
617                         mContext.createPackageContext(appPackageName, 0));
618             } catch (PackageManager.NameNotFoundException e) {
619                 Slog.e(TAG, "Unknown package name " + appPackageName);
620                 return;
621             }
622             final File file = getXmlFileNameForUser(appPackageName, userId);
623 
624             if (overrideLocaleConfig == null) {
625                 if (file.exists()) {
626                     Slog.d(TAG, "remove the override LocaleConfig");
627                     file.delete();
628                 }
629                 removeUnsupportedAppLocales(appPackageName, userId, resLocaleConfig,
630                         FrameworkStatsLog
631                                 .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE
632                 );
633                 atomRecord.setOverrideRemoved(true);
634                 atomRecord.setStatus(FrameworkStatsLog
635                         .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS);
636                 return;
637             } else {
638                 if (overrideLocaleConfig.isSameLocaleConfig(
639                         getOverrideLocaleConfig(appPackageName, userId))) {
640                     Slog.d(TAG, "the same override, ignore it");
641                     atomRecord.setSameAsPrevConfig(true);
642                     return;
643                 }
644 
645                 LocaleList localeList = overrideLocaleConfig.getSupportedLocales();
646                 // Normally the LocaleList object should not be null. However we reassign it as the
647                 // empty list in case it happens.
648                 if (localeList == null) {
649                     localeList = LocaleList.getEmptyLocaleList();
650                 }
651                 if (DEBUG) {
652                     Slog.d(TAG,
653                             "setOverrideLocaleConfig, localeList: " + localeList.toLanguageTags());
654                 }
655                 atomRecord.setNumLocales(localeList.size());
656 
657                 // Store the override LocaleConfig to the file storage.
658                 final AtomicFile atomicFile = new AtomicFile(file);
659                 FileOutputStream stream = null;
660                 try {
661                     stream = atomicFile.startWrite();
662                     stream.write(toXmlByteArray(localeList));
663                 } catch (Exception e) {
664                     Slog.e(TAG, "Failed to write file " + atomicFile, e);
665                     if (stream != null) {
666                         atomicFile.failWrite(stream);
667                     }
668                     atomRecord.setStatus(FrameworkStatsLog
669                             .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_WRITE_TO_STORAGE);
670                     return;
671                 }
672                 atomicFile.finishWrite(stream);
673                 // Clear per-app locales if they are not in the override LocaleConfig.
674                 removeUnsupportedAppLocales(appPackageName, userId, overrideLocaleConfig,
675                         FrameworkStatsLog
676                                 .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE
677                 );
678                 if (overrideLocaleConfig.isSameLocaleConfig(resLocaleConfig)) {
679                     Slog.d(TAG, "setOverrideLocaleConfig, same as the app's LocaleConfig");
680                     atomRecord.setSameAsResConfig(true);
681                 }
682                 atomRecord.setStatus(FrameworkStatsLog
683                         .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS);
684                 if (DEBUG) {
685                     Slog.i(TAG, "Successfully written to " + atomicFile);
686                 }
687             }
688         }
689     }
690 
691     /**
692      * Checks if the per-app locales are in the LocaleConfig. Per-app locales missing from the
693      * LocaleConfig will be removed.
694      *
695      * <p><b>Note:</b> Check whether to remove the per-app locales when the app is upgraded or
696      * the LocaleConfig is overridden.
697      */
removeUnsupportedAppLocales(String appPackageName, int userId, LocaleConfig localeConfig, int caller)698     void removeUnsupportedAppLocales(String appPackageName, int userId,
699             LocaleConfig localeConfig, int caller) {
700         LocaleList appLocales = getApplicationLocalesUnchecked(appPackageName, userId);
701         // Remove the per-app locales from the locale list if they don't exist in the LocaleConfig.
702         boolean resetAppLocales = false;
703         List<Locale> newAppLocales = new ArrayList<Locale>();
704 
705         if (localeConfig == null) {
706             //Reset the app locales to the system default
707             Slog.i(TAG, "There is no LocaleConfig, reset app locales");
708             resetAppLocales = true;
709         } else {
710             for (int i = 0; i < appLocales.size(); i++) {
711                 if (!localeConfig.containsLocale(appLocales.get(i))) {
712                     Slog.i(TAG, "Missing from the LocaleConfig, reset app locales");
713                     resetAppLocales = true;
714                     continue;
715                 }
716                 newAppLocales.add(appLocales.get(i));
717             }
718         }
719 
720         if (resetAppLocales) {
721             // Reset the app locales
722             Locale[] locales = new Locale[newAppLocales.size()];
723             try {
724                 setApplicationLocales(appPackageName, userId,
725                         new LocaleList(newAppLocales.toArray(locales)),
726                         mBackupHelper.areLocalesSetFromDelegate(userId, appPackageName), caller);
727             } catch (RemoteException | IllegalArgumentException e) {
728                 Slog.e(TAG, "Could not set locales for " + appPackageName, e);
729             }
730         }
731     }
732 
enforceSetAppSpecificLocaleConfigPermission( AppSupportedLocalesChangedAtomRecord atomRecord)733     private void enforceSetAppSpecificLocaleConfigPermission(
734             AppSupportedLocalesChangedAtomRecord atomRecord) {
735         try {
736             mContext.enforceCallingOrSelfPermission(
737                     android.Manifest.permission.SET_APP_SPECIFIC_LOCALECONFIG,
738                     "setOverrideLocaleConfig");
739         } catch (SecurityException e) {
740             atomRecord.setStatus(FrameworkStatsLog
741                     .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_PERMISSION_ABSENT);
742             throw e;
743         }
744     }
745 
746     /**
747      * Returns the override LocaleConfig for a specified app.
748      */
749     @Nullable
getOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId)750     public LocaleConfig getOverrideLocaleConfig(@NonNull String appPackageName,
751             @UserIdInt int userId) {
752         if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) {
753             return null;
754         }
755 
756         requireNonNull(appPackageName);
757 
758         // Allow apps with INTERACT_ACROSS_USERS permission to query the override LocaleConfig for
759         // different user.
760         userId = mActivityManagerInternal.handleIncomingUser(
761                 Binder.getCallingPid(), Binder.getCallingUid(), userId,
762                 false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
763                 "getOverrideLocaleConfig", /* callerPackage= */ null);
764 
765         final File file = getXmlFileNameForUser(appPackageName, userId);
766         if (!file.exists()) {
767             if (DEBUG) {
768                 Slog.i(TAG, "getOverrideLocaleConfig, the file is not existed.");
769             }
770             return null;
771         }
772 
773         try (InputStream in = new FileInputStream(file)) {
774             final TypedXmlPullParser parser = Xml.resolvePullParser(in);
775             List<String> overrideLocales = loadFromXml(parser);
776             if (DEBUG) {
777                 Slog.i(TAG, "getOverrideLocaleConfig, Loaded locales: " + overrideLocales);
778             }
779             LocaleConfig storedLocaleConfig = new LocaleConfig(
780                     LocaleList.forLanguageTags(String.join(",", overrideLocales)));
781 
782             return storedLocaleConfig;
783         } catch (IOException | XmlPullParserException e) {
784             Slog.e(TAG, "Failed to parse XML configuration from " + file, e);
785         }
786 
787         return null;
788     }
789 
790     /**
791      * Delete an override {@link LocaleConfig} for a specified app from the file storage.
792      *
793      * <p>Clear the override LocaleConfig from the storage when the app is uninstalled.
794      */
deleteOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId)795     void deleteOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId) {
796         final File file = getXmlFileNameForUser(appPackageName, userId);
797 
798         if (file.exists()) {
799             Slog.d(TAG, "Delete the override LocaleConfig.");
800             file.delete();
801         }
802     }
803 
toXmlByteArray(LocaleList localeList)804     private byte[] toXmlByteArray(LocaleList localeList) {
805         try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
806             TypedXmlSerializer out = Xml.newFastSerializer();
807             out.setOutput(os, StandardCharsets.UTF_8.name());
808             out.startDocument(/* encoding= */ null, /* standalone= */ true);
809             out.startTag(/* namespace= */ null, LocaleConfig.TAG_LOCALE_CONFIG);
810 
811             List<String> locales = new ArrayList<String>(
812                     Arrays.asList(localeList.toLanguageTags().split(",")));
813             for (String locale : locales) {
814                 out.startTag(null, LocaleConfig.TAG_LOCALE);
815                 out.attribute(null, ATTR_NAME, locale);
816                 out.endTag(null, LocaleConfig.TAG_LOCALE);
817             }
818 
819             out.endTag(/* namespace= */ null, LocaleConfig.TAG_LOCALE_CONFIG);
820             out.endDocument();
821 
822             if (DEBUG) {
823                 Slog.d(TAG, "setOverrideLocaleConfig toXmlByteArray, output: " + os.toString());
824             }
825             return os.toByteArray();
826         } catch (IOException e) {
827             return null;
828         }
829     }
830 
831     @NonNull
loadFromXml(TypedXmlPullParser parser)832     private List<String> loadFromXml(TypedXmlPullParser parser)
833             throws IOException, XmlPullParserException {
834         List<String> localeList = new ArrayList<>();
835 
836         XmlUtils.beginDocument(parser, LocaleConfig.TAG_LOCALE_CONFIG);
837         int depth = parser.getDepth();
838         while (XmlUtils.nextElementWithin(parser, depth)) {
839             final String tagName = parser.getName();
840             if (LocaleConfig.TAG_LOCALE.equals(tagName)) {
841                 String locale = parser.getAttributeValue(/* namespace= */ null, ATTR_NAME);
842                 localeList.add(locale);
843             } else {
844                 Slog.w(TAG, "Unexpected tag name: " + tagName);
845                 XmlUtils.skipCurrentTag(parser);
846             }
847         }
848 
849         return localeList;
850     }
851 
852     @NonNull
getXmlFileNameForUser(@onNull String appPackageName, @UserIdInt int userId)853     private File getXmlFileNameForUser(@NonNull String appPackageName, @UserIdInt int userId) {
854         final File dir = new File(Environment.getDataSystemCeDirectory(userId), LOCALE_CONFIGS);
855         return new File(dir, appPackageName + SUFFIX_FILE_NAME);
856     }
857 
logAppSupportedLocalesChangedMetric( @onNull AppSupportedLocalesChangedAtomRecord atomRecord)858     private void logAppSupportedLocalesChangedMetric(
859             @NonNull AppSupportedLocalesChangedAtomRecord atomRecord) {
860         FrameworkStatsLog.write(FrameworkStatsLog.APP_SUPPORTED_LOCALES_CHANGED,
861                 atomRecord.mCallingUid,
862                 atomRecord.mTargetUid,
863                 atomRecord.mNumLocales,
864                 atomRecord.mOverrideRemoved,
865                 atomRecord.mSameAsResConfig,
866                 atomRecord.mSameAsPrevConfig,
867                 atomRecord.mStatus);
868     }
869 }
870