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 */
16package com.android.car.dialer;
17
18import android.content.ContentResolver;
19import android.content.Context;
20import android.database.Cursor;
21import android.graphics.PorterDuff;
22import android.os.Handler;
23import android.provider.CallLog;
24import android.support.annotation.Nullable;
25import android.support.v7.widget.RecyclerView;
26import android.text.TextUtils;
27import android.text.format.DateUtils;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31
32import androidx.car.widget.PagedListView;
33
34import com.android.car.dialer.telecom.PhoneLoader;
35import com.android.car.dialer.telecom.TelecomUtils;
36import com.android.car.dialer.telecom.UiCallManager;
37
38import java.util.ArrayList;
39import java.util.Collections;
40import java.util.HashMap;
41import 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 */
52public 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