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