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