Home | History | Annotate | Download | only in calllog
      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