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.Bundle;
36 import android.os.Environment;
37 import android.os.HandlerThread;
38 import android.os.LocaleList;
39 import android.os.RemoteException;
40 import android.os.UserHandle;
41 import android.text.TextUtils;
42 import android.util.ArraySet;
43 import android.util.Slog;
44 import android.util.SparseArray;
45 import android.util.Xml;
46 
47 import com.android.internal.annotations.VisibleForTesting;
48 import com.android.internal.util.FrameworkStatsLog;
49 import com.android.internal.util.XmlUtils;
50 import com.android.modules.utils.TypedXmlPullParser;
51 import com.android.modules.utils.TypedXmlSerializer;
52 
53 import org.xmlpull.v1.XmlPullParserException;
54 
55 import java.io.ByteArrayInputStream;
56 import java.io.ByteArrayOutputStream;
57 import java.io.File;
58 import java.io.IOException;
59 import java.io.OutputStream;
60 import java.io.UnsupportedEncodingException;
61 import java.nio.charset.StandardCharsets;
62 import java.time.Clock;
63 import java.time.Duration;
64 import java.util.Collections;
65 import java.util.HashMap;
66 import java.util.Set;
67 
68 /**
69  * Helper class for managing backup and restore of app-specific locales.
70  */
71 class LocaleManagerBackupHelper {
72     private static final String TAG = "LocaleManagerBkpHelper"; // must be < 23 chars
73 
74     // Tags and attributes for xml.
75     private static final String LOCALES_XML_TAG = "locales";
76     private static final String PACKAGE_XML_TAG = "package";
77     private static final String ATTR_PACKAGE_NAME = "name";
78     private static final String ATTR_LOCALES = "locales";
79     private static final String ATTR_DELEGATE_SELECTOR = "delegate_selector";
80 
81     private static final String SYSTEM_BACKUP_PACKAGE_KEY = "android";
82     /**
83      * The name of the xml file used to persist the target package name that sets per-app locales
84      * from the delegate selector.
85      */
86     private static final String LOCALES_FROM_DELEGATE_PREFS = "LocalesFromDelegatePrefs.xml";
87     private static final String LOCALES_STAGED_DATA_PREFS = "LocalesStagedDataPrefs.xml";
88     private static final String ARCHIVED_PACKAGES_PREFS = "ArchivedPackagesPrefs.xml";
89     // Stage data would be deleted on reboot since it's stored in memory. So it's retained until
90     // retention period OR next reboot, whichever happens earlier.
91     private static final Duration STAGE_DATA_RETENTION_PERIOD = Duration.ofDays(3);
92     // Store the locales staged data for the specified package in the SharedPreferences. The format
93     // is locales s:setFromDelegate
94     // For example: en-US s:true
95     private static final String STRING_SPLIT = " s:";
96     private static final String KEY_STAGED_DATA_TIME = "staged_data_time";
97 
98     private final LocaleManagerService mLocaleManagerService;
99     private final PackageManager mPackageManager;
100     private final Clock mClock;
101     private final Context mContext;
102     private final Object mStagedDataLock = new Object();
103 
104     // SharedPreferences to store packages whose app-locale was set by a delegate, as opposed to
105     // the application setting the app-locale itself.
106     private final SharedPreferences mDelegateAppLocalePackages;
107     // For unit tests
108     private final SparseArray<File> mStagedDataFiles;
109     private final File mArchivedPackagesFile;
110 
111     private final BroadcastReceiver mUserMonitor;
112 
LocaleManagerBackupHelper(LocaleManagerService localeManagerService, PackageManager packageManager, HandlerThread broadcastHandlerThread)113     LocaleManagerBackupHelper(LocaleManagerService localeManagerService,
114             PackageManager packageManager, HandlerThread broadcastHandlerThread) {
115         this(localeManagerService.mContext, localeManagerService, packageManager, Clock.systemUTC(),
116                 broadcastHandlerThread, null, null, null);
117     }
118 
119     @VisibleForTesting
LocaleManagerBackupHelper(Context context, LocaleManagerService localeManagerService, PackageManager packageManager, Clock clock, HandlerThread broadcastHandlerThread, SparseArray<File> stagedDataFiles, File archivedPackagesFile, SharedPreferences delegateAppLocalePackages)120     LocaleManagerBackupHelper(Context context, LocaleManagerService localeManagerService,
121             PackageManager packageManager, Clock clock, HandlerThread broadcastHandlerThread,
122             SparseArray<File> stagedDataFiles, File archivedPackagesFile,
123             SharedPreferences delegateAppLocalePackages) {
124         mContext = context;
125         mLocaleManagerService = localeManagerService;
126         mPackageManager = packageManager;
127         mClock = clock;
128         mDelegateAppLocalePackages = delegateAppLocalePackages != null ? delegateAppLocalePackages
129             : createPersistedInfo();
130         mArchivedPackagesFile = archivedPackagesFile;
131         mStagedDataFiles = stagedDataFiles;
132         mUserMonitor = new UserMonitor();
133         IntentFilter filter = new IntentFilter();
134         filter.addAction(Intent.ACTION_USER_REMOVED);
135         context.registerReceiverAsUser(mUserMonitor, UserHandle.ALL, filter,
136                 null, broadcastHandlerThread.getThreadHandler());
137     }
138 
139     @VisibleForTesting
getUserMonitor()140     BroadcastReceiver getUserMonitor() {
141         return mUserMonitor;
142     }
143 
144     /**
145      * @see LocaleManagerInternal#getBackupPayload(int userId)
146      */
getBackupPayload(int userId)147     public byte[] getBackupPayload(int userId) {
148         if (DEBUG) {
149             Slog.d(TAG, "getBackupPayload invoked for user id " + userId);
150         }
151 
152         synchronized (mStagedDataLock) {
153             cleanStagedDataForOldEntriesLocked(userId);
154         }
155 
156         HashMap<String, LocalesInfo> pkgStates = new HashMap<>();
157         for (ApplicationInfo appInfo : mPackageManager.getInstalledApplicationsAsUser(
158                 PackageManager.ApplicationInfoFlags.of(0), userId)) {
159             try {
160                 LocaleList appLocales = mLocaleManagerService.getApplicationLocales(
161                         appInfo.packageName,
162                         userId);
163                 // Backup locales and package names for per-app locales set from a delegate
164                 // selector only for apps which do have app-specific overrides.
165                 if (!appLocales.isEmpty()) {
166                     if (DEBUG) {
167                         Slog.d(TAG, "Add package=" + appInfo.packageName + " locales="
168                                 + appLocales.toLanguageTags() + " to backup payload");
169                     }
170                     boolean localeSetFromDelegate = false;
171                     if (mDelegateAppLocalePackages != null) {
172                         localeSetFromDelegate = mDelegateAppLocalePackages.getStringSet(
173                                 Integer.toString(userId), Collections.<String>emptySet()).contains(
174                                 appInfo.packageName);
175                     }
176                     LocalesInfo localesInfo = new LocalesInfo(appLocales.toLanguageTags(),
177                             localeSetFromDelegate);
178                     pkgStates.put(appInfo.packageName, localesInfo);
179                 }
180             } catch (RemoteException | IllegalArgumentException e) {
181                 Slog.e(TAG, "Exception when getting locales for package: " + appInfo.packageName,
182                         e);
183             }
184         }
185 
186         if (pkgStates.isEmpty()) {
187             if (DEBUG) {
188                 Slog.d(TAG, "Final payload=null");
189             }
190             // Returning null here will ensure deletion of the entry for LMS from the backup data.
191             return null;
192         }
193 
194         final ByteArrayOutputStream out = new ByteArrayOutputStream();
195         try {
196             writeToXml(out, pkgStates);
197         } catch (IOException e) {
198             Slog.e(TAG, "Could not write to xml for backup ", e);
199             return null;
200         }
201 
202         if (DEBUG) {
203             try {
204                 Slog.d(TAG, "Final payload=" + out.toString("UTF-8"));
205             } catch (UnsupportedEncodingException e) {
206                 Slog.w(TAG, "Could not encode payload to UTF-8", e);
207             }
208         }
209         return out.toByteArray();
210     }
211 
cleanStagedDataForOldEntriesLocked(@serIdInt int userId)212     private void cleanStagedDataForOldEntriesLocked(@UserIdInt int userId) {
213         Long created_time = getStagedDataSp(userId).getLong(KEY_STAGED_DATA_TIME, -1);
214         if (created_time != -1
215                 && created_time < mClock.millis() - STAGE_DATA_RETENTION_PERIOD.toMillis()) {
216             deleteStagedDataLocked(userId);
217         }
218     }
219 
220     /**
221      * @see LocaleManagerInternal#stageAndApplyRestoredPayload(byte[] payload, int userId)
222      */
stageAndApplyRestoredPayload(byte[] payload, int userId)223     public void stageAndApplyRestoredPayload(byte[] payload, int userId) {
224         if (DEBUG) {
225             Slog.d(TAG, "stageAndApplyRestoredPayload user=" + userId + " payload="
226                     + (payload != null ? new String(payload, StandardCharsets.UTF_8) : null));
227         }
228         if (payload == null) {
229             Slog.e(TAG, "stageAndApplyRestoredPayload: no payload to restore for user " + userId);
230             return;
231         }
232 
233         final ByteArrayInputStream inputStream = new ByteArrayInputStream(payload);
234 
235         HashMap<String, LocalesInfo> pkgStates;
236         try {
237             // Parse the input blob into a list of BackupPackageState.
238             final TypedXmlPullParser parser = Xml.newFastPullParser();
239             parser.setInput(inputStream, StandardCharsets.UTF_8.name());
240 
241             XmlUtils.beginDocument(parser, LOCALES_XML_TAG);
242             pkgStates = readFromXml(parser);
243         } catch (IOException | XmlPullParserException e) {
244             Slog.e(TAG, "Could not parse payload ", e);
245             return;
246         }
247 
248         // We need a lock here to prevent race conditions when accessing the stage file.
249         // It might happen that a restore was triggered (manually using bmgr cmd) and at the same
250         // time a new package is added. We want to ensure that both these operations aren't
251         // performed simultaneously.
252         synchronized (mStagedDataLock) {
253             // Backups for apps which are yet to be installed.
254             for (String pkgName : pkgStates.keySet()) {
255                 LocalesInfo localesInfo = pkgStates.get(pkgName);
256                 // Check if the application is already installed for the concerned user.
257                 if (isPackageInstalledForUser(pkgName, userId)) {
258                     removeFromArchivedPackagesInfo(userId, pkgName);
259                     // Don't apply the restore if the locales have already been set for the app.
260                     checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId);
261                 } else {
262                     // Stage the data if the app isn't installed.
263                     storeStagedDataInfo(userId, pkgName, localesInfo);
264                     if (DEBUG) {
265                         Slog.d(TAG, "Add locales=" + localesInfo.mLocales
266                                 + " fromDelegate=" + localesInfo.mSetFromDelegate
267                                 + " package=" + pkgName + " for lazy restore.");
268                     }
269                 }
270             }
271 
272             // Create the time if the data is being staged.
273             if (!getStagedDataSp(userId).getAll().isEmpty()) {
274                 storeStagedDataCreatedTime(userId);
275             }
276         }
277     }
278 
279     /**
280      * Notifies the backup manager to include the "android" package in the next backup pass.
281      */
notifyBackupManager()282     public void notifyBackupManager() {
283         BackupManager.dataChanged(SYSTEM_BACKUP_PACKAGE_KEY);
284     }
285 
286     /**
287      * <p><b>Note:</b> This is invoked by service's common monitor
288      * {@link LocaleManagerServicePackageMonitor#onPackageAddedWithExtras} when a new package is
289      * added on device.
290      */
onPackageAddedWithExtras(String packageName, int uid, Bundle extras)291     void onPackageAddedWithExtras(String packageName, int uid, Bundle extras) {
292         int userId = UserHandle.getUserId(uid);
293         if (extras != null) {
294             // To determine whether an app is pre-archived, check for Intent.EXTRA_ARCHIVAL upon
295             // receiving the initial PACKAGE_ADDED broadcast. If it is indeed pre-archived, perform
296             // the data restoration during the second PACKAGE_ADDED broadcast, which is sent
297             // subsequently when the app is installed.
298             boolean archived = extras.getBoolean(Intent.EXTRA_ARCHIVAL, false);
299             if (DEBUG) {
300                 Slog.d(TAG,
301                         "onPackageAddedWithExtras packageName: " + packageName + ", userId: "
302                                 + userId + ", archived: " + archived);
303             }
304             if (archived) {
305                 addInArchivedPackagesInfo(userId, packageName);
306             }
307         }
308         checkStageDataAndApplyRestore(packageName, userId);
309     }
310 
311     /**
312      * <p><b>Note:</b> This is invoked by service's common monitor
313      * {@link LocaleManagerServicePackageMonitor#onPackageUpdateFinished} when a package is upgraded
314      * on device.
315      */
onPackageUpdateFinished(String packageName, int uid)316     void onPackageUpdateFinished(String packageName, int uid) {
317         int userId = UserHandle.getUserId(uid);
318         if (DEBUG) {
319             Slog.d(TAG,
320                     "onPackageUpdateFinished userId: " + userId + ", packageName: " + packageName);
321         }
322         String user = Integer.toString(userId);
323         File file = getArchivedPackagesFile();
324         if (file.exists()) {
325             SharedPreferences sp = getArchivedPackagesSp(file);
326             Set<String> packageNames = new ArraySet<>(sp.getStringSet(user, new ArraySet<>()));
327             if (packageNames.remove(packageName)) {
328                 SharedPreferences.Editor editor = sp.edit();
329                 if (packageNames.isEmpty()) {
330                     if (!editor.remove(user).commit()) {
331                         Slog.e(TAG, "Failed to remove the user");
332                     }
333                     if (sp.getAll().isEmpty()) {
334                         file.delete();
335                     }
336                 } else {
337                     // commit and log the result.
338                     if (!editor.putStringSet(user, packageNames).commit()) {
339                         Slog.e(TAG, "failed to remove the package");
340                     }
341                 }
342                 checkStageDataAndApplyRestore(packageName, userId);
343             }
344         }
345         cleanApplicationLocalesIfNeeded(packageName, userId);
346     }
347 
348     /**
349      * <p><b>Note:</b> This is invoked by service's common monitor
350      * {@link LocaleManagerServicePackageMonitor#onPackageDataCleared} when a package's data
351      * is cleared.
352      */
onPackageDataCleared(String packageName, int uid)353     void onPackageDataCleared(String packageName, int uid) {
354         try {
355             notifyBackupManager();
356             int userId = UserHandle.getUserId(uid);
357             removePackageFromPersistedInfo(packageName, userId);
358         } catch (Exception e) {
359             Slog.e(TAG, "Exception in onPackageDataCleared.", e);
360         }
361     }
362 
363     /**
364      * <p><b>Note:</b> This is invoked by service's common monitor
365      * {@link LocaleManagerServicePackageMonitor#onPackageRemoved} when a package is removed
366      * from device.
367      */
onPackageRemoved(String packageName, int uid)368     void onPackageRemoved(String packageName, int uid) {
369         try {
370             notifyBackupManager();
371             int userId = UserHandle.getUserId(uid);
372             removePackageFromPersistedInfo(packageName, userId);
373         } catch (Exception e) {
374             Slog.e(TAG, "Exception in onPackageRemoved.", e);
375         }
376     }
377 
checkStageDataAndApplyRestore(String packageName, int userId)378     private void checkStageDataAndApplyRestore(String packageName, int userId) {
379         try {
380             synchronized (mStagedDataLock) {
381                 cleanStagedDataForOldEntriesLocked(userId);
382                 if (!getStagedDataSp(userId).getString(packageName, "").isEmpty()) {
383                     if (DEBUG) {
384                         Slog.d(TAG,
385                                 "checkStageDataAndApplyRestore, remove package and restore data");
386                     }
387                     removeFromArchivedPackagesInfo(userId, packageName);
388                     // Perform lazy restore only if the staged data exists.
389                     doLazyRestoreLocked(packageName, userId);
390                 }
391             }
392         } catch (Exception e) {
393             Slog.e(TAG, "Exception in onPackageAdded.", e);
394         }
395     }
396 
isPackageInstalledForUser(String packageName, int userId)397     private boolean isPackageInstalledForUser(String packageName, int userId) {
398         PackageInfo pkgInfo = null;
399         try {
400             pkgInfo = mContext.getPackageManager().getPackageInfoAsUser(
401                     packageName, /* flags= */ 0, userId);
402         } catch (PackageManager.NameNotFoundException e) {
403             if (DEBUG) {
404                 Slog.d(TAG, "Could not get package info for " + packageName, e);
405             }
406         }
407         return pkgInfo != null;
408     }
409 
410     /**
411      * Checks if locales already exist for the application and applies the restore accordingly.
412      * <p>
413      * The user might change the locales for an application before the restore is applied. In this
414      * case, we want to keep the user settings and discard the restore.
415      */
checkExistingLocalesAndApplyRestore(@onNull String pkgName, LocalesInfo localesInfo, int userId)416     private void checkExistingLocalesAndApplyRestore(@NonNull String pkgName,
417             LocalesInfo localesInfo, int userId) {
418         if (localesInfo == null) {
419             Slog.w(TAG, "No locales info for " + pkgName);
420             return;
421         }
422 
423         try {
424             LocaleList currLocales = mLocaleManagerService.getApplicationLocales(
425                     pkgName,
426                     userId);
427             if (!currLocales.isEmpty()) {
428                 return;
429             }
430         } catch (RemoteException | IllegalArgumentException e) {
431             Slog.e(TAG, "Could not check for current locales before restoring", e);
432         }
433 
434         // Restore the locale immediately
435         try {
436             mLocaleManagerService.setApplicationLocales(pkgName, userId,
437                     LocaleList.forLanguageTags(localesInfo.mLocales), localesInfo.mSetFromDelegate,
438                     FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE);
439             if (DEBUG) {
440                 Slog.d(TAG, "Restored locales=" + localesInfo.mLocales + " fromDelegate="
441                         + localesInfo.mSetFromDelegate + " for package=" + pkgName);
442             }
443         } catch (RemoteException | IllegalArgumentException e) {
444             Slog.e(TAG, "Could not restore locales for " + pkgName, e);
445         }
446     }
447 
deleteStagedDataLocked(@serIdInt int userId)448     void deleteStagedDataLocked(@UserIdInt int userId) {
449         File stagedFile = getStagedDataFile(userId);
450         SharedPreferences sp = getStagedDataSp(stagedFile);
451         // commit and log the result.
452         if (!sp.edit().clear().commit()) {
453             Slog.e(TAG, "Failed to commit data!");
454         }
455 
456         if (stagedFile.exists()) {
457             stagedFile.delete();
458         }
459     }
460 
461     /**
462      * Parses the backup data from the serialized xml input stream.
463      */
readFromXml(TypedXmlPullParser parser)464     private @NonNull HashMap<String, LocalesInfo> readFromXml(TypedXmlPullParser parser)
465             throws IOException, XmlPullParserException {
466         HashMap<String, LocalesInfo> packageStates = new HashMap<>();
467         int depth = parser.getDepth();
468         while (XmlUtils.nextElementWithin(parser, depth)) {
469             if (parser.getName().equals(PACKAGE_XML_TAG)) {
470                 String packageName = parser.getAttributeValue(/* namespace= */ null,
471                         ATTR_PACKAGE_NAME);
472                 String languageTags = parser.getAttributeValue(/* namespace= */ null, ATTR_LOCALES);
473                 boolean delegateSelector = parser.getAttributeBoolean(/* namespace= */ null,
474                         ATTR_DELEGATE_SELECTOR, false);
475 
476                 if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(languageTags)) {
477                     LocalesInfo localesInfo = new LocalesInfo(languageTags, delegateSelector);
478                     packageStates.put(packageName, localesInfo);
479                 }
480             }
481         }
482         return packageStates;
483     }
484 
485     /**
486      * Converts the list of app backup data into a serialized xml stream.
487      */
writeToXml(OutputStream stream, @NonNull HashMap<String, LocalesInfo> pkgStates)488     private static void writeToXml(OutputStream stream,
489             @NonNull HashMap<String, LocalesInfo> pkgStates) throws IOException {
490         if (pkgStates.isEmpty()) {
491             // No need to write anything at all if pkgStates is empty.
492             return;
493         }
494 
495         TypedXmlSerializer out = Xml.newFastSerializer();
496         out.setOutput(stream, StandardCharsets.UTF_8.name());
497         out.startDocument(/* encoding= */ null, /* standalone= */ true);
498         out.startTag(/* namespace= */ null, LOCALES_XML_TAG);
499 
500         for (String pkg : pkgStates.keySet()) {
501             out.startTag(/* namespace= */ null, PACKAGE_XML_TAG);
502             out.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, pkg);
503             out.attribute(/* namespace= */ null, ATTR_LOCALES, pkgStates.get(pkg).mLocales);
504             out.attributeBoolean(/* namespace= */ null, ATTR_DELEGATE_SELECTOR,
505                     pkgStates.get(pkg).mSetFromDelegate);
506             out.endTag(/*namespace= */ null, PACKAGE_XML_TAG);
507         }
508 
509         out.endTag(/* namespace= */ null, LOCALES_XML_TAG);
510         out.endDocument();
511     }
512 
513     static class LocalesInfo {
514         final String mLocales;
515         final boolean mSetFromDelegate;
516 
LocalesInfo(String locales, boolean setFromDelegate)517         LocalesInfo(String locales, boolean setFromDelegate) {
518             mLocales = locales;
519             mSetFromDelegate = setFromDelegate;
520         }
521     }
522 
523     /**
524      * Broadcast listener to capture user removed event.
525      *
526      * <p>The stage data is deleted when a user is removed.
527      */
528     private final class UserMonitor extends BroadcastReceiver {
529         @Override
onReceive(Context context, Intent intent)530         public void onReceive(Context context, Intent intent) {
531             try {
532                 String action = intent.getAction();
533                 if (action.equals(Intent.ACTION_USER_REMOVED)) {
534                     final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
535                     synchronized (mStagedDataLock) {
536                         deleteStagedDataLocked(userId);
537                         removeProfileFromPersistedInfo(userId);
538                         removeArchivedPackagesForUser(userId);
539                     }
540                 }
541             } catch (Exception e) {
542                 Slog.e(TAG, "Exception in user monitor.", e);
543             }
544         }
545     }
546 
547     /**
548      * Performs lazy restore from the staged data.
549      *
550      * <p>This is invoked by the package monitor on the package added callback.
551      */
doLazyRestoreLocked(String packageName, int userId)552     private void doLazyRestoreLocked(String packageName, int userId) {
553         if (DEBUG) {
554             Slog.d(TAG, "doLazyRestore package=" + packageName + " user=" + userId);
555         }
556 
557         // Check if the package is installed indeed
558         if (!isPackageInstalledForUser(packageName, userId)) {
559             Slog.e(TAG, packageName + " not installed for user " + userId
560                     + ". Could not restore locales from stage data");
561             return;
562         }
563 
564         SharedPreferences sp = getStagedDataSp(userId);
565         String value = sp.getString(packageName, "");
566         if (!value.isEmpty()) {
567             String[] info = value.split(STRING_SPLIT);
568             if (info == null || info.length != 2) {
569                 Slog.e(TAG, "Failed to restore data");
570                 return;
571             }
572             LocalesInfo localesInfo = new LocalesInfo(info[0], Boolean.parseBoolean(info[1]));
573             checkExistingLocalesAndApplyRestore(packageName, localesInfo, userId);
574 
575             // Remove the restored entry from the staged data list.
576             if (!sp.edit().remove(packageName).commit()) {
577                 Slog.e(TAG, "Failed to commit data!");
578             }
579         }
580 
581         // Remove the stage data entry for user if there are no more packages to restore.
582         if (sp.getAll().size() == 1 && sp.getLong(KEY_STAGED_DATA_TIME, -1) != -1) {
583             deleteStagedDataLocked(userId);
584         }
585     }
586 
getStagedDataFile(@serIdInt int userId)587     private File getStagedDataFile(@UserIdInt int userId) {
588         return mStagedDataFiles == null ? new File(Environment.getDataSystemDeDirectory(userId),
589             LOCALES_STAGED_DATA_PREFS) : mStagedDataFiles.get(userId);
590     }
591 
getStagedDataSp(File file)592     private SharedPreferences getStagedDataSp(File file) {
593         return mStagedDataFiles == null ? mContext.createDeviceProtectedStorageContext()
594             .getSharedPreferences(file, Context.MODE_PRIVATE)
595             : mContext.getSharedPreferences(file, Context.MODE_PRIVATE);
596     }
597 
getStagedDataSp(@serIdInt int userId)598     private SharedPreferences getStagedDataSp(@UserIdInt int userId) {
599         return mStagedDataFiles == null ? mContext.createDeviceProtectedStorageContext()
600             .getSharedPreferences(getStagedDataFile(userId), Context.MODE_PRIVATE)
601             : mContext.getSharedPreferences(mStagedDataFiles.get(userId), Context.MODE_PRIVATE);
602     }
603 
604     /**
605      * Store the staged locales info.
606      */
storeStagedDataInfo(@serIdInt int userId, @NonNull String packageName, @NonNull LocalesInfo localesInfo)607     private void storeStagedDataInfo(@UserIdInt int userId, @NonNull String packageName,
608             @NonNull LocalesInfo localesInfo) {
609         if (DEBUG) {
610             Slog.d(TAG, "storeStagedDataInfo, userId: " + userId + ", packageName: " + packageName
611                     + ", localesInfo.mLocales: " + localesInfo.mLocales
612                     + ", localesInfo.mSetFromDelegate: " + localesInfo.mSetFromDelegate);
613         }
614         String info =
615                 localesInfo.mLocales + STRING_SPLIT + String.valueOf(localesInfo.mSetFromDelegate);
616         SharedPreferences sp = getStagedDataSp(userId);
617         // commit and log the result.
618         if (!sp.edit().putString(packageName, info).commit()) {
619             Slog.e(TAG, "Failed to commit data!");
620         }
621     }
622 
623     /**
624      * Store the time of creation for staged locales info.
625      */
storeStagedDataCreatedTime(@serIdInt int userId)626     private void storeStagedDataCreatedTime(@UserIdInt int userId) {
627         SharedPreferences sp = getStagedDataSp(userId);
628         // commit and log the result.
629         if (!sp.edit().putLong(KEY_STAGED_DATA_TIME, mClock.millis()).commit()) {
630             Slog.e(TAG, "Failed to commit data!");
631         }
632     }
633 
getArchivedPackagesFile()634     private File getArchivedPackagesFile() {
635         return mArchivedPackagesFile == null ? new File(
636             Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM),
637             ARCHIVED_PACKAGES_PREFS) : mArchivedPackagesFile;
638     }
639 
getArchivedPackagesSp(File file)640     private SharedPreferences getArchivedPackagesSp(File file) {
641         return mArchivedPackagesFile == null ? mContext.createDeviceProtectedStorageContext()
642             .getSharedPreferences(file, Context.MODE_PRIVATE)
643             : mContext.getSharedPreferences(file, Context.MODE_PRIVATE);
644     }
645 
646     /**
647      * Add the package into the archived packages list.
648      */
addInArchivedPackagesInfo(@serIdInt int userId, @NonNull String packageName)649     private void addInArchivedPackagesInfo(@UserIdInt int userId, @NonNull String packageName) {
650         String user = Integer.toString(userId);
651         SharedPreferences sp = getArchivedPackagesSp(getArchivedPackagesFile());
652         Set<String> packageNames = new ArraySet<>(sp.getStringSet(user, new ArraySet<>()));
653         if (DEBUG) {
654             Slog.d(TAG, "addInArchivedPackagesInfo before packageNames: " + packageNames
655                     + ", packageName: " + packageName);
656         }
657         if (packageNames.add(packageName)) {
658             // commit and log the result.
659             if (!sp.edit().putStringSet(user, packageNames).commit()) {
660                 Slog.e(TAG, "failed to add the package");
661             }
662         }
663     }
664 
665     /**
666      * Remove the package from the archived packages list.
667      */
removeFromArchivedPackagesInfo(@serIdInt int userId, @NonNull String packageName)668     private void removeFromArchivedPackagesInfo(@UserIdInt int userId,
669             @NonNull String packageName) {
670         File file = getArchivedPackagesFile();
671         if (file.exists()) {
672             String user = Integer.toString(userId);
673             SharedPreferences sp = getArchivedPackagesSp(getArchivedPackagesFile());
674             Set<String> packageNames = new ArraySet<>(sp.getStringSet(user, new ArraySet<>()));
675             if (DEBUG) {
676                 Slog.d(TAG, "removeFromArchivedPackagesInfo before packageNames: " + packageNames
677                         + ", packageName: " + packageName);
678             }
679             if (packageNames.remove(packageName)) {
680                 SharedPreferences.Editor editor = sp.edit();
681                 if (packageNames.isEmpty()) {
682                     if (!editor.remove(user).commit()) {
683                         Slog.e(TAG, "Failed to remove user");
684                     }
685                     if (sp.getAll().isEmpty()) {
686                         file.delete();
687                     }
688                 } else {
689                     // commit and log the result.
690                     if (!editor.putStringSet(user, packageNames).commit()) {
691                         Slog.e(TAG, "failed to remove the package");
692                     }
693                 }
694             }
695         }
696     }
697 
698     /**
699      * Remove the user from the archived packages list.
700      */
removeArchivedPackagesForUser(@serIdInt int userId)701     private void removeArchivedPackagesForUser(@UserIdInt int userId) {
702         String user = Integer.toString(userId);
703         File file = getArchivedPackagesFile();
704         SharedPreferences sp = getArchivedPackagesSp(file);
705 
706         if (sp == null || !sp.contains(user)) {
707             Slog.w(TAG, "The profile is not existed in the archived package info");
708             return;
709         }
710 
711         if (!sp.edit().remove(user).commit()) {
712             Slog.e(TAG, "Failed to remove user");
713         }
714 
715         if (sp.getAll().isEmpty() && file.exists()) {
716             file.delete();
717         }
718     }
719 
createPersistedInfo()720     SharedPreferences createPersistedInfo() {
721         final File prefsFile = new File(
722                 Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM),
723                 LOCALES_FROM_DELEGATE_PREFS);
724         return mContext.createDeviceProtectedStorageContext().getSharedPreferences(prefsFile,
725                 Context.MODE_PRIVATE);
726     }
727 
getPersistedInfo()728     public SharedPreferences getPersistedInfo() {
729         return mDelegateAppLocalePackages;
730     }
731 
removePackageFromPersistedInfo(String packageName, @UserIdInt int userId)732     private void removePackageFromPersistedInfo(String packageName, @UserIdInt int userId) {
733         if (mDelegateAppLocalePackages == null) {
734             Slog.w(TAG, "Failed to persist data into the shared preference!");
735             return;
736         }
737 
738         String key = Integer.toString(userId);
739         Set<String> packageNames = new ArraySet<>(
740                 mDelegateAppLocalePackages.getStringSet(key, new ArraySet<>()));
741         if (packageNames.contains(packageName)) {
742             if (DEBUG) {
743                 Slog.d(TAG, "remove " + packageName + " from persisted info");
744             }
745             packageNames.remove(packageName);
746             SharedPreferences.Editor editor = mDelegateAppLocalePackages.edit();
747             editor.putStringSet(key, packageNames);
748 
749             // commit and log the result.
750             if (!editor.commit()) {
751                 Slog.e(TAG, "Failed to commit data!");
752             }
753         }
754     }
755 
removeProfileFromPersistedInfo(@serIdInt int userId)756     private void removeProfileFromPersistedInfo(@UserIdInt int userId) {
757         String key = Integer.toString(userId);
758 
759         if (mDelegateAppLocalePackages == null || !mDelegateAppLocalePackages.contains(key)) {
760             Slog.w(TAG, "The profile is not existed in the persisted info");
761             return;
762         }
763 
764         if (!mDelegateAppLocalePackages.edit().remove(key).commit()) {
765             Slog.e(TAG, "Failed to commit data!");
766         }
767     }
768 
769     /**
770      * Persists the package name of per-app locales set from a delegate selector.
771      *
772      * <p>This information is used when the user has set per-app locales for a specific application
773      * from the delegate selector, and then the LocaleConfig of that application is removed in the
774      * upgraded version, the per-app locales needs to be reset to system default locales to avoid
775      * the user being unable to change system locales setting.
776      */
persistLocalesModificationInfo(@serIdInt int userId, String packageName, boolean fromDelegate, boolean emptyLocales)777     void persistLocalesModificationInfo(@UserIdInt int userId, String packageName,
778             boolean fromDelegate, boolean emptyLocales) {
779         if (mDelegateAppLocalePackages == null) {
780             Slog.w(TAG, "Failed to persist data into the shared preference!");
781             return;
782         }
783 
784         SharedPreferences.Editor editor = mDelegateAppLocalePackages.edit();
785         String user = Integer.toString(userId);
786         Set<String> packageNames = new ArraySet<>(
787                 mDelegateAppLocalePackages.getStringSet(user, new ArraySet<>()));
788         if (fromDelegate && !emptyLocales) {
789             if (!packageNames.contains(packageName)) {
790                 if (DEBUG) {
791                     Slog.d(TAG, "persist package: " + packageName);
792                 }
793                 packageNames.add(packageName);
794                 editor.putStringSet(user, packageNames);
795             }
796         } else {
797             // Remove the package name if per-app locales was not set from the delegate selector
798             // or they were set to empty.
799             if (packageNames.contains(packageName)) {
800                 if (DEBUG) {
801                     Slog.d(TAG, "remove package: " + packageName);
802                 }
803                 packageNames.remove(packageName);
804                 editor.putStringSet(user, packageNames);
805             }
806         }
807 
808         // commit and log the result.
809         if (!editor.commit()) {
810             Slog.e(TAG, "failed to commit locale setter info");
811         }
812     }
813 
areLocalesSetFromDelegate(@serIdInt int userId, String packageName)814     boolean areLocalesSetFromDelegate(@UserIdInt int userId, String packageName) {
815         if (mDelegateAppLocalePackages == null) {
816             Slog.w(TAG, "Failed to persist data into the shared preference!");
817             return false;
818         }
819 
820         String user = Integer.toString(userId);
821         Set<String> packageNames = new ArraySet<>(
822                 mDelegateAppLocalePackages.getStringSet(user, new ArraySet<>()));
823 
824         return packageNames.contains(packageName);
825     }
826 
827     /**
828      * When the user has set per-app locales for a specific application from a delegate selector,
829      * and then the LocaleConfig of that application is removed in the upgraded version, the per-app
830      * locales need to be removed or reset to system default locales to avoid the user being unable
831      * to change system locales setting.
832      */
cleanApplicationLocalesIfNeeded(String packageName, int userId)833     private void cleanApplicationLocalesIfNeeded(String packageName, int userId) {
834         if (mDelegateAppLocalePackages == null) {
835             Slog.w(TAG, "Failed to persist data into the shared preference!");
836             return;
837         }
838 
839         String user = Integer.toString(userId);
840         Set<String> packageNames = new ArraySet<>(
841                 mDelegateAppLocalePackages.getStringSet(user, new ArraySet<>()));
842         try {
843             LocaleList appLocales = mLocaleManagerService.getApplicationLocales(packageName,
844                     userId);
845             if (appLocales.isEmpty() || !packageNames.contains(packageName)) {
846                 return;
847             }
848         } catch (RemoteException | IllegalArgumentException e) {
849             Slog.e(TAG, "Exception when getting locales for " + packageName, e);
850             return;
851         }
852 
853         try {
854             LocaleConfig localeConfig = new LocaleConfig(
855                     mContext.createPackageContextAsUser(packageName, 0, UserHandle.of(userId)));
856             mLocaleManagerService.removeUnsupportedAppLocales(packageName, userId, localeConfig,
857                     FrameworkStatsLog
858                             .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APP_UPDATE_LOCALES_CHANGE);
859         } catch (PackageManager.NameNotFoundException e) {
860             Slog.e(TAG, "Can not found the package name : " + packageName + " / " + e);
861         }
862     }
863 }
864