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