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