• 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.backup.BackupManager;
26 import android.content.BroadcastReceiver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageInfo;
32 import android.content.pm.PackageManager;
33 import android.os.HandlerThread;
34 import android.os.LocaleList;
35 import android.os.RemoteException;
36 import android.os.UserHandle;
37 import android.text.TextUtils;
38 import android.util.Slog;
39 import android.util.SparseArray;
40 import android.util.TypedXmlPullParser;
41 import android.util.TypedXmlSerializer;
42 import android.util.Xml;
43 
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.util.XmlUtils;
46 
47 import org.xmlpull.v1.XmlPullParser;
48 import org.xmlpull.v1.XmlPullParserException;
49 
50 import java.io.ByteArrayInputStream;
51 import java.io.ByteArrayOutputStream;
52 import java.io.IOException;
53 import java.io.OutputStream;
54 import java.io.UnsupportedEncodingException;
55 import java.nio.charset.StandardCharsets;
56 import java.time.Clock;
57 import java.time.Duration;
58 import java.util.HashMap;
59 
60 /**
61  * Helper class for managing backup and restore of app-specific locales.
62  */
63 class LocaleManagerBackupHelper {
64     private static final String TAG = "LocaleManagerBkpHelper"; // must be < 23 chars
65 
66     // Tags and attributes for xml.
67     private static final String LOCALES_XML_TAG = "locales";
68     private static final String PACKAGE_XML_TAG = "package";
69     private static final String ATTR_PACKAGE_NAME = "name";
70     private static final String ATTR_LOCALES = "locales";
71     private static final String ATTR_CREATION_TIME_MILLIS = "creationTimeMillis";
72 
73     private static final String SYSTEM_BACKUP_PACKAGE_KEY = "android";
74     // Stage data would be deleted on reboot since it's stored in memory. So it's retained until
75     // retention period OR next reboot, whichever happens earlier.
76     private static final Duration STAGE_DATA_RETENTION_PERIOD = Duration.ofDays(3);
77 
78     private final LocaleManagerService mLocaleManagerService;
79     private final PackageManager mPackageManager;
80     private final Clock mClock;
81     private final Context mContext;
82     private final Object mStagedDataLock = new Object();
83 
84     // Staged data map keyed by user-id to handle multi-user scenario / work profiles. We are using
85     // SparseArray because it is more memory-efficient than a HashMap.
86     private final SparseArray<StagedData> mStagedData;
87 
88     private final BroadcastReceiver mUserMonitor;
89 
LocaleManagerBackupHelper(LocaleManagerService localeManagerService, PackageManager packageManager, HandlerThread broadcastHandlerThread)90     LocaleManagerBackupHelper(LocaleManagerService localeManagerService,
91             PackageManager packageManager, HandlerThread broadcastHandlerThread) {
92         this(localeManagerService.mContext, localeManagerService, packageManager, Clock.systemUTC(),
93                 new SparseArray<>(), broadcastHandlerThread);
94     }
95 
LocaleManagerBackupHelper(Context context, LocaleManagerService localeManagerService, PackageManager packageManager, Clock clock, SparseArray<StagedData> stagedData, HandlerThread broadcastHandlerThread)96     @VisibleForTesting LocaleManagerBackupHelper(Context context,
97             LocaleManagerService localeManagerService,
98             PackageManager packageManager, Clock clock, SparseArray<StagedData> stagedData,
99             HandlerThread broadcastHandlerThread) {
100         mContext = context;
101         mLocaleManagerService = localeManagerService;
102         mPackageManager = packageManager;
103         mClock = clock;
104         mStagedData = stagedData;
105 
106         mUserMonitor = new UserMonitor();
107         IntentFilter filter = new IntentFilter();
108         filter.addAction(Intent.ACTION_USER_REMOVED);
109         context.registerReceiverAsUser(mUserMonitor, UserHandle.ALL, filter,
110                 null, broadcastHandlerThread.getThreadHandler());
111     }
112 
113     @VisibleForTesting
getUserMonitor()114     BroadcastReceiver getUserMonitor() {
115         return mUserMonitor;
116     }
117 
118     /**
119      * @see LocaleManagerInternal#getBackupPayload(int userId)
120      */
getBackupPayload(int userId)121     public byte[] getBackupPayload(int userId) {
122         if (DEBUG) {
123             Slog.d(TAG, "getBackupPayload invoked for user id " + userId);
124         }
125 
126         synchronized (mStagedDataLock) {
127             cleanStagedDataForOldEntriesLocked();
128         }
129 
130         HashMap<String, String> pkgStates = new HashMap<>();
131         for (ApplicationInfo appInfo : mPackageManager.getInstalledApplicationsAsUser(
132                 PackageManager.ApplicationInfoFlags.of(0), userId)) {
133             try {
134                 LocaleList appLocales = mLocaleManagerService.getApplicationLocales(
135                         appInfo.packageName,
136                         userId);
137                 // Backup locales only for apps which do have app-specific overrides.
138                 if (!appLocales.isEmpty()) {
139                     if (DEBUG) {
140                         Slog.d(TAG, "Add package=" + appInfo.packageName + " locales="
141                                 + appLocales.toLanguageTags() + " to backup payload");
142                     }
143                     pkgStates.put(appInfo.packageName, appLocales.toLanguageTags());
144                 }
145             } catch (RemoteException | IllegalArgumentException e) {
146                 Slog.e(TAG, "Exception when getting locales for package: " + appInfo.packageName,
147                         e);
148             }
149         }
150 
151         if (pkgStates.isEmpty()) {
152             if (DEBUG) {
153                 Slog.d(TAG, "Final payload=null");
154             }
155             // Returning null here will ensure deletion of the entry for LMS from the backup data.
156             return null;
157         }
158 
159         final ByteArrayOutputStream out = new ByteArrayOutputStream();
160         try {
161             writeToXml(out, pkgStates);
162         } catch (IOException e) {
163             Slog.e(TAG, "Could not write to xml for backup ", e);
164             return null;
165         }
166 
167         if (DEBUG) {
168             try {
169                 Slog.d(TAG, "Final payload=" + out.toString("UTF-8"));
170             } catch (UnsupportedEncodingException e) {
171                 Slog.w(TAG, "Could not encode payload to UTF-8", e);
172             }
173         }
174         return out.toByteArray();
175     }
176 
cleanStagedDataForOldEntriesLocked()177     private void cleanStagedDataForOldEntriesLocked() {
178         for (int i = 0; i < mStagedData.size(); i++) {
179             int userId = mStagedData.keyAt(i);
180             StagedData stagedData = mStagedData.get(userId);
181             if (stagedData.mCreationTimeMillis
182                     < mClock.millis() - STAGE_DATA_RETENTION_PERIOD.toMillis()) {
183                 deleteStagedDataLocked(userId);
184             }
185         }
186     }
187 
188     /**
189      * @see LocaleManagerInternal#stageAndApplyRestoredPayload(byte[] payload, int userId)
190      */
stageAndApplyRestoredPayload(byte[] payload, int userId)191     public void stageAndApplyRestoredPayload(byte[] payload, int userId) {
192         if (DEBUG) {
193             Slog.d(TAG, "stageAndApplyRestoredPayload user=" + userId + " payload="
194                     + (payload != null ? new String(payload, StandardCharsets.UTF_8) : null));
195         }
196         if (payload == null) {
197             Slog.e(TAG, "stageAndApplyRestoredPayload: no payload to restore for user " + userId);
198             return;
199         }
200 
201         final ByteArrayInputStream inputStream = new ByteArrayInputStream(payload);
202 
203         HashMap<String, String> pkgStates;
204         try {
205             // Parse the input blob into a list of BackupPackageState.
206             final TypedXmlPullParser parser = Xml.newFastPullParser();
207             parser.setInput(inputStream, StandardCharsets.UTF_8.name());
208 
209             XmlUtils.beginDocument(parser, LOCALES_XML_TAG);
210             pkgStates = readFromXml(parser);
211         } catch (IOException | XmlPullParserException e) {
212             Slog.e(TAG, "Could not parse payload ", e);
213             return;
214         }
215 
216         // We need a lock here to prevent race conditions when accessing the stage file.
217         // It might happen that a restore was triggered (manually using bmgr cmd) and at the same
218         // time a new package is added. We want to ensure that both these operations aren't
219         // performed simultaneously.
220         synchronized (mStagedDataLock) {
221             // Backups for apps which are yet to be installed.
222             StagedData stagedData = new StagedData(mClock.millis(), new HashMap<>());
223 
224             for (String pkgName : pkgStates.keySet()) {
225                 String languageTags = pkgStates.get(pkgName);
226                 // Check if the application is already installed for the concerned user.
227                 if (isPackageInstalledForUser(pkgName, userId)) {
228                     // Don't apply the restore if the locales have already been set for the app.
229                     checkExistingLocalesAndApplyRestore(pkgName, languageTags, userId);
230                 } else {
231                     // Stage the data if the app isn't installed.
232                     stagedData.mPackageStates.put(pkgName, languageTags);
233                     if (DEBUG) {
234                         Slog.d(TAG, "Add locales=" + languageTags
235                                 + " package=" + pkgName + " for lazy restore.");
236                     }
237                 }
238             }
239 
240             if (!stagedData.mPackageStates.isEmpty()) {
241                 mStagedData.put(userId, stagedData);
242             }
243         }
244     }
245 
246     /**
247      * Notifies the backup manager to include the "android" package in the next backup pass.
248      */
notifyBackupManager()249     public void notifyBackupManager() {
250         BackupManager.dataChanged(SYSTEM_BACKUP_PACKAGE_KEY);
251     }
252 
253     /**
254      * <p><b>Note:</b> This is invoked by service's common monitor
255      * {@link LocaleManagerServicePackageMonitor#onPackageAdded} when a new package is
256      * added on device.
257      */
onPackageAdded(String packageName, int uid)258     void onPackageAdded(String packageName, int uid) {
259         try {
260             synchronized (mStagedDataLock) {
261                 cleanStagedDataForOldEntriesLocked();
262 
263                 int userId = UserHandle.getUserId(uid);
264                 if (mStagedData.contains(userId)) {
265                     // Perform lazy restore only if the staged data exists.
266                     doLazyRestoreLocked(packageName, userId);
267                 }
268             }
269         } catch (Exception e) {
270             Slog.e(TAG, "Exception in onPackageAdded.", e);
271         }
272     }
273 
274     /**
275      * <p><b>Note:</b> This is invoked by service's common monitor
276      * {@link LocaleManagerServicePackageMonitor#onPackageDataCleared} when a package's data
277      * is cleared.
278      */
onPackageDataCleared()279     void onPackageDataCleared() {
280         try {
281             notifyBackupManager();
282         } catch (Exception e) {
283             Slog.e(TAG, "Exception in onPackageDataCleared.", e);
284         }
285     }
286 
287     /**
288      * <p><b>Note:</b> This is invoked by service's common monitor
289      * {@link LocaleManagerServicePackageMonitor#onPackageRemoved} when a package is removed
290      * from device.
291      */
onPackageRemoved()292     void onPackageRemoved() {
293         try {
294             notifyBackupManager();
295         } catch (Exception e) {
296             Slog.e(TAG, "Exception in onPackageRemoved.", e);
297         }
298     }
299 
isPackageInstalledForUser(String packageName, int userId)300     private boolean isPackageInstalledForUser(String packageName, int userId) {
301         PackageInfo pkgInfo = null;
302         try {
303             pkgInfo = mContext.getPackageManager().getPackageInfoAsUser(
304                     packageName, /* flags= */ 0, userId);
305         } catch (PackageManager.NameNotFoundException e) {
306             if (DEBUG) {
307                 Slog.d(TAG, "Could not get package info for " + packageName, e);
308             }
309         }
310         return pkgInfo != null;
311     }
312 
313     /**
314      * Checks if locales already exist for the application and applies the restore accordingly.
315      * <p>
316      * The user might change the locales for an application before the restore is applied. In this
317      * case, we want to keep the user settings and discard the restore.
318      */
checkExistingLocalesAndApplyRestore(@onNull String pkgName, @NonNull String languageTags, int userId)319     private void checkExistingLocalesAndApplyRestore(@NonNull String pkgName,
320             @NonNull String languageTags, int userId) {
321         try {
322             LocaleList currLocales = mLocaleManagerService.getApplicationLocales(
323                     pkgName,
324                     userId);
325             if (!currLocales.isEmpty()) {
326                 return;
327             }
328         } catch (RemoteException e) {
329             Slog.e(TAG, "Could not check for current locales before restoring", e);
330         }
331 
332         // Restore the locale immediately
333         try {
334             mLocaleManagerService.setApplicationLocales(pkgName, userId,
335                     LocaleList.forLanguageTags(languageTags));
336             if (DEBUG) {
337                 Slog.d(TAG, "Restored locales=" + languageTags + " for package=" + pkgName);
338             }
339         } catch (RemoteException | IllegalArgumentException e) {
340             Slog.e(TAG, "Could not restore locales for " + pkgName, e);
341         }
342     }
343 
deleteStagedDataLocked(@serIdInt int userId)344     private void deleteStagedDataLocked(@UserIdInt int userId) {
345         mStagedData.remove(userId);
346     }
347 
348     /**
349      * Parses the backup data from the serialized xml input stream.
350      */
readFromXml(XmlPullParser parser)351     private @NonNull HashMap<String, String> readFromXml(XmlPullParser parser)
352             throws IOException, XmlPullParserException {
353         HashMap<String, String> packageStates = new HashMap<>();
354         int depth = parser.getDepth();
355         while (XmlUtils.nextElementWithin(parser, depth)) {
356             if (parser.getName().equals(PACKAGE_XML_TAG)) {
357                 String packageName = parser.getAttributeValue(/* namespace= */ null,
358                         ATTR_PACKAGE_NAME);
359                 String languageTags = parser.getAttributeValue(/* namespace= */ null, ATTR_LOCALES);
360 
361                 if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(languageTags)) {
362                     packageStates.put(packageName, languageTags);
363                 }
364             }
365         }
366         return packageStates;
367     }
368 
369     /**
370      * Converts the list of app backup data into a serialized xml stream.
371      */
writeToXml(OutputStream stream, @NonNull HashMap<String, String> pkgStates)372     private static void writeToXml(OutputStream stream, @NonNull HashMap<String, String> pkgStates)
373             throws IOException {
374         if (pkgStates.isEmpty()) {
375             // No need to write anything at all if pkgStates is empty.
376             return;
377         }
378 
379         TypedXmlSerializer out = Xml.newFastSerializer();
380         out.setOutput(stream, StandardCharsets.UTF_8.name());
381         out.startDocument(/* encoding= */ null, /* standalone= */ true);
382         out.startTag(/* namespace= */ null, LOCALES_XML_TAG);
383 
384         for (String pkg : pkgStates.keySet()) {
385             out.startTag(/* namespace= */ null, PACKAGE_XML_TAG);
386             out.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, pkg);
387             out.attribute(/* namespace= */ null, ATTR_LOCALES, pkgStates.get(pkg));
388             out.endTag(/*namespace= */ null, PACKAGE_XML_TAG);
389         }
390 
391         out.endTag(/* namespace= */ null, LOCALES_XML_TAG);
392         out.endDocument();
393     }
394 
395     static class StagedData {
396         final long mCreationTimeMillis;
397         final HashMap<String, String> mPackageStates;
398 
StagedData(long creationTimeMillis, HashMap<String, String> pkgStates)399         StagedData(long creationTimeMillis, HashMap<String, String> pkgStates) {
400             mCreationTimeMillis = creationTimeMillis;
401             mPackageStates = pkgStates;
402         }
403     }
404 
405     /**
406      * Broadcast listener to capture user removed event.
407      *
408      * <p>The stage data is deleted when a user is removed.
409      */
410     private final class UserMonitor extends BroadcastReceiver {
411         @Override
onReceive(Context context, Intent intent)412         public void onReceive(Context context, Intent intent) {
413             try {
414                 String action = intent.getAction();
415                 if (action.equals(Intent.ACTION_USER_REMOVED)) {
416                     final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
417                     synchronized (mStagedDataLock) {
418                         deleteStagedDataLocked(userId);
419                     }
420                 }
421             } catch (Exception e) {
422                 Slog.e(TAG, "Exception in user monitor.", e);
423             }
424         }
425     }
426 
427     /**
428      * Performs lazy restore from the staged data.
429      *
430      * <p>This is invoked by the package monitor on the package added callback.
431      */
doLazyRestoreLocked(String packageName, int userId)432     private void doLazyRestoreLocked(String packageName, int userId) {
433         if (DEBUG) {
434             Slog.d(TAG, "doLazyRestore package=" + packageName + " user=" + userId);
435         }
436 
437         // Check if the package is installed indeed
438         if (!isPackageInstalledForUser(packageName, userId)) {
439             Slog.e(TAG, packageName + " not installed for user " + userId
440                     + ". Could not restore locales from stage data");
441             return;
442         }
443 
444         StagedData stagedData = mStagedData.get(userId);
445         for (String pkgName : stagedData.mPackageStates.keySet()) {
446             String languageTags = stagedData.mPackageStates.get(pkgName);
447 
448             if (pkgName.equals(packageName)) {
449 
450                 checkExistingLocalesAndApplyRestore(pkgName, languageTags, userId);
451 
452                 // Remove the restored entry from the staged data list.
453                 stagedData.mPackageStates.remove(pkgName);
454 
455                 // Remove the stage data entry for user if there are no more packages to restore.
456                 if (stagedData.mPackageStates.isEmpty()) {
457                     mStagedData.remove(userId);
458                 }
459 
460                 // No need to loop further after restoring locales because the staged data will
461                 // contain at most one entry for the newly added package.
462                 break;
463             }
464         }
465     }
466 }
467