1 /* 2 * Copyright (C) 2015 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.car.dialer; 17 18 import android.content.ContentResolver; 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.graphics.PorterDuff; 22 import android.provider.CallLog; 23 import android.support.annotation.Nullable; 24 import android.support.car.ui.PagedListView; 25 import android.support.v7.widget.RecyclerView; 26 import android.text.TextUtils; 27 import android.text.format.DateUtils; 28 import android.util.Log; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import com.android.car.dialer.telecom.PhoneLoader; 33 import com.android.car.dialer.telecom.TelecomUtils; 34 import com.android.car.dialer.telecom.UiCallManager; 35 36 import java.util.ArrayList; 37 import java.util.Collections; 38 import java.util.HashMap; 39 import java.util.List; 40 41 /** 42 * Adapter class for populating Contact data as loaded from the DB to an AA GroupingRecyclerView. 43 * 44 * <p>It handles two types of contacts: 45 * 46 * <ul> 47 * <li>Strequent contacts (starred and/or frequent)</li> 48 * <li>Last call contact</li> 49 * </ul> 50 */ 51 public class StrequentsAdapter extends RecyclerView.Adapter<CallLogViewHolder> 52 implements PagedListView.ItemCap { 53 // The possible view types in this adapter. 54 private static final int VIEW_TYPE_EMPTY = 0; 55 private static final int VIEW_TYPE_LASTCALL = 1; 56 private static final int VIEW_TYPE_STREQUENT = 2; 57 58 private final Context mContext; 59 private List<ContactEntry> mData; 60 61 private LastCallData mLastCallData; 62 63 private final ContentResolver mContentResolver; 64 65 public interface StrequentsListener<T> { 66 /** Notified when a row corresponding an individual Contact (not group) was clicked. */ 67 void onContactClicked(T viewHolder); 68 } 69 70 private View.OnFocusChangeListener mFocusChangeListener; 71 private StrequentsListener<CallLogViewHolder> mStrequentsListener; 72 73 private int mMaxItems = -1; 74 private boolean mIsEmpty; 75 76 public StrequentsAdapter(Context context) { 77 mContext = context; 78 mContentResolver = context.getContentResolver(); 79 } 80 81 public void setStrequentsListener(@Nullable StrequentsListener<CallLogViewHolder> listener) { 82 mStrequentsListener = listener; 83 } 84 85 public void setFocusChangeListener(@Nullable View.OnFocusChangeListener listener) { 86 mFocusChangeListener = listener; 87 } 88 89 public void setLastCallCursor(@Nullable Cursor cursor) { 90 Log.e("StrequentsAdapter", "cursor: " + cursor); 91 Log.e("StrequentsAdapter", "count: " + (cursor == null ? 0 : cursor.getCount())); 92 mLastCallData = convertLastCallCursor(cursor); 93 notifyDataSetChanged(); 94 } 95 96 public void setStrequentCursor(@Nullable Cursor cursor) { 97 if (cursor != null) { 98 setData(convertStrequentCursorToArray(cursor)); 99 } else { 100 setData(null); 101 } 102 notifyDataSetChanged(); 103 } 104 105 private void setData(List<ContactEntry> data) { 106 mData = data; 107 notifyDataSetChanged(); 108 } 109 110 @Override 111 public void setMaxItems(int maxItems) { 112 mMaxItems = maxItems; 113 } 114 115 @Override 116 public int getItemViewType(int position) { 117 if (mIsEmpty) { 118 return VIEW_TYPE_EMPTY; 119 } else if (position == 0 && mLastCallData != null) { 120 return VIEW_TYPE_LASTCALL; 121 } else { 122 return VIEW_TYPE_STREQUENT; 123 } 124 } 125 126 @Override 127 public int getItemCount() { 128 int itemCount = mData == null ? 0 : mData.size(); 129 itemCount += mLastCallData == null ? 0 : 1; 130 131 mIsEmpty = itemCount == 0; 132 133 // If there is no data to display, add one to the item count to display the card in the 134 // empty state. 135 if (mIsEmpty) { 136 itemCount++; 137 } 138 139 return mMaxItems >= 0 ? Math.min(mMaxItems, itemCount) : itemCount; 140 } 141 142 @Override 143 public CallLogViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 144 View view; 145 switch (viewType) { 146 case VIEW_TYPE_LASTCALL: 147 view = LayoutInflater.from(parent.getContext()) 148 .inflate(R.layout.call_log_last_call_item_card, parent, false); 149 return new CallLogViewHolder(view); 150 151 case VIEW_TYPE_EMPTY: 152 view = LayoutInflater.from(parent.getContext()) 153 .inflate(R.layout.car_list_item_empty, parent, false); 154 return new CallLogViewHolder(view); 155 156 case VIEW_TYPE_STREQUENT: 157 default: 158 view = LayoutInflater.from(parent.getContext()) 159 .inflate(R.layout.call_log_list_item_card, parent, false); 160 return new CallLogViewHolder(view); 161 } 162 } 163 164 @Override 165 public void onBindViewHolder(final CallLogViewHolder viewHolder, int position) { 166 switch (viewHolder.getItemViewType()) { 167 case VIEW_TYPE_LASTCALL: 168 onBindLastCallRow(viewHolder); 169 break; 170 171 case VIEW_TYPE_EMPTY: 172 viewHolder.icon.setImageResource(R.drawable.ic_empty_speed_dial); 173 viewHolder.title.setText(R.string.speed_dial_empty); 174 viewHolder.title.setTextColor(mContext.getColor(R.color.car_body1_light)); 175 break; 176 177 case VIEW_TYPE_STREQUENT: 178 default: 179 int positionIntoData = position; 180 181 // If there is last call data, then decrement the position so there is not an out of 182 // bounds error on the mData. 183 if (mLastCallData != null) { 184 positionIntoData--; 185 } 186 187 onBindView(viewHolder, mData.get(positionIntoData)); 188 viewHolder.callType.setVisibility(View.VISIBLE); 189 } 190 } 191 192 private void onViewClicked(CallLogViewHolder viewHolder) { 193 if (mStrequentsListener != null) { 194 mStrequentsListener.onContactClicked(viewHolder); 195 } 196 } 197 198 @Override 199 public void onViewAttachedToWindow(CallLogViewHolder holder) { 200 if (mFocusChangeListener != null) { 201 holder.itemView.setOnFocusChangeListener(mFocusChangeListener); 202 } 203 } 204 205 @Override 206 public void onViewDetachedFromWindow(CallLogViewHolder holder) { 207 holder.itemView.setOnFocusChangeListener(null); 208 } 209 210 /** 211 * Converts the strequents data in the given cursor into a list of {@link ContactEntry}s. 212 */ 213 private List<ContactEntry> convertStrequentCursorToArray(Cursor cursor) { 214 List<ContactEntry> strequentContactEntries = new ArrayList<>(); 215 HashMap<Integer, ContactEntry> entryMap = new HashMap<>(); 216 cursor.moveToPosition(-1); 217 218 while (cursor.moveToNext()) { 219 final ContactEntry entry = ContactEntry.fromCursor(cursor, mContext); 220 entryMap.put(entry.hashCode(), entry); 221 } 222 223 strequentContactEntries.addAll(entryMap.values()); 224 Collections.sort(strequentContactEntries); 225 return strequentContactEntries; 226 } 227 228 /** 229 * Binds the views in the entry to the data of last call. 230 * 231 * @param viewHolder the view holder corresponding to this entry 232 */ 233 private void onBindLastCallRow(final CallLogViewHolder viewHolder) { 234 if (mLastCallData == null) { 235 return; 236 } 237 238 viewHolder.itemView.setOnClickListener(v -> onViewClicked(viewHolder)); 239 240 String primaryText = mLastCallData.getPrimaryText(); 241 String number = mLastCallData.getNumber(); 242 243 viewHolder.title.setText(mLastCallData.getPrimaryText()); 244 viewHolder.text.setText(mLastCallData.getSecondaryText()); 245 viewHolder.itemView.setTag(number); 246 viewHolder.callTypeIconsView.clear(); 247 viewHolder.callTypeIconsView.setVisibility(View.VISIBLE); 248 249 // mHasFirstItem is true only in main screen, or else it is in drawer, then we need to add 250 // call type icons for call history items. 251 viewHolder.smallIcon.setVisibility(View.GONE); 252 int[] callTypes = mLastCallData.getCallTypes(); 253 int icons = Math.min(callTypes.length, CallTypeIconsView.MAX_CALL_TYPE_ICONS); 254 for (int i = 0; i < icons; i++) { 255 viewHolder.callTypeIconsView.add(callTypes[i]); 256 } 257 258 setBackground(viewHolder); 259 260 TelecomUtils.setContactBitmapAsync(mContext, viewHolder.icon, primaryText, number); 261 } 262 263 /** 264 * Converts the last call information in the given cursor into a {@link LastCallData} object 265 * so that the cursor can be closed. 266 * 267 * @return A valid {@link LastCallData} or {@code null} if the cursor is {@code null} or has no 268 * data in it. 269 */ 270 @Nullable 271 public LastCallData convertLastCallCursor(@Nullable Cursor cursor) { 272 if (cursor == null || cursor.getCount() == 0) { 273 return null; 274 } 275 276 cursor.moveToFirst(); 277 278 final StringBuilder nameSb = new StringBuilder(); 279 int column = PhoneLoader.getNameColumnIndex(cursor); 280 String cachedName = cursor.getString(column); 281 final String number = PhoneLoader.getPhoneNumber(cursor, mContentResolver); 282 if (cachedName == null) { 283 cachedName = TelecomUtils.getDisplayName(mContext, number); 284 } 285 286 boolean isVoicemail = false; 287 if (cachedName == null) { 288 if (number.equals(TelecomUtils.getVoicemailNumber(mContext))) { 289 isVoicemail = true; 290 nameSb.append(mContext.getString(R.string.voicemail)); 291 } else { 292 String displayName = TelecomUtils.getFormattedNumber(mContext, number); 293 if (TextUtils.isEmpty(displayName)) { 294 displayName = mContext.getString(R.string.unknown); 295 } 296 nameSb.append(displayName); 297 } 298 } else { 299 nameSb.append(cachedName); 300 } 301 column = cursor.getColumnIndex(CallLog.Calls.DATE); 302 // If we set this to 0, getRelativeTime will return null and no relative time 303 // will be displayed. 304 long millis = column == -1 ? 0 : cursor.getLong(column); 305 StringBuffer secondaryText = new StringBuffer(); 306 CharSequence relativeDate = getRelativeTime(millis); 307 if (!isVoicemail) { 308 CharSequence type = TelecomUtils.getTypeFromNumber(mContext, number); 309 secondaryText.append(type); 310 if (!TextUtils.isEmpty(type) && !TextUtils.isEmpty(relativeDate)) { 311 secondaryText.append(", "); 312 } 313 } 314 if (relativeDate != null) { 315 secondaryText.append(relativeDate); 316 } 317 318 int[] callTypes = getCarTelecomManager().getCallTypes(cursor, 1); 319 320 return new LastCallData(number, nameSb.toString(), secondaryText.toString(), callTypes); 321 } 322 323 /** 324 * Bind view function for frequent call row. 325 */ 326 private void onBindView(final CallLogViewHolder viewHolder, final ContactEntry entry) { 327 viewHolder.itemView.setOnClickListener(v -> onViewClicked(viewHolder)); 328 329 final String number = entry.number; 330 // TODO(mcrico): Why is being a voicemail related to not having a name? 331 boolean isVoicemail = (entry.name == null) 332 && (number.equals(TelecomUtils.getVoicemailNumber(mContext))); 333 String secondaryText = ""; 334 if (!isVoicemail) { 335 secondaryText = String.valueOf(TelecomUtils.getTypeFromNumber(mContext, number)); 336 } 337 338 viewHolder.text.setText(secondaryText); 339 viewHolder.itemView.setTag(number); 340 viewHolder.callTypeIconsView.clear(); 341 342 String displayName = entry.getDisplayName(); 343 viewHolder.title.setText(displayName); 344 345 TelecomUtils.setContactBitmapAsync(mContext, viewHolder.icon, displayName, number); 346 347 if (entry.isStarred) { 348 viewHolder.smallIcon.setVisibility(View.VISIBLE); 349 final int iconColor = mContext.getColor(android.R.color.white); 350 viewHolder.smallIcon.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); 351 viewHolder.smallIcon.setImageResource(R.drawable.ic_favorite); 352 } else { 353 viewHolder.smallIcon.setVisibility(View.GONE); 354 } 355 356 setBackground(viewHolder); 357 } 358 359 /** 360 * Appropriately sets the background for the View that is being bound. This method will allow 361 * for rounded corners on either the top or bottom of a card. 362 */ 363 private void setBackground(CallLogViewHolder viewHolder) { 364 int itemCount = getItemCount(); 365 int adapterPosition = viewHolder.getAdapterPosition(); 366 367 if (itemCount == 1) { 368 // Only element - all corners are rounded 369 viewHolder.card.setBackgroundResource( 370 R.drawable.car_card_rounded_top_bottom_background); 371 } else if (adapterPosition == 0) { 372 // First element gets rounded top 373 viewHolder.card.setBackgroundResource(R.drawable.car_card_rounded_top_background); 374 } else if (adapterPosition == itemCount - 1) { 375 // Last one has a rounded bottom 376 viewHolder.card.setBackgroundResource(R.drawable.car_card_rounded_bottom_background); 377 } else { 378 // Middle have no rounded corners 379 viewHolder.card.setBackgroundResource(R.color.car_card); 380 } 381 } 382 383 /** 384 * Build any timestamp and label into a single string. If the given timestamp is invalid, then 385 * {@code null} is returned. 386 */ 387 @Nullable 388 private static CharSequence getRelativeTime(long millis) { 389 if (millis <= 0) { 390 return null; 391 } 392 393 return DateUtils.getRelativeTimeSpanString(millis, System.currentTimeMillis(), 394 DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE); 395 } 396 397 private UiCallManager getCarTelecomManager() { 398 return UiCallManager.getInstance(mContext); 399 } 400 401 /** 402 * A container for data relating to a last call entry. 403 */ 404 private class LastCallData { 405 private final String mNumber; 406 private final String mPrimaryText; 407 private final String mSecondaryText; 408 private final int[] mCallTypes; 409 410 LastCallData(String number, String primaryText, String secondaryText, 411 int[] callTypes) { 412 mNumber = number; 413 mPrimaryText = primaryText; 414 mSecondaryText = secondaryText; 415 mCallTypes = callTypes; 416 } 417 418 public String getNumber() { 419 return mNumber; 420 } 421 422 public String getPrimaryText() { 423 return mPrimaryText; 424 } 425 426 public String getSecondaryText() { 427 return mSecondaryText; 428 } 429 430 public int[] getCallTypes() { 431 return mCallTypes; 432 } 433 } 434 } 435