blob: 5e7f9e8458f903dddc3409b329d1760a2080958a [file] [log] [blame]
Brian Attwell20510ec2015-02-27 16:10:45 -08001/*
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
Gary Mai967cffd2016-08-01 16:54:25 -070019import android.content.Context;
20import android.database.Cursor;
21import android.graphics.drawable.Drawable;
22import android.os.Bundle;
23import android.provider.ContactsContract;
Aravind Sreekumar71212852018-04-06 15:47:45 -070024import androidx.core.view.ViewCompat;
Gary Mai967cffd2016-08-01 16:54:25 -070025import android.util.Log;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.view.ViewGroup;
29import android.view.accessibility.AccessibilityEvent;
30import android.view.animation.Animation;
31import android.view.animation.AnimationUtils;
32import android.widget.AbsListView;
33import android.widget.ImageView;
34import android.widget.TextView;
35
Wenyi Wang1114fde2016-07-11 21:44:02 -070036import com.android.contacts.R;
Wenyi Wang79675452016-08-17 10:43:28 -070037import com.android.contacts.activities.ActionBarAdapter;
Gary Mai0a49afa2016-12-05 15:53:58 -080038import com.android.contacts.group.GroupMembersFragment;
Gary Mai69c182a2016-12-05 13:07:03 -080039import com.android.contacts.list.MultiSelectEntryContactListAdapter.SelectedContactsListener;
40import com.android.contacts.logging.ListEvent.ActionType;
41import com.android.contacts.logging.Logger;
42import com.android.contacts.logging.SearchState;
43import com.android.contacts.model.AccountTypeManager;
44import com.android.contacts.model.account.AccountType;
45import com.android.contacts.model.account.AccountWithDataSet;
46import com.android.contacts.model.account.GoogleAccountType;
Brian Attwell20510ec2015-02-27 16:10:45 -080047
Walter Janga84fe612016-01-13 16:49:04 -080048import java.util.ArrayList;
49import java.util.List;
Brian Attwell20510ec2015-02-27 16:10:45 -080050import java.util.TreeSet;
51
52/**
53 * Fragment containing a contact list used for browsing contacts and optionally selecting
54 * multiple contacts via checkboxes.
55 */
Walter Jange9ea4f02016-05-10 09:39:46 -070056public abstract class MultiSelectContactsListFragment<T extends MultiSelectEntryContactListAdapter>
57 extends ContactEntryListFragment<T>
Brian Attwell20510ec2015-02-27 16:10:45 -080058 implements SelectedContactsListener {
59
Gary Mai967cffd2016-08-01 16:54:25 -070060 protected boolean mAnimateOnLoad;
Walter Jang9c1fa5d2016-05-17 09:04:40 -070061 private static final String TAG = "MultiContactsList";
62
Brian Attwell20510ec2015-02-27 16:10:45 -080063 public interface OnCheckBoxListActionListener {
64 void onStartDisplayingCheckBoxes();
65 void onSelectedContactIdsChanged();
Brian Attwell8f8937f2015-03-05 14:07:43 -080066 void onStopDisplayingCheckBoxes();
Brian Attwell20510ec2015-02-27 16:10:45 -080067 }
68
69 private static final String EXTRA_KEY_SELECTED_CONTACTS = "selected_contacts";
70
71 private OnCheckBoxListActionListener mCheckBoxListListener;
72
73 public void setCheckBoxListListener(OnCheckBoxListActionListener checkBoxListListener) {
74 mCheckBoxListListener = checkBoxListListener;
75 }
76
Gary Mai967cffd2016-08-01 16:54:25 -070077 public void setAnimateOnLoad(boolean shouldAnimate) {
78 mAnimateOnLoad = shouldAnimate;
79 }
80
Brian Attwell20510ec2015-02-27 16:10:45 -080081 @Override
82 public void onSelectedContactsChanged() {
Walter Jang9c1fa5d2016-05-17 09:04:40 -070083 if (mCheckBoxListListener != null) mCheckBoxListListener.onSelectedContactIdsChanged();
Brian Attwell20510ec2015-02-27 16:10:45 -080084 }
85
86 @Override
Gary Mai967cffd2016-08-01 16:54:25 -070087 public View onCreateView(LayoutInflater inflater, ViewGroup container,
88 Bundle savedInstanceState) {
89 super.onCreateView(inflater, container, savedInstanceState);
90 if (savedInstanceState == null && mAnimateOnLoad) {
91 setLayoutAnimation(getListView(), R.anim.slide_and_fade_in_layout_animation);
92 }
93 return getView();
94 }
95
96 @Override
Brian Attwell20510ec2015-02-27 16:10:45 -080097 public void onActivityCreated(Bundle savedInstanceState) {
98 super.onActivityCreated(savedInstanceState);
99 if (savedInstanceState != null) {
100 final TreeSet<Long> selectedContactIds = (TreeSet<Long>)
101 savedInstanceState.getSerializable(EXTRA_KEY_SELECTED_CONTACTS);
102 getAdapter().setSelectedContactIds(selectedContactIds);
Brian Attwell20510ec2015-02-27 16:10:45 -0800103 }
104 }
105
Wenyi Wang6927bf32016-08-15 18:31:24 -0700106 @Override
107 public void onStart() {
108 super.onStart();
109 if (mCheckBoxListListener != null) {
110 mCheckBoxListListener.onSelectedContactIdsChanged();
111 }
112 }
113
Brian Attwell20510ec2015-02-27 16:10:45 -0800114 public TreeSet<Long> getSelectedContactIds() {
Walter Jang9c1fa5d2016-05-17 09:04:40 -0700115 return getAdapter().getSelectedContactIds();
116 }
117
118 public long[] getSelectedContactIdsArray() {
119 return getAdapter().getSelectedContactIdsArray();
Brian Attwell20510ec2015-02-27 16:10:45 -0800120 }
121
122 @Override
Brian Attwell20510ec2015-02-27 16:10:45 -0800123 protected void configureAdapter() {
124 super.configureAdapter();
125 getAdapter().setSelectedContactsListener(this);
126 }
127
128 @Override
129 public void onSaveInstanceState(Bundle outState) {
130 super.onSaveInstanceState(outState);
131 outState.putSerializable(EXTRA_KEY_SELECTED_CONTACTS, getSelectedContactIds());
132 }
133
134 public void displayCheckBoxes(boolean displayCheckBoxes) {
Walter Jang9c1fa5d2016-05-17 09:04:40 -0700135 if (getAdapter() != null) {
136 getAdapter().setDisplayCheckBoxes(displayCheckBoxes);
137 if (!displayCheckBoxes) {
138 clearCheckBoxes();
139 }
Brian Attwell20510ec2015-02-27 16:10:45 -0800140 }
141 }
Brian Attwelld2962a32015-03-02 14:48:50 -0800142
143 public void clearCheckBoxes() {
144 getAdapter().setSelectedContactIds(new TreeSet<Long>());
145 }
146
Brian Attwell20510ec2015-02-27 16:10:45 -0800147 @Override
148 protected boolean onItemLongClick(int position, long id) {
Brian Attwell8f8937f2015-03-05 14:07:43 -0800149 final int previouslySelectedCount = getAdapter().getSelectedContactIds().size();
Walter Jang9c1fa5d2016-05-17 09:04:40 -0700150 final long contactId = getContactId(position);
Tingting Wang80dfab32015-07-29 14:42:40 -0700151 final int partition = getAdapter().getPartitionForPosition(position);
Walter Jang9c1fa5d2016-05-17 09:04:40 -0700152 if (contactId >= 0 && partition == ContactsContract.Directory.DEFAULT) {
153 if (mCheckBoxListListener != null) {
154 mCheckBoxListListener.onStartDisplayingCheckBoxes();
155 }
156 getAdapter().toggleSelectionOfContactId(contactId);
Wenyi Wang2b943992016-05-20 17:21:35 -0700157 Logger.logListEvent(ActionType.SELECT, getListType(),
158 /* count */ getAdapter().getCount(), /* clickedIndex */ position,
159 /* numSelected */ 1);
Walter Jang9c1fa5d2016-05-17 09:04:40 -0700160 // Manually send clicked event if there is a checkbox.
Wenyi Wang2b943992016-05-20 17:21:35 -0700161 // See b/24098561. TalkBack will not read it otherwise.
Walter Jang9c1fa5d2016-05-17 09:04:40 -0700162 final int index = position + getListView().getHeaderViewsCount() - getListView()
163 .getFirstVisiblePosition();
164 if (index >= 0 && index < getListView().getChildCount()) {
165 getListView().getChildAt(index).sendAccessibilityEvent(AccessibilityEvent
166 .TYPE_VIEW_CLICKED);
Brian Attwellc00112f2015-03-02 18:32:35 -0800167 }
Brian Attwell20510ec2015-02-27 16:10:45 -0800168 }
Brian Attwell8f8937f2015-03-05 14:07:43 -0800169 final int nowSelectedCount = getAdapter().getSelectedContactIds().size();
170 if (mCheckBoxListListener != null
171 && previouslySelectedCount != 0 && nowSelectedCount == 0) {
172 // Last checkbox has been unchecked. So we should stop displaying checkboxes.
173 mCheckBoxListListener.onStopDisplayingCheckBoxes();
174 }
Brian Attwell20510ec2015-02-27 16:10:45 -0800175 return true;
176 }
177
178 @Override
179 protected void onItemClick(int position, long id) {
Walter Jang9c1fa5d2016-05-17 09:04:40 -0700180 final long contactId = getContactId(position);
181 if (contactId < 0) {
Brian Attwell20510ec2015-02-27 16:10:45 -0800182 return;
183 }
184 if (getAdapter().isDisplayingCheckBoxes()) {
Walter Jang9c1fa5d2016-05-17 09:04:40 -0700185 getAdapter().toggleSelectionOfContactId(contactId);
Brian Attwell20510ec2015-02-27 16:10:45 -0800186 }
Brian Attwell8f8937f2015-03-05 14:07:43 -0800187 if (mCheckBoxListListener != null && getAdapter().getSelectedContactIds().size() == 0) {
188 mCheckBoxListListener.onStopDisplayingCheckBoxes();
189 }
Brian Attwell20510ec2015-02-27 16:10:45 -0800190 }
191
Walter Jang9c1fa5d2016-05-17 09:04:40 -0700192 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
Walter Janga84fe612016-01-13 16:49:04 -0800206 /**
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 }
Wenyi Wang1114fde2016-07-11 21:44:02 -0700272
Gary Mai967cffd2016-08-01 16:54:25 -0700273 protected void setLayoutAnimation(final ViewGroup view, int animationId) {
274 if (view == null) {
275 return;
276 }
277 view.setLayoutAnimationListener(new Animation.AnimationListener() {
278 @Override
279 public void onAnimationStart(Animation animation) {
280 }
281
282 @Override
283 public void onAnimationEnd(Animation animation) {
284 view.setLayoutAnimation(null);
285 }
286
287 @Override
288 public void onAnimationRepeat(Animation animation) {
289 }
290 });
291 view.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(getActivity(), animationId));
292 }
293
Wenyi Wang1114fde2016-07-11 21:44:02 -0700294 @Override
295 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
296 int totalItemCount) {
297 final View accountFilterContainer = getView().findViewById(
298 R.id.account_filter_header_container);
299 if (accountFilterContainer == null) {
300 return;
301 }
Wenyi Wang141b8372016-08-03 11:13:10 -0700302
303 int firstCompletelyVisibleItem = firstVisibleItem;
304 if (view != null && view.getChildAt(0) != null && view.getChildAt(0).getTop() < 0) {
305 firstCompletelyVisibleItem++;
306 }
307
308 if (firstCompletelyVisibleItem == 0) {
Wenyi Wang81067f52016-07-26 18:18:11 -0700309 ViewCompat.setElevation(accountFilterContainer, 0);
Wenyi Wang1114fde2016-07-11 21:44:02 -0700310 } else {
Wenyi Wang81067f52016-07-26 18:18:11 -0700311 ViewCompat.setElevation(accountFilterContainer,
312 getResources().getDimension(R.dimen.contact_list_header_elevation));
Wenyi Wang1114fde2016-07-11 21:44:02 -0700313 }
314 }
315
Walter Jang92942632016-07-14 19:49:32 +0000316 protected void bindListHeaderCustom(View listView, View accountFilterContainer) {
317 bindListHeaderCommon(listView, accountFilterContainer);
318
319 final TextView accountFilterHeader = (TextView) accountFilterContainer.findViewById(
320 R.id.account_filter_header);
321 accountFilterHeader.setText(R.string.listCustomView);
322 accountFilterHeader.setAllCaps(false);
323
324 final ImageView accountFilterHeaderIcon = (ImageView) accountFilterContainer
325 .findViewById(R.id.account_filter_icon);
Wenyi Wang20db9852016-07-18 18:41:41 -0700326 accountFilterHeaderIcon.setVisibility(View.GONE);
Walter Jang92942632016-07-14 19:49:32 +0000327 }
328
Wenyi Wang1114fde2016-07-11 21:44:02 -0700329 /**
330 * Show account icon, count of contacts and account name in the header of the list.
331 */
332 protected void bindListHeader(Context context, View listView, View accountFilterContainer,
333 AccountWithDataSet accountWithDataSet, int memberCount) {
334 if (memberCount < 0) {
335 hideHeaderAndAddPadding(context, listView, accountFilterContainer);
336 return;
337 }
338
Walter Jang92942632016-07-14 19:49:32 +0000339 bindListHeaderCommon(listView, accountFilterContainer);
Wenyi Wang1114fde2016-07-11 21:44:02 -0700340
Wenyi Wang96fb8b52016-11-07 14:12:11 -0800341 final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(context);
342 final AccountType accountType = accountTypeManager.getAccountType(
343 accountWithDataSet.type, accountWithDataSet.dataSet);
344
345 // Set text of count of contacts and account name
Wenyi Wang1114fde2016-07-11 21:44:02 -0700346 final TextView accountFilterHeader = (TextView) accountFilterContainer.findViewById(
347 R.id.account_filter_header);
Wenyi Wang96fb8b52016-11-07 14:12:11 -0800348 final String headerText = shouldShowAccountName(accountType)
Wenyi Wang3d8803e2016-07-18 14:29:37 -0700349 ? String.format(context.getResources().getQuantityString(
350 R.plurals.contacts_count_with_account, memberCount),
351 memberCount, accountWithDataSet.name)
352 : context.getResources().getQuantityString(
353 R.plurals.contacts_count, memberCount, memberCount);
354 accountFilterHeader.setText(headerText);
Wenyi Wang1114fde2016-07-11 21:44:02 -0700355 accountFilterHeader.setAllCaps(false);
356
357 // Set icon of the account
Wenyi Wang1114fde2016-07-11 21:44:02 -0700358 final Drawable icon = accountType != null ? accountType.getDisplayIcon(context) : null;
359 final ImageView accountFilterHeaderIcon = (ImageView) accountFilterContainer
360 .findViewById(R.id.account_filter_icon);
Wenyi Wang81067f52016-07-26 18:18:11 -0700361
362 // If it's a writable Google account, we set icon size as 24dp; otherwise, we set it as
363 // 20dp. And we need to change margin accordingly. This is because the Google icon looks
364 // smaller when the icons are of the same size.
365 if (accountType instanceof GoogleAccountType) {
366 accountFilterHeaderIcon.getLayoutParams().height = getResources()
367 .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size);
Wenyi Wang6c46e5b2016-11-17 10:57:42 -0800368 accountFilterHeaderIcon.getLayoutParams().width = getResources()
369 .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size);
Wenyi Wang81067f52016-07-26 18:18:11 -0700370
371 setMargins(accountFilterHeaderIcon,
372 getResources().getDimensionPixelOffset(
373 R.dimen.contact_browser_list_header_icon_left_margin),
374 getResources().getDimensionPixelOffset(
375 R.dimen.contact_browser_list_header_icon_right_margin));
376 } else {
377 accountFilterHeaderIcon.getLayoutParams().height = getResources()
378 .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size_alt);
Wenyi Wang6c46e5b2016-11-17 10:57:42 -0800379 accountFilterHeaderIcon.getLayoutParams().width = getResources()
380 .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size_alt);
Wenyi Wang81067f52016-07-26 18:18:11 -0700381
382 setMargins(accountFilterHeaderIcon,
383 getResources().getDimensionPixelOffset(
384 R.dimen.contact_browser_list_header_icon_left_margin_alt),
385 getResources().getDimensionPixelOffset(
386 R.dimen.contact_browser_list_header_icon_right_margin_alt));
387 }
388 accountFilterHeaderIcon.requestLayout();
389
Walter Jang92942632016-07-14 19:49:32 +0000390 accountFilterHeaderIcon.setVisibility(View.VISIBLE);
Wenyi Wang1114fde2016-07-11 21:44:02 -0700391 accountFilterHeaderIcon.setImageDrawable(icon);
392 }
393
Wenyi Wang96fb8b52016-11-07 14:12:11 -0800394 private boolean shouldShowAccountName(AccountType accountType) {
395 return (accountType.isGroupMembershipEditable() && this instanceof GroupMembersFragment)
396 || GoogleAccountType.ACCOUNT_TYPE.equals(accountType.accountType);
397 }
398
Wenyi Wang81067f52016-07-26 18:18:11 -0700399 private void setMargins(View v, int l, int r) {
400 if (v.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
401 ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
402 p.setMarginStart(l);
403 p.setMarginEnd(r);
404 v.setLayoutParams(p);
405 v.requestLayout();
406 }
407 }
408
Walter Jang92942632016-07-14 19:49:32 +0000409 private void bindListHeaderCommon(View listView, View accountFilterContainer) {
410 // Show header and remove top padding of the list
411 accountFilterContainer.setVisibility(View.VISIBLE);
Wenyi Wang7cd9af32016-07-17 16:00:43 -0700412 setListViewPaddingTop(listView, /* paddingTop */ 0);
Walter Jang92942632016-07-14 19:49:32 +0000413 }
414
Wenyi Wang1114fde2016-07-11 21:44:02 -0700415 /**
416 * Hide header of list view and add padding to the top of list view.
417 */
418 protected void hideHeaderAndAddPadding(Context context, View listView,
419 View accountFilterContainer) {
420 accountFilterContainer.setVisibility(View.GONE);
Wenyi Wang7cd9af32016-07-17 16:00:43 -0700421 setListViewPaddingTop(listView,
422 /* paddingTop */ context.getResources().getDimensionPixelSize(
423 R.dimen.contact_browser_list_item_padding_top_or_bottom));
424 }
425
426 private void setListViewPaddingTop(View listView, int paddingTop) {
427 listView.setPadding(listView.getPaddingLeft(), paddingTop, listView.getPaddingRight(),
428 listView.getPaddingBottom());
Wenyi Wang1114fde2016-07-11 21:44:02 -0700429 }
430
Wenyi Wang79675452016-08-17 10:43:28 -0700431 /**
432 * Returns the {@link ActionBarAdapter} object associated with list fragment.
433 */
434 public ActionBarAdapter getActionBarAdapter() {
435 return null;
436 }
Brian Attwell20510ec2015-02-27 16:10:45 -0800437}