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 android.content.Context; 20 import android.database.Cursor; 21 import android.graphics.drawable.Drawable; 22 import android.icu.text.MessageFormat; 23 import android.os.Bundle; 24 import android.provider.ContactsContract; 25 import androidx.core.view.ViewCompat; 26 import android.util.Log; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.accessibility.AccessibilityEvent; 31 import android.view.animation.Animation; 32 import android.view.animation.AnimationUtils; 33 import android.widget.AbsListView; 34 import android.widget.ImageView; 35 import android.widget.TextView; 36 37 import com.android.contacts.R; 38 import com.android.contacts.activities.ActionBarAdapter; 39 import com.android.contacts.group.GroupMembersFragment; 40 import com.android.contacts.list.MultiSelectEntryContactListAdapter.SelectedContactsListener; 41 import com.android.contacts.logging.ListEvent.ActionType; 42 import com.android.contacts.logging.Logger; 43 import com.android.contacts.logging.SearchState; 44 import com.android.contacts.model.AccountTypeManager; 45 import com.android.contacts.model.account.AccountType; 46 import com.android.contacts.model.account.AccountWithDataSet; 47 import com.android.contacts.model.account.GoogleAccountType; 48 49 import java.util.ArrayList; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Locale; 53 import java.util.Map; 54 import java.util.TreeSet; 55 56 /** 57 * Fragment containing a contact list used for browsing contacts and optionally selecting 58 * multiple contacts via checkboxes. 59 */ 60 public abstract class MultiSelectContactsListFragment<T extends MultiSelectEntryContactListAdapter> 61 extends ContactEntryListFragment<T> 62 implements SelectedContactsListener { 63 64 protected boolean mAnimateOnLoad; 65 private static final String TAG = "MultiContactsList"; 66 67 public interface OnCheckBoxListActionListener { onStartDisplayingCheckBoxes()68 void onStartDisplayingCheckBoxes(); onSelectedContactIdsChanged()69 void onSelectedContactIdsChanged(); onStopDisplayingCheckBoxes()70 void onStopDisplayingCheckBoxes(); 71 } 72 73 private static final String EXTRA_KEY_SELECTED_CONTACTS = "selected_contacts"; 74 75 private OnCheckBoxListActionListener mCheckBoxListListener; 76 setCheckBoxListListener(OnCheckBoxListActionListener checkBoxListListener)77 public void setCheckBoxListListener(OnCheckBoxListActionListener checkBoxListListener) { 78 mCheckBoxListListener = checkBoxListListener; 79 } 80 setAnimateOnLoad(boolean shouldAnimate)81 public void setAnimateOnLoad(boolean shouldAnimate) { 82 mAnimateOnLoad = shouldAnimate; 83 } 84 85 @Override onSelectedContactsChanged()86 public void onSelectedContactsChanged() { 87 if (mCheckBoxListListener != null) mCheckBoxListListener.onSelectedContactIdsChanged(); 88 } 89 90 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)91 public View onCreateView(LayoutInflater inflater, ViewGroup container, 92 Bundle savedInstanceState) { 93 super.onCreateView(inflater, container, savedInstanceState); 94 if (savedInstanceState == null && mAnimateOnLoad) { 95 setLayoutAnimation(getListView(), R.anim.slide_and_fade_in_layout_animation); 96 } 97 return getView(); 98 } 99 100 @Override onActivityCreated(Bundle savedInstanceState)101 public void onActivityCreated(Bundle savedInstanceState) { 102 super.onActivityCreated(savedInstanceState); 103 if (savedInstanceState != null) { 104 final TreeSet<Long> selectedContactIds = (TreeSet<Long>) 105 savedInstanceState.getSerializable(EXTRA_KEY_SELECTED_CONTACTS); 106 getAdapter().setSelectedContactIds(selectedContactIds); 107 } 108 } 109 110 @Override onStart()111 public void onStart() { 112 super.onStart(); 113 if (mCheckBoxListListener != null) { 114 mCheckBoxListListener.onSelectedContactIdsChanged(); 115 } 116 } 117 getSelectedContactIds()118 public TreeSet<Long> getSelectedContactIds() { 119 return getAdapter().getSelectedContactIds(); 120 } 121 getSelectedContactIdsArray()122 public long[] getSelectedContactIdsArray() { 123 return getAdapter().getSelectedContactIdsArray(); 124 } 125 126 @Override configureAdapter()127 protected void configureAdapter() { 128 super.configureAdapter(); 129 getAdapter().setSelectedContactsListener(this); 130 } 131 132 @Override onSaveInstanceState(Bundle outState)133 public void onSaveInstanceState(Bundle outState) { 134 super.onSaveInstanceState(outState); 135 outState.putSerializable(EXTRA_KEY_SELECTED_CONTACTS, getSelectedContactIds()); 136 } 137 displayCheckBoxes(boolean displayCheckBoxes)138 public void displayCheckBoxes(boolean displayCheckBoxes) { 139 if (getAdapter() != null) { 140 getAdapter().setDisplayCheckBoxes(displayCheckBoxes); 141 if (!displayCheckBoxes) { 142 clearCheckBoxes(); 143 } 144 } 145 } 146 clearCheckBoxes()147 public void clearCheckBoxes() { 148 getAdapter().setSelectedContactIds(new TreeSet<Long>()); 149 } 150 151 @Override onItemLongClick(int position, long id)152 protected boolean onItemLongClick(int position, long id) { 153 final int previouslySelectedCount = getAdapter().getSelectedContactIds().size(); 154 final long contactId = getContactId(position); 155 final int partition = getAdapter().getPartitionForPosition(position); 156 if (contactId >= 0 && partition == ContactsContract.Directory.DEFAULT) { 157 if (mCheckBoxListListener != null) { 158 mCheckBoxListListener.onStartDisplayingCheckBoxes(); 159 } 160 getAdapter().toggleSelectionOfContactId(contactId); 161 Logger.logListEvent(ActionType.SELECT, getListType(), 162 /* count */ getAdapter().getCount(), /* clickedIndex */ position, 163 /* numSelected */ 1); 164 // Manually send clicked event if there is a checkbox. 165 // See b/24098561. TalkBack will not read it otherwise. 166 final int index = position + getListView().getHeaderViewsCount() - getListView() 167 .getFirstVisiblePosition(); 168 if (index >= 0 && index < getListView().getChildCount()) { 169 getListView().getChildAt(index).sendAccessibilityEvent(AccessibilityEvent 170 .TYPE_VIEW_CLICKED); 171 } 172 } 173 final int nowSelectedCount = getAdapter().getSelectedContactIds().size(); 174 if (mCheckBoxListListener != null 175 && previouslySelectedCount != 0 && nowSelectedCount == 0) { 176 // Last checkbox has been unchecked. So we should stop displaying checkboxes. 177 mCheckBoxListListener.onStopDisplayingCheckBoxes(); 178 } 179 return true; 180 } 181 182 @Override onItemClick(int position, long id)183 protected void onItemClick(int position, long id) { 184 final long contactId = getContactId(position); 185 if (contactId < 0) { 186 return; 187 } 188 if (getAdapter().isDisplayingCheckBoxes()) { 189 getAdapter().toggleSelectionOfContactId(contactId); 190 } 191 if (mCheckBoxListListener != null && getAdapter().getSelectedContactIds().size() == 0) { 192 mCheckBoxListListener.onStopDisplayingCheckBoxes(); 193 } 194 } 195 getContactId(int position)196 private long getContactId(int position) { 197 final int contactIdColumnIndex = getAdapter().getContactColumnIdIndex(); 198 199 final Cursor cursor = (Cursor) getAdapter().getItem(position); 200 if (cursor != null) { 201 if (cursor.getColumnCount() > contactIdColumnIndex) { 202 return cursor.getLong(contactIdColumnIndex); 203 } 204 } 205 206 Log.w(TAG, "Failed to get contact ID from cursor column " + contactIdColumnIndex); 207 return -1; 208 } 209 210 /** 211 * Returns the state of the search results currently presented to the user. 212 */ createSearchState()213 public SearchState createSearchState() { 214 return createSearchState(/* selectedPosition */ -1); 215 } 216 217 /** 218 * Returns the state of the search results presented to the user 219 * at the time the result in the given position was clicked. 220 */ createSearchStateForSearchResultClick(int selectedPosition)221 public SearchState createSearchStateForSearchResultClick(int selectedPosition) { 222 return createSearchState(selectedPosition); 223 } 224 createSearchState(int selectedPosition)225 private SearchState createSearchState(int selectedPosition) { 226 final MultiSelectEntryContactListAdapter adapter = getAdapter(); 227 if (adapter == null) { 228 return null; 229 } 230 final SearchState searchState = new SearchState(); 231 searchState.queryLength = adapter.getQueryString() == null 232 ? 0 : adapter.getQueryString().length(); 233 searchState.numPartitions = adapter.getPartitionCount(); 234 235 // Set the number of results displayed to the user. Note that the adapter.getCount(), 236 // value does not always match the number of results actually displayed to the user, 237 // which is why we calculate it manually. 238 final List<Integer> numResultsInEachPartition = new ArrayList<>(); 239 for (int i = 0; i < adapter.getPartitionCount(); i++) { 240 final Cursor cursor = adapter.getCursor(i); 241 if (cursor == null || cursor.isClosed()) { 242 // Something went wrong, abort. 243 numResultsInEachPartition.clear(); 244 break; 245 } 246 numResultsInEachPartition.add(cursor.getCount()); 247 } 248 if (!numResultsInEachPartition.isEmpty()) { 249 int numResults = 0; 250 for (int i = 0; i < numResultsInEachPartition.size(); i++) { 251 numResults += numResultsInEachPartition.get(i); 252 } 253 searchState.numResults = numResults; 254 } 255 256 // If a selection was made, set additional search state 257 if (selectedPosition >= 0) { 258 searchState.selectedPartition = adapter.getPartitionForPosition(selectedPosition); 259 searchState.selectedIndexInPartition = adapter.getOffsetInPartition(selectedPosition); 260 final Cursor cursor = adapter.getCursor(searchState.selectedPartition); 261 searchState.numResultsInSelectedPartition = 262 cursor == null || cursor.isClosed() ? -1 : cursor.getCount(); 263 264 // Calculate the index across all partitions 265 if (!numResultsInEachPartition.isEmpty()) { 266 int selectedIndex = 0; 267 for (int i = 0; i < searchState.selectedPartition; i++) { 268 selectedIndex += numResultsInEachPartition.get(i); 269 } 270 selectedIndex += searchState.selectedIndexInPartition; 271 searchState.selectedIndex = selectedIndex; 272 } 273 } 274 return searchState; 275 } 276 setLayoutAnimation(final ViewGroup view, int animationId)277 protected void setLayoutAnimation(final ViewGroup view, int animationId) { 278 if (view == null) { 279 return; 280 } 281 view.setLayoutAnimationListener(new Animation.AnimationListener() { 282 @Override 283 public void onAnimationStart(Animation animation) { 284 } 285 286 @Override 287 public void onAnimationEnd(Animation animation) { 288 view.setLayoutAnimation(null); 289 } 290 291 @Override 292 public void onAnimationRepeat(Animation animation) { 293 } 294 }); 295 view.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(getActivity(), animationId)); 296 } 297 298 @Override onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)299 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 300 int totalItemCount) { 301 final View accountFilterContainer = getView().findViewById( 302 R.id.account_filter_header_container); 303 if (accountFilterContainer == null) { 304 return; 305 } 306 307 int firstCompletelyVisibleItem = firstVisibleItem; 308 if (view != null && view.getChildAt(0) != null && view.getChildAt(0).getTop() < 0) { 309 firstCompletelyVisibleItem++; 310 } 311 312 if (firstCompletelyVisibleItem == 0) { 313 ViewCompat.setElevation(accountFilterContainer, 0); 314 } else { 315 ViewCompat.setElevation(accountFilterContainer, 316 getResources().getDimension(R.dimen.contact_list_header_elevation)); 317 } 318 } 319 bindListHeaderCustom(View listView, View accountFilterContainer)320 protected void bindListHeaderCustom(View listView, View accountFilterContainer) { 321 bindListHeaderCommon(listView, accountFilterContainer); 322 323 final TextView accountFilterHeader = (TextView) accountFilterContainer.findViewById( 324 R.id.account_filter_header); 325 accountFilterHeader.setText(R.string.listCustomView); 326 accountFilterHeader.setAllCaps(false); 327 328 final ImageView accountFilterHeaderIcon = (ImageView) accountFilterContainer 329 .findViewById(R.id.account_filter_icon); 330 accountFilterHeaderIcon.setVisibility(View.GONE); 331 } 332 333 /** 334 * Show account icon, count of contacts and account name in the header of the list. 335 */ bindListHeader(Context context, View listView, View accountFilterContainer, AccountWithDataSet accountWithDataSet, int memberCount)336 protected void bindListHeader(Context context, View listView, View accountFilterContainer, 337 AccountWithDataSet accountWithDataSet, int memberCount) { 338 if (memberCount < 0) { 339 hideHeaderAndAddPadding(context, listView, accountFilterContainer); 340 return; 341 } 342 343 bindListHeaderCommon(listView, accountFilterContainer); 344 345 final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(context); 346 final AccountType accountType = accountTypeManager.getAccountType( 347 accountWithDataSet.type, accountWithDataSet.dataSet); 348 349 // Set text of count of contacts and account name 350 final TextView accountFilterHeader = (TextView) accountFilterContainer.findViewById( 351 R.id.account_filter_header); 352 String headerText; 353 Map<String, Object> arguments = new HashMap<>(); 354 arguments.put("count", memberCount); 355 if (shouldShowAccountName(accountType)) { 356 arguments.put("account", accountWithDataSet.name); 357 MessageFormat msgFormat = new MessageFormat( 358 getResources().getString(R.string.contacts_count_with_account), 359 Locale.getDefault()); 360 headerText = msgFormat.format(arguments); 361 } else { 362 MessageFormat msgFormat = new MessageFormat( 363 getResources().getString(R.string.contacts_count), 364 Locale.getDefault()); 365 headerText = msgFormat.format(arguments); 366 } 367 accountFilterHeader.setText(headerText); 368 accountFilterHeader.setAllCaps(false); 369 370 // Set icon of the account 371 final Drawable icon = accountType != null ? accountType.getDisplayIcon(context) : null; 372 final ImageView accountFilterHeaderIcon = (ImageView) accountFilterContainer 373 .findViewById(R.id.account_filter_icon); 374 375 // If it's a writable Google account, we set icon size as 24dp; otherwise, we set it as 376 // 20dp. And we need to change margin accordingly. This is because the Google icon looks 377 // smaller when the icons are of the same size. 378 if (accountType instanceof GoogleAccountType) { 379 accountFilterHeaderIcon.getLayoutParams().height = getResources() 380 .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size); 381 accountFilterHeaderIcon.getLayoutParams().width = getResources() 382 .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size); 383 384 setMargins(accountFilterHeaderIcon, 385 getResources().getDimensionPixelOffset( 386 R.dimen.contact_browser_list_header_icon_left_margin), 387 getResources().getDimensionPixelOffset( 388 R.dimen.contact_browser_list_header_icon_right_margin)); 389 } else { 390 accountFilterHeaderIcon.getLayoutParams().height = getResources() 391 .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size_alt); 392 accountFilterHeaderIcon.getLayoutParams().width = getResources() 393 .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size_alt); 394 395 setMargins(accountFilterHeaderIcon, 396 getResources().getDimensionPixelOffset( 397 R.dimen.contact_browser_list_header_icon_left_margin_alt), 398 getResources().getDimensionPixelOffset( 399 R.dimen.contact_browser_list_header_icon_right_margin_alt)); 400 } 401 accountFilterHeaderIcon.requestLayout(); 402 403 accountFilterHeaderIcon.setVisibility(View.VISIBLE); 404 accountFilterHeaderIcon.setImageDrawable(icon); 405 } 406 shouldShowAccountName(AccountType accountType)407 private boolean shouldShowAccountName(AccountType accountType) { 408 return (accountType.isGroupMembershipEditable() && this instanceof GroupMembersFragment) 409 || GoogleAccountType.ACCOUNT_TYPE.equals(accountType.accountType); 410 } 411 setMargins(View v, int l, int r)412 private void setMargins(View v, int l, int r) { 413 if (v.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) { 414 ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); 415 p.setMarginStart(l); 416 p.setMarginEnd(r); 417 v.setLayoutParams(p); 418 v.requestLayout(); 419 } 420 } 421 bindListHeaderCommon(View listView, View accountFilterContainer)422 private void bindListHeaderCommon(View listView, View accountFilterContainer) { 423 // Show header and remove top padding of the list 424 accountFilterContainer.setVisibility(View.VISIBLE); 425 setListViewPaddingTop(listView, /* paddingTop */ 0); 426 } 427 428 /** 429 * Hide header of list view and add padding to the top of list view. 430 */ hideHeaderAndAddPadding(Context context, View listView, View accountFilterContainer)431 protected void hideHeaderAndAddPadding(Context context, View listView, 432 View accountFilterContainer) { 433 accountFilterContainer.setVisibility(View.GONE); 434 setListViewPaddingTop(listView, 435 /* paddingTop */ context.getResources().getDimensionPixelSize( 436 R.dimen.contact_browser_list_item_padding_top_or_bottom)); 437 } 438 setListViewPaddingTop(View listView, int paddingTop)439 private void setListViewPaddingTop(View listView, int paddingTop) { 440 listView.setPadding(listView.getPaddingLeft(), paddingTop, listView.getPaddingRight(), 441 listView.getPaddingBottom()); 442 } 443 444 /** 445 * Returns the {@link ActionBarAdapter} object associated with list fragment. 446 */ getActionBarAdapter()447 public ActionBarAdapter getActionBarAdapter() { 448 return null; 449 } 450 } 451