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