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