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