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 {
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         // 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 
onBackupValue(String name, String value)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      */
setRingtone(String name, String value)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 
getCanonicalRingtoneValue(String value)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 
isAlreadyConfiguredCriticalAccessibilitySetting(String name)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 
setAutoRestore(boolean enabled)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 
setGpsLocation(String value)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 
setSoundEffects(boolean enable)298     private void setSoundEffects(boolean enable) {
299         if (enable) {
300             mAudioManager.loadSoundEffects();
301         } else {
302             mAudioManager.unloadSoundEffects();
303         }
304     }
305 
getLocaleData()306     /* package */ byte[] getLocaleData() {
307         Configuration conf = mContext.getResources().getConfiguration();
308         return conf.getLocales().toLanguageTags().getBytes();
309     }
310 
toFullLocale(@onNull Locale locale)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
resolveLocales(LocaleList restore, LocaleList current, String[] supportedLocales)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      */
setLocaleData(byte[] data, int size)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      */
applyAudioSettings()415     void applyAudioSettings() {
416         AudioManager am = new AudioManager(mContext);
417         am.reloadAudioSettings();
418     }
419 }
420