• 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 android.os.UserHandle.USER_NULL;
20 
21 import static com.android.server.locales.LocaleManagerService.DEBUG;
22 
23 import android.annotation.NonNull;
24 import android.annotation.UserIdInt;
25 import android.app.LocaleConfig;
26 import android.app.backup.BackupManager;
27 import android.content.BroadcastReceiver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.SharedPreferences;
32 import android.content.pm.ApplicationInfo;
33 import android.content.pm.PackageInfo;
34 import android.content.pm.PackageManager;
35 import android.os.Environment;
36 import android.os.HandlerThread;
37 import android.os.LocaleList;
38 import android.os.RemoteException;
39 import android.os.UserHandle;
40 import android.text.TextUtils;
41 import android.util.ArraySet;
42 import android.util.Slog;
43 import android.util.SparseArray;
44 import android.util.Xml;
45 
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.internal.util.FrameworkStatsLog;
48 import com.android.internal.util.XmlUtils;
49 import com.android.modules.utils.TypedXmlPullParser;
50 import com.android.modules.utils.TypedXmlSerializer;
51 
52 import org.xmlpull.v1.XmlPullParserException;
53 
54 import java.io.ByteArrayInputStream;
55 import java.io.ByteArrayOutputStream;
56 import java.io.File;
57 import java.io.IOException;
58 import java.io.OutputStream;
59 import java.io.UnsupportedEncodingException;
60 import java.nio.charset.StandardCharsets;
61 import java.time.Clock;
62 import java.time.Duration;
63 import java.util.Collections;
64 import java.util.HashMap;
65 import java.util.Set;
66 
67 /**
68  * Helper class for managing backup and restore of app-specific locales.
69  */
70 class LocaleManagerBackupHelper {
71     private static final String TAG = "LocaleManagerBkpHelper"; // must be < 23 chars
72 
73     // Tags and attributes for xml.
74     private static final String LOCALES_XML_TAG = "locales";
75     private static final String PACKAGE_XML_TAG = "package";
76     private static final String ATTR_PACKAGE_NAME = "name";
77     private static final String ATTR_LOCALES = "locales";
78     private static final String ATTR_DELEGATE_SELECTOR = "delegate_selector";
79 
80     private static final String SYSTEM_BACKUP_PACKAGE_KEY = "android";
81     /**
82      * The name of the xml file used to persist the target package name that sets per-app locales
83      * from the delegate selector.
84      */
85     private static final String LOCALES_FROM_DELEGATE_PREFS = "LocalesFromDelegatePrefs.xml";
86     // Stage data would be deleted on reboot since it's stored in memory. So it's retained until
87     // retention period OR next reboot, whichever happens earlier.
88     private static final Duration STAGE_DATA_RETENTION_PERIOD = Duration.ofDays(3);
89 
90     private final LocaleManagerService mLocaleManagerService;
91     private final PackageManager mPackageManager;
92     private final Clock mClock;
93     private final Context mContext;
94     private final Object mStagedDataLock = new Object();
95 
96     // Staged data map keyed by user-id to handle multi-user scenario / work profiles. We are using
97     // SparseArray because it is more memory-efficient than a HashMap.
98     private final SparseArray<StagedData> mStagedData;
99 
100     // SharedPreferences to store packages whose app-locale was set by a delegate, as opposed to
101     // the application setting the app-locale itself.
102     private final SharedPreferences mDelegateAppLocalePackages;
103     private final BroadcastReceiver mUserMonitor;
104 
LocaleManagerBackupHelper(LocaleManagerService localeManagerService, PackageManager packageManager, HandlerThread broadcastHandlerThread)105     LocaleManagerBackupHelper(LocaleManagerService localeManagerService,
106             PackageManager packageManager, HandlerThread broadcastHandlerThread) {
107         this(localeManagerService.mContext, localeManagerService, packageManager, Clock.systemUTC(),
108                 new SparseArray<>(), broadcastHandlerThread, null);
109     }
110 
LocaleManagerBackupHelper(Context context, LocaleManagerService localeManagerService, PackageManager packageManager, Clock clock, SparseArray<StagedData> stagedData, HandlerThread broadcastHandlerThread, SharedPreferences delegateAppLocalePackages)111     @VisibleForTesting LocaleManagerBackupHelper(Context context,
112             LocaleManagerService localeManagerService,
113             PackageManager packageManager, Clock clock, SparseArray<StagedData> stagedData,
114             HandlerThread broadcastHandlerThread, SharedPreferences delegateAppLocalePackages) {
115         mContext = context;
116         mLocaleManagerService = localeManagerService;
117         mPackageManager = packageManager;
118         mClock = clock;
119         mStagedData = stagedData;
120         mDelegateAppLocalePackages = delegateAppLocalePackages != null ? delegateAppLocalePackages
121                 : createPersistedInfo();
122 
123         mUserMonitor = new UserMonitor();
124         IntentFilter filter = new IntentFilter();
125         filter.addAction(Intent.ACTION_USER_REMOVED);
126         context.registerReceiverAsUser(mUserMonitor, UserHandle.ALL, filter,
127                 null, broadcastHandlerThread.getThreadHandler());
128     }
129 
130     @VisibleForTesting
getUserMonitor()131     BroadcastReceiver getUserMonitor() {
132         return mUserMonitor;
133     }
134 
135     /**
136      * @see LocaleManagerInternal#getBackupPayload(int userId)
137      */
getBackupPayload(int userId)138     public byte[] getBackupPayload(int userId) {
139         if (DEBUG) {
140             Slog.d(TAG, "getBackupPayload invoked for user id " + userId);
141         }
142 
143         synchronized (mStagedDataLock) {
144             cleanStagedDataForOldEntriesLocked();
145         }
146 
147         HashMap<String, LocalesInfo> pkgStates = new HashMap<>();
148         for (ApplicationInfo appInfo : mPackageManager.getInstalledApplicationsAsUser(
149                 PackageManager.ApplicationInfoFlags.of(0), userId)) {
150             try {
151                 LocaleList appLocales = mLocaleManagerService.getApplicationLocales(
152                         appInfo.packageName,
153                         userId);
154                 // Backup locales and package names for per-app locales set from a delegate
155                 // selector only for apps which do have app-specific overrides.
156                 if (!appLocales.isEmpty()) {
157                     if (DEBUG) {
158                         Slog.d(TAG, "Add package=" + appInfo.packageName + " locales="
159                                 + appLocales.toLanguageTags() + " to backup payload");
160                     }
161                     boolean localeSetFromDelegate = false;
162                     if (mDelegateAppLocalePackages != null) {
163                         localeSetFromDelegate = mDelegateAppLocalePackages.getStringSet(
164                                 Integer.toString(userId), Collections.<String>emptySet()).contains(
165                                 appInfo.packageName);
166                     }
167                     LocalesInfo localesInfo = new LocalesInfo(appLocales.toLanguageTags(),
168                             localeSetFromDelegate);
169                     pkgStates.put(appInfo.packageName, localesInfo);
170                 }
171             } catch (RemoteException | IllegalArgumentException e) {
172                 Slog.e(TAG, "Exception when getting locales for package: " + appInfo.packageName,
173                         e);
174             }
175         }
176 
177         if (pkgStates.isEmpty()) {
178             if (DEBUG) {
179                 Slog.d(TAG, "Final payload=null");
180             }
181             // Returning null here will ensure deletion of the entry for LMS from the backup data.
182             return null;
183         }
184 
185         final ByteArrayOutputStream out = new ByteArrayOutputStream();
186         try {
187             writeToXml(out, pkgStates);
188         } catch (IOException e) {
189             Slog.e(TAG, "Could not write to xml for backup ", e);
190             return null;
191         }
192 
193         if (DEBUG) {
194             try {
195                 Slog.d(TAG, "Final payload=" + out.toString("UTF-8"));
196             } catch (UnsupportedEncodingException e) {
197                 Slog.w(TAG, "Could not encode payload to UTF-8", e);
198             }
199         }
200         return out.toByteArray();
201     }
202 
cleanStagedDataForOldEntriesLocked()203     private void cleanStagedDataForOldEntriesLocked() {
204         for (int i = 0; i < mStagedData.size(); i++) {
205             int userId = mStagedData.keyAt(i);
206             StagedData stagedData = mStagedData.get(userId);
207             if (stagedData.mCreationTimeMillis
208                     < mClock.millis() - STAGE_DATA_RETENTION_PERIOD.toMillis()) {
209                 deleteStagedDataLocked(userId);
210             }
211         }
212     }
213 
214     /**
215      * @see LocaleManagerInternal#stageAndApplyRestoredPayload(byte[] payload, int userId)
216      */
stageAndApplyRestoredPayload(byte[] payload, int userId)217     public void stageAndApplyRestoredPayload(byte[] payload, int userId) {
218         if (DEBUG) {
219             Slog.d(TAG, "stageAndApplyRestoredPayload user=" + userId + " payload="
220                     + (payload != null ? new String(payload, StandardCharsets.UTF_8) : null));
221         }
222         if (payload == null) {
223             Slog.e(TAG, "stageAndApplyRestoredPayload: no payload to restore for user " + userId);
224             return;
225         }
226 
227         final ByteArrayInputStream inputStream = new ByteArrayInputStream(payload);
228 
229         HashMap<String, LocalesInfo> pkgStates;
230         try {
231             // Parse the input blob into a list of BackupPackageState.
232             final TypedXmlPullParser parser = Xml.newFastPullParser();
233             parser.setInput(inputStream, StandardCharsets.UTF_8.name());
234 
235             XmlUtils.beginDocument(parser, LOCALES_XML_TAG);
236             pkgStates = readFromXml(parser);
237         } catch (IOException | XmlPullParserException e) {
238             Slog.e(TAG, "Could not parse payload ", e);
239             return;
240         }
241 
242         // We need a lock here to prevent race conditions when accessing the stage file.
243         // It might happen that a restore was triggered (manually using bmgr cmd) and at the same
244         // time a new package is added. We want to ensure that both these operations aren't
245         // performed simultaneously.
246         synchronized (mStagedDataLock) {
247             // Backups for apps which are yet to be installed.
248             StagedData stagedData = new StagedData(mClock.millis(), new HashMap<>());
249 
250             for (String pkgName : pkgStates.keySet()) {
251                 LocalesInfo localesInfo = pkgStates.get(pkgName);
252                 // Check if the application is already installed for the concerned user.
253                 if (isPackageInstalledForUser(pkgName, userId)) {
254                     // Don't apply the restore if the locales have already been set for the app.
255                     checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId);
256                 } else {
257                     // Stage the data if the app isn't installed.
258                     stagedData.mPackageStates.put(pkgName, localesInfo);
259                     if (DEBUG) {
260                         Slog.d(TAG, "Add locales=" + localesInfo.mLocales
261                                 + " fromDelegate=" + localesInfo.mSetFromDelegate
262                                 + " package=" + pkgName + " for lazy restore.");
263                     }
264                 }
265             }
266 
267             if (!stagedData.mPackageStates.isEmpty()) {
268                 mStagedData.put(userId, stagedData);
269             }
270         }
271     }
272 
273     /**
274      * Notifies the backup manager to include the "android" package in the next backup pass.
275      */
notifyBackupManager()276     public void notifyBackupManager() {
277         BackupManager.dataChanged(SYSTEM_BACKUP_PACKAGE_KEY);
278     }
279 
280     /**
281      * <p><b>Note:</b> This is invoked by service's common monitor
282      * {@link LocaleManagerServicePackageMonitor#onPackageAdded} when a new package is
283      * added on device.
284      */
onPackageAdded(String packageName, int uid)285     void onPackageAdded(String packageName, int uid) {
286         try {
287             synchronized (mStagedDataLock) {
288                 cleanStagedDataForOldEntriesLocked();
289 
290                 int userId = UserHandle.getUserId(uid);
291                 if (mStagedData.contains(userId)) {
292                     // Perform lazy restore only if the staged data exists.
293                     doLazyRestoreLocked(packageName, userId);
294                 }
295             }
296         } catch (Exception e) {
297             Slog.e(TAG, "Exception in onPackageAdded.", e);
298         }
299     }
300 
301     /**
302      * <p><b>Note:</b> This is invoked by service's common monitor
303      * {@link LocaleManagerServicePackageMonitor#onPackageUpdateFinished} when a package is upgraded
304      * on device.
305      */
onPackageUpdateFinished(String packageName, int uid)306     void onPackageUpdateFinished(String packageName, int uid) {
307         int userId = UserHandle.getUserId(uid);
308         cleanApplicationLocalesIfNeeded(packageName, userId);
309     }
310 
311     /**
312      * <p><b>Note:</b> This is invoked by service's common monitor
313      * {@link LocaleManagerServicePackageMonitor#onPackageDataCleared} when a package's data
314      * is cleared.
315      */
onPackageDataCleared(String packageName, int uid)316     void onPackageDataCleared(String packageName, int uid) {
317         try {
318             notifyBackupManager();
319             int userId = UserHandle.getUserId(uid);
320             removePackageFromPersistedInfo(packageName, userId);
321         } catch (Exception e) {
322             Slog.e(TAG, "Exception in onPackageDataCleared.", e);
323         }
324     }
325 
326     /**
327      * <p><b>Note:</b> This is invoked by service's common monitor
328      * {@link LocaleManagerServicePackageMonitor#onPackageRemoved} when a package is removed
329      * from device.
330      */
onPackageRemoved(String packageName, int uid)331     void onPackageRemoved(String packageName, int uid) {
332         try {
333             notifyBackupManager();
334             int userId = UserHandle.getUserId(uid);
335             removePackageFromPersistedInfo(packageName, userId);
336         } catch (Exception e) {
337             Slog.e(TAG, "Exception in onPackageRemoved.", e);
338         }
339     }
340 
isPackageInstalledForUser(String packageName, int userId)341     private boolean isPackageInstalledForUser(String packageName, int userId) {
342         PackageInfo pkgInfo = null;
343         try {
344             pkgInfo = mContext.getPackageManager().getPackageInfoAsUser(
345                     packageName, /* flags= */ 0, userId);
346         } catch (PackageManager.NameNotFoundException e) {
347             if (DEBUG) {
348                 Slog.d(TAG, "Could not get package info for " + packageName, e);
349             }
350         }
351         return pkgInfo != null;
352     }
353 
354     /**
355      * Checks if locales already exist for the application and applies the restore accordingly.
356      * <p>
357      * The user might change the locales for an application before the restore is applied. In this
358      * case, we want to keep the user settings and discard the restore.
359      */
checkExistingLocalesAndApplyRestore(@onNull String pkgName, LocalesInfo localesInfo, int userId)360     private void checkExistingLocalesAndApplyRestore(@NonNull String pkgName,
361             LocalesInfo localesInfo, int userId) {
362         if (localesInfo == null) {
363             Slog.w(TAG, "No locales info for " + pkgName);
364             return;
365         }
366 
367         try {
368             LocaleList currLocales = mLocaleManagerService.getApplicationLocales(
369                     pkgName,
370                     userId);
371             if (!currLocales.isEmpty()) {
372                 return;
373             }
374         } catch (RemoteException | IllegalArgumentException e) {
375             Slog.e(TAG, "Could not check for current locales before restoring", e);
376         }
377 
378         // Restore the locale immediately
379         try {
380             mLocaleManagerService.setApplicationLocales(pkgName, userId,
381                     LocaleList.forLanguageTags(localesInfo.mLocales), localesInfo.mSetFromDelegate,
382                     FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE);
383             if (DEBUG) {
384                 Slog.d(TAG, "Restored locales=" + localesInfo.mLocales + " fromDelegate="
385                         + localesInfo.mSetFromDelegate + " for package=" + pkgName);
386             }
387         } catch (RemoteException | IllegalArgumentException e) {
388             Slog.e(TAG, "Could not restore locales for " + pkgName, e);
389         }
390     }
391 
deleteStagedDataLocked(@serIdInt int userId)392     private void deleteStagedDataLocked(@UserIdInt int userId) {
393         mStagedData.remove(userId);
394     }
395 
396     /**
397      * Parses the backup data from the serialized xml input stream.
398      */
readFromXml(TypedXmlPullParser parser)399     private @NonNull HashMap<String, LocalesInfo> readFromXml(TypedXmlPullParser parser)
400             throws IOException, XmlPullParserException {
401         HashMap<String, LocalesInfo> packageStates = new HashMap<>();
402         int depth = parser.getDepth();
403         while (XmlUtils.nextElementWithin(parser, depth)) {
404             if (parser.getName().equals(PACKAGE_XML_TAG)) {
405                 String packageName = parser.getAttributeValue(/* namespace= */ null,
406                         ATTR_PACKAGE_NAME);
407                 String languageTags = parser.getAttributeValue(/* namespace= */ null, ATTR_LOCALES);
408                 boolean delegateSelector = parser.getAttributeBoolean(/* namespace= */ null,
409                         ATTR_DELEGATE_SELECTOR);
410 
411                 if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(languageTags)) {
412                     LocalesInfo localesInfo = new LocalesInfo(languageTags, delegateSelector);
413                     packageStates.put(packageName, localesInfo);
414                 }
415             }
416         }
417         return packageStates;
418     }
419 
420     /**
421      * Converts the list of app backup data into a serialized xml stream.
422      */
writeToXml(OutputStream stream, @NonNull HashMap<String, LocalesInfo> pkgStates)423     private static void writeToXml(OutputStream stream,
424             @NonNull HashMap<String, LocalesInfo> pkgStates) throws IOException {
425         if (pkgStates.isEmpty()) {
426             // No need to write anything at all if pkgStates is empty.
427             return;
428         }
429 
430         TypedXmlSerializer out = Xml.newFastSerializer();
431         out.setOutput(stream, StandardCharsets.UTF_8.name());
432         out.startDocument(/* encoding= */ null, /* standalone= */ true);
433         out.startTag(/* namespace= */ null, LOCALES_XML_TAG);
434 
435         for (String pkg : pkgStates.keySet()) {
436             out.startTag(/* namespace= */ null, PACKAGE_XML_TAG);
437             out.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, pkg);
438             out.attribute(/* namespace= */ null, ATTR_LOCALES, pkgStates.get(pkg).mLocales);
439             out.attributeBoolean(/* namespace= */ null, ATTR_DELEGATE_SELECTOR,
440                     pkgStates.get(pkg).mSetFromDelegate);
441             out.endTag(/*namespace= */ null, PACKAGE_XML_TAG);
442         }
443 
444         out.endTag(/* namespace= */ null, LOCALES_XML_TAG);
445         out.endDocument();
446     }
447 
448     static class StagedData {
449         final long mCreationTimeMillis;
450         final HashMap<String, LocalesInfo> mPackageStates;
451 
StagedData(long creationTimeMillis, HashMap<String, LocalesInfo> pkgStates)452         StagedData(long creationTimeMillis, HashMap<String, LocalesInfo> pkgStates) {
453             mCreationTimeMillis = creationTimeMillis;
454             mPackageStates = pkgStates;
455         }
456     }
457 
458     static class LocalesInfo {
459         final String mLocales;
460         final boolean mSetFromDelegate;
461 
LocalesInfo(String locales, boolean setFromDelegate)462         LocalesInfo(String locales, boolean setFromDelegate) {
463             mLocales = locales;
464             mSetFromDelegate = setFromDelegate;
465         }
466     }
467 
468     /**
469      * Broadcast listener to capture user removed event.
470      *
471      * <p>The stage data is deleted when a user is removed.
472      */
473     private final class UserMonitor extends BroadcastReceiver {
474         @Override
onReceive(Context context, Intent intent)475         public void onReceive(Context context, Intent intent) {
476             try {
477                 String action = intent.getAction();
478                 if (action.equals(Intent.ACTION_USER_REMOVED)) {
479                     final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
480                     synchronized (mStagedDataLock) {
481                         deleteStagedDataLocked(userId);
482                         removeProfileFromPersistedInfo(userId);
483                     }
484                 }
485             } catch (Exception e) {
486                 Slog.e(TAG, "Exception in user monitor.", e);
487             }
488         }
489     }
490 
491     /**
492      * Performs lazy restore from the staged data.
493      *
494      * <p>This is invoked by the package monitor on the package added callback.
495      */
doLazyRestoreLocked(String packageName, int userId)496     private void doLazyRestoreLocked(String packageName, int userId) {
497         if (DEBUG) {
498             Slog.d(TAG, "doLazyRestore package=" + packageName + " user=" + userId);
499         }
500 
501         // Check if the package is installed indeed
502         if (!isPackageInstalledForUser(packageName, userId)) {
503             Slog.e(TAG, packageName + " not installed for user " + userId
504                     + ". Could not restore locales from stage data");
505             return;
506         }
507 
508         StagedData stagedData = mStagedData.get(userId);
509         for (String pkgName : stagedData.mPackageStates.keySet()) {
510             LocalesInfo localesInfo = stagedData.mPackageStates.get(pkgName);
511 
512             if (pkgName.equals(packageName)) {
513 
514                 checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId);
515 
516                 // Remove the restored entry from the staged data list.
517                 stagedData.mPackageStates.remove(pkgName);
518 
519                 // Remove the stage data entry for user if there are no more packages to restore.
520                 if (stagedData.mPackageStates.isEmpty()) {
521                     mStagedData.remove(userId);
522                 }
523 
524                 // No need to loop further after restoring locales because the staged data will
525                 // contain at most one entry for the newly added package.
526                 break;
527             }
528         }
529     }
530 
createPersistedInfo()531     SharedPreferences createPersistedInfo() {
532         final File prefsFile = new File(
533                 Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM),
534                 LOCALES_FROM_DELEGATE_PREFS);
535         return mContext.createDeviceProtectedStorageContext().getSharedPreferences(prefsFile,
536                 Context.MODE_PRIVATE);
537     }
538 
getPersistedInfo()539     public SharedPreferences getPersistedInfo() {
540         return mDelegateAppLocalePackages;
541     }
542 
removePackageFromPersistedInfo(String packageName, @UserIdInt int userId)543     private void removePackageFromPersistedInfo(String packageName, @UserIdInt int userId) {
544         if (mDelegateAppLocalePackages == null) {
545             Slog.w(TAG, "Failed to persist data into the shared preference!");
546             return;
547         }
548 
549         String key = Integer.toString(userId);
550         Set<String> packageNames = new ArraySet<>(
551                 mDelegateAppLocalePackages.getStringSet(key, new ArraySet<>()));
552         if (packageNames.contains(packageName)) {
553             if (DEBUG) {
554                 Slog.d(TAG, "remove " + packageName + " from persisted info");
555             }
556             packageNames.remove(packageName);
557             SharedPreferences.Editor editor = mDelegateAppLocalePackages.edit();
558             editor.putStringSet(key, packageNames);
559 
560             // commit and log the result.
561             if (!editor.commit()) {
562                 Slog.e(TAG, "Failed to commit data!");
563             }
564         }
565     }
566 
removeProfileFromPersistedInfo(@serIdInt int userId)567     private void removeProfileFromPersistedInfo(@UserIdInt int userId) {
568         String key = Integer.toString(userId);
569 
570         if (mDelegateAppLocalePackages == null || !mDelegateAppLocalePackages.contains(key)) {
571             Slog.w(TAG, "The profile is not existed in the persisted info");
572             return;
573         }
574 
575         if (!mDelegateAppLocalePackages.edit().remove(key).commit()) {
576             Slog.e(TAG, "Failed to commit data!");
577         }
578     }
579 
580     /**
581      * Persists the package name of per-app locales set from a delegate selector.
582      *
583      * <p>This information is used when the user has set per-app locales for a specific application
584      * from the delegate selector, and then the LocaleConfig of that application is removed in the
585      * upgraded version, the per-app locales needs to be reset to system default locales to avoid
586      * the user being unable to change system locales setting.
587      */
persistLocalesModificationInfo(@serIdInt int userId, String packageName, boolean fromDelegate, boolean emptyLocales)588     void persistLocalesModificationInfo(@UserIdInt int userId, String packageName,
589             boolean fromDelegate, boolean emptyLocales) {
590         if (mDelegateAppLocalePackages == null) {
591             Slog.w(TAG, "Failed to persist data into the shared preference!");
592             return;
593         }
594 
595         SharedPreferences.Editor editor = mDelegateAppLocalePackages.edit();
596         String user = Integer.toString(userId);
597         Set<String> packageNames = new ArraySet<>(
598                 mDelegateAppLocalePackages.getStringSet(user, new ArraySet<>()));
599         if (fromDelegate && !emptyLocales) {
600             if (!packageNames.contains(packageName)) {
601                 if (DEBUG) {
602                     Slog.d(TAG, "persist package: " + packageName);
603                 }
604                 packageNames.add(packageName);
605                 editor.putStringSet(user, packageNames);
606             }
607         } else {
608             // Remove the package name if per-app locales was not set from the delegate selector
609             // or they were set to empty.
610             if (packageNames.contains(packageName)) {
611                 if (DEBUG) {
612                     Slog.d(TAG, "remove package: " + packageName);
613                 }
614                 packageNames.remove(packageName);
615                 editor.putStringSet(user, packageNames);
616             }
617         }
618 
619         // commit and log the result.
620         if (!editor.commit()) {
621             Slog.e(TAG, "failed to commit locale setter info");
622         }
623     }
624 
areLocalesSetFromDelegate(@serIdInt int userId, String packageName)625     boolean areLocalesSetFromDelegate(@UserIdInt int userId, String packageName) {
626         if (mDelegateAppLocalePackages == null) {
627             Slog.w(TAG, "Failed to persist data into the shared preference!");
628             return false;
629         }
630 
631         String user = Integer.toString(userId);
632         Set<String> packageNames = new ArraySet<>(
633                 mDelegateAppLocalePackages.getStringSet(user, new ArraySet<>()));
634 
635         return packageNames.contains(packageName);
636     }
637 
638     /**
639      * When the user has set per-app locales for a specific application from a delegate selector,
640      * and then the LocaleConfig of that application is removed in the upgraded version, the per-app
641      * locales need to be removed or reset to system default locales to avoid the user being unable
642      * to change system locales setting.
643      */
cleanApplicationLocalesIfNeeded(String packageName, int userId)644     private void cleanApplicationLocalesIfNeeded(String packageName, int userId) {
645         if (mDelegateAppLocalePackages == null) {
646             Slog.w(TAG, "Failed to persist data into the shared preference!");
647             return;
648         }
649 
650         String user = Integer.toString(userId);
651         Set<String> packageNames = new ArraySet<>(
652                 mDelegateAppLocalePackages.getStringSet(user, new ArraySet<>()));
653         try {
654             LocaleList appLocales = mLocaleManagerService.getApplicationLocales(packageName,
655                     userId);
656             if (appLocales.isEmpty() || !packageNames.contains(packageName)) {
657                 return;
658             }
659         } catch (RemoteException | IllegalArgumentException e) {
660             Slog.e(TAG, "Exception when getting locales for " + packageName, e);
661             return;
662         }
663 
664         try {
665             LocaleConfig localeConfig = new LocaleConfig(
666                     mContext.createPackageContextAsUser(packageName, 0, UserHandle.of(userId)));
667             mLocaleManagerService.removeUnsupportedAppLocales(packageName, userId, localeConfig,
668                     FrameworkStatsLog
669                             .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APP_UPDATE_LOCALES_CHANGE);
670         } catch (PackageManager.NameNotFoundException e) {
671             Slog.e(TAG, "Can not found the package name : " + packageName + " / " + e);
672         }
673     }
674 }
675