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