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