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