1 /* 2 * Copyright (C) 2015 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.list; 18 19 import com.android.contacts.common.list.ContactListAdapter; 20 import com.android.contacts.common.list.ContactListItemView; 21 import com.android.contacts.common.list.DefaultContactListAdapter; 22 import com.android.contacts.common.logging.SearchState; 23 import com.android.contacts.list.MultiSelectEntryContactListAdapter.SelectedContactsListener; 24 import com.android.contacts.common.logging.Logger; 25 26 import android.database.Cursor; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.provider.ContactsContract; 30 import android.text.TextUtils; 31 import android.view.accessibility.AccessibilityEvent; 32 33 import java.util.ArrayList; 34 import java.util.List; 35 import java.util.TreeSet; 36 37 /** 38 * Fragment containing a contact list used for browsing contacts and optionally selecting 39 * multiple contacts via checkboxes. 40 */ 41 public class MultiSelectContactsListFragment extends DefaultContactBrowseListFragment 42 implements SelectedContactsListener { 43 44 public interface OnCheckBoxListActionListener { onStartDisplayingCheckBoxes()45 void onStartDisplayingCheckBoxes(); onSelectedContactIdsChanged()46 void onSelectedContactIdsChanged(); onStopDisplayingCheckBoxes()47 void onStopDisplayingCheckBoxes(); 48 } 49 50 private static final String EXTRA_KEY_SELECTED_CONTACTS = "selected_contacts"; 51 52 private static final String KEY_SEARCH_RESULT_CLICKED = "search_result_clicked"; 53 54 private OnCheckBoxListActionListener mCheckBoxListListener; 55 private boolean mSearchResultClicked; 56 setCheckBoxListListener(OnCheckBoxListActionListener checkBoxListListener)57 public void setCheckBoxListListener(OnCheckBoxListActionListener checkBoxListListener) { 58 mCheckBoxListListener = checkBoxListListener; 59 } 60 61 /** 62 * Whether a search result was clicked by the user. Tracked so that we can distinguish 63 * between exiting the search mode after a result was clicked from existing w/o clicking 64 * any search result. 65 */ wasSearchResultClicked()66 public boolean wasSearchResultClicked() { 67 return mSearchResultClicked; 68 } 69 70 /** 71 * Resets whether a search result was clicked by the user to false. 72 */ resetSearchResultClicked()73 public void resetSearchResultClicked() { 74 mSearchResultClicked = false; 75 } 76 77 @Override onSelectedContactsChanged()78 public void onSelectedContactsChanged() { 79 if (mCheckBoxListListener != null) { 80 mCheckBoxListListener.onSelectedContactIdsChanged(); 81 } 82 } 83 84 @Override onSelectedContactsChangedViaCheckBox()85 public void onSelectedContactsChangedViaCheckBox() { 86 if (getAdapter().getSelectedContactIds().size() == 0) { 87 // Last checkbox has been unchecked. So we should stop displaying checkboxes. 88 mCheckBoxListListener.onStopDisplayingCheckBoxes(); 89 } else { 90 onSelectedContactsChanged(); 91 } 92 } 93 94 @Override onActivityCreated(Bundle savedInstanceState)95 public void onActivityCreated(Bundle savedInstanceState) { 96 super.onActivityCreated(savedInstanceState); 97 if (savedInstanceState != null) { 98 final TreeSet<Long> selectedContactIds = (TreeSet<Long>) 99 savedInstanceState.getSerializable(EXTRA_KEY_SELECTED_CONTACTS); 100 getAdapter().setSelectedContactIds(selectedContactIds); 101 if (mCheckBoxListListener != null) { 102 mCheckBoxListListener.onSelectedContactIdsChanged(); 103 } 104 mSearchResultClicked = savedInstanceState.getBoolean(KEY_SEARCH_RESULT_CLICKED); 105 } 106 } 107 getSelectedContactIds()108 public TreeSet<Long> getSelectedContactIds() { 109 final MultiSelectEntryContactListAdapter adapter = getAdapter(); 110 return adapter.getSelectedContactIds(); 111 } 112 113 @Override getAdapter()114 public MultiSelectEntryContactListAdapter getAdapter() { 115 return (MultiSelectEntryContactListAdapter) super.getAdapter(); 116 } 117 118 @Override configureAdapter()119 protected void configureAdapter() { 120 super.configureAdapter(); 121 getAdapter().setSelectedContactsListener(this); 122 } 123 124 @Override onSaveInstanceState(Bundle outState)125 public void onSaveInstanceState(Bundle outState) { 126 super.onSaveInstanceState(outState); 127 outState.putSerializable(EXTRA_KEY_SELECTED_CONTACTS, getSelectedContactIds()); 128 outState.putBoolean(KEY_SEARCH_RESULT_CLICKED, mSearchResultClicked); 129 } 130 displayCheckBoxes(boolean displayCheckBoxes)131 public void displayCheckBoxes(boolean displayCheckBoxes) { 132 getAdapter().setDisplayCheckBoxes(displayCheckBoxes); 133 if (!displayCheckBoxes) { 134 clearCheckBoxes(); 135 } 136 } 137 clearCheckBoxes()138 public void clearCheckBoxes() { 139 getAdapter().setSelectedContactIds(new TreeSet<Long>()); 140 } 141 142 @Override onItemLongClick(int position, long id)143 protected boolean onItemLongClick(int position, long id) { 144 final int previouslySelectedCount = getAdapter().getSelectedContactIds().size(); 145 final Uri uri = getAdapter().getContactUri(position); 146 final int partition = getAdapter().getPartitionForPosition(position); 147 if (uri != null && (partition == ContactsContract.Directory.DEFAULT 148 && (position > 0 || !getAdapter().hasProfile()))) { 149 final String contactId = uri.getLastPathSegment(); 150 if (!TextUtils.isEmpty(contactId)) { 151 if (mCheckBoxListListener != null) { 152 mCheckBoxListListener.onStartDisplayingCheckBoxes(); 153 } 154 getAdapter().toggleSelectionOfContactId(Long.valueOf(contactId)); 155 // Manually send clicked event if there is a checkbox. 156 // See b/24098561. TalkBack will not read it otherwise. 157 final int index = position + getListView().getHeaderViewsCount() - getListView() 158 .getFirstVisiblePosition(); 159 if (index >= 0 && index < getListView().getChildCount()) { 160 getListView().getChildAt(index).sendAccessibilityEvent(AccessibilityEvent 161 .TYPE_VIEW_CLICKED); 162 } 163 } 164 } 165 final int nowSelectedCount = getAdapter().getSelectedContactIds().size(); 166 if (mCheckBoxListListener != null 167 && previouslySelectedCount != 0 && nowSelectedCount == 0) { 168 // Last checkbox has been unchecked. So we should stop displaying checkboxes. 169 mCheckBoxListListener.onStopDisplayingCheckBoxes(); 170 } 171 return true; 172 } 173 174 @Override onItemClick(int position, long id)175 protected void onItemClick(int position, long id) { 176 final Uri uri = getAdapter().getContactUri(position); 177 if (uri == null) { 178 return; 179 } 180 if (getAdapter().isDisplayingCheckBoxes()) { 181 final String contactId = uri.getLastPathSegment(); 182 if (!TextUtils.isEmpty(contactId)) { 183 getAdapter().toggleSelectionOfContactId(Long.valueOf(contactId)); 184 } 185 } else { 186 if (isSearchMode()) { 187 mSearchResultClicked = true; 188 Logger.logSearchEvent(createSearchStateForSearchResultClick(position)); 189 } 190 super.onItemClick(position, id); 191 } 192 if (mCheckBoxListListener != null && getAdapter().getSelectedContactIds().size() == 0) { 193 mCheckBoxListListener.onStopDisplayingCheckBoxes(); 194 } 195 } 196 197 /** 198 * Returns the state of the search results currently presented to the user. 199 */ createSearchState()200 public SearchState createSearchState() { 201 return createSearchState(/* selectedPosition */ -1); 202 } 203 204 /** 205 * Returns the state of the search results presented to the user 206 * at the time the result in the given position was clicked. 207 */ createSearchStateForSearchResultClick(int selectedPosition)208 public SearchState createSearchStateForSearchResultClick(int selectedPosition) { 209 return createSearchState(selectedPosition); 210 } 211 createSearchState(int selectedPosition)212 private SearchState createSearchState(int selectedPosition) { 213 final MultiSelectEntryContactListAdapter adapter = getAdapter(); 214 if (adapter == null) { 215 return null; 216 } 217 final SearchState searchState = new SearchState(); 218 searchState.queryLength = adapter.getQueryString() == null 219 ? 0 : adapter.getQueryString().length(); 220 searchState.numPartitions = adapter.getPartitionCount(); 221 222 // Set the number of results displayed to the user. Note that the adapter.getCount(), 223 // value does not always match the number of results actually displayed to the user, 224 // which is why we calculate it manually. 225 final List<Integer> numResultsInEachPartition = new ArrayList<>(); 226 for (int i = 0; i < adapter.getPartitionCount(); i++) { 227 final Cursor cursor = adapter.getCursor(i); 228 if (cursor == null || cursor.isClosed()) { 229 // Something went wrong, abort. 230 numResultsInEachPartition.clear(); 231 break; 232 } 233 numResultsInEachPartition.add(cursor.getCount()); 234 } 235 if (!numResultsInEachPartition.isEmpty()) { 236 int numResults = 0; 237 for (int i = 0; i < numResultsInEachPartition.size(); i++) { 238 numResults += numResultsInEachPartition.get(i); 239 } 240 searchState.numResults = numResults; 241 } 242 243 // If a selection was made, set additional search state 244 if (selectedPosition >= 0) { 245 searchState.selectedPartition = adapter.getPartitionForPosition(selectedPosition); 246 searchState.selectedIndexInPartition = adapter.getOffsetInPartition(selectedPosition); 247 final Cursor cursor = adapter.getCursor(searchState.selectedPartition); 248 searchState.numResultsInSelectedPartition = 249 cursor == null || cursor.isClosed() ? -1 : cursor.getCount(); 250 251 // Calculate the index across all partitions 252 if (!numResultsInEachPartition.isEmpty()) { 253 int selectedIndex = 0; 254 for (int i = 0; i < searchState.selectedPartition; i++) { 255 selectedIndex += numResultsInEachPartition.get(i); 256 } 257 selectedIndex += searchState.selectedIndexInPartition; 258 searchState.selectedIndex = selectedIndex; 259 } 260 } 261 return searchState; 262 } 263 264 @Override createListAdapter()265 protected ContactListAdapter createListAdapter() { 266 DefaultContactListAdapter adapter = new MultiSelectEntryContactListAdapter(getContext()); 267 adapter.setSectionHeaderDisplayEnabled(isSectionHeaderDisplayEnabled()); 268 adapter.setDisplayPhotos(true); 269 adapter.setPhotoPosition( 270 ContactListItemView.getDefaultPhotoPosition(/* opposite = */ false)); 271 return adapter; 272 } 273 } 274