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