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