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