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.accounts.Account; 20 import android.app.Activity; 21 import android.app.LoaderManager; 22 import android.content.BroadcastReceiver; 23 import android.content.ContentUris; 24 import android.content.Context; 25 import android.content.CursorLoader; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.Loader; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ResolveInfo; 31 import android.content.res.Resources; 32 import android.database.Cursor; 33 import android.icu.text.MessageFormat; 34 import android.net.Uri; 35 import android.os.Bundle; 36 import android.os.StrictMode; 37 import android.preference.Preference; 38 import android.preference.PreferenceFragment; 39 import android.provider.BlockedNumberContract; 40 import android.provider.ContactsContract.Contacts; 41 import android.provider.ContactsContract.DisplayNameSources; 42 import android.provider.ContactsContract.Profile; 43 import android.provider.ContactsContract.Settings; 44 import com.google.android.material.snackbar.Snackbar; 45 import androidx.localbroadcastmanager.content.LocalBroadcastManager; 46 import android.telecom.TelecomManager; 47 import android.telephony.TelephonyManager; 48 import android.text.BidiFormatter; 49 import android.text.TextDirectionHeuristics; 50 import android.view.LayoutInflater; 51 import android.view.View; 52 import android.view.ViewGroup; 53 import android.widget.FrameLayout; 54 55 import com.android.contacts.ContactsUtils; 56 import com.android.contacts.R; 57 import com.android.contacts.SimImportService; 58 import com.android.contacts.compat.TelecomManagerUtil; 59 import com.android.contacts.compat.TelephonyManagerCompat; 60 import com.android.contacts.interactions.ExportDialogFragment; 61 import com.android.contacts.interactions.ImportDialogFragment; 62 import com.android.contacts.list.ContactListFilter; 63 import com.android.contacts.list.ContactListFilterController; 64 import com.android.contacts.logging.ScreenEvent.ScreenType; 65 import com.android.contacts.model.AccountTypeManager; 66 import com.android.contacts.model.account.AccountInfo; 67 import com.android.contacts.model.account.AccountWithDataSet; 68 import com.android.contacts.model.account.AccountsLoader; 69 import com.android.contacts.util.AccountFilterUtil; 70 import com.android.contacts.util.ImplicitIntentsUtil; 71 import com.android.contactsbind.HelpUtils; 72 73 import java.util.Collections; 74 import java.util.HashMap; 75 import java.util.List; 76 import java.util.Locale; 77 import java.util.Map; 78 79 /** 80 * This fragment shows the preferences for "display options" 81 */ 82 public class DisplayOptionsPreferenceFragment extends PreferenceFragment 83 implements Preference.OnPreferenceClickListener, AccountsLoader.AccountsListener { 84 85 private static final int REQUEST_CODE_CUSTOM_CONTACTS_FILTER = 0; 86 private static final int REQUEST_CODE_SET_DEFAULT_ACCOUNT_CP2 = 1; 87 88 private static final String ARG_CONTACTS_AVAILABLE = "are_contacts_available"; 89 private static final String ARG_NEW_LOCAL_PROFILE = "new_local_profile"; 90 91 private static final String KEY_ABOUT = "about"; 92 private static final String KEY_ACCOUNTS = "accounts"; 93 private static final String KEY_DEFAULT_ACCOUNT = "defaultAccount"; 94 private static final String KEY_BLOCKED_NUMBERS = "blockedNumbers"; 95 private static final String KEY_DISPLAY_ORDER = "displayOrder"; 96 private static final String KEY_CUSTOM_CONTACTS_FILTER = "customContactsFilter"; 97 private static final String KEY_IMPORT = "import"; 98 private static final String KEY_EXPORT = "export"; 99 private static final String KEY_MY_INFO = "myInfo"; 100 private static final String KEY_SORT_ORDER = "sortOrder"; 101 private static final String KEY_PHONETIC_NAME_DISPLAY = "phoneticNameDisplay"; 102 103 private static final int LOADER_PROFILE = 0; 104 private static final int LOADER_ACCOUNTS = 1; 105 106 /** 107 * Callbacks for hosts of the {@link DisplayOptionsPreferenceFragment}. 108 */ 109 public interface ProfileListener { 110 /** 111 * Invoked after profile has been loaded. 112 */ onProfileLoaded(Cursor data)113 void onProfileLoaded(Cursor data); 114 } 115 116 /** 117 * The projections that are used to obtain user profile 118 */ 119 public static class ProfileQuery { 120 /** 121 * Not instantiable. 122 */ ProfileQuery()123 private ProfileQuery() {} 124 125 private static final String[] PROFILE_PROJECTION_PRIMARY = new String[] { 126 Contacts._ID, // 0 127 Contacts.DISPLAY_NAME_PRIMARY, // 1 128 Contacts.IS_USER_PROFILE, // 2 129 Contacts.DISPLAY_NAME_SOURCE, // 3 130 }; 131 132 private static final String[] PROFILE_PROJECTION_ALTERNATIVE = new String[] { 133 Contacts._ID, // 0 134 Contacts.DISPLAY_NAME_ALTERNATIVE, // 1 135 Contacts.IS_USER_PROFILE, // 2 136 Contacts.DISPLAY_NAME_SOURCE, // 3 137 }; 138 139 public static final int CONTACT_ID = 0; 140 public static final int CONTACT_DISPLAY_NAME = 1; 141 public static final int CONTACT_IS_USER_PROFILE = 2; 142 public static final int DISPLAY_NAME_SOURCE = 3; 143 } 144 145 private String mNewLocalProfileExtra; 146 private boolean mAreContactsAvailable; 147 148 private boolean mHasProfile; 149 private long mProfileContactId; 150 151 private Preference mMyInfoPreference; 152 153 private ProfileListener mListener; 154 155 private ViewGroup mRootView; 156 private SaveServiceResultListener mSaveServiceListener; 157 158 private List<AccountInfo> accounts = Collections.emptyList(); 159 160 private final LoaderManager.LoaderCallbacks<Cursor> mProfileLoaderListener = 161 new LoaderManager.LoaderCallbacks<Cursor>() { 162 163 @Override 164 public CursorLoader onCreateLoader(int id, Bundle args) { 165 final CursorLoader loader = createCursorLoader(getContext()); 166 loader.setUri(Profile.CONTENT_URI); 167 loader.setProjection(getProjection(getContext())); 168 return loader; 169 } 170 171 @Override 172 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 173 if (mListener != null) { 174 mListener.onProfileLoaded(data); 175 } 176 } 177 178 public void onLoaderReset(Loader<Cursor> loader) { 179 } 180 }; 181 newInstance(String newLocalProfileExtra, boolean areContactsAvailable)182 public static DisplayOptionsPreferenceFragment newInstance(String newLocalProfileExtra, 183 boolean areContactsAvailable) { 184 final DisplayOptionsPreferenceFragment fragment = new DisplayOptionsPreferenceFragment(); 185 final Bundle args = new Bundle(); 186 args.putString(ARG_NEW_LOCAL_PROFILE, newLocalProfileExtra); 187 args.putBoolean(ARG_CONTACTS_AVAILABLE, areContactsAvailable); 188 fragment.setArguments(args); 189 return fragment; 190 } 191 192 @Override onAttach(Activity activity)193 public void onAttach(Activity activity) { 194 super.onAttach(activity); 195 try { 196 mListener = (ProfileListener) activity; 197 } catch (ClassCastException e) { 198 throw new ClassCastException(activity.toString() + " must implement ProfileListener"); 199 } 200 } 201 202 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)203 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 204 // Wrap the preference view in a FrameLayout so we can show a snackbar 205 mRootView = new FrameLayout(getActivity()); 206 final View list = super.onCreateView(inflater, mRootView, savedInstanceState); 207 mRootView.addView(list); 208 return mRootView; 209 } 210 211 @Override onViewCreated(View view, Bundle savedInstanceState)212 public void onViewCreated(View view, Bundle savedInstanceState) { 213 super.onViewCreated(view, savedInstanceState); 214 215 mSaveServiceListener = new SaveServiceResultListener(); 216 LocalBroadcastManager.getInstance(getActivity()).registerReceiver( 217 mSaveServiceListener, 218 new IntentFilter(SimImportService.BROADCAST_SIM_IMPORT_COMPLETE)); 219 } 220 221 @Override onCreate(Bundle savedInstanceState)222 public void onCreate(Bundle savedInstanceState) { 223 super.onCreate(savedInstanceState); 224 225 // Load the preferences from an XML resource 226 addPreferencesFromResource(R.xml.preference_display_options); 227 228 final Bundle args = getArguments(); 229 mNewLocalProfileExtra = args.getString(ARG_NEW_LOCAL_PROFILE); 230 mAreContactsAvailable = args.getBoolean(ARG_CONTACTS_AVAILABLE); 231 232 removeUnsupportedPreferences(); 233 234 mMyInfoPreference = findPreference(KEY_MY_INFO); 235 236 final Preference accountsPreference = findPreference(KEY_ACCOUNTS); 237 accountsPreference.setOnPreferenceClickListener(this); 238 239 final Preference importPreference = findPreference(KEY_IMPORT); 240 importPreference.setOnPreferenceClickListener(this); 241 242 final Preference exportPreference = findPreference(KEY_EXPORT); 243 if (exportPreference != null) { 244 exportPreference.setOnPreferenceClickListener(this); 245 } 246 247 final Preference blockedNumbersPreference = findPreference(KEY_BLOCKED_NUMBERS); 248 if (blockedNumbersPreference != null) { 249 blockedNumbersPreference.setOnPreferenceClickListener(this); 250 } 251 252 final Preference aboutPreference = findPreference(KEY_ABOUT); 253 if (aboutPreference != null) { 254 aboutPreference.setOnPreferenceClickListener(this); 255 } 256 257 final Preference customFilterPreference = findPreference(KEY_CUSTOM_CONTACTS_FILTER); 258 if (customFilterPreference != null) { 259 customFilterPreference.setOnPreferenceClickListener(this); 260 setCustomContactsFilterSummary(); 261 } 262 263 final Preference defaultAccountPreference = findPreference(KEY_DEFAULT_ACCOUNT); 264 if (defaultAccountPreference != null) { 265 defaultAccountPreference.setOnPreferenceClickListener(this); 266 defaultAccountPreference.setSummary(getDefaultAccountSummary()); 267 } 268 } 269 270 @Override onActivityCreated(Bundle savedInstanceState)271 public void onActivityCreated(Bundle savedInstanceState) { 272 super.onActivityCreated(savedInstanceState); 273 getLoaderManager().initLoader(LOADER_PROFILE, null, mProfileLoaderListener); 274 AccountsLoader.loadAccounts(this, LOADER_ACCOUNTS, AccountTypeManager.writableFilter()); 275 } 276 277 @Override onDestroyView()278 public void onDestroyView() { 279 super.onDestroyView(); 280 LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mSaveServiceListener); 281 mRootView = null; 282 } 283 updateMyInfoPreference(boolean hasProfile, String displayName, long contactId, int displayNameSource)284 public void updateMyInfoPreference(boolean hasProfile, String displayName, long contactId, 285 int displayNameSource) { 286 final CharSequence summary = !hasProfile ? 287 getString(R.string.set_up_profile) : 288 displayNameSource == DisplayNameSources.PHONE ? 289 BidiFormatter.getInstance().unicodeWrap(displayName, TextDirectionHeuristics.LTR) : 290 displayName; 291 mMyInfoPreference.setSummary(summary); 292 mHasProfile = hasProfile; 293 mProfileContactId = contactId; 294 mMyInfoPreference.setOnPreferenceClickListener(this); 295 } 296 removeUnsupportedPreferences()297 private void removeUnsupportedPreferences() { 298 // Disable sort order for CJK locales where it is not supported 299 final Resources resources = getResources(); 300 if (!resources.getBoolean(R.bool.config_sort_order_user_changeable)) { 301 getPreferenceScreen().removePreference(findPreference(KEY_SORT_ORDER)); 302 } 303 304 if (!resources.getBoolean(R.bool.config_phonetic_name_display_user_changeable)) { 305 getPreferenceScreen().removePreference(findPreference(KEY_PHONETIC_NAME_DISPLAY)); 306 } 307 308 if (HelpUtils.isHelpAndFeedbackAvailable()) { 309 getPreferenceScreen().removePreference(findPreference(KEY_ABOUT)); 310 } 311 312 // Disable display order for CJK locales as well 313 if (!resources.getBoolean(R.bool.config_display_order_user_changeable)) { 314 getPreferenceScreen().removePreference(findPreference(KEY_DISPLAY_ORDER)); 315 } 316 317 final boolean isPhone = TelephonyManagerCompat.isVoiceCapable( 318 (TelephonyManager) getContext().getSystemService(Context.TELEPHONY_SERVICE)); 319 final boolean showBlockedNumbers = isPhone && ContactsUtils.FLAG_N_FEATURE 320 && BlockedNumberContract.canCurrentUserBlockNumbers(getContext()); 321 if (!showBlockedNumbers) { 322 getPreferenceScreen().removePreference(findPreference(KEY_BLOCKED_NUMBERS)); 323 } 324 325 if (!mAreContactsAvailable) { 326 getPreferenceScreen().removePreference(findPreference(KEY_EXPORT)); 327 } 328 } 329 330 @Override onAccountsLoaded(List<AccountInfo> accounts)331 public void onAccountsLoaded(List<AccountInfo> accounts) { 332 // Hide accounts preferences if no writable accounts exist 333 this.accounts = accounts; 334 final Preference defaultAccountPreference = 335 findPreference(KEY_DEFAULT_ACCOUNT); 336 defaultAccountPreference.setSummary(getDefaultAccountSummary()); 337 } 338 339 @Override getContext()340 public Context getContext() { 341 return getActivity(); 342 } 343 createCursorLoader(Context context)344 private CursorLoader createCursorLoader(Context context) { 345 return new CursorLoader(context) { 346 @Override 347 protected Cursor onLoadInBackground() { 348 try { 349 return super.onLoadInBackground(); 350 } catch (RuntimeException e) { 351 return null; 352 } 353 } 354 }; 355 } 356 357 private String[] getProjection(Context context) { 358 final ContactsPreferences contactsPrefs = new ContactsPreferences(context); 359 final int displayOrder = contactsPrefs.getDisplayOrder(); 360 if (displayOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { 361 return ProfileQuery.PROFILE_PROJECTION_PRIMARY; 362 } 363 return ProfileQuery.PROFILE_PROJECTION_ALTERNATIVE; 364 } 365 366 @Override 367 public boolean onPreferenceClick(Preference p) { 368 final String prefKey = p.getKey(); 369 370 if (KEY_ABOUT.equals(prefKey)) { 371 ((ContactsPreferenceActivity) getActivity()).showAboutFragment(); 372 return true; 373 } else if (KEY_IMPORT.equals(prefKey)) { 374 ImportDialogFragment.show(getFragmentManager()); 375 return true; 376 } else if (KEY_EXPORT.equals(prefKey)) { 377 ExportDialogFragment.show(getFragmentManager(), ContactsPreferenceActivity.class, 378 ExportDialogFragment.EXPORT_MODE_ALL_CONTACTS); 379 return true; 380 } else if (KEY_MY_INFO.equals(prefKey)) { 381 if (mHasProfile) { 382 final Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, mProfileContactId); 383 ImplicitIntentsUtil.startQuickContact(getActivity(), uri, ScreenType.ME_CONTACT); 384 } else { 385 final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); 386 intent.putExtra(mNewLocalProfileExtra, true); 387 ImplicitIntentsUtil.startActivityInApp(getActivity(), intent); 388 } 389 return true; 390 } else if (KEY_ACCOUNTS.equals(prefKey)) { 391 ImplicitIntentsUtil.startActivityOutsideApp(getContext(), 392 ImplicitIntentsUtil.getIntentForAddingAccount()); 393 return true; 394 } else if (KEY_BLOCKED_NUMBERS.equals(prefKey)) { 395 final Intent intent = TelecomManagerUtil.createManageBlockedNumbersIntent( 396 (TelecomManager) getContext().getSystemService(Context.TELECOM_SERVICE)); 397 startActivity(intent); 398 return true; 399 } else if (KEY_CUSTOM_CONTACTS_FILTER.equals(prefKey)) { 400 final ContactListFilter filter = 401 ContactListFilterController.getInstance(getContext()).getFilter(); 402 AccountFilterUtil.startAccountFilterActivityForResult( 403 this, REQUEST_CODE_CUSTOM_CONTACTS_FILTER, filter); 404 } else if (KEY_DEFAULT_ACCOUNT.equals(prefKey)) { 405 String packageName = getSetDefaultAccountActivityPackage(); 406 Intent intent = new Intent(Settings.ACTION_SET_DEFAULT_ACCOUNT); 407 if (packageName != null) { 408 intent.setPackage(packageName); 409 startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_ACCOUNT_CP2); 410 } 411 } 412 return false; 413 } 414 415 @Override 416 public void onActivityResult(int requestCode, int resultCode, Intent data) { 417 if (requestCode == REQUEST_CODE_CUSTOM_CONTACTS_FILTER 418 && resultCode == Activity.RESULT_OK) { 419 AccountFilterUtil.handleAccountFilterResult( 420 ContactListFilterController.getInstance(getContext()), resultCode, data); 421 setCustomContactsFilterSummary(); 422 } else if (requestCode == REQUEST_CODE_SET_DEFAULT_ACCOUNT_CP2 423 && resultCode == Activity.RESULT_OK) { 424 final Preference defaultAccountPreference = findPreference(KEY_DEFAULT_ACCOUNT); 425 if (defaultAccountPreference != null) { 426 defaultAccountPreference.setSummary(getDefaultAccountSummary()); 427 } 428 } else { 429 super.onActivityResult(requestCode, resultCode, data); 430 } 431 } 432 433 private void setCustomContactsFilterSummary() { 434 final Preference customFilterPreference = findPreference(KEY_CUSTOM_CONTACTS_FILTER); 435 if (customFilterPreference != null) { 436 final ContactListFilter filter = 437 ContactListFilterController.getInstance(getContext()).getPersistedFilter(); 438 if (filter != null) { 439 if (filter.filterType == ContactListFilter.FILTER_TYPE_DEFAULT || 440 filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) { 441 customFilterPreference.setSummary(R.string.list_filter_all_accounts); 442 } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) { 443 customFilterPreference.setSummary(R.string.listCustomView); 444 } else { 445 customFilterPreference.setSummary(null); 446 } 447 } 448 } 449 } 450 451 private CharSequence getDefaultAccountSummary() { 452 ContactsPreferences preferences = new ContactsPreferences(getContext()); 453 AccountWithDataSet defaultAccountWithDataSet = preferences.getDefaultAccount(); 454 AccountInfo defaultAccountInfo = AccountInfo.getAccount( 455 accounts, defaultAccountWithDataSet); 456 if (defaultAccountInfo != null) { 457 return defaultAccountInfo.getNameLabel(); 458 } else { 459 return null; 460 } 461 } 462 463 private String getSetDefaultAccountActivityPackage() { 464 // Only preloaded Contacts App has the permission to call setDefaultAccount. 465 Intent intent = new Intent(Settings.ACTION_SET_DEFAULT_ACCOUNT); 466 PackageManager packageManager = getContext().getPackageManager(); 467 List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(intent, 0); 468 for (ResolveInfo resolveInfo : resolveInfos) { 469 String packageName = resolveInfo.activityInfo.packageName; 470 if (packageManager.checkPermission( 471 android.Manifest.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS, packageName) 472 == PackageManager.PERMISSION_GRANTED) { 473 return packageName; 474 } 475 } 476 return null; 477 } 478 479 private class SaveServiceResultListener extends BroadcastReceiver { 480 @Override 481 public void onReceive(Context context, Intent intent) { 482 final long now = System.currentTimeMillis(); 483 final long opStart = intent.getLongExtra( 484 SimImportService.EXTRA_OPERATION_REQUESTED_AT_TIME, now); 485 486 // If it's been over 30 seconds the user is likely in a different context so suppress 487 // the toast message. 488 if (now - opStart > 30*1000) return; 489 490 final int code = intent.getIntExtra(SimImportService.EXTRA_RESULT_CODE, 491 SimImportService.RESULT_UNKNOWN); 492 final int count = intent.getIntExtra(SimImportService.EXTRA_RESULT_COUNT, -1); 493 if (code == SimImportService.RESULT_SUCCESS && count > 0) { 494 MessageFormat msgFormat = new MessageFormat( 495 getResources().getString(R.string.sim_import_success_toast_fmt), 496 Locale.getDefault()); 497 Map<String, Object> arguments = new HashMap<>(); 498 arguments.put("count", count); 499 Snackbar.make(mRootView, msgFormat.format(arguments), 500 Snackbar.LENGTH_LONG).show(); 501 } else if (code == SimImportService.RESULT_FAILURE) { 502 Snackbar.make(mRootView, R.string.sim_import_failed_toast, 503 Snackbar.LENGTH_LONG).show(); 504 } 505 } 506 } 507 } 508 509