1/*
2 * Copyright (C) 2016 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.content.res.Resources;
21import android.database.Cursor;
22import android.graphics.Bitmap;
23import android.os.AsyncTask;
24import android.provider.CallLog;
25import android.support.annotation.NonNull;
26import android.telephony.PhoneNumberUtils;
27import android.text.TextUtils;
28import android.text.format.DateUtils;
29
30import com.android.car.apps.common.CircleBitmapDrawable;
31import com.android.car.apps.common.LetterTileDrawable;
32import com.android.car.dialer.telecom.PhoneLoader;
33import com.android.car.dialer.telecom.TelecomUtils;
34
35import java.util.ArrayList;
36import java.util.List;
37
38class CallLogListingTask extends AsyncTask<Void, Void, Void> {
39    static class CallLogItem {
40        final String mTitle;
41        final String mText;
42        final String mNumber;
43        final Bitmap mIcon;
44
45        public CallLogItem(String title, String text, String number, Bitmap icon) {
46            mTitle = title;
47            mText = text;
48            mNumber = number;
49            mIcon = icon;
50        }
51    }
52
53    interface LoadCompleteListener {
54        void onLoadComplete(List<CallLogItem> items);
55    }
56
57
58    // Like a constant but needs a context so not static.
59    private final String VOICEMAIL_NUMBER;
60
61    private Context mContext;
62    private Cursor mCursor;
63    private List<CallLogItem> mItems;
64    private LoadCompleteListener mListener;
65
66    CallLogListingTask(Context context, Cursor cursor,
67            @NonNull LoadCompleteListener listener) {
68        mContext = context;
69        mCursor = cursor;
70        mItems = new ArrayList<>(mCursor.getCount());
71        mListener = listener;
72        VOICEMAIL_NUMBER = TelecomUtils.getVoicemailNumber(mContext);
73    }
74
75    private String maybeAppendCount(StringBuilder sb, int count) {
76        if (count > 1) {
77            sb.append(" (").append(count).append(")");
78        }
79        return sb.toString();
80    }
81
82    private String getContactName(String cachedName, String number,
83            int count, boolean isVoicemail) {
84        if (cachedName != null) {
85            return maybeAppendCount(new StringBuilder(cachedName), count);
86        }
87
88        StringBuilder sb = new StringBuilder();
89        if (isVoicemail) {
90            sb.append(mContext.getString(R.string.voicemail));
91        } else {
92            String displayName = TelecomUtils.getDisplayName(mContext, number);
93            if (TextUtils.isEmpty(displayName)) {
94                displayName = mContext.getString(R.string.unknown);
95            }
96            sb.append(displayName);
97        }
98
99        return maybeAppendCount(sb, count);
100    }
101
102    private Bitmap getContactImage(Context context, ContentResolver contentResolver,
103            String name, String number) {
104        Resources r = context.getResources();
105        int size = r.getDimensionPixelSize(R.dimen.dialer_menu_icon_container_width);
106
107        Bitmap bitmap = TelecomUtils.getContactPhotoFromNumber(contentResolver, number);
108        if (bitmap != null) {
109            return new CircleBitmapDrawable(r, bitmap).toBitmap(size);
110        }
111
112        LetterTileDrawable letterTileDrawable = new LetterTileDrawable(r);
113        letterTileDrawable.setContactDetails(name, number);
114        letterTileDrawable.setIsCircular(true);
115        return letterTileDrawable.toBitmap(size);
116    }
117
118    private static CharSequence getRelativeTime(long millis) {
119        boolean validTimestamp = millis > 0;
120
121        return validTimestamp ? DateUtils.getRelativeTimeSpanString(
122                millis, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
123                DateUtils.FORMAT_ABBREV_RELATIVE) : null;
124    }
125
126    @Override
127    protected Void doInBackground(Void... voids) {
128        ContentResolver resolver = mContext.getContentResolver();
129
130        try {
131            if (mCursor != null) {
132                int cachedNameColumn = PhoneLoader.getNameColumnIndex(mCursor);
133                int numberColumn = PhoneLoader.getNumberColumnIndex(mCursor);
134                int dateColumn = mCursor.getColumnIndex(CallLog.Calls.DATE);
135
136                while (mCursor.moveToNext()) {
137                    int count = 1;
138                    String number = mCursor.getString(numberColumn);
139
140                    // We want to group calls to the same number into one so seek forward as many
141                    // entries as possible as long as the number is the same.
142                    int position = mCursor.getPosition();
143                    while (mCursor.moveToNext()) {
144                        String nextNumber = mCursor.getString(numberColumn);
145                        if (equalNumbers(number, nextNumber)) {
146                            count++;
147                        } else {
148                            break;
149                        }
150                    }
151                    mCursor.moveToPosition(position);
152
153                    boolean isVoicemail = number.equals(VOICEMAIL_NUMBER);
154                    String name = getContactName(mCursor.getString(cachedNameColumn),
155                            number, count, isVoicemail);
156
157                    // Not sure why this is the only column checked here but I'm assuming this was
158                    // to work around some bug on some device.
159                    long millis = dateColumn == -1 ? 0 : mCursor.getLong(dateColumn);
160
161                    StringBuffer secondaryText = new StringBuffer();
162                    CharSequence relativeDate = getRelativeTime(millis);
163
164                    // Append the type (work, mobile etc.) if it isnt voicemail.
165                    if (!isVoicemail) {
166                        CharSequence type = TelecomUtils.getTypeFromNumber(mContext, number);
167                        secondaryText.append(type);
168                        if (!TextUtils.isEmpty(type) && !TextUtils.isEmpty(relativeDate)) {
169                            secondaryText.append(", ");
170                        }
171                    }
172
173                    // Add in the timestamp.
174                    if (relativeDate != null) {
175                        secondaryText.append(relativeDate);
176                    }
177
178                    Bitmap contactImage = getContactImage(mContext, resolver, name, number);
179
180                    CallLogItem item =
181                            new CallLogItem(name, secondaryText.toString(), number, contactImage);
182                    mItems.add(item);
183
184                    // Since we deduplicated count rows, we can move all the way to that row so the
185                    // next iteration takes us to the row following the last duplicate row.
186                    if (count > 1) {
187                        mCursor.moveToPosition(position + count - 1);
188                    }
189                }
190            }
191        } finally {
192            if (mCursor != null) {
193                mCursor.close();
194            }
195        }
196        return null;
197    }
198
199    @Override
200    protected void onPostExecute(Void aVoid) {
201        mListener.onLoadComplete(mItems);
202    }
203
204    /**
205     * Determines if the specified number is actually a URI
206     * (i.e. a SIP address) rather than a regular PSTN phone number,
207     * based on whether or not the number contains an "@" character.
208     *
209     * @return true if number contains @
210     *
211     * from android.telephony.PhoneNumberUtils
212     */
213    public static boolean isUriNumber(String number) {
214        // Note we allow either "@" or "%40" to indicate a URI, in case
215        // the passed-in string is URI-escaped.  (Neither "@" nor "%40"
216        // will ever be found in a legal PSTN number.)
217        return number != null && (number.contains("@") || number.contains("%40"));
218    }
219
220    private static boolean equalNumbers(String number1, String number2) {
221        if (isUriNumber(number1) || isUriNumber(number2)) {
222            return compareSipAddresses(number1, number2);
223        } else {
224            return PhoneNumberUtils.compare(number1, number2);
225        }
226    }
227
228    private static boolean compareSipAddresses(String number1, String number2) {
229        if (number1 == null || number2 == null) {
230            return number1 == null && number2 == null;
231        }
232
233        String[] address1 = splitSipAddress(number1);
234        String[] address2 = splitSipAddress(number2);
235
236        return address1[0].equals(address2[0]) && address1[1].equals(address2[1]);
237    }
238
239    /**
240     * Splits a sip address on either side of the @ sign and returns both halves.
241     * If there is no @ sign, user info will be number and rest will be empty string
242     * @param number the sip number to split
243     * @return a string array of size 2. Element 0 is the user info (left side of @ sign) and
244     *         element 1 is the rest (right side of @ sign).
245     */
246    private static String[] splitSipAddress(String number) {
247        String[] values = new String[2];
248        int index = number.indexOf('@');
249        if (index == -1) {
250            values[0] = number;
251            values[1] = "";
252        } else {
253            values[0] = number.substring(0, index);
254            values[1] = number.substring(index);
255        }
256        return values;
257    }
258}
259