/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.contacts.list; import android.accounts.Account; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.CursorLoader; import android.content.Intent; import android.content.Loader; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.database.Cursor; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.provider.ContactsContract; import android.provider.ContactsContract.Directory; import android.support.v4.content.ContextCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.text.TextUtils; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout.LayoutParams; import android.widget.TextView; import android.widget.Toast; import com.android.contacts.ContactSaveService; import com.android.contacts.Experiments; import com.android.contacts.R; import com.android.contacts.activities.ActionBarAdapter; import com.android.contacts.activities.PeopleActivity; import com.android.contacts.compat.CompatUtils; import com.android.contacts.interactions.ContactDeletionInteraction; import com.android.contacts.interactions.ContactMultiDeletionInteraction; import com.android.contacts.interactions.ContactMultiDeletionInteraction.MultiContactDeleteListener; import com.android.contacts.logging.ListEvent; import com.android.contacts.logging.Logger; import com.android.contacts.logging.ScreenEvent; import com.android.contacts.model.AccountTypeManager; import com.android.contacts.model.account.AccountInfo; import com.android.contacts.model.account.AccountWithDataSet; import com.android.contacts.quickcontact.QuickContactActivity; import com.android.contacts.util.AccountFilterUtil; import com.android.contacts.util.ImplicitIntentsUtil; import com.android.contacts.util.SharedPreferenceUtil; import com.android.contacts.util.SyncUtil; import com.android.contactsbind.FeatureHighlightHelper; import com.android.contactsbind.experiments.Flags; import com.google.common.util.concurrent.Futures; import java.util.List; import java.util.Locale; import java.util.concurrent.Future; /** * Fragment containing a contact list used for browsing (as compared to * picking a contact with one of the PICK intents). */ public class DefaultContactBrowseListFragment extends ContactBrowseListFragment implements EnableGlobalSyncDialogFragment.Listener { private static final String TAG = "DefaultListFragment"; private static final String ENABLE_DEBUG_OPTIONS_HIDDEN_CODE = "debug debug!"; private static final String KEY_DELETION_IN_PROGRESS = "deletionInProgress"; private static final String KEY_SEARCH_RESULT_CLICKED = "search_result_clicked"; private static final int ACTIVITY_REQUEST_CODE_SHARE = 0; private View mSearchHeaderView; private View mSearchProgress; private View mEmptyAccountView; private View mEmptyHomeView; private View mAccountFilterContainer; private TextView mSearchProgressText; private SwipeRefreshLayout mSwipeRefreshLayout; private final Handler mHandler = new Handler(); private final Runnable mCancelRefresh = new Runnable() { @Override public void run() { if (mSwipeRefreshLayout.isRefreshing()) { mSwipeRefreshLayout.setRefreshing(false); } } }; private View mAlertContainer; private TextView mAlertText; private ImageView mAlertDismissIcon; private int mReasonSyncOff = SyncUtil.SYNC_SETTING_SYNC_ON; private boolean mContactsAvailable; private boolean mEnableDebugMenuOptions; private boolean mIsRecreatedInstance; private boolean mOptionsMenuContactsAvailable; private boolean mCanSetActionBar = false; /** * If {@link #configureFragment()} is already called. Used to avoid calling it twice * in {@link #onResume()}. * (This initialization only needs to be done once in onResume() when the Activity was just * created from scratch -- i.e. onCreate() was just called) */ private boolean mFragmentInitialized; private boolean mFromOnNewIntent; /** * This is to tell whether we need to restart ContactMultiDeletionInteraction and set listener. * if screen is rotated while deletion dialog is shown. */ private boolean mIsDeletionInProgress; /** * This is to disable {@link #onOptionsItemSelected} when we trying to stop the * activity/fragment. */ private boolean mDisableOptionItemSelected; private boolean mSearchResultClicked; private ActionBarAdapter mActionBarAdapter; private PeopleActivity mActivity; private ContactsRequest mContactsRequest; private ContactListFilterController mContactListFilterController; private Future> mWritableAccountsFuture; private final ActionBarAdapter.Listener mActionBarListener = new ActionBarAdapter.Listener() { @Override public void onAction(int action) { switch (action) { case ActionBarAdapter.Listener.Action.START_SELECTION_MODE: displayCheckBoxes(true); startSearchOrSelectionMode(); break; case ActionBarAdapter.Listener.Action.START_SEARCH_MODE: if (!mIsRecreatedInstance) { Logger.logScreenView(mActivity, ScreenEvent.ScreenType.SEARCH); } startSearchOrSelectionMode(); break; case ActionBarAdapter.Listener.Action.BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE: mActivity.showFabWithAnimation(/* showFab */ true); break; case ActionBarAdapter.Listener.Action.STOP_SEARCH_AND_SELECTION_MODE: // If queryString is empty, fragment data will not be reloaded, // so hamburger promo should be checked now. // Otherwise, promo should be checked and displayed after reloading, b/30706521. if (TextUtils.isEmpty(getQueryString())) { maybeShowHamburgerFeatureHighlight(); } setQueryTextToFragment(""); maybeHideCheckBoxes(); mActivity.invalidateOptionsMenu(); mActivity.showFabWithAnimation(/* showFab */ true); // Alert user if sync is off and not dismissed before setSyncOffAlert(); // Determine whether the account has pullToRefresh feature setSwipeRefreshLayoutEnabledOrNot(getFilter()); break; case ActionBarAdapter.Listener.Action.CHANGE_SEARCH_QUERY: final String queryString = mActionBarAdapter.getQueryString(); setQueryTextToFragment(queryString); updateDebugOptionsVisibility( ENABLE_DEBUG_OPTIONS_HIDDEN_CODE.equals(queryString)); break; default: throw new IllegalStateException("Unknown ActionBarAdapter action: " + action); } } private void startSearchOrSelectionMode() { configureContactListFragment(); maybeHideCheckBoxes(); mActivity.invalidateOptionsMenu(); mActivity.showFabWithAnimation(/* showFab */ false); final Context context = getContext(); if (!SharedPreferenceUtil.getHamburgerPromoTriggerActionHappenedBefore(context)) { SharedPreferenceUtil.setHamburgerPromoTriggerActionHappenedBefore(context); } } private void updateDebugOptionsVisibility(boolean visible) { if (mEnableDebugMenuOptions != visible) { mEnableDebugMenuOptions = visible; mActivity.invalidateOptionsMenu(); } } private void setQueryTextToFragment(String query) { setQueryString(query, true); setVisibleScrollbarEnabled(!isSearchMode()); } @Override public void onUpButtonPressed() { mActivity.onBackPressed(); } }; private final View.OnClickListener mAddContactListener = new View.OnClickListener() { @Override public void onClick(View v) { AccountFilterUtil.startEditorIntent(getContext(), mActivity.getIntent(), getFilter()); } }; public DefaultContactBrowseListFragment() { setPhotoLoaderEnabled(true); // Don't use a QuickContactBadge. Just use a regular ImageView. Using a QuickContactBadge // inside the ListView prevents us from using MODE_FULLY_EXPANDED and messes up ripples. setQuickContactEnabled(false); setSectionHeaderDisplayEnabled(true); setVisibleScrollbarEnabled(true); setDisplayDirectoryHeader(false); setHasOptionsMenu(true); } /** * Whether a search result was clicked by the user. Tracked so that we can distinguish * between exiting the search mode after a result was clicked from exiting w/o clicking * any search result. */ public boolean wasSearchResultClicked() { return mSearchResultClicked; } /** * Resets whether a search result was clicked by the user to false. */ public void resetSearchResultClicked() { mSearchResultClicked = false; } @Override public CursorLoader createCursorLoader(Context context) { return new FavoritesAndContactsLoader(context); } @Override public void onLoadFinished(Loader loader, Cursor data) { if (loader.getId() == Directory.DEFAULT) { bindListHeader(data == null ? 0 : data.getCount()); } super.onLoadFinished(loader, data); if (!isSearchMode()) { maybeShowHamburgerFeatureHighlight(); } if (mActionBarAdapter != null) { mActionBarAdapter.updateOverflowButtonColor(); } } private void maybeShowHamburgerFeatureHighlight() { if (mActionBarAdapter!= null && !mActionBarAdapter.isSearchMode() && !mActionBarAdapter.isSelectionMode() && !isTalkbackOnAndOnPreLollipopMr1() && SharedPreferenceUtil.getShouldShowHamburgerPromo(getContext())) { if (FeatureHighlightHelper.showHamburgerFeatureHighlight(mActivity)) { SharedPreferenceUtil.setHamburgerPromoDisplayedBefore(getContext()); } } } // There's a crash if we show feature highlight when Talkback is on, on API 21 and below. // See b/31180524. private boolean isTalkbackOnAndOnPreLollipopMr1(){ return ((AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE)) .isTouchExplorationEnabled() && !CompatUtils.isLollipopMr1Compatible(); } private void bindListHeader(int numberOfContacts) { final ContactListFilter filter = getFilter(); // If the phone has at least one Google account whose sync status is unsyncable or pending // or active, we have to make mAccountFilterContainer visible. if (!isSearchMode() && numberOfContacts <= 0 && shouldShowEmptyView(filter)) { if (filter != null && filter.isContactsFilterType()) { makeViewVisible(mEmptyHomeView); } else { makeViewVisible(mEmptyAccountView); } return; } makeViewVisible(mAccountFilterContainer); if (isSearchMode()) { hideHeaderAndAddPadding(getContext(), getListView(), mAccountFilterContainer); } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) { bindListHeaderCustom(getListView(), mAccountFilterContainer); } else if (filter.filterType != ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) { final AccountWithDataSet accountWithDataSet = new AccountWithDataSet( filter.accountName, filter.accountType, filter.dataSet); bindListHeader(getContext(), getListView(), mAccountFilterContainer, accountWithDataSet, numberOfContacts); } else { hideHeaderAndAddPadding(getContext(), getListView(), mAccountFilterContainer); } } /** * If at least one Google account is unsyncable or its sync status is pending or active, we * should not show empty view even if the number of contacts is 0. We should show sync status * with empty list instead. */ private boolean shouldShowEmptyView(ContactListFilter filter) { if (filter == null) { return true; } // TODO(samchen) : Check ContactListFilter.FILTER_TYPE_CUSTOM if (ContactListFilter.FILTER_TYPE_DEFAULT == filter.filterType || ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS == filter.filterType) { final List syncableAccounts = AccountTypeManager.getInstance(getContext()).getWritableGoogleAccounts(); if (syncableAccounts != null && syncableAccounts.size() > 0) { for (AccountInfo info : syncableAccounts) { // Won't be null because Google accounts have a non-null name and type. final Account account = info.getAccount().getAccountOrNull(); if (SyncUtil.isSyncStatusPendingOrActive(account) || SyncUtil.isUnsyncableGoogleAccount(account)) { return false; } } } } else if (ContactListFilter.FILTER_TYPE_ACCOUNT == filter.filterType) { final Account account = new Account(filter.accountName, filter.accountType); return !(SyncUtil.isSyncStatusPendingOrActive(account) || SyncUtil.isUnsyncableGoogleAccount(account)); } return true; } // Show the view that's specified by id and hide the other two. private void makeViewVisible(View view) { mEmptyAccountView.setVisibility(view == mEmptyAccountView ? View.VISIBLE : View.GONE); mEmptyHomeView.setVisibility(view == mEmptyHomeView ? View.VISIBLE : View.GONE); mAccountFilterContainer.setVisibility( view == mAccountFilterContainer ? View.VISIBLE : View.GONE); } public void scrollToTop() { if (getListView() != null) { getListView().setSelection(0); } } @Override protected void onItemClick(int position, long id) { final Uri uri = getAdapter().getContactUri(position); if (uri == null) { return; } if (getAdapter().isDisplayingCheckBoxes()) { super.onItemClick(position, id); return; } else { if (isSearchMode()) { mSearchResultClicked = true; Logger.logSearchEvent(createSearchStateForSearchResultClick(position)); } } viewContact(position, uri, getAdapter().isEnterpriseContact(position)); } @Override protected ContactListAdapter createListAdapter() { DefaultContactListAdapter adapter = new DefaultContactListAdapter(getContext()); adapter.setSectionHeaderDisplayEnabled(isSectionHeaderDisplayEnabled()); adapter.setDisplayPhotos(true); adapter.setPhotoPosition( ContactListItemView.getDefaultPhotoPosition(/* opposite = */ false)); return adapter; } @Override public ContactListFilter getFilter() { return mContactListFilterController.getFilter(); } @Override protected View inflateView(LayoutInflater inflater, ViewGroup container) { final View view = inflater.inflate(R.layout.contact_list_content, null); mAccountFilterContainer = view.findViewById(R.id.account_filter_header_container); // Add empty main view and account view to list. final FrameLayout contactListLayout = (FrameLayout) view.findViewById(R.id.contact_list); mEmptyAccountView = getEmptyAccountView(inflater); mEmptyHomeView = getEmptyHomeView(inflater); contactListLayout.addView(mEmptyAccountView); contactListLayout.addView(mEmptyHomeView); return view; } private View getEmptyHomeView(LayoutInflater inflater) { final View emptyHomeView = inflater.inflate(R.layout.empty_home_view, null); // Set image margins. final ImageView image = (ImageView) emptyHomeView.findViewById(R.id.empty_home_image); final LayoutParams params = (LayoutParams) image.getLayoutParams(); final int screenHeight = getResources().getDisplayMetrics().heightPixels; final int marginTop = screenHeight / 2 - getResources().getDimensionPixelSize(R.dimen.empty_home_view_image_offset) ; params.setMargins(0, marginTop, 0, 0); params.gravity = Gravity.CENTER_HORIZONTAL; image.setLayoutParams(params); // Set up add contact button. final Button addContactButton = (Button) emptyHomeView.findViewById(R.id.add_contact_button); addContactButton.setOnClickListener(mAddContactListener); return emptyHomeView; } private View getEmptyAccountView(LayoutInflater inflater) { final View emptyAccountView = inflater.inflate(R.layout.empty_account_view, null); // Set image margins. final ImageView image = (ImageView) emptyAccountView.findViewById(R.id.empty_account_image); final LayoutParams params = (LayoutParams) image.getLayoutParams(); final int height = getResources().getDisplayMetrics().heightPixels; final int divisor = getResources().getInteger(R.integer.empty_account_view_image_margin_divisor); final int offset = getResources().getDimensionPixelSize(R.dimen.empty_account_view_image_offset); params.setMargins(0, height / divisor + offset, 0, 0); params.gravity = Gravity.CENTER_HORIZONTAL; image.setLayoutParams(params); // Set up add contact button. final Button addContactButton = (Button) emptyAccountView.findViewById(R.id.add_contact_button); addContactButton.setOnClickListener(mAddContactListener); return emptyAccountView; } @Override public void onCreate(Bundle savedState) { super.onCreate(savedState); mIsRecreatedInstance = (savedState != null); mContactListFilterController = ContactListFilterController.getInstance(getContext()); mContactListFilterController.checkFilterValidity(false); // Use FILTER_TYPE_ALL_ACCOUNTS filter if the instance is not a re-created one. // This is useful when user upgrades app while an account filter was // stored in sharedPreference in a previous version of Contacts app. final ContactListFilter filter = mIsRecreatedInstance ? getFilter() : AccountFilterUtil.createContactsFilter(getContext()); setContactListFilter(filter); } @Override protected void onCreateView(LayoutInflater inflater, ViewGroup container) { super.onCreateView(inflater, container); initSwipeRefreshLayout(); // Putting the header view inside a container will allow us to make // it invisible later. See checkHeaderViewVisibility() final FrameLayout headerContainer = new FrameLayout(inflater.getContext()); mSearchHeaderView = inflater.inflate(R.layout.search_header, null, false); headerContainer.addView(mSearchHeaderView); getListView().addHeaderView(headerContainer, null, false); checkHeaderViewVisibility(); mSearchProgress = getView().findViewById(R.id.search_progress); mSearchProgressText = (TextView) mSearchHeaderView.findViewById(R.id.totalContactsText); mAlertContainer = getView().findViewById(R.id.alert_container); mAlertText = (TextView) mAlertContainer.findViewById(R.id.alert_text); mAlertDismissIcon = (ImageView) mAlertContainer.findViewById(R.id.alert_dismiss_icon); mAlertText.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { turnSyncOn(); } }); mAlertDismissIcon.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { dismiss(); } }); mAlertContainer.setVisibility(View.GONE); } private void turnSyncOn() { final ContactListFilter filter = getFilter(); if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT && mReasonSyncOff == SyncUtil.SYNC_SETTING_ACCOUNT_SYNC_OFF) { ContentResolver.setSyncAutomatically( new Account(filter.accountName, filter.accountType), ContactsContract.AUTHORITY, true); mAlertContainer.setVisibility(View.GONE); } else { final EnableGlobalSyncDialogFragment dialog = new EnableGlobalSyncDialogFragment(); dialog.show(this, filter); } } @Override public void onEnableAutoSync(ContactListFilter filter) { // Turn on auto-sync ContentResolver.setMasterSyncAutomatically(true); // This should be OK (won't block) because this only happens after a user action final List accountInfos = Futures.getUnchecked(mWritableAccountsFuture); // Also enable Contacts sync final List accounts = AccountInfo.extractAccounts(accountInfos); final List syncableAccounts = filter.getSyncableAccounts(accounts); if (syncableAccounts != null && syncableAccounts.size() > 0) { for (Account account : syncableAccounts) { ContentResolver.setSyncAutomatically(new Account(account.name, account.type), ContactsContract.AUTHORITY, true); } } mAlertContainer.setVisibility(View.GONE); } private void dismiss() { if (mReasonSyncOff == SyncUtil.SYNC_SETTING_GLOBAL_SYNC_OFF) { SharedPreferenceUtil.incNumOfDismissesForAutoSyncOff(getContext()); } else if (mReasonSyncOff == SyncUtil.SYNC_SETTING_ACCOUNT_SYNC_OFF) { SharedPreferenceUtil.incNumOfDismissesForAccountSyncOff( getContext(), getFilter().accountName); } mAlertContainer.setVisibility(View.GONE); } private void initSwipeRefreshLayout() { mSwipeRefreshLayout = (SwipeRefreshLayout) mView.findViewById(R.id.swipe_refresh); if (mSwipeRefreshLayout == null) { return; } mSwipeRefreshLayout.setEnabled(true); // Request sync contacts mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { mHandler.removeCallbacks(mCancelRefresh); final boolean isNetworkConnected = SyncUtil.isNetworkConnected(getContext()); if (!isNetworkConnected) { mSwipeRefreshLayout.setRefreshing(false); ((PeopleActivity)getActivity()).showConnectionErrorMsg(); return; } syncContacts(getFilter()); mHandler.postDelayed(mCancelRefresh, Flags.getInstance() .getInteger(Experiments.PULL_TO_REFRESH_CANCEL_REFRESH_MILLIS)); } }); mSwipeRefreshLayout.setColorSchemeResources( R.color.swipe_refresh_color1, R.color.swipe_refresh_color2, R.color.swipe_refresh_color3, R.color.swipe_refresh_color4); mSwipeRefreshLayout.setDistanceToTriggerSync( (int) getResources().getDimension(R.dimen.pull_to_refresh_distance)); } /** * Request sync for the Google accounts (not include Google+ accounts) specified by the given * filter. */ private void syncContacts(ContactListFilter filter) { if (filter == null) { return; } final Bundle bundle = new Bundle(); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); final List accounts = AccountInfo.extractAccounts( Futures.getUnchecked(mWritableAccountsFuture)); final List syncableAccounts = filter.getSyncableAccounts(accounts); if (syncableAccounts != null && syncableAccounts.size() > 0) { for (Account account : syncableAccounts) { // We can prioritize Contacts sync if sync is not initialized yet. if (!SyncUtil.isSyncStatusPendingOrActive(account) || SyncUtil.isUnsyncableGoogleAccount(account)) { ContentResolver.requestSync(account, ContactsContract.AUTHORITY, bundle); } } } } private void setSyncOffAlert() { final ContactListFilter filter = getFilter(); final Account account = filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT && filter.isGoogleAccountType() ? new Account(filter.accountName, filter.accountType) : null; if (account == null && !filter.isContactsFilterType()) { mAlertContainer.setVisibility(View.GONE); } else { mReasonSyncOff = SyncUtil.calculateReasonSyncOff(getContext(), account); final boolean isAlertVisible = SyncUtil.isAlertVisible(getContext(), account, mReasonSyncOff); setSyncOffMsg(mReasonSyncOff); mAlertContainer.setVisibility(isAlertVisible ? View.VISIBLE : View.GONE); } } private void setSyncOffMsg(int reason) { final Resources resources = getResources(); switch (reason) { case SyncUtil.SYNC_SETTING_GLOBAL_SYNC_OFF: mAlertText.setText(resources.getString(R.string.auto_sync_off)); break; case SyncUtil.SYNC_SETTING_ACCOUNT_SYNC_OFF: mAlertText.setText(resources.getString(R.string.account_sync_off)); break; default: } } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mActivity = (PeopleActivity) getActivity(); mActionBarAdapter = new ActionBarAdapter(mActivity, mActionBarListener, mActivity.getSupportActionBar(), mActivity.getToolbar(), R.string.enter_contact_name); mActionBarAdapter.setShowHomeIcon(true); initializeActionBarAdapter(savedInstanceState); if (isSearchMode()) { mActionBarAdapter.setFocusOnSearchView(); } setCheckBoxListListener(new CheckBoxListListener()); setOnContactListActionListener(new ContactBrowserActionListener()); if (savedInstanceState != null) { if (savedInstanceState.getBoolean(KEY_DELETION_IN_PROGRESS)) { deleteSelectedContacts(); } mSearchResultClicked = savedInstanceState.getBoolean(KEY_SEARCH_RESULT_CLICKED); } setDirectorySearchMode(); mCanSetActionBar = true; } public void initializeActionBarAdapter(Bundle savedInstanceState) { if (mActionBarAdapter != null) { mActionBarAdapter.initialize(savedInstanceState, mContactsRequest); } } private void configureFragment() { if (mFragmentInitialized && !mFromOnNewIntent) { return; } mFragmentInitialized = true; if (mFromOnNewIntent || !mIsRecreatedInstance) { mFromOnNewIntent = false; configureFragmentForRequest(); } configureContactListFragment(); } private void configureFragmentForRequest() { ContactListFilter filter = null; final int actionCode = mContactsRequest.getActionCode(); boolean searchMode = mContactsRequest.isSearchMode(); switch (actionCode) { case ContactsRequest.ACTION_ALL_CONTACTS: filter = AccountFilterUtil.createContactsFilter(getContext()); break; case ContactsRequest.ACTION_CONTACTS_WITH_PHONES: filter = ContactListFilter.createFilterWithType( ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY); break; case ContactsRequest.ACTION_FREQUENT: case ContactsRequest.ACTION_STREQUENT: case ContactsRequest.ACTION_STARRED: case ContactsRequest.ACTION_VIEW_CONTACT: default: break; } if (filter != null) { setContactListFilter(filter); searchMode = false; } if (mContactsRequest.getContactUri() != null) { searchMode = false; } mActionBarAdapter.setSearchMode(searchMode); configureContactListFragmentForRequest(); } private void configureContactListFragmentForRequest() { final Uri contactUri = mContactsRequest.getContactUri(); if (contactUri != null) { setSelectedContactUri(contactUri); } setQueryString(mActionBarAdapter.getQueryString(), true); setVisibleScrollbarEnabled(!isSearchMode()); } private void setDirectorySearchMode() { if (mContactsRequest != null && mContactsRequest.isDirectorySearchEnabled()) { setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_DEFAULT); } else { setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE); } } @Override public void onResume() { super.onResume(); configureFragment(); maybeShowHamburgerFeatureHighlight(); // Re-register the listener, which may have been cleared when onSaveInstanceState was // called. See also: onSaveInstanceState mActionBarAdapter.setListener(mActionBarListener); mDisableOptionItemSelected = false; maybeHideCheckBoxes(); mWritableAccountsFuture = AccountTypeManager.getInstance(getContext()).filterAccountsAsync( AccountTypeManager.writableFilter()); } private void maybeHideCheckBoxes() { if (!mActionBarAdapter.isSelectionMode()) { displayCheckBoxes(false); } } public ActionBarAdapter getActionBarAdapter(){ return mActionBarAdapter; } @Override protected void setSearchMode(boolean flag) { super.setSearchMode(flag); checkHeaderViewVisibility(); if (!flag) showSearchProgress(false); } /** Show or hide the directory-search progress spinner. */ private void showSearchProgress(boolean show) { if (mSearchProgress != null) { mSearchProgress.setVisibility(show ? View.VISIBLE : View.GONE); } } private void checkHeaderViewVisibility() { // Hide the search header by default. if (mSearchHeaderView != null) { mSearchHeaderView.setVisibility(View.GONE); } } @Override protected void setListHeader() { if (!isSearchMode()) { return; } ContactListAdapter adapter = getAdapter(); if (adapter == null) { return; } // In search mode we only display the header if there is nothing found if (TextUtils.isEmpty(getQueryString()) || !adapter.areAllPartitionsEmpty()) { mSearchHeaderView.setVisibility(View.GONE); showSearchProgress(false); } else { mSearchHeaderView.setVisibility(View.VISIBLE); if (adapter.isLoading()) { mSearchProgressText.setText(R.string.search_results_searching); showSearchProgress(true); } else { mSearchProgressText.setText(R.string.listFoundAllContactsZero); mSearchProgressText.sendAccessibilityEvent( AccessibilityEvent.TYPE_VIEW_SELECTED); showSearchProgress(false); } } } public SwipeRefreshLayout getSwipeRefreshLayout() { return mSwipeRefreshLayout; } private final class CheckBoxListListener implements OnCheckBoxListActionListener { @Override public void onStartDisplayingCheckBoxes() { mActionBarAdapter.setSelectionMode(true); mActivity.invalidateOptionsMenu(); } @Override public void onSelectedContactIdsChanged() { mActionBarAdapter.setSelectionCount(getSelectedContactIds().size()); mActivity.invalidateOptionsMenu(); mActionBarAdapter.updateOverflowButtonColor(); } @Override public void onStopDisplayingCheckBoxes() { mActionBarAdapter.setSelectionMode(false); } } public void setFilterAndUpdateTitle(ContactListFilter filter) { setFilterAndUpdateTitle(filter, true); } private void setFilterAndUpdateTitle(ContactListFilter filter, boolean restoreSelectedUri) { setContactListFilter(filter); updateListFilter(filter, restoreSelectedUri); mActivity.setTitle(AccountFilterUtil.getActionBarTitleForFilter(mActivity, filter)); // Alert user if sync is off and not dismissed before setSyncOffAlert(); // Determine whether the account has pullToRefresh feature setSwipeRefreshLayoutEnabledOrNot(filter); } private void setSwipeRefreshLayoutEnabledOrNot(ContactListFilter filter) { final SwipeRefreshLayout swipeRefreshLayout = getSwipeRefreshLayout(); if (swipeRefreshLayout == null) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Can not load swipeRefreshLayout, swipeRefreshLayout is null"); } return; } swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setEnabled(false); if (filter != null && !mActionBarAdapter.isSearchMode() && !mActionBarAdapter.isSelectionMode()) { if (filter.isSyncable() || (filter.shouldShowSyncState() && SyncUtil.hasSyncableAccount(AccountTypeManager.getInstance(getContext())))) { swipeRefreshLayout.setEnabled(true); } } } private void configureContactListFragment() { // Filter may be changed when activity is in background. setFilterAndUpdateTitle(getFilter()); setVerticalScrollbarPosition(getScrollBarPosition()); setSelectionVisible(false); mActivity.invalidateOptionsMenu(); } private int getScrollBarPosition() { final Locale locale = Locale.getDefault(); final boolean isRTL = TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL; return isRTL ? View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT; } private final class ContactBrowserActionListener implements OnContactBrowserActionListener { ContactBrowserActionListener() {} @Override public void onSelectionChange() { } @Override public void onViewContactAction(int position, Uri contactLookupUri, boolean isEnterpriseContact) { if (isEnterpriseContact) { // No implicit intent as user may have a different contacts app in work profile. ContactsContract.QuickContact.showQuickContact(getContext(), new Rect(), contactLookupUri, QuickContactActivity.MODE_FULLY_EXPANDED, null); } else { final int previousScreen; if (isSearchMode()) { previousScreen = ScreenEvent.ScreenType.SEARCH; } else { if (isAllContactsFilter(getFilter())) { if (position < getAdapter().getNumberOfFavorites()) { previousScreen = ScreenEvent.ScreenType.FAVORITES; } else { previousScreen = ScreenEvent.ScreenType.ALL_CONTACTS; } } else { previousScreen = ScreenEvent.ScreenType.LIST_ACCOUNT; } } Logger.logListEvent(ListEvent.ActionType.CLICK, /* listType */ getListTypeIncludingSearch(), /* count */ getAdapter().getCount(), /* clickedIndex */ position, /* numSelected */ 0); ImplicitIntentsUtil.startQuickContact( getActivity(), contactLookupUri, previousScreen); } } @Override public void onDeleteContactAction(Uri contactUri) { ContactDeletionInteraction.start(mActivity, contactUri, false); } @Override public void onFinishAction() { mActivity.onBackPressed(); } @Override public void onInvalidSelection() { ContactListFilter filter; ContactListFilter currentFilter = getFilter(); if (currentFilter != null && currentFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { filter = AccountFilterUtil.createContactsFilter(getContext()); setFilterAndUpdateTitle(filter); } else { filter = ContactListFilter.createFilterWithType( ContactListFilter.FILTER_TYPE_SINGLE_CONTACT); setFilterAndUpdateTitle(filter, /* restoreSelectedUri */ false); } setContactListFilter(filter); } } private boolean isAllContactsFilter(ContactListFilter filter) { return filter != null && filter.isContactsFilterType(); } public void setContactsAvailable(boolean contactsAvailable) { mContactsAvailable = contactsAvailable; } /** * Set filter via ContactListFilterController */ private void setContactListFilter(ContactListFilter filter) { mContactListFilterController.setContactListFilter(filter, /* persistent */ isAllContactsFilter(filter)); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (!mContactsAvailable || mActivity.isInSecondLevel()) { // If contacts aren't available or this fragment is not visible, hide all menu items. return; } super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.people_options, menu); } @Override public void onPrepareOptionsMenu(Menu menu) { mOptionsMenuContactsAvailable = mContactsAvailable; if (!mOptionsMenuContactsAvailable) { return; } final boolean isSearchOrSelectionMode = mActionBarAdapter.isSearchMode() || mActionBarAdapter.isSelectionMode(); makeMenuItemVisible(menu, R.id.menu_search, !isSearchOrSelectionMode); final boolean showSelectedContactOptions = mActionBarAdapter.isSelectionMode() && getSelectedContactIds().size() != 0; makeMenuItemVisible(menu, R.id.menu_share, showSelectedContactOptions); makeMenuItemVisible(menu, R.id.menu_delete, showSelectedContactOptions); final boolean showLinkContactsOptions = mActionBarAdapter.isSelectionMode() && getSelectedContactIds().size() > 1; makeMenuItemVisible(menu, R.id.menu_join, showLinkContactsOptions); // Debug options need to be visible even in search mode. makeMenuItemVisible(menu, R.id.export_database, mEnableDebugMenuOptions && hasExportIntentHandler()); // Light tint the icons for normal mode, dark tint for search or selection mode. for (int i = 0; i < menu.size(); ++i) { final Drawable icon = menu.getItem(i).getIcon(); if (icon != null && !isSearchOrSelectionMode) { icon.mutate().setColorFilter(ContextCompat.getColor(getContext(), R.color.actionbar_icon_color), PorterDuff.Mode.SRC_ATOP); } } } private void makeMenuItemVisible(Menu menu, int itemId, boolean visible) { final MenuItem item = menu.findItem(itemId); if (item != null) { item.setVisible(visible); } } private boolean hasExportIntentHandler() { final Intent intent = new Intent(); intent.setAction("com.android.providers.contacts.DUMP_DATABASE"); final List receivers = getContext().getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); return receivers != null && receivers.size() > 0; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (mDisableOptionItemSelected) { return false; } final int id = item.getItemId(); if (id == android.R.id.home) { if (mActionBarAdapter.isUpShowing()) { // "UP" icon press -- should be treated as "back". mActivity.onBackPressed(); } return true; } else if (id == R.id.menu_search) { if (!mActionBarAdapter.isSelectionMode()) { mActionBarAdapter.setSearchMode(true); } return true; } else if (id == R.id.menu_share) { shareSelectedContacts(); return true; } else if (id == R.id.menu_join) { Logger.logListEvent(ListEvent.ActionType.LINK, /* listType */ getListTypeIncludingSearch(), /* count */ getAdapter().getCount(), /* clickedIndex */ -1, /* numSelected */ getAdapter().getSelectedContactIds().size()); joinSelectedContacts(); return true; } else if (id == R.id.menu_delete) { deleteSelectedContacts(); return true; } else if (id == R.id.export_database) { final Intent intent = new Intent("com.android.providers.contacts.DUMP_DATABASE"); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); ImplicitIntentsUtil.startActivityOutsideApp(getContext(), intent); return true; } return super.onOptionsItemSelected(item); } /** * Share all contacts that are currently selected. This method is pretty inefficient for * handling large numbers of contacts. I don't expect this to be a problem. */ private void shareSelectedContacts() { final StringBuilder uriListBuilder = new StringBuilder(); for (Long contactId : getSelectedContactIds()) { final Uri contactUri = ContentUris.withAppendedId( ContactsContract.Contacts.CONTENT_URI, contactId); final Uri lookupUri = ContactsContract.Contacts.getLookupUri( getContext().getContentResolver(), contactUri); if (lookupUri == null) { continue; } final List pathSegments = lookupUri.getPathSegments(); if (pathSegments.size() < 2) { continue; } final String lookupKey = pathSegments.get(pathSegments.size() - 2); if (uriListBuilder.length() > 0) { uriListBuilder.append(':'); } uriListBuilder.append(Uri.encode(lookupKey)); } if (uriListBuilder.length() == 0) { return; } final Uri uri = Uri.withAppendedPath( ContactsContract.Contacts.CONTENT_MULTI_VCARD_URI, Uri.encode(uriListBuilder.toString())); final Intent intent = new Intent(Intent.ACTION_SEND); intent.setType(ContactsContract.Contacts.CONTENT_VCARD_TYPE); intent.putExtra(Intent.EXTRA_STREAM, uri); try { startActivityForResult(Intent.createChooser(intent, getResources().getQuantityString( R.plurals.title_share_via,/* quantity */ getSelectedContactIds().size())) , ACTIVITY_REQUEST_CODE_SHARE); } catch (final ActivityNotFoundException ex) { Toast.makeText(getContext(), R.string.share_error, Toast.LENGTH_SHORT).show(); } } private void joinSelectedContacts() { final Context context = getContext(); final Intent intent = ContactSaveService.createJoinSeveralContactsIntent( context, getSelectedContactIdsArray()); context.startService(intent); mActionBarAdapter.setSelectionMode(false); } private void deleteSelectedContacts() { final ContactMultiDeletionInteraction multiDeletionInteraction = ContactMultiDeletionInteraction.start(this, getSelectedContactIds()); multiDeletionInteraction.setListener(new MultiDeleteListener()); mIsDeletionInProgress = true; } private final class MultiDeleteListener implements MultiContactDeleteListener { @Override public void onDeletionFinished() { // The parameters count and numSelected are both the number of contacts before deletion. Logger.logListEvent(ListEvent.ActionType.DELETE, /* listType */ getListTypeIncludingSearch(), /* count */ getAdapter().getCount(), /* clickedIndex */ -1, /* numSelected */ getSelectedContactIds().size()); mActionBarAdapter.setSelectionMode(false); mIsDeletionInProgress = false; } } private int getListTypeIncludingSearch() { return isSearchMode() ? ListEvent.ListType.SEARCH_RESULT : getListType(); } public void setParameters(ContactsRequest contactsRequest, boolean fromOnNewIntent) { mContactsRequest = contactsRequest; mFromOnNewIntent = fromOnNewIntent; } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { // TODO: Using the new startActivityWithResultFromFragment API this should not be needed // anymore case ContactEntryListFragment.ACTIVITY_REQUEST_CODE_PICKER: if (resultCode == Activity.RESULT_OK) { onPickerResult(data); } case ACTIVITY_REQUEST_CODE_SHARE: Logger.logListEvent(ListEvent.ActionType.SHARE, /* listType */ getListTypeIncludingSearch(), /* count */ getAdapter().getCount(), /* clickedIndex */ -1, /* numSelected */ getAdapter().getSelectedContactIds().size()); // TODO fix or remove multipicker code: ag/54762 // else if (resultCode == RESULT_CANCELED && mMode == MODE_PICK_MULTIPLE_PHONES) { // // Finish the activity if the sub activity was canceled as back key is used // // to confirm user selection in MODE_PICK_MULTIPLE_PHONES. // finish(); // } // break; } } public boolean getOptionsMenuContactsAvailable() { return mOptionsMenuContactsAvailable; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // Clear the listener to make sure we don't get callbacks after onSaveInstanceState, // in order to avoid doing fragment transactions after it. // TODO Figure out a better way to deal with the issue (ag/120686). if (mActionBarAdapter != null) { mActionBarAdapter.setListener(null); mActionBarAdapter.onSaveInstanceState(outState); } mDisableOptionItemSelected = true; outState.putBoolean(KEY_DELETION_IN_PROGRESS, mIsDeletionInProgress); outState.putBoolean(KEY_SEARCH_RESULT_CLICKED, mSearchResultClicked); } @Override public void onPause() { mOptionsMenuContactsAvailable = false; super.onPause(); } @Override public void onDestroy() { if (mActionBarAdapter != null) { mActionBarAdapter.setListener(null); } super.onDestroy(); } public boolean onKeyDown(int unicodeChar) { if (mActionBarAdapter != null && mActionBarAdapter.isSelectionMode()) { // Ignore keyboard input when in selection mode. return true; } if (mActionBarAdapter != null && !mActionBarAdapter.isSearchMode()) { final String query = new String(new int[]{unicodeChar}, 0, 1); mActionBarAdapter.setSearchMode(true); mActionBarAdapter.setQueryString(query); return true; } return false; } public boolean canSetActionBar() { return mCanSetActionBar; } }