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