1 /*
2  * Copyright (C) 2010 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.contacts.preference;
18 
19 import static android.Manifest.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS;
20 
21 import android.accounts.Account;
22 import android.annotation.SuppressLint;
23 import android.app.backup.BackupManager;
24 import android.content.Context;
25 import android.content.pm.PackageManager;
26 import android.content.SharedPreferences;
27 import android.content.SharedPreferences.Editor;
28 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
29 import android.os.Handler;
30 import android.os.Looper;
31 import android.os.StrictMode;
32 import android.preference.PreferenceManager;
33 import android.provider.ContactsContract;
34 import android.provider.Settings;
35 import android.provider.Settings.SettingNotFoundException;
36 import android.text.TextUtils;
37 
38 import androidx.annotation.NonNull;
39 import androidx.annotation.RequiresApi;
40 import androidx.annotation.VisibleForTesting;
41 import androidx.core.os.BuildCompat;
42 
43 import com.android.contacts.R;
44 import com.android.contacts.model.account.AccountWithDataSet;
45 
46 import java.util.List;
47 
48 /**
49  * Manages user preferences for contacts.
50  */
51 public class ContactsPreferences implements OnSharedPreferenceChangeListener {
52 
53     /**
54      * The value for the DISPLAY_ORDER key to show the given name first.
55      */
56     public static final int DISPLAY_ORDER_PRIMARY = 1;
57 
58     /**
59      * The value for the DISPLAY_ORDER key to show the family name first.
60      */
61     public static final int DISPLAY_ORDER_ALTERNATIVE = 2;
62 
63     public static final String DISPLAY_ORDER_KEY = "android.contacts.DISPLAY_ORDER";
64 
65     /**
66      * The value for the SORT_ORDER key corresponding to sort by given name first.
67      */
68     public static final int SORT_ORDER_PRIMARY = 1;
69 
70     public static final String SORT_ORDER_KEY = "android.contacts.SORT_ORDER";
71 
72     /**
73      * The value for the SORT_ORDER key corresponding to sort by family name first.
74      */
75     public static final int SORT_ORDER_ALTERNATIVE = 2;
76 
77     public static final String PREF_DISPLAY_ONLY_PHONES = "only_phones";
78 
79     public static final boolean PREF_DISPLAY_ONLY_PHONES_DEFAULT = false;
80 
81     public static final String PHONETIC_NAME_DISPLAY_KEY = "Phonetic_name_display";
82 
83     /**
84      * Value to use when a preference is unassigned and needs to be read from the shared preferences
85      */
86     private static final int PREFERENCE_UNASSIGNED = -1;
87 
88     private final Context mContext;
89     private int mSortOrder = PREFERENCE_UNASSIGNED;
90     private int mDisplayOrder = PREFERENCE_UNASSIGNED;
91     private int mPhoneticNameDisplayPreference = PREFERENCE_UNASSIGNED;
92 
93     private AccountWithDataSet mDefaultAccount = null;
94     private ChangeListener mListener = null;
95     private Handler mHandler;
96     private final SharedPreferences mPreferences;
97     private final BackupManager mBackupManager;
98     private final boolean mIsDefaultAccountUserChangeable;
99     private String mDefaultAccountKey;
100 
ContactsPreferences(Context context)101     public ContactsPreferences(Context context) {
102         this(context,
103                 context.getResources().getBoolean(R.bool.config_default_account_user_changeable));
104     }
105 
106     @VisibleForTesting
ContactsPreferences(Context context, boolean isDefaultAccountUserChangeable)107     ContactsPreferences(Context context, boolean isDefaultAccountUserChangeable) {
108         mContext = context;
109         mIsDefaultAccountUserChangeable = isDefaultAccountUserChangeable;
110 
111         mBackupManager = new BackupManager(mContext);
112 
113         mHandler = new Handler(Looper.getMainLooper());
114         mPreferences = mContext.getSharedPreferences(context.getPackageName(),
115                 Context.MODE_PRIVATE);
116         mDefaultAccountKey = mContext.getResources().getString(
117                 R.string.contact_editor_default_account_key);
118         maybeMigrateSystemSettings();
119     }
120 
isSortOrderUserChangeable()121     public boolean isSortOrderUserChangeable() {
122         return mContext.getResources().getBoolean(R.bool.config_sort_order_user_changeable);
123     }
124 
getDefaultSortOrder()125     public int getDefaultSortOrder() {
126         if (mContext.getResources().getBoolean(R.bool.config_default_sort_order_primary)) {
127             return SORT_ORDER_PRIMARY;
128         } else {
129             return SORT_ORDER_ALTERNATIVE;
130         }
131     }
132 
getSortOrder()133     public int getSortOrder() {
134         if (!isSortOrderUserChangeable()) {
135             return getDefaultSortOrder();
136         }
137         if (mSortOrder == PREFERENCE_UNASSIGNED) {
138             mSortOrder = mPreferences.getInt(SORT_ORDER_KEY, getDefaultSortOrder());
139         }
140         return mSortOrder;
141     }
142 
setSortOrder(int sortOrder)143     public void setSortOrder(int sortOrder) {
144         mSortOrder = sortOrder;
145         final Editor editor = mPreferences.edit();
146         editor.putInt(SORT_ORDER_KEY, sortOrder);
147         editor.commit();
148         mBackupManager.dataChanged();
149     }
150 
isDisplayOrderUserChangeable()151     public boolean isDisplayOrderUserChangeable() {
152         return mContext.getResources().getBoolean(R.bool.config_display_order_user_changeable);
153     }
154 
getDefaultDisplayOrder()155     public int getDefaultDisplayOrder() {
156         if (mContext.getResources().getBoolean(R.bool.config_default_display_order_primary)) {
157             return DISPLAY_ORDER_PRIMARY;
158         } else {
159             return DISPLAY_ORDER_ALTERNATIVE;
160         }
161     }
162 
getDisplayOrder()163     public int getDisplayOrder() {
164         if (!isDisplayOrderUserChangeable()) {
165             return getDefaultDisplayOrder();
166         }
167         if (mDisplayOrder == PREFERENCE_UNASSIGNED) {
168             mDisplayOrder = mPreferences.getInt(DISPLAY_ORDER_KEY, getDefaultDisplayOrder());
169         }
170         return mDisplayOrder;
171     }
172 
setDisplayOrder(int displayOrder)173     public void setDisplayOrder(int displayOrder) {
174         mDisplayOrder = displayOrder;
175         final Editor editor = mPreferences.edit();
176         editor.putInt(DISPLAY_ORDER_KEY, displayOrder);
177         editor.commit();
178         mBackupManager.dataChanged();
179     }
180 
getDefaultPhoneticNameDisplayPreference()181     public int getDefaultPhoneticNameDisplayPreference() {
182         if (mContext.getResources().getBoolean(R.bool.config_default_hide_phonetic_name_if_empty)) {
183             return PhoneticNameDisplayPreference.HIDE_IF_EMPTY;
184         } else {
185             return PhoneticNameDisplayPreference.SHOW_ALWAYS;
186         }
187     }
188 
isPhoneticNameDisplayPreferenceChangeable()189     public boolean isPhoneticNameDisplayPreferenceChangeable() {
190         return mContext.getResources().getBoolean(
191                 R.bool.config_phonetic_name_display_user_changeable);
192     }
193 
setPhoneticNameDisplayPreference(int phoneticNameDisplayPreference)194     public void setPhoneticNameDisplayPreference(int phoneticNameDisplayPreference) {
195         mPhoneticNameDisplayPreference = phoneticNameDisplayPreference;
196         final Editor editor = mPreferences.edit();
197         editor.putInt(PHONETIC_NAME_DISPLAY_KEY, phoneticNameDisplayPreference);
198         editor.commit();
199         mBackupManager.dataChanged();
200     }
201 
getPhoneticNameDisplayPreference()202     public int getPhoneticNameDisplayPreference() {
203         if (!isPhoneticNameDisplayPreferenceChangeable()) {
204             return getDefaultPhoneticNameDisplayPreference();
205         }
206         if (mPhoneticNameDisplayPreference == PREFERENCE_UNASSIGNED) {
207             mPhoneticNameDisplayPreference = mPreferences.getInt(PHONETIC_NAME_DISPLAY_KEY,
208                     getDefaultPhoneticNameDisplayPreference());
209         }
210         return mPhoneticNameDisplayPreference;
211     }
212 
shouldHidePhoneticNamesIfEmpty()213     public boolean shouldHidePhoneticNamesIfEmpty() {
214         return getPhoneticNameDisplayPreference() == PhoneticNameDisplayPreference.HIDE_IF_EMPTY;
215     }
216 
isDefaultAccountUserChangeable()217     public boolean isDefaultAccountUserChangeable() {
218         return mIsDefaultAccountUserChangeable;
219     }
220 
221     @SuppressLint("NewApi")
getDefaultAccount()222     public AccountWithDataSet getDefaultAccount() {
223         if (!isDefaultAccountUserChangeable()) {
224             return mDefaultAccount;
225         }
226         if (mDefaultAccount == null) {
227             Account cp2DefaultAccount = null;
228             if (BuildCompat.isAtLeastT()) {
229                 cp2DefaultAccount = getDefaultAccountFromCp2();
230             }
231 
232             mDefaultAccount = cp2DefaultAccount == null
233                     ? AccountWithDataSet.getNullAccount()
234                     : new AccountWithDataSet(cp2DefaultAccount.name, cp2DefaultAccount.type, null);
235         }
236         return mDefaultAccount;
237     }
238 
239     @RequiresApi(33)
getDefaultAccountFromCp2()240     private Account getDefaultAccountFromCp2() {
241         StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
242         StrictMode.setThreadPolicy(
243                 new StrictMode.ThreadPolicy.Builder(oldPolicy)
244                 .permitDiskReads()
245                 .build());
246         try {
247             return ContactsContract.Settings.getDefaultAccount(
248                     mContext.getContentResolver());
249         } finally {
250             StrictMode.setThreadPolicy(oldPolicy);
251         }
252     }
253 
clearDefaultAccount()254     public void clearDefaultAccount() {
255         if (mContext.checkSelfPermission(SET_DEFAULT_ACCOUNT_FOR_CONTACTS)
256                 == PackageManager.PERMISSION_GRANTED) {
257             mDefaultAccount = null;
258             setDefaultAccountToCp2(null);
259         }
260     }
261 
setDefaultAccount(@onNull AccountWithDataSet accountWithDataSet)262     public void setDefaultAccount(@NonNull AccountWithDataSet accountWithDataSet) {
263         if (accountWithDataSet == null) {
264             throw new IllegalArgumentException(
265                     "argument should not be null");
266         }
267         if (mContext.checkSelfPermission(SET_DEFAULT_ACCOUNT_FOR_CONTACTS)
268                 == PackageManager.PERMISSION_GRANTED) {
269             mDefaultAccount = accountWithDataSet;
270             setDefaultAccountToCp2(accountWithDataSet);
271         }
272     }
273 
setDefaultAccountToCp2(AccountWithDataSet accountWithDataSet)274     private void setDefaultAccountToCp2(AccountWithDataSet accountWithDataSet) {
275         StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
276         StrictMode.setThreadPolicy(
277                 new StrictMode.ThreadPolicy.Builder(oldPolicy)
278                         .permitDiskWrites()
279                         .permitDiskReads()
280                         .build());
281         try {
282             ContactsContract.Settings.setDefaultAccount(mContext.getContentResolver(),
283                     accountWithDataSet == null ? null : accountWithDataSet.getAccountOrNull());
284         } finally {
285             StrictMode.setThreadPolicy(oldPolicy);
286         }
287     }
288 
isDefaultAccountSet()289     public boolean isDefaultAccountSet() {
290         return mDefaultAccount != null;
291     }
292 
293     /**
294      * @return false if there is only one writable account or no requirement to return true is met.
295      *         true if the contact editor should show the "accounts changed" notification, that is:
296      *              - If it's the first launch.
297      *              - Or, if the default account has been removed.
298      *              (And some extra soundness check)
299      *
300      * Note if this method returns {@code false}, the caller can safely assume that
301      * {@link #getDefaultAccount} will return a valid account.  (Either an account which still
302      * exists, or {@code null} which should be interpreted as "local only".)
303      */
shouldShowAccountChangedNotification(List<AccountWithDataSet> currentWritableAccounts)304     public boolean shouldShowAccountChangedNotification(List<AccountWithDataSet>
305             currentWritableAccounts) {
306         final AccountWithDataSet defaultAccount = getDefaultAccount();
307 
308         AccountWithDataSet localAccount = AccountWithDataSet.getLocalAccount(mContext);
309         // This shouldn't occur anymore because a "device" account is added in the case that there
310         // are no other accounts but if there are no writable accounts then the default has been
311         // initialized if it is "device"
312         if (currentWritableAccounts.isEmpty()) {
313             return defaultAccount == null || !defaultAccount.equals(localAccount);
314         }
315 
316         if (currentWritableAccounts.size() == 1
317                 && !currentWritableAccounts.get(0).equals(localAccount)) {
318             return false;
319         }
320 
321         if (defaultAccount == null) {
322             return true;
323         }
324 
325         if (!currentWritableAccounts.contains(defaultAccount)) {
326             return true;
327         }
328 
329         // All good.
330         return false;
331     }
332 
registerChangeListener(ChangeListener listener)333     public void registerChangeListener(ChangeListener listener) {
334         if (mListener != null) unregisterChangeListener();
335 
336         mListener = listener;
337 
338         // Reset preferences to "unknown" because they may have changed while the
339         // listener was unregistered.
340         mDisplayOrder = PREFERENCE_UNASSIGNED;
341         mSortOrder = PREFERENCE_UNASSIGNED;
342         mPhoneticNameDisplayPreference = PREFERENCE_UNASSIGNED;
343         mDefaultAccount = null;
344 
345         mPreferences.registerOnSharedPreferenceChangeListener(this);
346     }
347 
unregisterChangeListener()348     public void unregisterChangeListener() {
349         if (mListener != null) {
350             mListener = null;
351         }
352 
353         mPreferences.unregisterOnSharedPreferenceChangeListener(this);
354     }
355 
356     @Override
onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key)357     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
358         // This notification is not sent on the Ui thread. Use the previously created Handler
359         // to switch to the Ui thread
360         mHandler.post(new Runnable() {
361             @Override
362             public void run() {
363                 refreshValue(key);
364             }
365         });
366     }
367 
368     /**
369      * Forces the value for the given key to be looked up from shared preferences and notifies
370      * the registered {@link ChangeListener}
371      *
372      * @param key the {@link SharedPreferences} key to look up
373      */
refreshValue(String key)374     public void refreshValue(String key) {
375         if (DISPLAY_ORDER_KEY.equals(key)) {
376             mDisplayOrder = PREFERENCE_UNASSIGNED;
377             mDisplayOrder = getDisplayOrder();
378         } else if (SORT_ORDER_KEY.equals(key)) {
379             mSortOrder = PREFERENCE_UNASSIGNED;
380             mSortOrder = getSortOrder();
381         } else if (PHONETIC_NAME_DISPLAY_KEY.equals(key)) {
382             mPhoneticNameDisplayPreference = PREFERENCE_UNASSIGNED;
383             mPhoneticNameDisplayPreference = getPhoneticNameDisplayPreference();
384         } else if (mDefaultAccountKey.equals(key)) {
385             mDefaultAccount = null;
386             mDefaultAccount = getDefaultAccount();
387         }
388         if (mListener != null) mListener.onChange();
389     }
390 
391     public interface ChangeListener {
onChange()392         void onChange();
393     }
394 
395     /**
396      * If there are currently no preferences (which means this is the first time we are run),
397      * For sort order and display order, check to see if there are any preferences stored in
398      * system settings (pre-L) which can be copied into our own SharedPreferences.
399      * For default account setting, check to see if there are any preferences stored in the previous
400      * SharedPreferences which can be copied into current SharedPreferences.
401      */
maybeMigrateSystemSettings()402     private void maybeMigrateSystemSettings() {
403         if (!mPreferences.contains(SORT_ORDER_KEY)) {
404             int sortOrder = getDefaultSortOrder();
405             try {
406                  sortOrder = Settings.System.getInt(mContext.getContentResolver(),
407                         SORT_ORDER_KEY);
408             } catch (SettingNotFoundException e) {
409             }
410             setSortOrder(sortOrder);
411         }
412 
413         if (!mPreferences.contains(DISPLAY_ORDER_KEY)) {
414             int displayOrder = getDefaultDisplayOrder();
415             try {
416                 displayOrder = Settings.System.getInt(mContext.getContentResolver(),
417                         DISPLAY_ORDER_KEY);
418             } catch (SettingNotFoundException e) {
419             }
420             setDisplayOrder(displayOrder);
421         }
422 
423         if (!mPreferences.contains(PHONETIC_NAME_DISPLAY_KEY)) {
424             int phoneticNameFieldsDisplay = getDefaultPhoneticNameDisplayPreference();
425             try {
426                 phoneticNameFieldsDisplay = Settings.System.getInt(mContext.getContentResolver(),
427                         PHONETIC_NAME_DISPLAY_KEY);
428             } catch (SettingNotFoundException e) {
429             }
430             setPhoneticNameDisplayPreference(phoneticNameFieldsDisplay);
431         }
432 
433         if (!mPreferences.contains(mDefaultAccountKey)) {
434             final SharedPreferences previousPrefs =
435                     PreferenceManager.getDefaultSharedPreferences(mContext);
436             final String defaultAccount = previousPrefs.getString(mDefaultAccountKey, null);
437             if (!TextUtils.isEmpty(defaultAccount)) {
438                 final AccountWithDataSet accountWithDataSet = AccountWithDataSet.unstringify(
439                         defaultAccount);
440                 setDefaultAccount(accountWithDataSet);
441             }
442         }
443 
444         if (mPreferences.contains(mDefaultAccountKey) && getDefaultAccount() == null) {
445             String defaultAccount = mPreferences.getString(mDefaultAccountKey, null);
446             if (!TextUtils.isEmpty(defaultAccount)) {
447                 final AccountWithDataSet accountWithDataSet = AccountWithDataSet.unstringify(
448                         defaultAccount);
449                 setDefaultAccount(accountWithDataSet);
450             }
451         }
452     }
453 
454 }
455