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 = lookupKey; 260 contact.lookupUri = ContentUris.withAppendedId( 261 Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id); 262 contact.isFavorite = cursor.getInt(mStarredIndex) > 0; 263 264 // Set phone number and label 265 if (mDisplayType == DisplayType.STREQUENT_PHONE_ONLY) { 266 int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex); 267 String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex); 268 contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType, 269 phoneNumberCustomLabel); 270 contact.phoneNumber = cursor.getString(mPhoneNumberIndex); 271 } else { 272 // Set presence icon and status message 273 Drawable icon = null; 274 int presence = 0; 275 if (!cursor.isNull(mPresenceIndex)) { 276 presence = cursor.getInt(mPresenceIndex); 277 icon = ContactPresenceIconUtil.getPresenceIcon(mContext, presence); 278 } 279 contact.presenceIcon = icon; 280 281 String statusMessage = null; 282 if (mStatusIndex != 0 && !cursor.isNull(mStatusIndex)) { 283 statusMessage = cursor.getString(mStatusIndex); 284 } 285 // If there is no status message from the contact, but there was a presence value, 286 // then use the default status message string 287 if (statusMessage == null && presence != 0) { 288 statusMessage = ContactStatusUtil.getStatusString(mContext, presence); 289 } 290 contact.status = statusMessage; 291 } 292 293 return contact; 294 } 295 296 /** 297 * Returns the number of frequents that will be displayed in the list. 298 */ 299 public int getNumFrequents() { 300 return mNumFrequents; 301 } 302 303 @Override 304 public int getCount() { 305 if (mContactCursor == null || mContactCursor.isClosed()) { 306 return 0; 307 } 308 309 switch (mDisplayType) { 310 case STARRED_ONLY: 311 return getRowCount(mContactCursor.getCount()); 312 case STREQUENT: 313 case STREQUENT_PHONE_ONLY: 314 // Takes numbers of rows the Starred Contacts Occupy 315 int starredRowCount = getRowCount(mDividerPosition); 316 317 // Compute the frequent row count which is 1 plus the number of frequents 318 // (to account for the divider) or 0 if there are no frequents. 319 int frequentRowCount = mNumFrequents == 0 ? 0 : mNumFrequents + 1; 320 321 // Return the number of starred plus frequent rows 322 return starredRowCount + frequentRowCount; 323 case FREQUENT_ONLY: 324 // Number of frequent contacts 325 return mContactCursor.getCount(); 326 default: 327 throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType); 328 } 329 } 330 331 /** 332 * Returns the number of rows required to show the provided number of entries 333 * with the current number of columns. 334 */ 335 protected int getRowCount(int entryCount) { 336 return entryCount == 0 ? 0 : ((entryCount - 1) / mColumnCount) + 1; 337 } 338 339 public int getColumnCount() { 340 return mColumnCount; 341 } 342 343 /** 344 * Returns an ArrayList of the {@link ContactEntry}s that are to appear 345 * on the row for the given position. 346 */ 347 @Override 348 public ArrayList<ContactEntry> getItem(int position) { 349 ArrayList<ContactEntry> resultList = new ArrayList<ContactEntry>(mColumnCount); 350 int contactIndex = position * mColumnCount; 351 352 switch (mDisplayType) { 353 case FREQUENT_ONLY: 354 resultList.add(createContactEntryFromCursor(mContactCursor, position)); 355 break; 356 case STARRED_ONLY: 357 for (int columnCounter = 0; columnCounter < mColumnCount; columnCounter++) { 358 resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); 359 contactIndex++; 360 } 361 break; 362 case STREQUENT: 363 case STREQUENT_PHONE_ONLY: 364 if (position < getRowCount(mDividerPosition)) { 365 for (int columnCounter = 0; columnCounter < mColumnCount && 366 contactIndex != mDividerPosition; columnCounter++) { 367 resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); 368 contactIndex++; 369 } 370 } else { 371 /* 372 * Current position minus how many rows are before the divider and 373 * Minus 1 for the divider itself provides the relative index of the frequent 374 * contact being displayed. Then add the dividerPostion to give the offset 375 * into the contacts cursor to get the absoulte index. 376 */ 377 contactIndex = position - getRowCount(mDividerPosition) - 1 + mDividerPosition; 378 resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex)); 379 } 380 break; 381 default: 382 throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType); 383 } 384 return resultList; 385 } 386 387 @Override 388 public long getItemId(int position) { 389 // As we show several selectable items for each ListView row, 390 // we can not determine a stable id. But as we don't rely on ListView's selection, 391 // this should not be a problem. 392 return position; 393 } 394 395 @Override 396 public boolean areAllItemsEnabled() { 397 return (mDisplayType != DisplayType.STREQUENT && 398 mDisplayType != DisplayType.STREQUENT_PHONE_ONLY); 399 } 400 401 @Override 402 public boolean isEnabled(int position) { 403 return position != getRowCount(mDividerPosition); 404 } 405 406 @Override 407 public View getView(int position, View convertView, ViewGroup parent) { 408 int itemViewType = getItemViewType(position); 409 410 if (itemViewType == ViewTypes.DIVIDER) { 411 // Checking For Divider First so not to cast convertView 412 return convertView == null ? getDivider() : convertView; 413 } 414 415 ContactTileRow contactTileRowView = (ContactTileRow) convertView; 416 ArrayList<ContactEntry> contactList = getItem(position); 417 418 if (contactTileRowView == null) { 419 // Creating new row if needed 420 contactTileRowView = new ContactTileRow(mContext, itemViewType); 421 } 422 423 contactTileRowView.configureRow(contactList, position == getCount() - 1); 424 return contactTileRowView; 425 } 426 427 /** 428 * Divider uses a list_seperator.xml along with text to denote 429 * the most frequently contacted contacts. 430 */ 431 public View getDivider() { 432 return MoreContactUtils.createHeaderView(mContext, 433 mDisplayType == DisplayType.STREQUENT_PHONE_ONLY ? 434 R.string.favoritesFrequentCalled : R.string.favoritesFrequentContacted); 435 } 436 437 private int getLayoutResourceId(int viewType) { 438 switch (viewType) { 439 case ViewTypes.STARRED: 440 return mIsQuickContactEnabled ? 441 R.layout.contact_tile_starred_quick_contact : R.layout.contact_tile_starred; 442 case ViewTypes.FREQUENT: 443 return mDisplayType == DisplayType.STREQUENT_PHONE_ONLY ? 444 R.layout.contact_tile_frequent_phone : R.layout.contact_tile_frequent; 445 case ViewTypes.STARRED_PHONE: 446 return R.layout.contact_tile_phone_starred; 447 default: 448 throw new IllegalArgumentException("Unrecognized viewType " + viewType); 449 } 450 } 451 @Override 452 public int getViewTypeCount() { 453 return ViewTypes.COUNT; 454 } 455 456 @Override 457 public int getItemViewType(int position) { 458 /* 459 * Returns view type based on {@link DisplayType}. 460 * {@link DisplayType#STARRED_ONLY} and {@link DisplayType#GROUP_MEMBERS} 461 * are {@link ViewTypes#STARRED}. 462 * {@link DisplayType#FREQUENT_ONLY} is {@link ViewTypes#FREQUENT}. 463 * {@link DisplayType#STREQUENT} mixes both {@link ViewTypes} 464 * and also adds in {@link ViewTypes#DIVIDER}. 465 */ 466 switch (mDisplayType) { 467 case STREQUENT: 468 if (position < getRowCount(mDividerPosition)) { 469 return ViewTypes.STARRED; 470 } else if (position == getRowCount(mDividerPosition)) { 471 return ViewTypes.DIVIDER; 472 } else { 473 return ViewTypes.FREQUENT; 474 } 475 case STREQUENT_PHONE_ONLY: 476 if (position < getRowCount(mDividerPosition)) { 477 return ViewTypes.STARRED_PHONE; 478 } else if (position == getRowCount(mDividerPosition)) { 479 return ViewTypes.DIVIDER; 480 } else { 481 return ViewTypes.FREQUENT; 482 } 483 case STARRED_ONLY: 484 return ViewTypes.STARRED; 485 case FREQUENT_ONLY: 486 return ViewTypes.FREQUENT; 487 default: 488 throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType); 489 } 490 } 491 492 /** 493 * Returns the "frequent header" position. Only available when STREQUENT or 494 * STREQUENT_PHONE_ONLY is used for its display type. 495 */ 496 public int getFrequentHeaderPosition() { 497 return getRowCount(mDividerPosition); 498 } 499 500 /** 501 * Acts as a row item composed of {@link ContactTileView} 502 * 503 * TODO: FREQUENT doesn't really need it. Just let {@link #getView} return 504 */ 505 private class ContactTileRow extends FrameLayout { 506 private int mItemViewType; 507 private int mLayoutResId; 508 509 public ContactTileRow(Context context, int itemViewType) { 510 super(context); 511 mItemViewType = itemViewType; 512 mLayoutResId = getLayoutResourceId(mItemViewType); 513 514 // Remove row (but not children) from accessibility node tree. 515 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 516 } 517 518 /** 519 * Configures the row to add {@link ContactEntry}s information to the views 520 */ 521 public void configureRow(ArrayList<ContactEntry> list, boolean isLastRow) { 522 int columnCount = mItemViewType == ViewTypes.FREQUENT ? 1 : mColumnCount; 523 524 // Adding tiles to row and filling in contact information 525 for (int columnCounter = 0; columnCounter < columnCount; columnCounter++) { 526 ContactEntry entry = 527 columnCounter < list.size() ? list.get(columnCounter) : null; 528 addTileFromEntry(entry, columnCounter, isLastRow); 529 } 530 } 531 532 private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) { 533 final ContactTileView contactTile; 534 535 if (getChildCount() <= childIndex) { 536 contactTile = (ContactTileView) inflate(mContext, mLayoutResId, null); 537 // Note: the layoutparam set here is only actually used for FREQUENT. 538 // We override onMeasure() for STARRED and we don't care the layout param there. 539 Resources resources = mContext.getResources(); 540 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( 541 ViewGroup.LayoutParams.WRAP_CONTENT, 542 ViewGroup.LayoutParams.WRAP_CONTENT); 543 params.setMargins( 544 resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 545 0, 546 resources.getDimensionPixelSize(R.dimen.detail_item_side_margin), 547 0); 548 contactTile.setLayoutParams(params); 549 contactTile.setPhotoManager(mPhotoManager); 550 contactTile.setListener(mListener); 551 addView(contactTile); 552 } else { 553 contactTile = (ContactTileView) getChildAt(childIndex); 554 } 555 contactTile.loadFromContact(entry); 556 557 switch (mItemViewType) { 558 case ViewTypes.STARRED_PHONE: 559 case ViewTypes.STARRED: 560 // Setting divider visibilities 561 contactTile.setPaddingRelative(0, 0, 562 childIndex >= mColumnCount - 1 ? 0 : mPaddingInPixels, 563 isLastRow ? 0 : mPaddingInPixels); 564 break; 565 case ViewTypes.FREQUENT: 566 contactTile.setHorizontalDividerVisibility( 567 isLastRow ? View.GONE : View.VISIBLE); 568 break; 569 default: 570 break; 571 } 572 } 573 574 @Override 575 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 576 switch (mItemViewType) { 577 case ViewTypes.STARRED_PHONE: 578 case ViewTypes.STARRED: 579 onLayoutForTiles(); 580 return; 581 default: 582 super.onLayout(changed, left, top, right, bottom); 583 return; 584 } 585 } 586 587 private void onLayoutForTiles() { 588 final int count = getChildCount(); 589 590 // Just line up children horizontally. 591 int childLeft = 0; 592 for (int i = 0; i < count; i++) { 593 final View child = getChildAt(i); 594 595 // Note MeasuredWidth includes the padding. 596 final int childWidth = child.getMeasuredWidth(); 597 child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight()); 598 childLeft += childWidth; 599 } 600 } 601 602 @Override 603 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 604 switch (mItemViewType) { 605 case ViewTypes.STARRED_PHONE: 606 case ViewTypes.STARRED: 607 onMeasureForTiles(widthMeasureSpec); 608 return; 609 default: 610 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 611 return; 612 } 613 } 614 615 private void onMeasureForTiles(int widthMeasureSpec) { 616 final int width = MeasureSpec.getSize(widthMeasureSpec); 617 618 final int childCount = getChildCount(); 619 if (childCount == 0) { 620 // Just in case... 621 setMeasuredDimension(width, 0); 622 return; 623 } 624 625 // 1. Calculate image size. 626 // = ([total width] - [total padding]) / [child count] 627 // 628 // 2. Set it to width/height of each children. 629 // If we have a remainder, some tiles will have 1 pixel larger width than its height. 630 // 631 // 3. Set the dimensions of itself. 632 // Let width = given width. 633 // Let height = image size + bottom paddding. 634 635 final int totalPaddingsInPixels = (mColumnCount - 1) * mPaddingInPixels; 636 637 // Preferred width / height for images (excluding the padding). 638 // The actual width may be 1 pixel larger than this if we have a remainder. 639 final int imageSize = (width - totalPaddingsInPixels) / mColumnCount; 640 final int remainder = width - (imageSize * mColumnCount) - totalPaddingsInPixels; 641 642 for (int i = 0; i < childCount; i++) { 643 final View child = getChildAt(i); 644 final int childWidth = imageSize + child.getPaddingRight() 645 // Compensate for the remainder 646 + (i < remainder ? 1 : 0); 647 final int childHeight = imageSize + child.getPaddingBottom(); 648 child.measure( 649 MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), 650 MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY) 651 ); 652 } 653 setMeasuredDimension(width, imageSize + getChildAt(0).getPaddingBottom()); 654 } 655 } 656 657 protected static class ViewTypes { 658 public static final int COUNT = 4; 659 public static final int STARRED = 0; 660 public static final int DIVIDER = 1; 661 public static final int FREQUENT = 2; 662 public static final int STARRED_PHONE = 3; 663 } 664 } 665