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