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<>(); Arrays.asList(CALL_LOG_PROJECTION_INTERNAL)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 } projectionList.trimToSize()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 { onDeleteCall()120 void onDeleteCall(); onDeleteVoicemail()121 void onDeleteVoicemail(); onGetCallDetails(PhoneCallDetails[] details)122 void onGetCallDetails(PhoneCallDetails[] details); 123 } 124 125 public interface OnCallLogQueryFinishedListener { onQueryFinished(boolean hasEntry)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 initTaskExecutor()136 private static void initTaskExecutor() { 137 sAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor(); 138 } 139 getCallDetails( final Context context, final Uri[] callUris, final CallLogAsyncTaskListener callLogAsyncTaskListener)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 */ getPhoneCallDetailsForUri(Context context, Uri callUri)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 */ deleteCalls( final Context context, final String callIds, final CallLogAsyncTaskListener callLogAsyncTaskListener)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 */ deleteBlockedCall( final Context context, final String number, final long timeAddedMs, final OnCallLogQueryFinishedListener listener)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 markVoicemailAsRead(final Context context, final Uri voicemailUri)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 deleteVoicemail( final Context context, final Uri voicemailUri, final CallLogAsyncTaskListener callLogAsyncTaskListener)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 markCallAsRead(final Context context, final long[] callIds)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 */ updateVoicemailDuration( final Context context, final Uri voicemailUri, final long duration)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 resetForTest()460 public static void resetForTest() { 461 sAsyncTaskExecutor = null; 462 } 463 } 464