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