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