1 /* 2 * Copyright (C) 2008 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.providers.settings; 18 19 import android.annotation.NonNull; 20 import android.app.ActivityManager; 21 import android.app.IActivityManager; 22 import android.app.backup.IBackupManager; 23 import android.content.ContentResolver; 24 import android.content.ContentValues; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.res.Configuration; 28 import android.hardware.display.ColorDisplayManager; 29 import android.icu.util.ULocale; 30 import android.media.AudioManager; 31 import android.media.RingtoneManager; 32 import android.net.Uri; 33 import android.os.LocaleList; 34 import android.os.RemoteException; 35 import android.os.ServiceManager; 36 import android.os.UserHandle; 37 import android.provider.Settings; 38 import android.telephony.TelephonyManager; 39 import android.text.TextUtils; 40 import android.util.ArraySet; 41 import android.util.Log; 42 43 import com.android.internal.annotations.VisibleForTesting; 44 import com.android.internal.app.LocalePicker; 45 import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; 46 47 import java.io.FileNotFoundException; 48 import java.util.ArrayList; 49 import java.util.HashMap; 50 import java.util.Locale; 51 import java.util.Set; 52 53 public class SettingsHelper { 54 private static final String TAG = "SettingsHelper"; 55 private static final String SILENT_RINGTONE = "_silent"; 56 private static final String SETTINGS_REPLACED_KEY = "backup_skip_user_facing_data"; 57 private static final String SETTING_ORIGINAL_KEY_SUFFIX = "_original"; 58 private static final String UNICODE_LOCALE_EXTENSION_FW = "fw"; 59 private static final String UNICODE_LOCALE_EXTENSION_MU = "mu"; 60 private static final String UNICODE_LOCALE_EXTENSION_NU = "nu"; 61 private static final float FLOAT_TOLERANCE = 0.01f; 62 63 /** See frameworks/base/core/res/res/values/config.xml#config_longPressOnPowerBehavior **/ 64 private static final int LONG_PRESS_POWER_NOTHING = 0; 65 private static final int LONG_PRESS_POWER_GLOBAL_ACTIONS = 1; 66 private static final int LONG_PRESS_POWER_FOR_ASSISTANT = 5; 67 /** See frameworks/base/core/res/res/values/config.xml#config_keyChordPowerVolumeUp **/ 68 private static final int KEY_CHORD_POWER_VOLUME_UP_GLOBAL_ACTIONS = 2; 69 70 private Context mContext; 71 private AudioManager mAudioManager; 72 private TelephonyManager mTelephonyManager; 73 74 /** 75 * A few settings elements are special in that a restore of those values needs to 76 * be post-processed by relevant parts of the OS. A restore of any settings element 77 * mentioned in this table will therefore cause the system to send a broadcast with 78 * the {@link Intent#ACTION_SETTING_RESTORED} action, with extras naming the 79 * affected setting and supplying its pre-restore value for comparison. 80 * 81 * @see Intent#ACTION_SETTING_RESTORED 82 * @see System#SETTINGS_TO_BACKUP 83 * @see Secure#SETTINGS_TO_BACKUP 84 * @see Global#SETTINGS_TO_BACKUP 85 * 86 * {@hide} 87 */ 88 private static final ArraySet<String> sBroadcastOnRestore; 89 private static final ArraySet<String> sBroadcastOnRestoreSystemUI; 90 static { 91 sBroadcastOnRestore = new ArraySet<String>(9); 92 sBroadcastOnRestore.add(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS); 93 sBroadcastOnRestore.add(Settings.Secure.ENABLED_VR_LISTENERS); 94 sBroadcastOnRestore.add(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); 95 sBroadcastOnRestore.add(Settings.Global.BLUETOOTH_ON); 96 sBroadcastOnRestore.add(Settings.Secure.UI_NIGHT_MODE); 97 sBroadcastOnRestore.add(Settings.Secure.DARK_THEME_CUSTOM_START_TIME); 98 sBroadcastOnRestore.add(Settings.Secure.DARK_THEME_CUSTOM_END_TIME); 99 sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED); 100 sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); 101 sBroadcastOnRestoreSystemUI = new ArraySet<String>(2); 102 sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_TILES); 103 sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_AUTO_ADDED_TILES); 104 } 105 106 private static final ArraySet<String> UNICODE_LOCALE_SUPPORTED_EXTENSIONS = new ArraySet<>(); 107 108 /** 109 * Current supported extensions are fw (first day of week) and mu (temperature unit) extension. 110 * User can set these extensions in Settings app, and it will be appended to the locale, 111 * for example: zh-Hant-TW-u-fw-mon-mu-celsius. So after the factory reset, these extensions 112 * should be restored as well because they are set by users. 113 * We do not put the nu (numbering system) extension here because it is an Android supported 114 * extension and defined in some particular locales, for example: 115 * ar-Arab-MA-u-nu-arab and ar-Arab-YE-u-nu-latn. See 116 * <code>frameworks/base/core/res/res/values/locale_config.xml</code> 117 * The nu extension should not be appended to the current/restored locale after factory reset 118 * if the current/restored locale does not have it. 119 */ 120 static { 121 UNICODE_LOCALE_SUPPORTED_EXTENSIONS.add(UNICODE_LOCALE_EXTENSION_FW); 122 UNICODE_LOCALE_SUPPORTED_EXTENSIONS.add(UNICODE_LOCALE_EXTENSION_MU); 123 } 124 125 private interface SettingsLookup { lookup(ContentResolver resolver, String name, int userHandle)126 public String lookup(ContentResolver resolver, String name, int userHandle); 127 } 128 129 private static SettingsLookup sSystemLookup = new SettingsLookup() { 130 public String lookup(ContentResolver resolver, String name, int userHandle) { 131 return Settings.System.getStringForUser(resolver, name, userHandle); 132 } 133 }; 134 135 private static SettingsLookup sSecureLookup = new SettingsLookup() { 136 public String lookup(ContentResolver resolver, String name, int userHandle) { 137 return Settings.Secure.getStringForUser(resolver, name, userHandle); 138 } 139 }; 140 141 private static SettingsLookup sGlobalLookup = new SettingsLookup() { 142 public String lookup(ContentResolver resolver, String name, int userHandle) { 143 return Settings.Global.getStringForUser(resolver, name, userHandle); 144 } 145 }; 146 SettingsHelper(Context context)147 public SettingsHelper(Context context) { 148 mContext = context; 149 mAudioManager = (AudioManager) context 150 .getSystemService(Context.AUDIO_SERVICE); 151 mTelephonyManager = (TelephonyManager) context 152 .getSystemService(Context.TELEPHONY_SERVICE); 153 } 154 155 /** 156 * Sets the property via a call to the appropriate API, if any, and returns 157 * whether or not the setting should be saved to the database as well. 158 * @param name the name of the setting 159 * @param value the string value of the setting 160 * @return whether to continue with writing the value to the database. In 161 * some cases the data will be written by the call to the appropriate API, 162 * and in some cases the property value needs to be modified before setting. 163 */ restoreValue(Context context, ContentResolver cr, ContentValues contentValues, Uri destination, String name, String value, int restoredFromSdkInt)164 public void restoreValue(Context context, ContentResolver cr, ContentValues contentValues, 165 Uri destination, String name, String value, int restoredFromSdkInt) { 166 if (isReplacedSystemSetting(name)) { 167 return; 168 } 169 170 // Will we need a post-restore broadcast for this element? 171 String oldValue = null; 172 boolean sendBroadcast = false; 173 boolean sendBroadcastSystemUI = false; 174 final SettingsLookup table; 175 176 if (destination.equals(Settings.Secure.CONTENT_URI)) { 177 table = sSecureLookup; 178 } else if (destination.equals(Settings.System.CONTENT_URI)) { 179 table = sSystemLookup; 180 } else { /* must be GLOBAL; this was preflighted by the caller */ 181 table = sGlobalLookup; 182 } 183 184 sendBroadcast = sBroadcastOnRestore.contains(name); 185 sendBroadcastSystemUI = sBroadcastOnRestoreSystemUI.contains(name); 186 187 if (sendBroadcast || sendBroadcastSystemUI) { 188 // TODO: http://b/22388012 189 oldValue = table.lookup(cr, name, UserHandle.USER_SYSTEM); 190 } 191 192 try { 193 if (Settings.System.SOUND_EFFECTS_ENABLED.equals(name)) { 194 setSoundEffects(Integer.parseInt(value) == 1); 195 // fall through to the ordinary write to settings 196 } else if (Settings.Secure.BACKUP_AUTO_RESTORE.equals(name)) { 197 setAutoRestore(Integer.parseInt(value) == 1); 198 } else if (isAlreadyConfiguredCriticalAccessibilitySetting(name)) { 199 return; 200 } else if (Settings.System.RINGTONE.equals(name) 201 || Settings.System.NOTIFICATION_SOUND.equals(name) 202 || Settings.System.ALARM_ALERT.equals(name)) { 203 setRingtone(name, value); 204 return; 205 } else if (Settings.System.DISPLAY_COLOR_MODE.equals(name)) { 206 int mode = Integer.parseInt(value); 207 String restoredVendorHint = Settings.System.getString(mContext.getContentResolver(), 208 Settings.System.DISPLAY_COLOR_MODE_VENDOR_HINT); 209 final String deviceVendorHint = mContext.getResources().getString( 210 com.android.internal.R.string.config_vendorColorModesRestoreHint); 211 boolean displayColorModeVendorModeHintsMatch = 212 !TextUtils.isEmpty(deviceVendorHint) 213 && deviceVendorHint.equals(restoredVendorHint); 214 // Replace vendor hint with new device's vendor hint. 215 contentValues.clear(); 216 contentValues.put(Settings.NameValueTable.NAME, 217 Settings.System.DISPLAY_COLOR_MODE_VENDOR_HINT); 218 contentValues.put(Settings.NameValueTable.VALUE, deviceVendorHint); 219 cr.insert(destination, contentValues); 220 // If vendor hints match, modes in the vendor range can be restored. Otherwise, only 221 // map standard modes. 222 if (!ColorDisplayManager.isStandardColorMode(mode) 223 && !displayColorModeVendorModeHintsMatch) { 224 return; 225 } 226 } else if (Settings.Global.POWER_BUTTON_LONG_PRESS.equals(name)) { 227 setLongPressPowerBehavior(cr, value); 228 return; 229 } else if (Settings.System.ACCELEROMETER_ROTATION.equals(name) 230 && shouldSkipAutoRotateRestore()) { 231 return; 232 } 233 234 // Default case: write the restored value to settings 235 contentValues.clear(); 236 contentValues.put(Settings.NameValueTable.NAME, name); 237 contentValues.put(Settings.NameValueTable.VALUE, value); 238 cr.insert(destination, contentValues); 239 } catch (Exception e) { 240 // If we fail to apply the setting, by definition nothing happened 241 sendBroadcast = false; 242 sendBroadcastSystemUI = false; 243 Log.e(TAG, "Failed to restore setting name: " + name + " + value: " + value, e); 244 } finally { 245 // If this was an element of interest, send the "we just restored it" 246 // broadcast with the historical value now that the new value has 247 // been committed and observers kicked off. 248 if (sendBroadcast || sendBroadcastSystemUI) { 249 Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED) 250 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) 251 .putExtra(Intent.EXTRA_SETTING_NAME, name) 252 .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, value) 253 .putExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE, oldValue) 254 .putExtra(Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, restoredFromSdkInt); 255 256 if (sendBroadcast) { 257 intent.setPackage("android"); 258 context.sendBroadcastAsUser(intent, UserHandle.SYSTEM, null); 259 } 260 if (sendBroadcastSystemUI) { 261 intent.setPackage( 262 context.getString(com.android.internal.R.string.config_systemUi)); 263 context.sendBroadcastAsUser(intent, UserHandle.SYSTEM, null); 264 } 265 } 266 } 267 } 268 shouldSkipAutoRotateRestore()269 private boolean shouldSkipAutoRotateRestore() { 270 // When device state based auto rotation settings are available, let's skip the restoring 271 // of the standard auto rotation settings to avoid conflicting setting values. 272 return DeviceStateRotationLockSettingsManager.isDeviceStateRotationLockEnabled(mContext); 273 } 274 onBackupValue(String name, String value)275 public String onBackupValue(String name, String value) { 276 // Special processing for backing up ringtones & notification sounds 277 if (Settings.System.RINGTONE.equals(name) 278 || Settings.System.NOTIFICATION_SOUND.equals(name) 279 || Settings.System.ALARM_ALERT.equals(name)) { 280 if (value == null) { 281 if (Settings.System.RINGTONE.equals(name)) { 282 // For ringtones, we need to distinguish between non-telephony vs telephony 283 if (mTelephonyManager != null && mTelephonyManager.isVoiceCapable()) { 284 // Backup a null ringtone as silent on voice-capable devices 285 return SILENT_RINGTONE; 286 } else { 287 // Skip backup of ringtone on non-telephony devices. 288 return null; 289 } 290 } else { 291 // Backup a null notification sound or alarm alert as silent 292 return SILENT_RINGTONE; 293 } 294 } else { 295 return getCanonicalRingtoneValue(value); 296 } 297 } 298 // Return the original value 299 return isReplacedSystemSetting(name) ? getRealValueForSystemSetting(name) : value; 300 } 301 302 /** 303 * The setting value might have been replaced temporarily. If that's the case, return the real 304 * value instead of the temporary one. 305 */ 306 @VisibleForTesting getRealValueForSystemSetting(String setting)307 public String getRealValueForSystemSetting(String setting) { 308 // The real value irrespectively of the original setting's namespace is stored in 309 // Settings.Secure. 310 return Settings.Secure.getString(mContext.getContentResolver(), 311 setting + SETTING_ORIGINAL_KEY_SUFFIX); 312 } 313 314 @VisibleForTesting isReplacedSystemSetting(String setting)315 public boolean isReplacedSystemSetting(String setting) { 316 // This list should not be modified. 317 if (!Settings.System.SCREEN_OFF_TIMEOUT.equals(setting)) { 318 return false; 319 } 320 // If this flag is set, values for the system settings from the list above have been 321 // temporarily replaced. We don't want to back up the temporary value or run restore for 322 // such settings. 323 // TODO(154822946): Remove this logic in the next release. 324 return Settings.Secure.getInt(mContext.getContentResolver(), SETTINGS_REPLACED_KEY, 325 /* def */ 0) != 0; 326 } 327 328 /** 329 * Sets the ringtone of type specified by the name. 330 * 331 * @param name should be Settings.System.RINGTONE, Settings.System.NOTIFICATION_SOUND 332 * or Settings.System.ALARM_ALERT. 333 * @param value can be a canonicalized uri or "_silent" to indicate a silent (null) ringtone. 334 */ setRingtone(String name, String value)335 private void setRingtone(String name, String value) { 336 Log.v(TAG, "Set ringtone for name: " + name + " value: " + value); 337 338 // If it's null, don't change the default. 339 if (value == null) return; 340 final int ringtoneType = getRingtoneType(name); 341 if (SILENT_RINGTONE.equals(value)) { 342 // SILENT_RINGTONE is a special constant generated by onBackupValue in the source 343 // device. 344 RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, null); 345 return; 346 } 347 348 Uri ringtoneUri = null; 349 try { 350 ringtoneUri = 351 RingtoneManager.getRingtoneUriForRestore( 352 mContext.getContentResolver(), value, ringtoneType); 353 } catch (FileNotFoundException | IllegalArgumentException e) { 354 Log.w(TAG, "Failed to resolve " + value + ": " + e); 355 // Unrecognized or invalid Uri, don't restore 356 return; 357 } 358 359 Log.v(TAG, "setActualDefaultRingtoneUri type: " + ringtoneType + ", uri: " + ringtoneUri); 360 RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, ringtoneUri); 361 } 362 getRingtoneType(String name)363 private int getRingtoneType(String name) { 364 switch (name) { 365 case Settings.System.RINGTONE: 366 return RingtoneManager.TYPE_RINGTONE; 367 case Settings.System.NOTIFICATION_SOUND: 368 return RingtoneManager.TYPE_NOTIFICATION; 369 case Settings.System.ALARM_ALERT: 370 return RingtoneManager.TYPE_ALARM; 371 default: 372 throw new IllegalArgumentException("Incorrect ringtone name: " + name); 373 } 374 } 375 getCanonicalRingtoneValue(String value)376 private String getCanonicalRingtoneValue(String value) { 377 final Uri ringtoneUri = Uri.parse(value); 378 final Uri canonicalUri = mContext.getContentResolver().canonicalize(ringtoneUri); 379 return canonicalUri == null ? null : canonicalUri.toString(); 380 } 381 isAlreadyConfiguredCriticalAccessibilitySetting(String name)382 private boolean isAlreadyConfiguredCriticalAccessibilitySetting(String name) { 383 // These are the critical accessibility settings that are required for users with 384 // accessibility needs to be able to interact with the device. If these settings are 385 // already configured, we will not overwrite them. If they are already set, 386 // it means that the user has performed a global gesture to enable accessibility or set 387 // these settings in the Accessibility portion of the Setup Wizard, and definitely needs 388 // these features working after the restore. 389 switch (name) { 390 case Settings.Secure.ACCESSIBILITY_ENABLED: 391 case Settings.Secure.TOUCH_EXPLORATION_ENABLED: 392 case Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED: 393 case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED: 394 case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED: 395 return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) != 0; 396 case Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES: 397 case Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES: 398 case Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER: 399 return !TextUtils.isEmpty(Settings.Secure.getString( 400 mContext.getContentResolver(), name)); 401 case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE: 402 float defaultScale = mContext.getResources().getFraction( 403 R.fraction.def_accessibility_display_magnification_scale, 1, 1); 404 float currentScale = Settings.Secure.getFloat( 405 mContext.getContentResolver(), name, defaultScale); 406 return Math.abs(currentScale - defaultScale) >= FLOAT_TOLERANCE; 407 case Settings.System.FONT_SCALE: 408 return Settings.System.getFloat(mContext.getContentResolver(), name, 1.0f) != 1.0f; 409 default: 410 return false; 411 } 412 } 413 setAutoRestore(boolean enabled)414 private void setAutoRestore(boolean enabled) { 415 try { 416 IBackupManager bm = IBackupManager.Stub.asInterface( 417 ServiceManager.getService(Context.BACKUP_SERVICE)); 418 if (bm != null) { 419 bm.setAutoRestore(enabled); 420 } 421 } catch (RemoteException e) {} 422 } 423 setSoundEffects(boolean enable)424 private void setSoundEffects(boolean enable) { 425 if (enable) { 426 mAudioManager.loadSoundEffects(); 427 } else { 428 mAudioManager.unloadSoundEffects(); 429 } 430 } 431 432 /** 433 * Correctly sets long press power button Behavior. 434 * 435 * The issue is that setting for LongPressPower button Behavior is not available on all devices 436 * and actually changes default Behavior of two properties - the long press power button 437 * and volume up + power button combo. OEM can also reconfigure these Behaviors in config.xml, 438 * so we need to be careful that we don't irreversibly change power button Behavior when 439 * restoring. Or switch to a non-default button Behavior. 440 */ setLongPressPowerBehavior(ContentResolver cr, String value)441 private void setLongPressPowerBehavior(ContentResolver cr, String value) { 442 // We will not restore the value if the long press power setting option is unavailable. 443 if (!mContext.getResources().getBoolean( 444 com.android.internal.R.bool.config_longPressOnPowerForAssistantSettingAvailable)) { 445 return; 446 } 447 448 int longPressOnPowerBehavior; 449 try { 450 longPressOnPowerBehavior = Integer.parseInt(value); 451 } catch (NumberFormatException e) { 452 return; 453 } 454 455 if (longPressOnPowerBehavior < LONG_PRESS_POWER_NOTHING 456 || longPressOnPowerBehavior > LONG_PRESS_POWER_FOR_ASSISTANT) { 457 return; 458 } 459 460 // When user enables long press power for Assistant, we also switch the meaning 461 // of Volume Up + Power key chord to the "Show power menu" option. 462 // If the user disables long press power for Assistant, we switch back to default OEM 463 // Behavior configured in config.xml. If the default Behavior IS "LPP for Assistant", 464 // then we fall back to "Long press for Power Menu" Behavior. 465 if (longPressOnPowerBehavior == LONG_PRESS_POWER_FOR_ASSISTANT) { 466 Settings.Global.putInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, 467 LONG_PRESS_POWER_FOR_ASSISTANT); 468 Settings.Global.putInt(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, 469 KEY_CHORD_POWER_VOLUME_UP_GLOBAL_ACTIONS); 470 } else { 471 // We're restoring "LPP for Assistant Disabled" state, prefer OEM config.xml Behavior 472 // if possible. 473 int longPressOnPowerDeviceBehavior = mContext.getResources().getInteger( 474 com.android.internal.R.integer.config_longPressOnPowerBehavior); 475 if (longPressOnPowerDeviceBehavior == LONG_PRESS_POWER_FOR_ASSISTANT) { 476 // The default on device IS "LPP for Assistant Enabled" so fall back to power menu. 477 Settings.Global.putInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, 478 LONG_PRESS_POWER_GLOBAL_ACTIONS); 479 } else { 480 // The default is non-Assistant Behavior, so restore that default. 481 Settings.Global.putInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, 482 longPressOnPowerDeviceBehavior); 483 } 484 485 // Clear and restore default power + volume up Behavior as well. 486 int powerVolumeUpDefaultBehavior = mContext.getResources().getInteger( 487 com.android.internal.R.integer.config_keyChordPowerVolumeUp); 488 Settings.Global.putInt(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, 489 powerVolumeUpDefaultBehavior); 490 } 491 } 492 getLocaleData()493 /* package */ byte[] getLocaleData() { 494 Configuration conf = mContext.getResources().getConfiguration(); 495 return conf.getLocales().toLanguageTags().getBytes(); 496 } 497 toFullLocale(@onNull Locale locale)498 private static Locale toFullLocale(@NonNull Locale locale) { 499 if (locale.getScript().isEmpty() || locale.getCountry().isEmpty()) { 500 return ULocale.addLikelySubtags(ULocale.forLocale(locale)).toLocale(); 501 } 502 return locale; 503 } 504 505 /** 506 * Merging the locale came from backup server and current device locale. 507 * 508 * Merge works with following rules. 509 * - The backup locales are appended to the current locale with keeping order. 510 * e.g. current locale "en-US,zh-CN" and backup locale "ja-JP,ko-KR" are merged to 511 * "en-US,zh-CH,ja-JP,ko-KR". 512 * 513 * - Duplicated locales are dropped. 514 * e.g. current locale "en-US,zh-CN" and backup locale "ja-JP,zh-Hans-CN,en-US" are merged to 515 * "en-US,zh-CN,ja-JP". 516 * 517 * - Unsupported locales are dropped. 518 * e.g. current locale "en-US" and backup locale "ja-JP,zh-CN" but the supported locales 519 * are "en-US,zh-CN", the merged locale list is "en-US,zh-CN". 520 * 521 * - The final result locale list only contains the supported locales. 522 * e.g. current locale "en-US" and backup locale "zh-Hans-CN" and supported locales are 523 * "en-US,zh-CN", the merged locale list is "en-US,zh-CN". 524 * 525 * @param restore The locale list that came from backup server. 526 * @param current The device's locale setting. 527 * @param supportedLocales The list of language tags supported by this device. 528 */ 529 @VisibleForTesting resolveLocales(LocaleList restore, LocaleList current, String[] supportedLocales)530 public static LocaleList resolveLocales(LocaleList restore, LocaleList current, 531 String[] supportedLocales) { 532 final HashMap<Locale, Locale> allLocales = new HashMap<>(supportedLocales.length); 533 for (String supportedLocaleStr : supportedLocales) { 534 final Locale locale = Locale.forLanguageTag(supportedLocaleStr); 535 allLocales.put(toFullLocale(locale), locale); 536 } 537 538 // After restoring to reset locales, need to get extensions from restored locale. Get the 539 // first restored locale to check its extension. 540 final Locale restoredLocale = restore.isEmpty() 541 ? Locale.ROOT 542 : restore.get(0); 543 final ArrayList<Locale> filtered = new ArrayList<>(current.size()); 544 for (int i = 0; i < current.size(); i++) { 545 Locale locale = copyExtensionToTargetLocale(restoredLocale, current.get(i)); 546 allLocales.remove(toFullLocale(locale)); 547 filtered.add(locale); 548 } 549 550 for (int i = 0; i < restore.size(); i++) { 551 final Locale restoredLocaleWithExtension = copyExtensionToTargetLocale(restoredLocale, 552 getFilteredLocale(restore.get(i), allLocales)); 553 if (restoredLocaleWithExtension != null) { 554 filtered.add(restoredLocaleWithExtension); 555 } 556 } 557 if (filtered.size() == current.size()) { 558 return current; // Nothing added to current locale list. 559 } 560 561 return new LocaleList(filtered.toArray(new Locale[filtered.size()])); 562 } 563 copyExtensionToTargetLocale(Locale restoredLocale, Locale targetLocale)564 private static Locale copyExtensionToTargetLocale(Locale restoredLocale, 565 Locale targetLocale) { 566 if (!restoredLocale.hasExtensions()) { 567 return targetLocale; 568 } 569 570 if (targetLocale == null) { 571 return null; 572 } 573 574 Locale.Builder builder = new Locale.Builder() 575 .setLocale(targetLocale); 576 Set<String> unicodeLocaleKeys = restoredLocale.getUnicodeLocaleKeys(); 577 unicodeLocaleKeys.stream().forEach(key -> { 578 // Copy all supported extensions from restored locales except "nu" extension. The "nu" 579 // extension has been added in #getFilteredLocale(Locale, HashMap<Locale, Locale>) 580 // already, we don't need to add it again. 581 if (UNICODE_LOCALE_SUPPORTED_EXTENSIONS.contains(key)) { 582 builder.setUnicodeLocaleKeyword(key, restoredLocale.getUnicodeLocaleType(key)); 583 } 584 }); 585 return builder.build(); 586 } 587 getFilteredLocale(Locale restoreLocale, HashMap<Locale, Locale> allLocales)588 private static Locale getFilteredLocale(Locale restoreLocale, 589 HashMap<Locale, Locale> allLocales) { 590 Locale locale = allLocales.remove(toFullLocale(restoreLocale)); 591 if (locale != null) { 592 return locale; 593 } 594 595 Locale filteredLocale = new Locale.Builder() 596 .setLocale(restoreLocale.stripExtensions()) 597 .setUnicodeLocaleKeyword(UNICODE_LOCALE_EXTENSION_NU, 598 restoreLocale.getUnicodeLocaleType(UNICODE_LOCALE_EXTENSION_NU)) 599 .build(); 600 return allLocales.remove(toFullLocale(filteredLocale)); 601 } 602 603 /** 604 * Sets the locale specified. Input data is the byte representation of comma separated 605 * multiple BCP-47 language tags. For backwards compatibility, strings of the form 606 * {@code ll_CC} are also accepted, where {@code ll} is a two letter language 607 * code and {@code CC} is a two letter country code. 608 * 609 * @param data the comma separated BCP-47 language tags in bytes. 610 */ setLocaleData(byte[] data, int size)611 /* package */ void setLocaleData(byte[] data, int size) { 612 final Configuration conf = mContext.getResources().getConfiguration(); 613 614 // Replace "_" with "-" to deal with older backups. 615 final String localeCodes = new String(data, 0, size).replace('_', '-'); 616 final LocaleList localeList = LocaleList.forLanguageTags(localeCodes); 617 if (localeList.isEmpty()) { 618 return; 619 } 620 621 final String[] supportedLocales = LocalePicker.getSupportedLocales(mContext); 622 final LocaleList currentLocales = conf.getLocales(); 623 624 final LocaleList merged = resolveLocales(localeList, currentLocales, supportedLocales); 625 if (merged.equals(currentLocales)) { 626 return; 627 } 628 629 try { 630 IActivityManager am = ActivityManager.getService(); 631 final Configuration config = new Configuration(); 632 config.setLocales(merged); 633 // indicate this isn't some passing default - the user wants this remembered 634 config.userSetLocale = true; 635 636 am.updatePersistentConfigurationWithAttribution(config, mContext.getOpPackageName(), 637 mContext.getAttributionTag()); 638 } catch (RemoteException e) { 639 // Intentionally left blank 640 } 641 } 642 643 /** 644 * Informs the audio service of changes to the settings so that 645 * they can be re-read and applied. 646 */ applyAudioSettings()647 void applyAudioSettings() { 648 AudioManager am = new AudioManager(mContext); 649 am.reloadAudioSettings(); 650 } 651 } 652