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 42 import com.android.internal.annotations.VisibleForTesting; 43 import com.android.internal.app.LocalePicker; 44 45 import java.util.ArrayList; 46 import java.util.HashMap; 47 import java.util.Locale; 48 49 public class SettingsHelper { 50 private static final String TAG = "SettingsHelper"; 51 private static final String SILENT_RINGTONE = "_silent"; 52 private static final String SETTINGS_REPLACED_KEY = "backup_skip_user_facing_data"; 53 private static final String SETTING_ORIGINAL_KEY_SUFFIX = "_original"; 54 private static final float FLOAT_TOLERANCE = 0.01f; 55 56 private Context mContext; 57 private AudioManager mAudioManager; 58 private TelephonyManager mTelephonyManager; 59 60 /** 61 * A few settings elements are special in that a restore of those values needs to 62 * be post-processed by relevant parts of the OS. A restore of any settings element 63 * mentioned in this table will therefore cause the system to send a broadcast with 64 * the {@link Intent#ACTION_SETTING_RESTORED} action, with extras naming the 65 * affected setting and supplying its pre-restore value for comparison. 66 * 67 * @see Intent#ACTION_SETTING_RESTORED 68 * @see System#SETTINGS_TO_BACKUP 69 * @see Secure#SETTINGS_TO_BACKUP 70 * @see Global#SETTINGS_TO_BACKUP 71 * 72 * {@hide} 73 */ 74 private static final ArraySet<String> sBroadcastOnRestore; 75 private static final ArraySet<String> sBroadcastOnRestoreSystemUI; 76 static { 77 sBroadcastOnRestore = new ArraySet<String>(9); 78 sBroadcastOnRestore.add(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS); 79 sBroadcastOnRestore.add(Settings.Secure.ENABLED_VR_LISTENERS); 80 sBroadcastOnRestore.add(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); 81 sBroadcastOnRestore.add(Settings.Global.BLUETOOTH_ON); 82 sBroadcastOnRestore.add(Settings.Secure.UI_NIGHT_MODE); 83 sBroadcastOnRestore.add(Settings.Secure.DARK_THEME_CUSTOM_START_TIME); 84 sBroadcastOnRestore.add(Settings.Secure.DARK_THEME_CUSTOM_END_TIME); 85 sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED); 86 sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); 87 sBroadcastOnRestoreSystemUI = new ArraySet<String>(2); 88 sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_TILES); 89 sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_AUTO_ADDED_TILES); 90 } 91 92 private interface SettingsLookup { lookup(ContentResolver resolver, String name, int userHandle)93 public String lookup(ContentResolver resolver, String name, int userHandle); 94 } 95 96 private static SettingsLookup sSystemLookup = new SettingsLookup() { 97 public String lookup(ContentResolver resolver, String name, int userHandle) { 98 return Settings.System.getStringForUser(resolver, name, userHandle); 99 } 100 }; 101 102 private static SettingsLookup sSecureLookup = new SettingsLookup() { 103 public String lookup(ContentResolver resolver, String name, int userHandle) { 104 return Settings.Secure.getStringForUser(resolver, name, userHandle); 105 } 106 }; 107 108 private static SettingsLookup sGlobalLookup = new SettingsLookup() { 109 public String lookup(ContentResolver resolver, String name, int userHandle) { 110 return Settings.Global.getStringForUser(resolver, name, userHandle); 111 } 112 }; 113 SettingsHelper(Context context)114 public SettingsHelper(Context context) { 115 mContext = context; 116 mAudioManager = (AudioManager) context 117 .getSystemService(Context.AUDIO_SERVICE); 118 mTelephonyManager = (TelephonyManager) context 119 .getSystemService(Context.TELEPHONY_SERVICE); 120 } 121 122 /** 123 * Sets the property via a call to the appropriate API, if any, and returns 124 * whether or not the setting should be saved to the database as well. 125 * @param name the name of the setting 126 * @param value the string value of the setting 127 * @return whether to continue with writing the value to the database. In 128 * some cases the data will be written by the call to the appropriate API, 129 * and in some cases the property value needs to be modified before setting. 130 */ restoreValue(Context context, ContentResolver cr, ContentValues contentValues, Uri destination, String name, String value, int restoredFromSdkInt)131 public void restoreValue(Context context, ContentResolver cr, ContentValues contentValues, 132 Uri destination, String name, String value, int restoredFromSdkInt) { 133 if (isReplacedSystemSetting(name)) { 134 return; 135 } 136 137 // Will we need a post-restore broadcast for this element? 138 String oldValue = null; 139 boolean sendBroadcast = false; 140 boolean sendBroadcastSystemUI = false; 141 final SettingsLookup table; 142 143 if (destination.equals(Settings.Secure.CONTENT_URI)) { 144 table = sSecureLookup; 145 } else if (destination.equals(Settings.System.CONTENT_URI)) { 146 table = sSystemLookup; 147 } else { /* must be GLOBAL; this was preflighted by the caller */ 148 table = sGlobalLookup; 149 } 150 151 sendBroadcast = sBroadcastOnRestore.contains(name); 152 sendBroadcastSystemUI = sBroadcastOnRestoreSystemUI.contains(name); 153 154 if (sendBroadcast || sendBroadcastSystemUI) { 155 // TODO: http://b/22388012 156 oldValue = table.lookup(cr, name, UserHandle.USER_SYSTEM); 157 } 158 159 try { 160 if (Settings.System.SOUND_EFFECTS_ENABLED.equals(name)) { 161 setSoundEffects(Integer.parseInt(value) == 1); 162 // fall through to the ordinary write to settings 163 } else if (Settings.Secure.BACKUP_AUTO_RESTORE.equals(name)) { 164 setAutoRestore(Integer.parseInt(value) == 1); 165 } else if (isAlreadyConfiguredCriticalAccessibilitySetting(name)) { 166 return; 167 } else if (Settings.System.RINGTONE.equals(name) 168 || Settings.System.NOTIFICATION_SOUND.equals(name) 169 || Settings.System.ALARM_ALERT.equals(name)) { 170 setRingtone(name, value); 171 return; 172 } else if (Settings.System.DISPLAY_COLOR_MODE.equals(name)) { 173 int mode = Integer.parseInt(value); 174 String restoredVendorHint = Settings.System.getString(mContext.getContentResolver(), 175 Settings.System.DISPLAY_COLOR_MODE_VENDOR_HINT); 176 final String deviceVendorHint = mContext.getResources().getString( 177 com.android.internal.R.string.config_vendorColorModesRestoreHint); 178 boolean displayColorModeVendorModeHintsMatch = 179 !TextUtils.isEmpty(deviceVendorHint) 180 && deviceVendorHint.equals(restoredVendorHint); 181 // Replace vendor hint with new device's vendor hint. 182 contentValues.clear(); 183 contentValues.put(Settings.NameValueTable.NAME, 184 Settings.System.DISPLAY_COLOR_MODE_VENDOR_HINT); 185 contentValues.put(Settings.NameValueTable.VALUE, deviceVendorHint); 186 cr.insert(destination, contentValues); 187 // If vendor hints match, modes in the vendor range can be restored. Otherwise, only 188 // map standard modes. 189 if (!ColorDisplayManager.isStandardColorMode(mode) 190 && !displayColorModeVendorModeHintsMatch) { 191 return; 192 } 193 } 194 195 // Default case: write the restored value to settings 196 contentValues.clear(); 197 contentValues.put(Settings.NameValueTable.NAME, name); 198 contentValues.put(Settings.NameValueTable.VALUE, value); 199 cr.insert(destination, contentValues); 200 } catch (Exception e) { 201 // If we fail to apply the setting, by definition nothing happened 202 sendBroadcast = false; 203 sendBroadcastSystemUI = false; 204 } finally { 205 // If this was an element of interest, send the "we just restored it" 206 // broadcast with the historical value now that the new value has 207 // been committed and observers kicked off. 208 if (sendBroadcast || sendBroadcastSystemUI) { 209 Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED) 210 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) 211 .putExtra(Intent.EXTRA_SETTING_NAME, name) 212 .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, value) 213 .putExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE, oldValue) 214 .putExtra(Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, restoredFromSdkInt); 215 216 if (sendBroadcast) { 217 intent.setPackage("android"); 218 context.sendBroadcastAsUser(intent, UserHandle.SYSTEM, null); 219 } 220 if (sendBroadcastSystemUI) { 221 intent.setPackage( 222 context.getString(com.android.internal.R.string.config_systemUi)); 223 context.sendBroadcastAsUser(intent, UserHandle.SYSTEM, null); 224 } 225 } 226 } 227 } 228 onBackupValue(String name, String value)229 public String onBackupValue(String name, String value) { 230 // Special processing for backing up ringtones & notification sounds 231 if (Settings.System.RINGTONE.equals(name) 232 || Settings.System.NOTIFICATION_SOUND.equals(name) 233 || Settings.System.ALARM_ALERT.equals(name)) { 234 if (value == null) { 235 if (Settings.System.RINGTONE.equals(name)) { 236 // For ringtones, we need to distinguish between non-telephony vs telephony 237 if (mTelephonyManager != null && mTelephonyManager.isVoiceCapable()) { 238 // Backup a null ringtone as silent on voice-capable devices 239 return SILENT_RINGTONE; 240 } else { 241 // Skip backup of ringtone on non-telephony devices. 242 return null; 243 } 244 } else { 245 // Backup a null notification sound or alarm alert as silent 246 return SILENT_RINGTONE; 247 } 248 } else { 249 return getCanonicalRingtoneValue(value); 250 } 251 } 252 // Return the original value 253 return isReplacedSystemSetting(name) ? getRealValueForSystemSetting(name) : value; 254 } 255 256 /** 257 * The setting value might have been replaced temporarily. If that's the case, return the real 258 * value instead of the temporary one. 259 */ 260 @VisibleForTesting getRealValueForSystemSetting(String setting)261 public String getRealValueForSystemSetting(String setting) { 262 // The real value irrespectively of the original setting's namespace is stored in 263 // Settings.Secure. 264 return Settings.Secure.getString(mContext.getContentResolver(), 265 setting + SETTING_ORIGINAL_KEY_SUFFIX); 266 } 267 268 @VisibleForTesting isReplacedSystemSetting(String setting)269 public boolean isReplacedSystemSetting(String setting) { 270 // This list should not be modified. 271 if (!Settings.System.SCREEN_OFF_TIMEOUT.equals(setting)) { 272 return false; 273 } 274 // If this flag is set, values for the system settings from the list above have been 275 // temporarily replaced. We don't want to back up the temporary value or run restore for 276 // such settings. 277 // TODO(154822946): Remove this logic in the next release. 278 return Settings.Secure.getInt(mContext.getContentResolver(), SETTINGS_REPLACED_KEY, 279 /* def */ 0) != 0; 280 } 281 282 /** 283 * Sets the ringtone of type specified by the name. 284 * 285 * @param name should be Settings.System.RINGTONE, Settings.System.NOTIFICATION_SOUND 286 * or Settings.System.ALARM_ALERT. 287 * @param value can be a canonicalized uri or "_silent" to indicate a silent (null) ringtone. 288 */ setRingtone(String name, String value)289 private void setRingtone(String name, String value) { 290 // If it's null, don't change the default 291 if (value == null) return; 292 final Uri ringtoneUri; 293 if (SILENT_RINGTONE.equals(value)) { 294 ringtoneUri = null; 295 } else { 296 Uri canonicalUri = Uri.parse(value); 297 ringtoneUri = mContext.getContentResolver().uncanonicalize(canonicalUri); 298 if (ringtoneUri == null) { 299 // Unrecognized or invalid Uri, don't restore 300 return; 301 } 302 } 303 final int ringtoneType = getRingtoneType(name); 304 305 RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, ringtoneUri); 306 } 307 getRingtoneType(String name)308 private int getRingtoneType(String name) { 309 switch (name) { 310 case Settings.System.RINGTONE: 311 return RingtoneManager.TYPE_RINGTONE; 312 case Settings.System.NOTIFICATION_SOUND: 313 return RingtoneManager.TYPE_NOTIFICATION; 314 case Settings.System.ALARM_ALERT: 315 return RingtoneManager.TYPE_ALARM; 316 default: 317 throw new IllegalArgumentException("Incorrect ringtone name: " + name); 318 } 319 } 320 getCanonicalRingtoneValue(String value)321 private String getCanonicalRingtoneValue(String value) { 322 final Uri ringtoneUri = Uri.parse(value); 323 final Uri canonicalUri = mContext.getContentResolver().canonicalize(ringtoneUri); 324 return canonicalUri == null ? null : canonicalUri.toString(); 325 } 326 isAlreadyConfiguredCriticalAccessibilitySetting(String name)327 private boolean isAlreadyConfiguredCriticalAccessibilitySetting(String name) { 328 // These are the critical accessibility settings that are required for users with 329 // accessibility needs to be able to interact with the device. If these settings are 330 // already configured, we will not overwrite them. If they are already set, 331 // it means that the user has performed a global gesture to enable accessibility or set 332 // these settings in the Accessibility portion of the Setup Wizard, and definitely needs 333 // these features working after the restore. 334 switch (name) { 335 case Settings.Secure.ACCESSIBILITY_ENABLED: 336 case Settings.Secure.TOUCH_EXPLORATION_ENABLED: 337 case Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED: 338 case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED: 339 case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED: 340 return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) != 0; 341 case Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES: 342 case Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES: 343 case Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER: 344 return !TextUtils.isEmpty(Settings.Secure.getString( 345 mContext.getContentResolver(), name)); 346 case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE: 347 float defaultScale = mContext.getResources().getFraction( 348 R.fraction.def_accessibility_display_magnification_scale, 1, 1); 349 float currentScale = Settings.Secure.getFloat( 350 mContext.getContentResolver(), name, defaultScale); 351 return Math.abs(currentScale - defaultScale) >= FLOAT_TOLERANCE; 352 case Settings.System.FONT_SCALE: 353 return Settings.System.getFloat(mContext.getContentResolver(), name, 1.0f) != 1.0f; 354 default: 355 return false; 356 } 357 } 358 setAutoRestore(boolean enabled)359 private void setAutoRestore(boolean enabled) { 360 try { 361 IBackupManager bm = IBackupManager.Stub.asInterface( 362 ServiceManager.getService(Context.BACKUP_SERVICE)); 363 if (bm != null) { 364 bm.setAutoRestore(enabled); 365 } 366 } catch (RemoteException e) {} 367 } 368 setSoundEffects(boolean enable)369 private void setSoundEffects(boolean enable) { 370 if (enable) { 371 mAudioManager.loadSoundEffects(); 372 } else { 373 mAudioManager.unloadSoundEffects(); 374 } 375 } 376 getLocaleData()377 /* package */ byte[] getLocaleData() { 378 Configuration conf = mContext.getResources().getConfiguration(); 379 return conf.getLocales().toLanguageTags().getBytes(); 380 } 381 toFullLocale(@onNull Locale locale)382 private static Locale toFullLocale(@NonNull Locale locale) { 383 if (locale.getScript().isEmpty() || locale.getCountry().isEmpty()) { 384 return ULocale.addLikelySubtags(ULocale.forLocale(locale)).toLocale(); 385 } 386 return locale; 387 } 388 389 /** 390 * Merging the locale came from backup server and current device locale. 391 * 392 * Merge works with following rules. 393 * - The backup locales are appended to the current locale with keeping order. 394 * e.g. current locale "en-US,zh-CN" and backup locale "ja-JP,ko-KR" are merged to 395 * "en-US,zh-CH,ja-JP,ko-KR". 396 * 397 * - Duplicated locales are dropped. 398 * e.g. current locale "en-US,zh-CN" and backup locale "ja-JP,zh-Hans-CN,en-US" are merged to 399 * "en-US,zh-CN,ja-JP". 400 * 401 * - Unsupported locales are dropped. 402 * e.g. current locale "en-US" and backup locale "ja-JP,zh-CN" but the supported locales 403 * are "en-US,zh-CN", the merged locale list is "en-US,zh-CN". 404 * 405 * - The final result locale list only contains the supported locales. 406 * e.g. current locale "en-US" and backup locale "zh-Hans-CN" and supported locales are 407 * "en-US,zh-CN", the merged locale list is "en-US,zh-CN". 408 * 409 * @param restore The locale list that came from backup server. 410 * @param current The device's locale setting. 411 * @param supportedLocales The list of language tags supported by this device. 412 */ 413 @VisibleForTesting resolveLocales(LocaleList restore, LocaleList current, String[] supportedLocales)414 public static LocaleList resolveLocales(LocaleList restore, LocaleList current, 415 String[] supportedLocales) { 416 final HashMap<Locale, Locale> allLocales = new HashMap<>(supportedLocales.length); 417 for (String supportedLocaleStr : supportedLocales) { 418 final Locale locale = Locale.forLanguageTag(supportedLocaleStr); 419 allLocales.put(toFullLocale(locale), locale); 420 } 421 422 final ArrayList<Locale> filtered = new ArrayList<>(current.size()); 423 for (int i = 0; i < current.size(); i++) { 424 final Locale locale = current.get(i); 425 allLocales.remove(toFullLocale(locale)); 426 filtered.add(locale); 427 } 428 429 for (int i = 0; i < restore.size(); i++) { 430 final Locale locale = allLocales.remove(toFullLocale(restore.get(i))); 431 if (locale != null) { 432 filtered.add(locale); 433 } 434 } 435 436 if (filtered.size() == current.size()) { 437 return current; // Nothing added to current locale list. 438 } 439 440 return new LocaleList(filtered.toArray(new Locale[filtered.size()])); 441 } 442 443 /** 444 * Sets the locale specified. Input data is the byte representation of comma separated 445 * multiple BCP-47 language tags. For backwards compatibility, strings of the form 446 * {@code ll_CC} are also accepted, where {@code ll} is a two letter language 447 * code and {@code CC} is a two letter country code. 448 * 449 * @param data the comma separated BCP-47 language tags in bytes. 450 */ setLocaleData(byte[] data, int size)451 /* package */ void setLocaleData(byte[] data, int size) { 452 final Configuration conf = mContext.getResources().getConfiguration(); 453 454 // Replace "_" with "-" to deal with older backups. 455 final String localeCodes = new String(data, 0, size).replace('_', '-'); 456 final LocaleList localeList = LocaleList.forLanguageTags(localeCodes); 457 if (localeList.isEmpty()) { 458 return; 459 } 460 461 final String[] supportedLocales = LocalePicker.getSupportedLocales(mContext); 462 final LocaleList currentLocales = conf.getLocales(); 463 464 final LocaleList merged = resolveLocales(localeList, currentLocales, supportedLocales); 465 if (merged.equals(currentLocales)) { 466 return; 467 } 468 469 try { 470 IActivityManager am = ActivityManager.getService(); 471 Configuration config = am.getConfiguration(); 472 config.setLocales(merged); 473 // indicate this isn't some passing default - the user wants this remembered 474 config.userSetLocale = true; 475 476 am.updatePersistentConfigurationWithAttribution(config, mContext.getOpPackageName(), 477 mContext.getAttributionTag()); 478 } catch (RemoteException e) { 479 // Intentionally left blank 480 } 481 } 482 483 /** 484 * Informs the audio service of changes to the settings so that 485 * they can be re-read and applied. 486 */ applyAudioSettings()487 void applyAudioSettings() { 488 AudioManager am = new AudioManager(mContext); 489 am.reloadAudioSettings(); 490 } 491 } 492