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 import android.util.Log;
42 
43 import com.android.internal.annotations.VisibleForTesting;
44 import com.android.internal.app.LocalePicker;
45 import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager;
46 
47 import java.io.FileNotFoundException;
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.Locale;
51 import java.util.Set;
52 
53 public class SettingsHelper {
54     private static final String TAG = "SettingsHelper";
55     private static final String SILENT_RINGTONE = "_silent";
56     private static final String SETTINGS_REPLACED_KEY = "backup_skip_user_facing_data";
57     private static final String SETTING_ORIGINAL_KEY_SUFFIX = "_original";
58     private static final String UNICODE_LOCALE_EXTENSION_FW = "fw";
59     private static final String UNICODE_LOCALE_EXTENSION_MU = "mu";
60     private static final String UNICODE_LOCALE_EXTENSION_NU = "nu";
61     private static final float FLOAT_TOLERANCE = 0.01f;
62 
63     /** See frameworks/base/core/res/res/values/config.xml#config_longPressOnPowerBehavior **/
64     private static final int LONG_PRESS_POWER_NOTHING = 0;
65     private static final int LONG_PRESS_POWER_GLOBAL_ACTIONS = 1;
66     private static final int LONG_PRESS_POWER_FOR_ASSISTANT = 5;
67     /** See frameworks/base/core/res/res/values/config.xml#config_keyChordPowerVolumeUp **/
68     private static final int KEY_CHORD_POWER_VOLUME_UP_GLOBAL_ACTIONS = 2;
69 
70     private Context mContext;
71     private AudioManager mAudioManager;
72     private TelephonyManager mTelephonyManager;
73 
74     /**
75      * A few settings elements are special in that a restore of those values needs to
76      * be post-processed by relevant parts of the OS.  A restore of any settings element
77      * mentioned in this table will therefore cause the system to send a broadcast with
78      * the {@link Intent#ACTION_SETTING_RESTORED} action, with extras naming the
79      * affected setting and supplying its pre-restore value for comparison.
80      *
81      * @see Intent#ACTION_SETTING_RESTORED
82      * @see System#SETTINGS_TO_BACKUP
83      * @see Secure#SETTINGS_TO_BACKUP
84      * @see Global#SETTINGS_TO_BACKUP
85      *
86      * {@hide}
87      */
88     private static final ArraySet<String> sBroadcastOnRestore;
89     private static final ArraySet<String> sBroadcastOnRestoreSystemUI;
90     static {
91         sBroadcastOnRestore = new ArraySet<String>(9);
92         sBroadcastOnRestore.add(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
93         sBroadcastOnRestore.add(Settings.Secure.ENABLED_VR_LISTENERS);
94         sBroadcastOnRestore.add(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
95         sBroadcastOnRestore.add(Settings.Global.BLUETOOTH_ON);
96         sBroadcastOnRestore.add(Settings.Secure.UI_NIGHT_MODE);
97         sBroadcastOnRestore.add(Settings.Secure.DARK_THEME_CUSTOM_START_TIME);
98         sBroadcastOnRestore.add(Settings.Secure.DARK_THEME_CUSTOM_END_TIME);
99         sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED);
100         sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
101         sBroadcastOnRestoreSystemUI = new ArraySet<String>(2);
102         sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_TILES);
103         sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_AUTO_ADDED_TILES);
104     }
105 
106     private static final ArraySet<String> UNICODE_LOCALE_SUPPORTED_EXTENSIONS = new ArraySet<>();
107 
108     /**
109      * Current supported extensions are fw (first day of week) and mu (temperature unit) extension.
110      * User can set these extensions in Settings app, and it will be appended to the locale,
111      * for example: zh-Hant-TW-u-fw-mon-mu-celsius. So after the factory reset, these extensions
112      * should be restored as well because they are set by users.
113      * We do not put the nu (numbering system) extension here because it is an Android supported
114      * extension and defined in some particular locales, for example:
115      * ar-Arab-MA-u-nu-arab and ar-Arab-YE-u-nu-latn. See
116      * <code>frameworks/base/core/res/res/values/locale_config.xml</code>
117      * The nu extension should not be appended to the current/restored locale after factory reset
118      * if the current/restored locale does not have it.
119      */
120     static {
121         UNICODE_LOCALE_SUPPORTED_EXTENSIONS.add(UNICODE_LOCALE_EXTENSION_FW);
122         UNICODE_LOCALE_SUPPORTED_EXTENSIONS.add(UNICODE_LOCALE_EXTENSION_MU);
123     }
124 
125     private interface SettingsLookup {
lookup(ContentResolver resolver, String name, int userHandle)126         public String lookup(ContentResolver resolver, String name, int userHandle);
127     }
128 
129     private static SettingsLookup sSystemLookup = new SettingsLookup() {
130         public String lookup(ContentResolver resolver, String name, int userHandle) {
131             return Settings.System.getStringForUser(resolver, name, userHandle);
132         }
133     };
134 
135     private static SettingsLookup sSecureLookup = new SettingsLookup() {
136         public String lookup(ContentResolver resolver, String name, int userHandle) {
137             return Settings.Secure.getStringForUser(resolver, name, userHandle);
138         }
139     };
140 
141     private static SettingsLookup sGlobalLookup = new SettingsLookup() {
142         public String lookup(ContentResolver resolver, String name, int userHandle) {
143             return Settings.Global.getStringForUser(resolver, name, userHandle);
144         }
145     };
146 
SettingsHelper(Context context)147     public SettingsHelper(Context context) {
148         mContext = context;
149         mAudioManager = (AudioManager) context
150                 .getSystemService(Context.AUDIO_SERVICE);
151         mTelephonyManager = (TelephonyManager) context
152                 .getSystemService(Context.TELEPHONY_SERVICE);
153     }
154 
155     /**
156      * Sets the property via a call to the appropriate API, if any, and returns
157      * whether or not the setting should be saved to the database as well.
158      * @param name the name of the setting
159      * @param value the string value of the setting
160      * @return whether to continue with writing the value to the database. In
161      * some cases the data will be written by the call to the appropriate API,
162      * and in some cases the property value needs to be modified before setting.
163      */
restoreValue(Context context, ContentResolver cr, ContentValues contentValues, Uri destination, String name, String value, int restoredFromSdkInt)164     public void restoreValue(Context context, ContentResolver cr, ContentValues contentValues,
165             Uri destination, String name, String value, int restoredFromSdkInt) {
166         if (isReplacedSystemSetting(name)) {
167             return;
168         }
169 
170         // Will we need a post-restore broadcast for this element?
171         String oldValue = null;
172         boolean sendBroadcast = false;
173         boolean sendBroadcastSystemUI = false;
174         final SettingsLookup table;
175 
176         if (destination.equals(Settings.Secure.CONTENT_URI)) {
177             table = sSecureLookup;
178         } else if (destination.equals(Settings.System.CONTENT_URI)) {
179             table = sSystemLookup;
180         } else { /* must be GLOBAL; this was preflighted by the caller */
181             table = sGlobalLookup;
182         }
183 
184         sendBroadcast = sBroadcastOnRestore.contains(name);
185         sendBroadcastSystemUI = sBroadcastOnRestoreSystemUI.contains(name);
186 
187         if (sendBroadcast || sendBroadcastSystemUI) {
188             // TODO: http://b/22388012
189             oldValue = table.lookup(cr, name, UserHandle.USER_SYSTEM);
190         }
191 
192         try {
193             if (Settings.System.SOUND_EFFECTS_ENABLED.equals(name)) {
194                 setSoundEffects(Integer.parseInt(value) == 1);
195                 // fall through to the ordinary write to settings
196             } else if (Settings.Secure.BACKUP_AUTO_RESTORE.equals(name)) {
197                 setAutoRestore(Integer.parseInt(value) == 1);
198             } else if (isAlreadyConfiguredCriticalAccessibilitySetting(name)) {
199                 return;
200             } else if (Settings.System.RINGTONE.equals(name)
201                     || Settings.System.NOTIFICATION_SOUND.equals(name)
202                     || Settings.System.ALARM_ALERT.equals(name)) {
203                 setRingtone(name, value);
204                 return;
205             } else if (Settings.System.DISPLAY_COLOR_MODE.equals(name)) {
206                 int mode = Integer.parseInt(value);
207                 String restoredVendorHint = Settings.System.getString(mContext.getContentResolver(),
208                         Settings.System.DISPLAY_COLOR_MODE_VENDOR_HINT);
209                 final String deviceVendorHint = mContext.getResources().getString(
210                         com.android.internal.R.string.config_vendorColorModesRestoreHint);
211                 boolean displayColorModeVendorModeHintsMatch =
212                         !TextUtils.isEmpty(deviceVendorHint)
213                                 && deviceVendorHint.equals(restoredVendorHint);
214                 // Replace vendor hint with new device's vendor hint.
215                 contentValues.clear();
216                 contentValues.put(Settings.NameValueTable.NAME,
217                         Settings.System.DISPLAY_COLOR_MODE_VENDOR_HINT);
218                 contentValues.put(Settings.NameValueTable.VALUE, deviceVendorHint);
219                 cr.insert(destination, contentValues);
220                 // If vendor hints match, modes in the vendor range can be restored. Otherwise, only
221                 // map standard modes.
222                 if (!ColorDisplayManager.isStandardColorMode(mode)
223                         && !displayColorModeVendorModeHintsMatch) {
224                     return;
225                 }
226             } else if (Settings.Global.POWER_BUTTON_LONG_PRESS.equals(name)) {
227                 setLongPressPowerBehavior(cr, value);
228                 return;
229             } else if (Settings.System.ACCELEROMETER_ROTATION.equals(name)
230                     && shouldSkipAutoRotateRestore()) {
231                 return;
232             }
233 
234             // Default case: write the restored value to settings
235             contentValues.clear();
236             contentValues.put(Settings.NameValueTable.NAME, name);
237             contentValues.put(Settings.NameValueTable.VALUE, value);
238             cr.insert(destination, contentValues);
239         } catch (Exception e) {
240             // If we fail to apply the setting, by definition nothing happened
241             sendBroadcast = false;
242             sendBroadcastSystemUI = false;
243             Log.e(TAG, "Failed to restore setting name: " + name + " + value: " + value, e);
244         } finally {
245             // If this was an element of interest, send the "we just restored it"
246             // broadcast with the historical value now that the new value has
247             // been committed and observers kicked off.
248             if (sendBroadcast || sendBroadcastSystemUI) {
249                 Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
250                         .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
251                         .putExtra(Intent.EXTRA_SETTING_NAME, name)
252                         .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, value)
253                         .putExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE, oldValue)
254                         .putExtra(Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, restoredFromSdkInt);
255 
256                 if (sendBroadcast) {
257                     intent.setPackage("android");
258                     context.sendBroadcastAsUser(intent, UserHandle.SYSTEM, null);
259                 }
260                 if (sendBroadcastSystemUI) {
261                     intent.setPackage(
262                             context.getString(com.android.internal.R.string.config_systemUi));
263                     context.sendBroadcastAsUser(intent, UserHandle.SYSTEM, null);
264                 }
265             }
266         }
267     }
268 
shouldSkipAutoRotateRestore()269     private boolean shouldSkipAutoRotateRestore() {
270         // When device state based auto rotation settings are available, let's skip the restoring
271         // of the standard auto rotation settings to avoid conflicting setting values.
272         return DeviceStateRotationLockSettingsManager.isDeviceStateRotationLockEnabled(mContext);
273     }
274 
onBackupValue(String name, String value)275     public String onBackupValue(String name, String value) {
276         // Special processing for backing up ringtones & notification sounds
277         if (Settings.System.RINGTONE.equals(name)
278                 || Settings.System.NOTIFICATION_SOUND.equals(name)
279                 || Settings.System.ALARM_ALERT.equals(name)) {
280             if (value == null) {
281                 if (Settings.System.RINGTONE.equals(name)) {
282                     // For ringtones, we need to distinguish between non-telephony vs telephony
283                     if (mTelephonyManager != null && mTelephonyManager.isVoiceCapable()) {
284                         // Backup a null ringtone as silent on voice-capable devices
285                         return SILENT_RINGTONE;
286                     } else {
287                         // Skip backup of ringtone on non-telephony devices.
288                         return null;
289                     }
290                 } else {
291                     // Backup a null notification sound or alarm alert as silent
292                     return SILENT_RINGTONE;
293                 }
294             } else {
295                 return getCanonicalRingtoneValue(value);
296             }
297         }
298         // Return the original value
299         return isReplacedSystemSetting(name) ? getRealValueForSystemSetting(name) : value;
300     }
301 
302     /**
303      * The setting value might have been replaced temporarily. If that's the case, return the real
304      * value instead of the temporary one.
305      */
306     @VisibleForTesting
getRealValueForSystemSetting(String setting)307     public String getRealValueForSystemSetting(String setting) {
308         // The real value irrespectively of the original setting's namespace is stored in
309         // Settings.Secure.
310         return Settings.Secure.getString(mContext.getContentResolver(),
311                 setting + SETTING_ORIGINAL_KEY_SUFFIX);
312     }
313 
314     @VisibleForTesting
isReplacedSystemSetting(String setting)315     public boolean isReplacedSystemSetting(String setting) {
316         // This list should not be modified.
317         if (!Settings.System.SCREEN_OFF_TIMEOUT.equals(setting)) {
318             return false;
319         }
320         // If this flag is set, values for the system settings from the list above have been
321         // temporarily replaced. We don't want to back up the temporary value or run restore for
322         // such settings.
323         // TODO(154822946): Remove this logic in the next release.
324         return Settings.Secure.getInt(mContext.getContentResolver(), SETTINGS_REPLACED_KEY,
325                 /* def */ 0) != 0;
326     }
327 
328     /**
329      * Sets the ringtone of type specified by the name.
330      *
331      * @param name should be Settings.System.RINGTONE, Settings.System.NOTIFICATION_SOUND
332      * or Settings.System.ALARM_ALERT.
333      * @param value can be a canonicalized uri or "_silent" to indicate a silent (null) ringtone.
334      */
setRingtone(String name, String value)335     private void setRingtone(String name, String value) {
336         Log.v(TAG, "Set ringtone for name: " + name + " value: " + value);
337 
338         // If it's null, don't change the default.
339         if (value == null) return;
340         final int ringtoneType = getRingtoneType(name);
341         if (SILENT_RINGTONE.equals(value)) {
342             // SILENT_RINGTONE is a special constant generated by onBackupValue in the source
343             // device.
344             RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, null);
345             return;
346         }
347 
348         Uri ringtoneUri = null;
349         try {
350             ringtoneUri =
351                     RingtoneManager.getRingtoneUriForRestore(
352                             mContext.getContentResolver(), value, ringtoneType);
353         } catch (FileNotFoundException | IllegalArgumentException e) {
354             Log.w(TAG, "Failed to resolve " + value + ": " + e);
355             // Unrecognized or invalid Uri, don't restore
356             return;
357         }
358 
359         Log.v(TAG, "setActualDefaultRingtoneUri type: " + ringtoneType + ", uri: " + ringtoneUri);
360         RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, ringtoneUri);
361     }
362 
getRingtoneType(String name)363     private int getRingtoneType(String name) {
364         switch (name) {
365             case Settings.System.RINGTONE:
366                 return RingtoneManager.TYPE_RINGTONE;
367             case Settings.System.NOTIFICATION_SOUND:
368                 return RingtoneManager.TYPE_NOTIFICATION;
369             case Settings.System.ALARM_ALERT:
370                 return RingtoneManager.TYPE_ALARM;
371             default:
372                 throw new IllegalArgumentException("Incorrect ringtone name: " + name);
373         }
374     }
375 
getCanonicalRingtoneValue(String value)376     private String getCanonicalRingtoneValue(String value) {
377         final Uri ringtoneUri = Uri.parse(value);
378         final Uri canonicalUri = mContext.getContentResolver().canonicalize(ringtoneUri);
379         return canonicalUri == null ? null : canonicalUri.toString();
380     }
381 
isAlreadyConfiguredCriticalAccessibilitySetting(String name)382     private boolean isAlreadyConfiguredCriticalAccessibilitySetting(String name) {
383         // These are the critical accessibility settings that are required for users with
384         // accessibility needs to be able to interact with the device. If these settings are
385         // already configured, we will not overwrite them. If they are already set,
386         // it means that the user has performed a global gesture to enable accessibility or set
387         // these settings in the Accessibility portion of the Setup Wizard, and definitely needs
388         // these features working after the restore.
389         switch (name) {
390             case Settings.Secure.ACCESSIBILITY_ENABLED:
391             case Settings.Secure.TOUCH_EXPLORATION_ENABLED:
392             case Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED:
393             case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED:
394             case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED:
395                 return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) != 0;
396             case Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES:
397             case Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES:
398             case Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER:
399                 return !TextUtils.isEmpty(Settings.Secure.getString(
400                         mContext.getContentResolver(), name));
401             case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE:
402                 float defaultScale = mContext.getResources().getFraction(
403                         R.fraction.def_accessibility_display_magnification_scale, 1, 1);
404                 float currentScale = Settings.Secure.getFloat(
405                         mContext.getContentResolver(), name, defaultScale);
406                 return Math.abs(currentScale - defaultScale) >= FLOAT_TOLERANCE;
407             case Settings.System.FONT_SCALE:
408                 return Settings.System.getFloat(mContext.getContentResolver(), name, 1.0f) != 1.0f;
409             default:
410                 return false;
411         }
412     }
413 
setAutoRestore(boolean enabled)414     private void setAutoRestore(boolean enabled) {
415         try {
416             IBackupManager bm = IBackupManager.Stub.asInterface(
417                     ServiceManager.getService(Context.BACKUP_SERVICE));
418             if (bm != null) {
419                 bm.setAutoRestore(enabled);
420             }
421         } catch (RemoteException e) {}
422     }
423 
setSoundEffects(boolean enable)424     private void setSoundEffects(boolean enable) {
425         if (enable) {
426             mAudioManager.loadSoundEffects();
427         } else {
428             mAudioManager.unloadSoundEffects();
429         }
430     }
431 
432     /**
433      * Correctly sets long press power button Behavior.
434      *
435      * The issue is that setting for LongPressPower button Behavior is not available on all devices
436      * and actually changes default Behavior of two properties - the long press power button
437      * and volume up + power button combo. OEM can also reconfigure these Behaviors in config.xml,
438      * so we need to be careful that we don't irreversibly change power button Behavior when
439      * restoring. Or switch to a non-default button Behavior.
440      */
setLongPressPowerBehavior(ContentResolver cr, String value)441     private void setLongPressPowerBehavior(ContentResolver cr, String value) {
442         // We will not restore the value if the long press power setting option is unavailable.
443         if (!mContext.getResources().getBoolean(
444                 com.android.internal.R.bool.config_longPressOnPowerForAssistantSettingAvailable)) {
445             return;
446         }
447 
448         int longPressOnPowerBehavior;
449         try {
450             longPressOnPowerBehavior = Integer.parseInt(value);
451         } catch (NumberFormatException e) {
452             return;
453         }
454 
455         if (longPressOnPowerBehavior < LONG_PRESS_POWER_NOTHING
456                 || longPressOnPowerBehavior > LONG_PRESS_POWER_FOR_ASSISTANT) {
457             return;
458         }
459 
460         // When user enables long press power for Assistant, we also switch the meaning
461         // of Volume Up + Power key chord to the "Show power menu" option.
462         // If the user disables long press power for Assistant, we switch back to default OEM
463         // Behavior configured in config.xml. If the default Behavior IS "LPP for Assistant",
464         // then we fall back to "Long press for Power Menu" Behavior.
465         if (longPressOnPowerBehavior == LONG_PRESS_POWER_FOR_ASSISTANT) {
466             Settings.Global.putInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS,
467                     LONG_PRESS_POWER_FOR_ASSISTANT);
468             Settings.Global.putInt(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP,
469                     KEY_CHORD_POWER_VOLUME_UP_GLOBAL_ACTIONS);
470         } else {
471             // We're restoring "LPP for Assistant Disabled" state, prefer OEM config.xml Behavior
472             // if possible.
473             int longPressOnPowerDeviceBehavior = mContext.getResources().getInteger(
474                     com.android.internal.R.integer.config_longPressOnPowerBehavior);
475             if (longPressOnPowerDeviceBehavior == LONG_PRESS_POWER_FOR_ASSISTANT) {
476                 // The default on device IS "LPP for Assistant Enabled" so fall back to power menu.
477                 Settings.Global.putInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS,
478                         LONG_PRESS_POWER_GLOBAL_ACTIONS);
479             } else {
480                 // The default is non-Assistant Behavior, so restore that default.
481                 Settings.Global.putInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS,
482                         longPressOnPowerDeviceBehavior);
483             }
484 
485             // Clear and restore default power + volume up Behavior as well.
486             int powerVolumeUpDefaultBehavior = mContext.getResources().getInteger(
487                     com.android.internal.R.integer.config_keyChordPowerVolumeUp);
488             Settings.Global.putInt(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP,
489                     powerVolumeUpDefaultBehavior);
490         }
491     }
492 
getLocaleData()493     /* package */ byte[] getLocaleData() {
494         Configuration conf = mContext.getResources().getConfiguration();
495         return conf.getLocales().toLanguageTags().getBytes();
496     }
497 
toFullLocale(@onNull Locale locale)498     private static Locale toFullLocale(@NonNull Locale locale) {
499         if (locale.getScript().isEmpty() || locale.getCountry().isEmpty()) {
500             return ULocale.addLikelySubtags(ULocale.forLocale(locale)).toLocale();
501         }
502         return locale;
503     }
504 
505     /**
506      * Merging the locale came from backup server and current device locale.
507      *
508      * Merge works with following rules.
509      * - The backup locales are appended to the current locale with keeping order.
510      *   e.g. current locale "en-US,zh-CN" and backup locale "ja-JP,ko-KR" are merged to
511      *   "en-US,zh-CH,ja-JP,ko-KR".
512      *
513      * - Duplicated locales are dropped.
514      *   e.g. current locale "en-US,zh-CN" and backup locale "ja-JP,zh-Hans-CN,en-US" are merged to
515      *   "en-US,zh-CN,ja-JP".
516      *
517      * - Unsupported locales are dropped.
518      *   e.g. current locale "en-US" and backup locale "ja-JP,zh-CN" but the supported locales
519      *   are "en-US,zh-CN", the merged locale list is "en-US,zh-CN".
520      *
521      * - The final result locale list only contains the supported locales.
522      *   e.g. current locale "en-US" and backup locale "zh-Hans-CN" and supported locales are
523      *   "en-US,zh-CN", the merged locale list is "en-US,zh-CN".
524      *
525      * @param restore The locale list that came from backup server.
526      * @param current The device's locale setting.
527      * @param supportedLocales The list of language tags supported by this device.
528      */
529     @VisibleForTesting
resolveLocales(LocaleList restore, LocaleList current, String[] supportedLocales)530     public static LocaleList resolveLocales(LocaleList restore, LocaleList current,
531             String[] supportedLocales) {
532         final HashMap<Locale, Locale> allLocales = new HashMap<>(supportedLocales.length);
533         for (String supportedLocaleStr : supportedLocales) {
534             final Locale locale = Locale.forLanguageTag(supportedLocaleStr);
535             allLocales.put(toFullLocale(locale), locale);
536         }
537 
538         // After restoring to reset locales, need to get extensions from restored locale. Get the
539         // first restored locale to check its extension.
540         final Locale restoredLocale = restore.isEmpty()
541                 ? Locale.ROOT
542                 : restore.get(0);
543         final ArrayList<Locale> filtered = new ArrayList<>(current.size());
544         for (int i = 0; i < current.size(); i++) {
545             Locale locale = copyExtensionToTargetLocale(restoredLocale, current.get(i));
546             allLocales.remove(toFullLocale(locale));
547             filtered.add(locale);
548         }
549 
550         for (int i = 0; i < restore.size(); i++) {
551             final Locale restoredLocaleWithExtension = copyExtensionToTargetLocale(restoredLocale,
552                     getFilteredLocale(restore.get(i), allLocales));
553             if (restoredLocaleWithExtension != null) {
554                 filtered.add(restoredLocaleWithExtension);
555             }
556         }
557         if (filtered.size() == current.size()) {
558             return current;  // Nothing added to current locale list.
559         }
560 
561         return new LocaleList(filtered.toArray(new Locale[filtered.size()]));
562     }
563 
copyExtensionToTargetLocale(Locale restoredLocale, Locale targetLocale)564     private static Locale copyExtensionToTargetLocale(Locale restoredLocale,
565             Locale targetLocale) {
566         if (!restoredLocale.hasExtensions()) {
567             return targetLocale;
568         }
569 
570         if (targetLocale == null) {
571             return null;
572         }
573 
574         Locale.Builder builder = new Locale.Builder()
575                 .setLocale(targetLocale);
576         Set<String> unicodeLocaleKeys = restoredLocale.getUnicodeLocaleKeys();
577         unicodeLocaleKeys.stream().forEach(key -> {
578             // Copy all supported extensions from restored locales except "nu" extension. The "nu"
579             // extension has been added in #getFilteredLocale(Locale, HashMap<Locale, Locale>)
580             // already, we don't need to add it again.
581             if (UNICODE_LOCALE_SUPPORTED_EXTENSIONS.contains(key)) {
582                 builder.setUnicodeLocaleKeyword(key, restoredLocale.getUnicodeLocaleType(key));
583             }
584         });
585         return builder.build();
586     }
587 
getFilteredLocale(Locale restoreLocale, HashMap<Locale, Locale> allLocales)588     private static Locale getFilteredLocale(Locale restoreLocale,
589             HashMap<Locale, Locale> allLocales) {
590         Locale locale = allLocales.remove(toFullLocale(restoreLocale));
591         if (locale != null) {
592             return locale;
593         }
594 
595         Locale filteredLocale = new Locale.Builder()
596                 .setLocale(restoreLocale.stripExtensions())
597                 .setUnicodeLocaleKeyword(UNICODE_LOCALE_EXTENSION_NU,
598                         restoreLocale.getUnicodeLocaleType(UNICODE_LOCALE_EXTENSION_NU))
599                 .build();
600         return allLocales.remove(toFullLocale(filteredLocale));
601     }
602 
603     /**
604      * Sets the locale specified. Input data is the byte representation of comma separated
605      * multiple BCP-47 language tags. For backwards compatibility, strings of the form
606      * {@code ll_CC} are also accepted, where {@code ll} is a two letter language
607      * code and {@code CC} is a two letter country code.
608      *
609      * @param data the comma separated BCP-47 language tags in bytes.
610      */
setLocaleData(byte[] data, int size)611     /* package */ void setLocaleData(byte[] data, int size) {
612         final Configuration conf = mContext.getResources().getConfiguration();
613 
614         // Replace "_" with "-" to deal with older backups.
615         final String localeCodes = new String(data, 0, size).replace('_', '-');
616         final LocaleList localeList = LocaleList.forLanguageTags(localeCodes);
617         if (localeList.isEmpty()) {
618             return;
619         }
620 
621         final String[] supportedLocales = LocalePicker.getSupportedLocales(mContext);
622         final LocaleList currentLocales = conf.getLocales();
623 
624         final LocaleList merged = resolveLocales(localeList, currentLocales, supportedLocales);
625         if (merged.equals(currentLocales)) {
626             return;
627         }
628 
629         try {
630             IActivityManager am = ActivityManager.getService();
631             final Configuration config = new Configuration();
632             config.setLocales(merged);
633             // indicate this isn't some passing default - the user wants this remembered
634             config.userSetLocale = true;
635 
636             am.updatePersistentConfigurationWithAttribution(config, mContext.getOpPackageName(),
637                     mContext.getAttributionTag());
638         } catch (RemoteException e) {
639             // Intentionally left blank
640         }
641     }
642 
643     /**
644      * Informs the audio service of changes to the settings so that
645      * they can be re-read and applied.
646      */
applyAudioSettings()647     void applyAudioSettings() {
648         AudioManager am = new AudioManager(mContext);
649         am.reloadAudioSettings();
650     }
651 }
652