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