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