1 /* 2 * Copyright (C) 2011 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 package com.android.contacts.common.list; 17 18 import android.content.ContentUris; 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.database.Cursor; 22 import android.graphics.drawable.Drawable; 23 import android.net.Uri; 24 import android.provider.ContactsContract.CommonDataKinds.Phone; 25 import android.provider.ContactsContract.Contacts; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.widget.BaseAdapter; 29 import android.widget.FrameLayout; 30 31 import com.android.contacts.common.ContactPhotoManager; 32 import com.android.contacts.common.ContactPresenceIconUtil; 33 import com.android.contacts.common.ContactStatusUtil; 34 import com.android.contacts.common.ContactTileLoaderFactory; 35 import com.android.contacts.common.MoreContactUtils; 36 import com.android.contacts.common.R; 37 38 import java.util.ArrayList; 39 40 /** 41 * Arranges contacts favorites according to provided {@link DisplayType}. 42 * Also allows for a configurable number of columns and {@link DisplayType} 43 */ 44 public class ContactTileAdapter extends BaseAdapter { 45 private static final String TAG = ContactTileAdapter.class.getSimpleName(); 46 47 private DisplayType mDisplayType; 48 private ContactTileView.Listener mListener; 49 private Context mContext; 50 private Resources mResources; 51 protected Cursor mContactCursor = null; 52 private ContactPhotoManager mPhotoManager; 53 protected int mNumFrequents; 54 55 /** 56 * Index of the first NON starred contact in the {@link Cursor} 57 * Only valid when {@link DisplayType#STREQUENT} is true 58 */ 59 private int mDividerPosition; 60 protected int mColumnCount; 61 private int mStarredIndex; 62 63 protected int mIdIndex; 64 protected int mLookupIndex; 65 protected int mPhotoUriIndex; 66 protected int mNameIndex; 67 protected int mPresenceIndex; 68 protected int mStatusIndex; 69 70 /** 71 * Only valid when {@link DisplayType#STREQUENT_PHONE_ONLY} is true 72 */ 73 private int mPhoneNumberIndex; 74 private int mPhoneNumberTypeIndex; 75 private int mPhoneNumberLabelIndex; 76 77 private boolean mIsQuickContactEnabled = false; 78 private final int mPaddingInPixels; 79 80 /** 81 * Configures the adapter to filter and display contacts using different view types. 82 * TODO: Create Uris to support getting Starred_only and Frequent_only cursors. 83 */ 84 public enum DisplayType { 85 /** 86 * Displays a mixed view type of starred and frequent contacts 87 */ 88 STREQUENT, 89 90 /** 91 * Displays a mixed view type of starred and frequent contacts based on phone data. 92 * Also includes secondary touch target. 93 */ 94 STREQUENT_PHONE_ONLY, 95 96 /** 97 * Display only starred contacts 98 */ 99 STARRED_ONLY, 100 101 /** 102 * Display only most frequently contacted 103 */ 104 FREQUENT_ONLY, 105 106 /** 107 * Display all contacts from a group in the cursor 108 * Use {@link com.android.contacts.GroupMemberLoader} 109 * when passing {@link Cursor} into loadFromCusor method. 110 * 111 * Group member logic has been moved into GroupMemberTileAdapter. This constant is still 112 * needed by calling classes. 113 */ 114 GROUP_MEMBERS 115 } 116 117 public ContactTileAdapter(Context context, ContactTileView.Listener listener, int numCols, 118 DisplayType displayType) { 119 mListener = listener; 120 mContext = context; 121 mResources = context.getResources(); 122 mColumnCount = (displayType == DisplayType.FREQUENT_ONLY ? 1 : numCols); 123 mDisplayType = displayType; 124 mNumFrequents = 0; 125 126 // Converting padding in dips to padding in pixels 127 mPaddingInPixels = mContext.getResources() 128 .getDimensionPixelSize(R.dimen.contact_tile_divider_padding); 129 130 bindColumnIndices(); 131 } 132 133 public void setPhotoLoader(ContactPhotoManager photoLoader) { 134 mPhotoManager = photoLoader; 135 } 136 137 public void setColumnCount(int columnCount) { 138 mColumnCount = columnCount; 139 } 140 141 public void setDisplayType(DisplayType displayType) { 142 mDisplayType = displayType; 143 } 144 145 public void enableQuickContact(boolean enableQuickContact) { 146 mIsQuickContactEnabled = enableQuickContact; 147 } 148 149 /** 150 * Sets the column indices for expected {@link Cursor} 151 * based on {@link DisplayType}. 152 */ 153 protected void bindColumnIndices() { 154 mIdIndex = ContactTileLoaderFactory.CONTACT_ID; 155 mLookupIndex = ContactTileLoaderFactory.LOOKUP_KEY; 156 mPhotoUriIndex = ContactTileLoaderFactory.PHOTO_URI; 157 mNameIndex = ContactTileLoaderFactory.DISPLAY_NAME; 158 mStarredIndex = ContactTileLoaderFactory.STARRED; 159 mPresenceIndex = ContactTileLoaderFactory.CONTACT_PRESENCE; 160 mStatusIndex = ContactTileLoaderFactory.CONTACT_STATUS; 161 162 mPhoneNumberIndex = ContactTileLoaderFactory.PHONE_NUMBER; 163 mPhoneNumberTypeIndex = ContactTileLoaderFactory.PHONE_NUMBER_TYPE; 164 mPhoneNumberLabelIndex = ContactTileLoaderFactory.PHONE_NUMBER_LABEL; 165 } 166 167 /** 168 * Gets the number of frequents from the passed in cursor. 169 * 170 * This methods is needed so the GroupMemberTileAdapter can override this. 171 * 172 * @param cursor The cursor to get number of frequents from. 173 */ 174 protected void saveNumFrequentsFromCursor(Cursor cursor) { 175 176 // count the number of frequents 177 switch (mDisplayType) { 178 case STARRED_ONLY: 179 mNumFrequents = 0; 180 break; 181 case STREQUENT: 182 case STREQUENT_PHONE_ONLY: 183 mNumFrequents = cursor.getCount() - mDividerPosition; 184 break; 185 case FREQUENT_ONLY: 186 mNumFrequents = cursor.getCount(); 187 break; 188 default: 189 throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType); 190 } 191 } 192 193 /** 194 * Creates {@link ContactTileView}s for each item in {@link Cursor}. 195 * 196 * Else use {@link ContactTileLoaderFactory} 197 */ 198 public void setContactCursor(Cursor cursor) { 199 mContactCursor = cursor; 200 mDividerPosition = getDividerPosition(cursor); 201 202 saveNumFrequentsFromCursor(cursor); 203 204 // cause a refresh of any views that rely on this data 205 notifyDataSetChanged(); 206 } 207 208 /** 209 * Iterates over the {@link Cursor} 210 * Returns position of the first NON Starred Contact 211 * Returns -1 if {@link DisplayType#STARRED_ONLY} 212 * Returns 0 if {@link DisplayType#FREQUENT_ONLY} 213 */ 214 protected int getDividerPosition(Cursor cursor) { 215 if (cursor == null || cursor.isClosed()) { 216 throw new IllegalStateException("Unable to access cursor"); 217 } 218 219 switch (mDisplayType) { 220 case STREQUENT: 221 case STREQUENT_PHONE_ONLY: 222 cursor.moveToPosition(-1); 223 while (cursor.moveToNext()) { 224 if (cursor.getInt(mStarredIndex) == 0) { 225 return cursor.getPosition(); 226 } 227 } 228 break; 229 case STARRED_ONLY: 230 // There is no divider 231 return -1; 232 case FREQUENT_ONLY: 233 // Divider is first 234 return 0; 235 default: 236 throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType); 237 } 238 239 // There are not NON Starred contacts in cursor 240 // Set divider positon to end 241 return cursor.getCount(); 242 } 243 244 protected ContactEntry createContactEntryFromCursor(Cursor cursor, int position) { 245 // If the loader was canceled we will be given a null cursor. 246 // In that case, show an empty list of contacts. 247 if (cursor == null || cursor.isClosed() || cursor.getCount() <= position) return null; 248 249 cursor.moveToPosition(position); 250 long id = cursor.getLong(mIdIndex); 251 String photoUri = cursor.getString(mPhotoUriIndex); 252 String lookupKey = cursor.getString(mLookupIndex); 253 254 ContactEntry contact = new ContactEntry(); 255 String name = cursor.getString(mNameIndex); 256 contact.name = (name != null) ? name : mResources.getString(R.string.missing_name); 257 contact.status = cursor.getString(mStatusIndex); 258 contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null); 259 contact.lookupKey = ContentUris.withAppendedId( 260 Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id); 261 contact.isFavorite = cursor.getInt(mStarredIndex) > 0; 262 263 // Set phone number and label 264 if (mDisplayType == DisplayType.STREQUENT_PHONE_ONLY) { 265 int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex); 266 String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex); 267 contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType, 268 phoneNumberCustomLabel); 269 contact.phoneNumber = cursor.getString(mPhoneNumberIndex); 270 } else { 271 // Set presence icon and status message 272 Drawable icon = null; 273 int presence = 0; 274 if (!cursor.isNull(mPresenceIndex)) { 275 presence = cursor.getInt(mPresenceIndex); 276 icon = ContactPresenceIconUtil.getPresenceIcon(mContext, presence); 277 } 278 contact.presenceIcon = icon; 279 280 String statusMessage = null; 281 if (mStatusIndex != 0 && !cursor.isNull(mStatusIndex)) { 282 statusMessage = cursor.getString(mStatusIndex); 283 } 284 // If there is no status message from the contact, but there was a presence value, 285 // then use the default status message string 286 if (statusMessage == null && presence != 0) { 287 statusMessage = ContactStatusUtil.getStatusString(mContext, presence); 288 } 289 contact.status = statusMessage; 290 } 291 292 return contact; 293 } 294 295 /** 296 * Returns the number of frequents that will be displayed in the list. 297 */ 298 public int getNumFrequents() { 299 return mNumFrequents; 300 } 301 302 @Override 303 public int getCount() { 304 if (mContactCursor == null || mContactCursor.isClosed()) { 305 return 0; 306 } 307 308 switch (mDisplayType) { 309 case STARRED_ONLY: 310 return getRowCount(mContactCursor.getCount()); 311 case STREQUENT: 312 case STREQUENT_PHONE_ONLY: 313 // Takes numbers of rows the Starred Contacts Occupy 314 int starredRowCount = getRowCount(mDividerPosition); 315 316 // Compute the frequent row count which is 1 plus the number of frequents 317 // (to account for the divider) or 0 if there are no frequents. 318 int frequentRowCount = mNumFrequents == 0 ? 0 : mNumFrequents + 1; 319 320 // Return the number of starred plus frequent rows 321 return starredRowCount + frequentRowCount; 322 case FREQUENT_ONLY: 323 // Number of frequent contacts 324 return mContactCursor.getCount(); 325 default: 326 throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType); 327 } 328 } 329 330 /** 331 * Returns the number of rows required to show the provided number of entries 332 * with the current number of columns. 333 */ 334 protected int getRowCount(int entryCount) { 335 return entryCount == 0 ? 0 : ((entryCount - 1) / mColumnCount) + 1; 336 } 337 338 public int getColumnCount() { 339 return mColumnCount; 340 } 341 342 /** 343 * Returns an ArrayList of the {@link ContactEntry}s that are to appear 344 * on the row for the given position. 345 */ 346 @Override 347 public ArrayList<ContactEntry> getItem(int position) { 348 ArrayList<ContactEntry> resultList = new ArrayList<ContactEntry>(mColumnCount); 349 int contactIndex = position * mColumnCount; 350 351 switch (mDisplayType) { 352 case FREQUENT_ONLY: 353 resultList.add(createContactEntryFromCursor(mContactCursor, position)); 354 break; 355 case STARRED_ONLY: 356 for (int columnCounter = 0; columnCounter < mColumnCount; columnCounter++) { 357 resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); 358 contactIndex++; 359 } 360 break; 361 case STREQUENT: 362 case STREQUENT_PHONE_ONLY: 363 if (position < getRowCount(mDividerPosition)) { 364 for (int columnCounter = 0; columnCounter < mColumnCount && 365 contactIndex != mDividerPosition; columnCounter++) { 366 resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); 367 contactIndex++; 368 } 369 } else { 370 /* 371 * Current position minus how many rows are before the divider and 372 * Minus 1 for the divider itself provides the relative index of the frequent 373 * contact being displayed. Then add the dividerPostion to give the offset 374 * into the contacts cursor to get the absoulte index. 375 */ 376 contactIndex = position - getRowCount(mDividerPosition) - 1 + mDividerPosition; 377 resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); 378 } 379 break; 380 default: 381 throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType); 382 } 383 return resultList; 384 } 385 386 @Override 387 public long getItemId(int position) { 388 // As we show several selectable items for each ListView row, 389 // we can not determine a stable id. But as we don't rely on ListView's selection, 390 // this should not be a problem. 391 return position; 392 } 393 394 @Override 395 public boolean areAllItemsEnabled() { 396 return (mDisplayType != DisplayType.STREQUENT && 397 mDisplayType != DisplayType.STREQUENT_PHONE_ONLY); 398 } 399 400 @Override 401 public boolean isEnabled(int position) { 402 return position != getRowCount(mDividerPosition); 403 } 404 405 @Override 406 public View getView(int position, View convertView, ViewGroup parent) { 407 int itemViewType = getItemViewType(position); 408 409 if (itemViewType == ViewTypes.DIVIDER) { 410 // Checking For Divider First so not to cast convertView 411 return convertView == null ? getDivider() : convertView; 412 } 413 414 ContactTileRow contactTileRowView = (ContactTileRow) convertView; 415 ArrayList<ContactEntry> contactList = getItem(position); 416 417 if (contactTileRowView == null) { 418 // Creating new row if needed 419 contactTileRowView = new ContactTileRow(mContext, itemViewType); 420 } 421 422 contactTileRowView.configureRow(contactList, position == getCount() - 1); 423 return contactTileRowView; 424 } 425 426 /** 427 * Divider uses a list_seperator.xml along with text to denote 428 * the most frequently contacted contacts. 429 */ 430 public View getDivider() { 431 return MoreContactUtils.createHeaderView(mContext, 432 mDisplayType == DisplayType.STREQUENT_PHONE_ONLY ? 433 R.string.favoritesFrequentCalled : R.string.favoritesFrequentContacted); 434 } 435 436 private int getLayoutResourceId(int viewType) { 437 switch (viewType) { 438 case ViewTypes.STARRED: 439 return mIsQuickContactEnabled ? 440 R.layout.contact_tile_starred_quick_contact : R.layout.contact_tile_starred; 441 case ViewTypes.FREQUENT: 442 return mDisplayType == DisplayType.STREQUENT_PHONE_ONLY ? 443 R.layout.contact_tile_frequent_phone : R.layout.contact_tile_frequent; 444 case ViewTypes.STARRED_PHONE: 445 return R.layout.contact_tile_phone_starred; 446 default: 447 throw new IllegalArgumentException("Unrecognized viewType " + viewType); 448 } 449 } 450 @Override 451 public int getViewTypeCount() { 452 return ViewTypes.COUNT; 453 } 454 455 @Override 456 public int getItemViewType(int position) { 457 /* 458 * Returns view type based on {@link DisplayType}. 459 * {@link DisplayType#STARRED_ONLY} and {@link DisplayType#GROUP_MEMBERS} 460 * are {@link ViewTypes#STARRED}. 461 * {@link DisplayType#FREQUENT_ONLY} is {@link ViewTypes#FREQUENT}. 462 * {@link DisplayType#STREQUENT} mixes both {@link ViewTypes} 463 * and also adds in {@link ViewTypes#DIVIDER}. 464 */ 465 switch (mDisplayType) { 466 case STREQUENT: 467 if (position < getRowCount(mDividerPosition)) { 468 return ViewTypes.STARRED; 469 } else if (position == getRowCount(mDividerPosition)) { 470 return ViewTypes.DIVIDER; 471 } else { 472 return ViewTypes.FREQUENT; 473 } 474 case STREQUENT_PHONE_ONLY: 475 if (position < getRowCount(mDividerPosition)) { 476 return ViewTypes.STARRED_PHONE; 477 } else if (position == getRowCount(mDividerPosition)) { 478 return ViewTypes.DIVIDER; 479 } else { 480 return ViewTypes.FREQUENT; 481 } 482 case STARRED_ONLY: 483 return ViewTypes.STARRED; 484 case FREQUENT_ONLY: 485 return ViewTypes.FREQUENT; 486 default: 487 throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType); 488 } 489 } 490 491 /** 492 * Returns the "frequent header" position. Only available when STREQUENT or 493 * STREQUENT_PHONE_ONLY is used for its display type. 494 */ 495 public int getFrequentHeaderPosition() { 496 return getRowCount(mDividerPosition); 497 } 498 499 /** 500 * Acts as a row item composed of {@link ContactTileView} 501 * 502 * TODO: FREQUENT doesn't really need it. Just let {@link #getView} return 503 */ 504 private class ContactTileRow extends FrameLayout { 505 private int mItemViewType; 506 private int mLayoutResId; 507 508 public ContactTileRow(Context context, int itemViewType) { 509 super(context); 510 mItemViewType = itemViewType; 511 mLayoutResId = getLayoutResourceId(mItemViewType); 512 513 // Remove row (but not children) from accessibility node tree. 514 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 515 } 516 517 /** 518 * Configures the row to add {@link ContactEntry}s information to the views 519 */ 520 public void configureRow(ArrayList<ContactEntry> list, boolean isLastRow) { 521 int columnCount = mItemViewType == ViewTypes.FREQUENT ? 1 : mColumnCount; 522 523 // Adding tiles to row and filling in contact information 524 for (int columnCounter = 0; columnCounter < columnCount; columnCounter++) { 525 ContactEntry entry = 526 columnCounter < list.size() ? list.get(columnCounter) : null; 527 addTileFromEntry(entry, columnCounter, isLastRow); 528 } 529 } 530 531 private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) { 532 final ContactTileView contactTile; 533 534 if (getChildCount() <= childIndex) { 535 contactTile = (ContactTileView) inflate(mContext, mLayoutResId, null); 536 // Note: the layoutparam set here is only actually used for FREQUENT. 537 // We override onMeasure() for STARRED and we don't care the layout param there. 538 Resources resources = mContext.getResources(); 539 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( 540 ViewGroup.LayoutParams.WRAP_CONTENT, 541 ViewGroup.LayoutParams.WRAP_CONTENT); 542 params.setMargins( 543 resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 544 0, 545 resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 546 0); 547 contactTile.setLayoutParams(params); 548 contactTile.setPhotoManager(mPhotoManager); 549 contactTile.setListener(mListener); 550 addView(contactTile); 551 } else { 552 contactTile = (ContactTileView) getChildAt(childIndex); 553 } 554 contactTile.loadFromContact(entry); 555 556 switch (mItemViewType) { 557 case ViewTypes.STARRED_PHONE: 558 case ViewTypes.STARRED: 559 // Setting divider visibilities 560 contactTile.setPaddingRelative(0, 0, 561 childIndex >= mColumnCount - 1 ? 0 : mPaddingInPixels, 562 isLastRow ? 0 : mPaddingInPixels); 563 break; 564 case ViewTypes.FREQUENT: 565 contactTile.setHorizontalDividerVisibility( 566 isLastRow ? View.GONE : View.VISIBLE); 567 break; 568 default: 569 break; 570 } 571 } 572 573 @Override 574 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 575 switch (mItemViewType) { 576 case ViewTypes.STARRED_PHONE: 577 case ViewTypes.STARRED: 578 onLayoutForTiles(); 579 return; 580 default: 581 super.onLayout(changed, left, top, right, bottom); 582 return; 583 } 584 } 585 586 private void onLayoutForTiles() { 587 final int count = getChildCount(); 588 589 // Just line up children horizontally. 590 int childLeft = 0; 591 for (int i = 0; i < count; i++) { 592 final View child = getChildAt(i); 593 594 // Note MeasuredWidth includes the padding. 595 final int childWidth = child.getMeasuredWidth(); 596 child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight()); 597 childLeft += childWidth; 598 } 599 } 600 601 @Override 602 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 603 switch (mItemViewType) { 604 case ViewTypes.STARRED_PHONE: 605 case ViewTypes.STARRED: 606 onMeasureForTiles(widthMeasureSpec); 607 return; 608 default: 609 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 610 return; 611 } 612 } 613 614 private void onMeasureForTiles(int widthMeasureSpec) { 615 final int width = MeasureSpec.getSize(widthMeasureSpec); 616 617 final int childCount = getChildCount(); 618 if (childCount == 0) { 619 // Just in case... 620 setMeasuredDimension(width, 0); 621 return; 622 } 623 624 // 1. Calculate image size. 625 // = ([total width] - [total padding]) / [child count] 626 // 627 // 2. Set it to width/height of each children. 628 // If we have a remainder, some tiles will have 1 pixel larger width than its height. 629 // 630 // 3. Set the dimensions of itself. 631 // Let width = given width. 632 // Let height = image size + bottom paddding. 633 634 final int totalPaddingsInPixels = (mColumnCount - 1) * mPaddingInPixels; 635 636 // Preferred width / height for images (excluding the padding). 637 // The actual width may be 1 pixel larger than this if we have a remainder. 638 final int imageSize = (width - totalPaddingsInPixels) / mColumnCount; 639 final int remainder = width - (imageSize * mColumnCount) - totalPaddingsInPixels; 640 641 for (int i = 0; i < childCount; i++) { 642 final View child = getChildAt(i); 643 final int childWidth = imageSize + child.getPaddingRight() 644 // Compensate for the remainder 645 + (i < remainder ? 1 : 0); 646 final int childHeight = imageSize + child.getPaddingBottom(); 647 child.measure( 648 MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), 649 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY) 650 ); 651 } 652 setMeasuredDimension(width, imageSize + getChildAt(0).getPaddingBottom()); 653 } 654 } 655 656 protected static class ViewTypes { 657 public static final int COUNT = 4; 658 public static final int STARRED = 0; 659 public static final int DIVIDER = 1; 660 public static final int FREQUENT = 2; 661 public static final int STARRED_PHONE = 3; 662 } 663 } 664