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