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.os.Bundle; 23 import android.provider.ContactsContract; 24 import android.support.v4.view.ViewCompat; 25 import android.util.Log; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.accessibility.AccessibilityEvent; 30 import android.view.animation.Animation; 31 import android.view.animation.AnimationUtils; 32 import android.widget.AbsListView; 33 import android.widget.ImageView; 34 import android.widget.TextView; 35 36 import com.android.contacts.R; 37 import com.android.contacts.activities.ActionBarAdapter; 38 import com.android.contacts.group.GroupMembersFragment; 39 import com.android.contacts.list.MultiSelectEntryContactListAdapter.SelectedContactsListener; 40 import com.android.contacts.logging.ListEvent.ActionType; 41 import com.android.contacts.logging.Logger; 42 import com.android.contacts.logging.SearchState; 43 import com.android.contacts.model.AccountTypeManager; 44 import com.android.contacts.model.account.AccountType; 45 import com.android.contacts.model.account.AccountWithDataSet; 46 import com.android.contacts.model.account.GoogleAccountType; 47 48 import java.util.ArrayList; 49 import java.util.List; 50 import java.util.TreeSet; 51 52 /** 53 * Fragment containing a contact list used for browsing contacts and optionally selecting 54 * multiple contacts via checkboxes. 55 */ 56 public abstract class MultiSelectContactsListFragment<T extends MultiSelectEntryContactListAdapter> 57 extends ContactEntryListFragment<T> 58 implements SelectedContactsListener { 59 60 protected boolean mAnimateOnLoad; 61 private static final String TAG = "MultiContactsList"; 62 63 public interface OnCheckBoxListActionListener { 64 void onStartDisplayingCheckBoxes(); 65 void onSelectedContactIdsChanged(); 66 void onStopDisplayingCheckBoxes(); 67 } 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 77 public void setAnimateOnLoad(boolean shouldAnimate) { 78 mAnimateOnLoad = shouldAnimate; 79 } 80 81 @Override 82 public void onSelectedContactsChanged() { 83 if (mCheckBoxListListener != null) mCheckBoxListListener.onSelectedContactIdsChanged(); 84 } 85 86 @Override 87 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 97 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); 103 } 104 } 105 106 @Override 107 public void onStart() { 108 super.onStart(); 109 if (mCheckBoxListListener != null) { 110 mCheckBoxListListener.onSelectedContactIdsChanged(); 111 } 112 } 113 114 public TreeSet<Long> getSelectedContactIds() { 115 return getAdapter().getSelectedContactIds(); 116 } 117 118 public long[] getSelectedContactIdsArray() { 119 return getAdapter().getSelectedContactIdsArray(); 120 } 121 122 @Override 123 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) { 135 if (getAdapter() != null) { 136 getAdapter().setDisplayCheckBoxes(displayCheckBoxes); 137 if (!displayCheckBoxes) { 138 clearCheckBoxes(); 139 } 140 } 141 } 142 143 public void clearCheckBoxes() { 144 getAdapter().setSelectedContactIds(new TreeSet<Long>()); 145 } 146 147 @Override 148 protected boolean onItemLongClick(int position, long id) { 149 final int previouslySelectedCount = getAdapter().getSelectedContactIds().size(); 150 final long contactId = getContactId(position); 151 final int partition = getAdapter().getPartitionForPosition(position); 152 if (contactId >= 0 && partition == ContactsContract.Directory.DEFAULT) { 153 if (mCheckBoxListListener != null) { 154 mCheckBoxListListener.onStartDisplayingCheckBoxes(); 155 } 156 getAdapter().toggleSelectionOfContactId(contactId); 157 Logger.logListEvent(ActionType.SELECT, getListType(), 158 /* count */ getAdapter().getCount(), /* clickedIndex */ position, 159 /* numSelected */ 1); 160 // Manually send clicked event if there is a checkbox. 161 // See b/24098561. TalkBack will not read it otherwise. 162 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); 167 } 168 } 169 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 } 175 return true; 176 } 177 178 @Override 179 protected void onItemClick(int position, long id) { 180 final long contactId = getContactId(position); 181 if (contactId < 0) { 182 return; 183 } 184 if (getAdapter().isDisplayingCheckBoxes()) { 185 getAdapter().toggleSelectionOfContactId(contactId); 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 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 294 @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 } 302 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) { 309 ViewCompat.setElevation(accountFilterContainer, 0); 310 } else { 311 ViewCompat.setElevation(accountFilterContainer, 312 getResources().getDimension(R.dimen.contact_list_header_elevation)); 313 } 314 } 315 316 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); 326 accountFilterHeaderIcon.setVisibility(View.GONE); 327 } 328 329 /** 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 339 bindListHeaderCommon(listView, accountFilterContainer); 340 341 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 346 final TextView accountFilterHeader = (TextView) accountFilterContainer.findViewById( 347 R.id.account_filter_header); 348 final String headerText = shouldShowAccountName(accountType) 349 ? 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); 355 accountFilterHeader.setAllCaps(false); 356 357 // Set icon of the account 358 final Drawable icon = accountType != null ? accountType.getDisplayIcon(context) : null; 359 final ImageView accountFilterHeaderIcon = (ImageView) accountFilterContainer 360 .findViewById(R.id.account_filter_icon); 361 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); 368 accountFilterHeaderIcon.getLayoutParams().width = getResources() 369 .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size); 370 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); 379 accountFilterHeaderIcon.getLayoutParams().width = getResources() 380 .getDimensionPixelOffset(R.dimen.contact_browser_list_header_icon_size_alt); 381 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 390 accountFilterHeaderIcon.setVisibility(View.VISIBLE); 391 accountFilterHeaderIcon.setImageDrawable(icon); 392 } 393 394 private boolean shouldShowAccountName(AccountType accountType) { 395 return (accountType.isGroupMembershipEditable() && this instanceof GroupMembersFragment) 396 || GoogleAccountType.ACCOUNT_TYPE.equals(accountType.accountType); 397 } 398 399 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 409 private void bindListHeaderCommon(View listView, View accountFilterContainer) { 410 // Show header and remove top padding of the list 411 accountFilterContainer.setVisibility(View.VISIBLE); 412 setListViewPaddingTop(listView, /* paddingTop */ 0); 413 } 414 415 /** 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); 421 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()); 429 } 430 431 /** 432 * Returns the {@link ActionBarAdapter} object associated with list fragment. 433 */ 434 public ActionBarAdapter getActionBarAdapter() { 435 return null; 436 } 437 } 438