• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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