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 17 package com.android.dialer.calllog; 18 19 import com.google.common.annotations.VisibleForTesting; 20 21 import android.content.ContentResolver; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.database.Cursor; 26 import android.net.Uri; 27 import android.os.AsyncTask; 28 import android.provider.CallLog; 29 import android.provider.VoicemailContract.Voicemails; 30 import android.telecom.PhoneAccountHandle; 31 import android.telephony.PhoneNumberUtils; 32 import android.text.TextUtils; 33 import android.util.Log; 34 35 import com.android.contacts.common.GeoUtil; 36 import com.android.contacts.common.compat.CompatUtils; 37 import com.android.contacts.common.util.PermissionsUtil; 38 import com.android.dialer.PhoneCallDetails; 39 import com.android.dialer.compat.CallsSdkCompat; 40 import com.android.dialer.database.VoicemailArchiveContract; 41 import com.android.dialer.util.AsyncTaskExecutor; 42 import com.android.dialer.util.AsyncTaskExecutors; 43 import com.android.dialer.util.PhoneNumberUtil; 44 import com.android.dialer.util.TelecomUtil; 45 46 import java.util.ArrayList; 47 import java.util.Arrays; 48 import java.util.Locale; 49 50 public class CallLogAsyncTaskUtil { 51 private static String TAG = CallLogAsyncTaskUtil.class.getSimpleName(); 52 53 /** The enumeration of {@link AsyncTask} objects used in this class. */ 54 public enum Tasks { 55 DELETE_VOICEMAIL, 56 DELETE_CALL, 57 DELETE_BLOCKED_CALL, 58 MARK_VOICEMAIL_READ, 59 MARK_CALL_READ, 60 GET_CALL_DETAILS, 61 UPDATE_DURATION 62 } 63 64 private static final class CallDetailQuery { 65 66 private static final String[] CALL_LOG_PROJECTION_INTERNAL = new String[] { 67 CallLog.Calls.DATE, 68 CallLog.Calls.DURATION, 69 CallLog.Calls.NUMBER, 70 CallLog.Calls.TYPE, 71 CallLog.Calls.COUNTRY_ISO, 72 CallLog.Calls.GEOCODED_LOCATION, 73 CallLog.Calls.NUMBER_PRESENTATION, 74 CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME, 75 CallLog.Calls.PHONE_ACCOUNT_ID, 76 CallLog.Calls.FEATURES, 77 CallLog.Calls.DATA_USAGE, 78 CallLog.Calls.TRANSCRIPTION 79 }; 80 public static final String[] CALL_LOG_PROJECTION; 81 82 static final int DATE_COLUMN_INDEX = 0; 83 static final int DURATION_COLUMN_INDEX = 1; 84 static final int NUMBER_COLUMN_INDEX = 2; 85 static final int CALL_TYPE_COLUMN_INDEX = 3; 86 static final int COUNTRY_ISO_COLUMN_INDEX = 4; 87 static final int GEOCODED_LOCATION_COLUMN_INDEX = 5; 88 static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6; 89 static final int ACCOUNT_COMPONENT_NAME = 7; 90 static final int ACCOUNT_ID = 8; 91 static final int FEATURES = 9; 92 static final int DATA_USAGE = 10; 93 static final int TRANSCRIPTION_COLUMN_INDEX = 11; 94 static final int POST_DIAL_DIGITS = 12; 95 static final int VIA_NUMBER = 13; 96 97 static { 98 ArrayList<String> projectionList = new ArrayList<>(); 99 projectionList.addAll(Arrays.asList(CALL_LOG_PROJECTION_INTERNAL)); 100 if (CompatUtils.isNCompatible()) { 101 projectionList.add(CallsSdkCompat.POST_DIAL_DIGITS); 102 projectionList.add(CallsSdkCompat.VIA_NUMBER); 103 } 104 projectionList.trimToSize(); 105 CALL_LOG_PROJECTION = projectionList.toArray(new String[projectionList.size()]); 106 } 107 } 108 109 private static class CallLogDeleteBlockedCallQuery { 110 static final String[] PROJECTION = new String[] { 111 CallLog.Calls._ID, 112 CallLog.Calls.DATE 113 }; 114 115 static final int ID_COLUMN_INDEX = 0; 116 static final int DATE_COLUMN_INDEX = 1; 117 } 118 119 public interface CallLogAsyncTaskListener { 120 void onDeleteCall(); 121 void onDeleteVoicemail(); 122 void onGetCallDetails(PhoneCallDetails[] details); 123 } 124 125 public interface OnCallLogQueryFinishedListener { 126 void onQueryFinished(boolean hasEntry); 127 } 128 129 // Try to identify if a call log entry corresponds to a number which was blocked. We match by 130 // by comparing its creation time to the time it was added in the InCallUi and seeing if they 131 // fall within a certain threshold. 132 private static final int MATCH_BLOCKED_CALL_THRESHOLD_MS = 3000; 133 134 private static AsyncTaskExecutor sAsyncTaskExecutor; 135 136 private static void initTaskExecutor() { 137 sAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor(); 138 } 139 140 public static void getCallDetails( 141 final Context context, 142 final Uri[] callUris, 143 final CallLogAsyncTaskListener callLogAsyncTaskListener) { 144 if (sAsyncTaskExecutor == null) { 145 initTaskExecutor(); 146 } 147 148 sAsyncTaskExecutor.submit(Tasks.GET_CALL_DETAILS, 149 new AsyncTask<Void, Void, PhoneCallDetails[]>() { 150 @Override 151 public PhoneCallDetails[] doInBackground(Void... params) { 152 // TODO: All calls correspond to the same person, so make a single lookup. 153 final int numCalls = callUris.length; 154 PhoneCallDetails[] details = new PhoneCallDetails[numCalls]; 155 try { 156 for (int index = 0; index < numCalls; ++index) { 157 details[index] = 158 getPhoneCallDetailsForUri(context, callUris[index]); 159 } 160 return details; 161 } catch (IllegalArgumentException e) { 162 // Something went wrong reading in our primary data. 163 Log.w(TAG, "Invalid URI starting call details", e); 164 return null; 165 } 166 } 167 168 @Override 169 public void onPostExecute(PhoneCallDetails[] phoneCallDetails) { 170 if (callLogAsyncTaskListener != null) { 171 callLogAsyncTaskListener.onGetCallDetails(phoneCallDetails); 172 } 173 } 174 }); 175 } 176 177 /** 178 * Return the phone call details for a given call log URI. 179 */ 180 private static PhoneCallDetails getPhoneCallDetailsForUri(Context context, Uri callUri) { 181 Cursor cursor = context.getContentResolver().query( 182 callUri, CallDetailQuery.CALL_LOG_PROJECTION, null, null, null); 183 184 try { 185 if (cursor == null || !cursor.moveToFirst()) { 186 throw new IllegalArgumentException("Cannot find content: " + callUri); 187 } 188 189 // Read call log. 190 final String countryIso = cursor.getString(CallDetailQuery.COUNTRY_ISO_COLUMN_INDEX); 191 final String number = cursor.getString(CallDetailQuery.NUMBER_COLUMN_INDEX); 192 final String postDialDigits = CompatUtils.isNCompatible() 193 ? cursor.getString(CallDetailQuery.POST_DIAL_DIGITS) : ""; 194 final String viaNumber = CompatUtils.isNCompatible() ? 195 cursor.getString(CallDetailQuery.VIA_NUMBER) : ""; 196 final int numberPresentation = 197 cursor.getInt(CallDetailQuery.NUMBER_PRESENTATION_COLUMN_INDEX); 198 199 final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount( 200 cursor.getString(CallDetailQuery.ACCOUNT_COMPONENT_NAME), 201 cursor.getString(CallDetailQuery.ACCOUNT_ID)); 202 203 // If this is not a regular number, there is no point in looking it up in the contacts. 204 ContactInfoHelper contactInfoHelper = 205 new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)); 206 boolean isVoicemail = PhoneNumberUtil.isVoicemailNumber(context, accountHandle, number); 207 boolean shouldLookupNumber = 208 PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation) && !isVoicemail; 209 ContactInfo info = ContactInfo.EMPTY; 210 211 if (shouldLookupNumber) { 212 ContactInfo lookupInfo = contactInfoHelper.lookupNumber(number, countryIso); 213 info = lookupInfo != null ? lookupInfo : ContactInfo.EMPTY; 214 } 215 216 PhoneCallDetails details = new PhoneCallDetails( 217 context, number, numberPresentation, info.formattedNumber, 218 postDialDigits, isVoicemail); 219 220 details.viaNumber = viaNumber; 221 details.accountHandle = accountHandle; 222 details.contactUri = info.lookupUri; 223 details.namePrimary = info.name; 224 details.nameAlternative = info.nameAlternative; 225 details.numberType = info.type; 226 details.numberLabel = info.label; 227 details.photoUri = info.photoUri; 228 details.sourceType = info.sourceType; 229 details.objectId = info.objectId; 230 231 details.callTypes = new int[] { 232 cursor.getInt(CallDetailQuery.CALL_TYPE_COLUMN_INDEX) 233 }; 234 details.date = cursor.getLong(CallDetailQuery.DATE_COLUMN_INDEX); 235 details.duration = cursor.getLong(CallDetailQuery.DURATION_COLUMN_INDEX); 236 details.features = cursor.getInt(CallDetailQuery.FEATURES); 237 details.geocode = cursor.getString(CallDetailQuery.GEOCODED_LOCATION_COLUMN_INDEX); 238 details.transcription = cursor.getString(CallDetailQuery.TRANSCRIPTION_COLUMN_INDEX); 239 240 details.countryIso = !TextUtils.isEmpty(countryIso) ? countryIso 241 : GeoUtil.getCurrentCountryIso(context); 242 243 if (!cursor.isNull(CallDetailQuery.DATA_USAGE)) { 244 details.dataUsage = cursor.getLong(CallDetailQuery.DATA_USAGE); 245 } 246 247 return details; 248 } finally { 249 if (cursor != null) { 250 cursor.close(); 251 } 252 } 253 } 254 255 256 /** 257 * Delete specified calls from the call log. 258 * 259 * @param context The context. 260 * @param callIds String of the callIds to delete from the call log, delimited by commas (","). 261 * @param callLogAsyncTaskListener The listener to invoke after the entries have been deleted. 262 */ 263 public static void deleteCalls( 264 final Context context, 265 final String callIds, 266 final CallLogAsyncTaskListener callLogAsyncTaskListener) { 267 if (sAsyncTaskExecutor == null) { 268 initTaskExecutor(); 269 } 270 271 sAsyncTaskExecutor.submit(Tasks.DELETE_CALL, new AsyncTask<Void, Void, Void>() { 272 @Override 273 public Void doInBackground(Void... params) { 274 context.getContentResolver().delete( 275 TelecomUtil.getCallLogUri(context), 276 CallLog.Calls._ID + " IN (" + callIds + ")", null); 277 return null; 278 } 279 280 @Override 281 public void onPostExecute(Void result) { 282 if (callLogAsyncTaskListener != null) { 283 callLogAsyncTaskListener.onDeleteCall(); 284 } 285 } 286 }); 287 } 288 289 /** 290 * Deletes the last call made by the number within a threshold of the call time added in the 291 * call log, assuming it is a blocked call for which no entry should be shown. 292 * 293 * @param context The context. 294 * @param number Number of blocked call, for which to delete the call log entry. 295 * @param timeAddedMs The time the number was added to InCall, in milliseconds. 296 * @param listener The listener to invoke after looking up for a call log entry matching the 297 * number and time added. 298 */ 299 public static void deleteBlockedCall( 300 final Context context, 301 final String number, 302 final long timeAddedMs, 303 final OnCallLogQueryFinishedListener listener) { 304 if (sAsyncTaskExecutor == null) { 305 initTaskExecutor(); 306 } 307 308 sAsyncTaskExecutor.submit(Tasks.DELETE_BLOCKED_CALL, new AsyncTask<Void, Void, Long>() { 309 @Override 310 public Long doInBackground(Void... params) { 311 // First, lookup the call log entry of the most recent call with this number. 312 Cursor cursor = context.getContentResolver().query( 313 TelecomUtil.getCallLogUri(context), 314 CallLogDeleteBlockedCallQuery.PROJECTION, 315 CallLog.Calls.NUMBER + "= ?", 316 new String[] { number }, 317 CallLog.Calls.DATE + " DESC LIMIT 1"); 318 319 // If match is found, delete this call log entry and return the call log entry id. 320 if (cursor.moveToFirst()) { 321 long creationTime = 322 cursor.getLong(CallLogDeleteBlockedCallQuery.DATE_COLUMN_INDEX); 323 if (timeAddedMs > creationTime 324 && timeAddedMs - creationTime < MATCH_BLOCKED_CALL_THRESHOLD_MS) { 325 long callLogEntryId = 326 cursor.getLong(CallLogDeleteBlockedCallQuery.ID_COLUMN_INDEX); 327 context.getContentResolver().delete( 328 TelecomUtil.getCallLogUri(context), 329 CallLog.Calls._ID + " IN (" + callLogEntryId + ")", 330 null); 331 return callLogEntryId; 332 } 333 } 334 return (long) -1; 335 } 336 337 @Override 338 public void onPostExecute(Long callLogEntryId) { 339 if (listener != null) { 340 listener.onQueryFinished(callLogEntryId >= 0); 341 } 342 } 343 }); 344 } 345 346 347 public static void markVoicemailAsRead(final Context context, final Uri voicemailUri) { 348 if (sAsyncTaskExecutor == null) { 349 initTaskExecutor(); 350 } 351 352 sAsyncTaskExecutor.submit(Tasks.MARK_VOICEMAIL_READ, new AsyncTask<Void, Void, Void>() { 353 @Override 354 public Void doInBackground(Void... params) { 355 ContentValues values = new ContentValues(); 356 values.put(Voicemails.IS_READ, true); 357 context.getContentResolver().update( 358 voicemailUri, values, Voicemails.IS_READ + " = 0", null); 359 360 Intent intent = new Intent(context, CallLogNotificationsService.class); 361 intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); 362 context.startService(intent); 363 return null; 364 } 365 }); 366 } 367 368 public static void deleteVoicemail( 369 final Context context, 370 final Uri voicemailUri, 371 final CallLogAsyncTaskListener callLogAsyncTaskListener) { 372 if (sAsyncTaskExecutor == null) { 373 initTaskExecutor(); 374 } 375 376 sAsyncTaskExecutor.submit(Tasks.DELETE_VOICEMAIL, new AsyncTask<Void, Void, Void>() { 377 @Override 378 public Void doInBackground(Void... params) { 379 context.getContentResolver().delete(voicemailUri, null, null); 380 return null; 381 } 382 383 @Override 384 public void onPostExecute(Void result) { 385 if (callLogAsyncTaskListener != null) { 386 callLogAsyncTaskListener.onDeleteVoicemail(); 387 } 388 } 389 }); 390 } 391 392 public static void markCallAsRead(final Context context, final long[] callIds) { 393 if (!PermissionsUtil.hasPhonePermissions(context)) { 394 return; 395 } 396 if (sAsyncTaskExecutor == null) { 397 initTaskExecutor(); 398 } 399 400 sAsyncTaskExecutor.submit(Tasks.MARK_CALL_READ, new AsyncTask<Void, Void, Void>() { 401 @Override 402 public Void doInBackground(Void... params) { 403 404 StringBuilder where = new StringBuilder(); 405 where.append(CallLog.Calls.TYPE).append(" = ").append(CallLog.Calls.MISSED_TYPE); 406 where.append(" AND "); 407 408 Long[] callIdLongs = new Long[callIds.length]; 409 for (int i = 0; i < callIds.length; i++) { 410 callIdLongs[i] = callIds[i]; 411 } 412 where.append(CallLog.Calls._ID).append( 413 " IN (" + TextUtils.join(",", callIdLongs) + ")"); 414 415 ContentValues values = new ContentValues(1); 416 values.put(CallLog.Calls.IS_READ, "1"); 417 context.getContentResolver().update( 418 CallLog.Calls.CONTENT_URI, values, where.toString(), null); 419 return null; 420 } 421 }); 422 } 423 424 /** 425 * Updates the duration of a voicemail call log entry if the duration given is greater than 0, 426 * and if if the duration currently in the database is less than or equal to 0 (non-existent). 427 */ 428 public static void updateVoicemailDuration( 429 final Context context, 430 final Uri voicemailUri, 431 final long duration) { 432 if (duration <= 0 || !PermissionsUtil.hasPhonePermissions(context)) { 433 return; 434 } 435 if (sAsyncTaskExecutor == null) { 436 initTaskExecutor(); 437 } 438 439 sAsyncTaskExecutor.submit(Tasks.UPDATE_DURATION, new AsyncTask<Void, Void, Void>() { 440 @Override 441 public Void doInBackground(Void... params) { 442 ContentResolver contentResolver = context.getContentResolver(); 443 Cursor cursor = contentResolver.query( 444 voicemailUri, 445 new String[] { VoicemailArchiveContract.VoicemailArchive.DURATION }, 446 null, null, null); 447 if (cursor != null && cursor.moveToFirst() && cursor.getInt( 448 cursor.getColumnIndex( 449 VoicemailArchiveContract.VoicemailArchive.DURATION)) <= 0) { 450 ContentValues values = new ContentValues(1); 451 values.put(CallLog.Calls.DURATION, duration); 452 context.getContentResolver().update(voicemailUri, values, null, null); 453 } 454 return null; 455 } 456 }); 457 } 458 459 @VisibleForTesting 460 public static void resetForTest() { 461 sAsyncTaskExecutor = null; 462 } 463 } 464