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