Home | History | Annotate | Download | only in systemcalllog
      1 /*
      2  * Copyright (C) 2017 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.datasources.systemcalllog;
     18 
     19 import android.Manifest.permission;
     20 import android.annotation.TargetApi;
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.database.Cursor;
     24 import android.os.Build;
     25 import android.os.Build.VERSION;
     26 import android.os.Build.VERSION_CODES;
     27 import android.provider.CallLog;
     28 import android.provider.CallLog.Calls;
     29 import android.provider.VoicemailContract;
     30 import android.provider.VoicemailContract.Voicemails;
     31 import android.support.annotation.ColorInt;
     32 import android.support.annotation.MainThread;
     33 import android.support.annotation.Nullable;
     34 import android.support.annotation.RequiresApi;
     35 import android.support.annotation.VisibleForTesting;
     36 import android.support.annotation.WorkerThread;
     37 import android.telecom.PhoneAccount;
     38 import android.telecom.PhoneAccountHandle;
     39 import android.telephony.PhoneNumberUtils;
     40 import android.text.TextUtils;
     41 import android.util.ArraySet;
     42 import com.android.dialer.DialerPhoneNumber;
     43 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
     44 import com.android.dialer.calllog.datasources.CallLogDataSource;
     45 import com.android.dialer.calllog.datasources.CallLogMutations;
     46 import com.android.dialer.calllog.datasources.util.RowCombiner;
     47 import com.android.dialer.calllog.observer.MarkDirtyObserver;
     48 import com.android.dialer.calllogutils.PhoneAccountUtils;
     49 import com.android.dialer.common.Assert;
     50 import com.android.dialer.common.LogUtil;
     51 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
     52 import com.android.dialer.compat.android.provider.VoicemailCompat;
     53 import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
     54 import com.android.dialer.storage.StorageComponent;
     55 import com.android.dialer.telecom.TelecomUtil;
     56 import com.android.dialer.theme.R;
     57 import com.android.dialer.util.PermissionsUtil;
     58 import com.google.common.collect.Iterables;
     59 import com.google.common.util.concurrent.ListenableFuture;
     60 import com.google.common.util.concurrent.ListeningExecutorService;
     61 import com.google.i18n.phonenumbers.PhoneNumberUtil;
     62 import java.util.ArrayList;
     63 import java.util.Arrays;
     64 import java.util.List;
     65 import java.util.Set;
     66 import javax.inject.Inject;
     67 
     68 /**
     69  * Responsible for defining the rows in the annotated call log and maintaining the columns in it
     70  * which are derived from the system call log.
     71  */
     72 @SuppressWarnings("MissingPermission")
     73 public class SystemCallLogDataSource implements CallLogDataSource {
     74 
     75   @VisibleForTesting
     76   static final String PREF_LAST_TIMESTAMP_PROCESSED = "systemCallLogLastTimestampProcessed";
     77 
     78   private final ListeningExecutorService backgroundExecutorService;
     79   private final MarkDirtyObserver markDirtyObserver;
     80 
     81   @Nullable private Long lastTimestampProcessed;
     82 
     83   @Inject
     84   SystemCallLogDataSource(
     85       @BackgroundExecutor ListeningExecutorService backgroundExecutorService,
     86       MarkDirtyObserver markDirtyObserver) {
     87     this.backgroundExecutorService = backgroundExecutorService;
     88     this.markDirtyObserver = markDirtyObserver;
     89   }
     90 
     91   @MainThread
     92   @Override
     93   public void registerContentObservers(Context appContext) {
     94     Assert.isMainThread();
     95 
     96     LogUtil.enterBlock("SystemCallLogDataSource.registerContentObservers");
     97 
     98     if (!PermissionsUtil.hasCallLogReadPermissions(appContext)) {
     99       LogUtil.i("SystemCallLogDataSource.registerContentObservers", "no call log permissions");
    100       return;
    101     }
    102     // TODO(zachh): Need to somehow register observers if user enables permission after launch?
    103 
    104     // The system call log has a last updated timestamp, but deletes are physical (the "deleted"
    105     // column is unused). This means that we can't detect deletes without scanning the entire table,
    106     // which would be too slow. So, we just rely on content observers to trigger rebuilds when any
    107     // change is made to the system call log.
    108     appContext
    109         .getContentResolver()
    110         .registerContentObserver(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, true, markDirtyObserver);
    111 
    112     if (!PermissionsUtil.hasAddVoicemailPermissions(appContext)) {
    113       LogUtil.i("SystemCallLogDataSource.registerContentObservers", "no add voicemail permissions");
    114       return;
    115     }
    116     // TODO(uabdullah): Need to somehow register observers if user enables permission after launch?
    117     appContext
    118         .getContentResolver()
    119         .registerContentObserver(VoicemailContract.Status.CONTENT_URI, true, markDirtyObserver);
    120   }
    121 
    122   @Override
    123   public ListenableFuture<Boolean> isDirty(Context appContext) {
    124     return backgroundExecutorService.submit(() -> isDirtyInternal(appContext));
    125   }
    126 
    127   @Override
    128   public ListenableFuture<Void> fill(Context appContext, CallLogMutations mutations) {
    129     return backgroundExecutorService.submit(() -> fillInternal(appContext, mutations));
    130   }
    131 
    132   @Override
    133   public ListenableFuture<Void> onSuccessfulFill(Context appContext) {
    134     return backgroundExecutorService.submit(() -> onSuccessfulFillInternal(appContext));
    135   }
    136 
    137   @WorkerThread
    138   private boolean isDirtyInternal(Context appContext) {
    139     Assert.isWorkerThread();
    140 
    141     /*
    142      * The system call log has a last updated timestamp, but deletes are physical (the "deleted"
    143      * column is unused). This means that we can't detect deletes without scanning the entire table,
    144      * which would be too slow. So, we just rely on content observers to trigger rebuilds when any
    145      * change is made to the system call log.
    146      *
    147      * Just return false unless the table has never been written to.
    148      */
    149     return !StorageComponent.get(appContext)
    150         .unencryptedSharedPrefs()
    151         .contains(PREF_LAST_TIMESTAMP_PROCESSED);
    152   }
    153 
    154   @WorkerThread
    155   private Void fillInternal(Context appContext, CallLogMutations mutations) {
    156     Assert.isWorkerThread();
    157 
    158     lastTimestampProcessed = null;
    159 
    160     if (!PermissionsUtil.hasPermission(appContext, permission.READ_CALL_LOG)) {
    161       LogUtil.i("SystemCallLogDataSource.fill", "no call log permissions");
    162       return null;
    163     }
    164 
    165     // This data source should always run first so the mutations should always be empty.
    166     Assert.checkArgument(mutations.isEmpty());
    167 
    168     Set<Long> annotatedCallLogIds = getAnnotatedCallLogIds(appContext);
    169 
    170     LogUtil.i(
    171         "SystemCallLogDataSource.fill",
    172         "found %d existing annotated call log ids",
    173         annotatedCallLogIds.size());
    174 
    175     handleInsertsAndUpdates(appContext, mutations, annotatedCallLogIds);
    176     handleDeletes(appContext, annotatedCallLogIds, mutations);
    177     return null;
    178   }
    179 
    180   @WorkerThread
    181   private Void onSuccessfulFillInternal(Context appContext) {
    182     // If a fill operation was a no-op, lastTimestampProcessed could still be null.
    183     if (lastTimestampProcessed != null) {
    184       StorageComponent.get(appContext)
    185           .unencryptedSharedPrefs()
    186           .edit()
    187           .putLong(PREF_LAST_TIMESTAMP_PROCESSED, lastTimestampProcessed)
    188           .apply();
    189     }
    190     return null;
    191   }
    192 
    193   @Override
    194   public ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc) {
    195     assertNoVoicemailsInRows(individualRowsSortedByTimestampDesc);
    196 
    197     return new RowCombiner(individualRowsSortedByTimestampDesc)
    198         .useMostRecentLong(AnnotatedCallLog.TIMESTAMP)
    199         .useMostRecentLong(AnnotatedCallLog.NEW)
    200         // Two different DialerPhoneNumbers could be combined if they are different but considered
    201         // to be an "exact match" by libphonenumber; in this case we arbitrarily select the most
    202         // recent one.
    203         .useMostRecentBlob(AnnotatedCallLog.NUMBER)
    204         .useMostRecentString(AnnotatedCallLog.FORMATTED_NUMBER)
    205         .useSingleValueInt(AnnotatedCallLog.NUMBER_PRESENTATION)
    206         .useMostRecentString(AnnotatedCallLog.GEOCODED_LOCATION)
    207         .useSingleValueString(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME)
    208         .useSingleValueString(AnnotatedCallLog.PHONE_ACCOUNT_ID)
    209         .useSingleValueString(AnnotatedCallLog.PHONE_ACCOUNT_LABEL)
    210         .useSingleValueLong(AnnotatedCallLog.PHONE_ACCOUNT_COLOR)
    211         .useMostRecentLong(AnnotatedCallLog.CALL_TYPE)
    212         // If any call in a group includes a feature (like Wifi/HD), consider the group to have the
    213         // feature.
    214         .bitwiseOr(AnnotatedCallLog.FEATURES)
    215         .combine();
    216   }
    217 
    218   private void assertNoVoicemailsInRows(List<ContentValues> individualRowsSortedByTimestampDesc) {
    219     for (ContentValues contentValue : individualRowsSortedByTimestampDesc) {
    220       if (contentValue.getAsLong(AnnotatedCallLog.CALL_TYPE) != null) {
    221         Assert.checkArgument(
    222             contentValue.getAsLong(AnnotatedCallLog.CALL_TYPE) != Calls.VOICEMAIL_TYPE);
    223       }
    224     }
    225   }
    226 
    227   @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources
    228   private void handleInsertsAndUpdates(
    229       Context appContext, CallLogMutations mutations, Set<Long> existingAnnotatedCallLogIds) {
    230     long previousTimestampProcessed =
    231         StorageComponent.get(appContext)
    232             .unencryptedSharedPrefs()
    233             .getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L);
    234 
    235     DialerPhoneNumberUtil dialerPhoneNumberUtil =
    236         new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance());
    237 
    238     // TODO(zachh): Really should be getting last 1000 by timestamp, not by last modified.
    239     try (Cursor cursor =
    240         appContext
    241             .getContentResolver()
    242             .query(
    243                 Calls.CONTENT_URI_WITH_VOICEMAIL,
    244                 getProjection(),
    245                 // TODO(a bug): LAST_MODIFIED not available on M
    246                 Calls.LAST_MODIFIED + " > ? AND " + Voicemails.DELETED + " = 0",
    247                 new String[] {String.valueOf(previousTimestampProcessed)},
    248                 Calls.LAST_MODIFIED + " DESC LIMIT 1000")) {
    249 
    250       if (cursor == null) {
    251         LogUtil.e("SystemCallLogDataSource.handleInsertsAndUpdates", "null cursor");
    252         return;
    253       }
    254 
    255       LogUtil.i(
    256           "SystemCallLogDataSource.handleInsertsAndUpdates",
    257           "found %d entries to insert/update",
    258           cursor.getCount());
    259 
    260       if (cursor.moveToFirst()) {
    261         int idColumn = cursor.getColumnIndexOrThrow(Calls._ID);
    262         int dateColumn = cursor.getColumnIndexOrThrow(Calls.DATE);
    263         int lastModifiedColumn = cursor.getColumnIndexOrThrow(Calls.LAST_MODIFIED);
    264         int numberColumn = cursor.getColumnIndexOrThrow(Calls.NUMBER);
    265         int presentationColumn = cursor.getColumnIndexOrThrow(Calls.NUMBER_PRESENTATION);
    266         int typeColumn = cursor.getColumnIndexOrThrow(Calls.TYPE);
    267         int countryIsoColumn = cursor.getColumnIndexOrThrow(Calls.COUNTRY_ISO);
    268         int durationsColumn = cursor.getColumnIndexOrThrow(Calls.DURATION);
    269         int dataUsageColumn = cursor.getColumnIndexOrThrow(Calls.DATA_USAGE);
    270         int transcriptionColumn = cursor.getColumnIndexOrThrow(Calls.TRANSCRIPTION);
    271         int voicemailUriColumn = cursor.getColumnIndexOrThrow(Calls.VOICEMAIL_URI);
    272         int isReadColumn = cursor.getColumnIndexOrThrow(Calls.IS_READ);
    273         int newColumn = cursor.getColumnIndexOrThrow(Calls.NEW);
    274         int geocodedLocationColumn = cursor.getColumnIndexOrThrow(Calls.GEOCODED_LOCATION);
    275         int phoneAccountComponentColumn =
    276             cursor.getColumnIndexOrThrow(Calls.PHONE_ACCOUNT_COMPONENT_NAME);
    277         int phoneAccountIdColumn = cursor.getColumnIndexOrThrow(Calls.PHONE_ACCOUNT_ID);
    278         int featuresColumn = cursor.getColumnIndexOrThrow(Calls.FEATURES);
    279         int postDialDigitsColumn = cursor.getColumnIndexOrThrow(Calls.POST_DIAL_DIGITS);
    280 
    281         // The cursor orders by LAST_MODIFIED DESC, so the first result is the most recent timestamp
    282         // processed.
    283         lastTimestampProcessed = cursor.getLong(lastModifiedColumn);
    284         do {
    285           long id = cursor.getLong(idColumn);
    286           long date = cursor.getLong(dateColumn);
    287           String numberAsStr = cursor.getString(numberColumn);
    288           int type;
    289           if (cursor.isNull(typeColumn) || (type = cursor.getInt(typeColumn)) == 0) {
    290             // CallLog.Calls#TYPE lists the allowed values, which are non-null and non-zero.
    291             throw new IllegalStateException("call type is missing");
    292           }
    293           int presentation;
    294           if (cursor.isNull(presentationColumn)
    295               || (presentation = cursor.getInt(presentationColumn)) == 0) {
    296             // CallLog.Calls#NUMBER_PRESENTATION lists the allowed values, which are non-null and
    297             // non-zero.
    298             throw new IllegalStateException("presentation is missing");
    299           }
    300           String countryIso = cursor.getString(countryIsoColumn);
    301           int duration = cursor.getInt(durationsColumn);
    302           int dataUsage = cursor.getInt(dataUsageColumn);
    303           String transcription = cursor.getString(transcriptionColumn);
    304           String voicemailUri = cursor.getString(voicemailUriColumn);
    305           int isRead = cursor.getInt(isReadColumn);
    306           int isNew = cursor.getInt(newColumn);
    307           String geocodedLocation = cursor.getString(geocodedLocationColumn);
    308           String phoneAccountComponentName = cursor.getString(phoneAccountComponentColumn);
    309           String phoneAccountId = cursor.getString(phoneAccountIdColumn);
    310           int features = cursor.getInt(featuresColumn);
    311           String postDialDigits = cursor.getString(postDialDigitsColumn);
    312 
    313           ContentValues contentValues = new ContentValues();
    314           contentValues.put(AnnotatedCallLog.TIMESTAMP, date);
    315 
    316           if (!TextUtils.isEmpty(numberAsStr)) {
    317             String numberWithPostDialDigits =
    318                 postDialDigits == null ? numberAsStr : numberAsStr + postDialDigits;
    319             DialerPhoneNumber dialerPhoneNumber =
    320                 dialerPhoneNumberUtil.parse(numberWithPostDialDigits, countryIso);
    321 
    322             contentValues.put(AnnotatedCallLog.NUMBER, dialerPhoneNumber.toByteArray());
    323             String formattedNumber =
    324                 PhoneNumberUtils.formatNumber(numberWithPostDialDigits, countryIso);
    325             if (formattedNumber == null) {
    326               formattedNumber = numberWithPostDialDigits;
    327             }
    328             contentValues.put(AnnotatedCallLog.FORMATTED_NUMBER, formattedNumber);
    329           } else {
    330             contentValues.put(
    331                 AnnotatedCallLog.NUMBER, DialerPhoneNumber.getDefaultInstance().toByteArray());
    332           }
    333           contentValues.put(AnnotatedCallLog.NUMBER_PRESENTATION, presentation);
    334           contentValues.put(AnnotatedCallLog.CALL_TYPE, type);
    335           contentValues.put(AnnotatedCallLog.IS_READ, isRead);
    336           contentValues.put(AnnotatedCallLog.NEW, isNew);
    337           contentValues.put(AnnotatedCallLog.GEOCODED_LOCATION, geocodedLocation);
    338           contentValues.put(
    339               AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME, phoneAccountComponentName);
    340           contentValues.put(AnnotatedCallLog.PHONE_ACCOUNT_ID, phoneAccountId);
    341           populatePhoneAccountLabelAndColor(
    342               appContext, contentValues, phoneAccountComponentName, phoneAccountId);
    343           contentValues.put(AnnotatedCallLog.FEATURES, features);
    344           contentValues.put(AnnotatedCallLog.DURATION, duration);
    345           contentValues.put(AnnotatedCallLog.DATA_USAGE, dataUsage);
    346           contentValues.put(AnnotatedCallLog.TRANSCRIPTION, transcription);
    347           contentValues.put(AnnotatedCallLog.VOICEMAIL_URI, voicemailUri);
    348           setTranscriptionState(cursor, contentValues);
    349 
    350           if (existingAnnotatedCallLogIds.contains(id)) {
    351             mutations.update(id, contentValues);
    352           } else {
    353             mutations.insert(id, contentValues);
    354           }
    355         } while (cursor.moveToNext());
    356       } // else no new results, do nothing.
    357     }
    358   }
    359 
    360   private void setTranscriptionState(Cursor cursor, ContentValues contentValues) {
    361     if (VERSION.SDK_INT >= VERSION_CODES.O) {
    362       int transcriptionStateColumn =
    363           cursor.getColumnIndexOrThrow(VoicemailCompat.TRANSCRIPTION_STATE);
    364       int transcriptionState = cursor.getInt(transcriptionStateColumn);
    365       contentValues.put(VoicemailCompat.TRANSCRIPTION_STATE, transcriptionState);
    366     }
    367   }
    368 
    369   private static final String[] PROJECTION_PRE_O =
    370       new String[] {
    371         Calls._ID,
    372         Calls.DATE,
    373         Calls.LAST_MODIFIED, // TODO(a bug): Not available in M
    374         Calls.NUMBER,
    375         Calls.NUMBER_PRESENTATION,
    376         Calls.TYPE,
    377         Calls.COUNTRY_ISO,
    378         Calls.DURATION,
    379         Calls.DATA_USAGE,
    380         Calls.TRANSCRIPTION,
    381         Calls.VOICEMAIL_URI,
    382         Calls.IS_READ,
    383         Calls.NEW,
    384         Calls.GEOCODED_LOCATION,
    385         Calls.PHONE_ACCOUNT_COMPONENT_NAME,
    386         Calls.PHONE_ACCOUNT_ID,
    387         Calls.FEATURES,
    388         Calls.POST_DIAL_DIGITS // TODO(a bug): Not available in M
    389       };
    390 
    391   @RequiresApi(VERSION_CODES.O)
    392   private static final String[] PROJECTION_O_AND_LATER;
    393 
    394   static {
    395     List<String> projectionList = new ArrayList<>(Arrays.asList(PROJECTION_PRE_O));
    396     projectionList.add(VoicemailCompat.TRANSCRIPTION_STATE);
    397     PROJECTION_O_AND_LATER = projectionList.toArray(new String[projectionList.size()]);
    398   }
    399 
    400   private String[] getProjection() {
    401     if (VERSION.SDK_INT >= VERSION_CODES.O) {
    402       return PROJECTION_O_AND_LATER;
    403     }
    404     return PROJECTION_PRE_O;
    405   }
    406 
    407   private void populatePhoneAccountLabelAndColor(
    408       Context appContext,
    409       ContentValues contentValues,
    410       String phoneAccountComponentName,
    411       String phoneAccountId) {
    412     PhoneAccountHandle phoneAccountHandle =
    413         TelecomUtil.composePhoneAccountHandle(phoneAccountComponentName, phoneAccountId);
    414     if (phoneAccountHandle == null) {
    415       return;
    416     }
    417     String label = PhoneAccountUtils.getAccountLabel(appContext, phoneAccountHandle);
    418     if (TextUtils.isEmpty(label)) {
    419       return;
    420     }
    421     contentValues.put(AnnotatedCallLog.PHONE_ACCOUNT_LABEL, label);
    422 
    423     @ColorInt int color = PhoneAccountUtils.getAccountColor(appContext, phoneAccountHandle);
    424     if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) {
    425       color =
    426           appContext
    427               .getResources()
    428               .getColor(R.color.dialer_secondary_text_color, appContext.getTheme());
    429     }
    430     contentValues.put(AnnotatedCallLog.PHONE_ACCOUNT_COLOR, color);
    431   }
    432 
    433   private static void handleDeletes(
    434       Context appContext, Set<Long> existingAnnotatedCallLogIds, CallLogMutations mutations) {
    435     Set<Long> systemCallLogIds =
    436         getIdsFromSystemCallLogThatMatch(appContext, existingAnnotatedCallLogIds);
    437     LogUtil.i(
    438         "SystemCallLogDataSource.handleDeletes",
    439         "found %d matching entries in system call log",
    440         systemCallLogIds.size());
    441     Set<Long> idsInAnnotatedCallLogNoLongerInSystemCallLog = new ArraySet<>();
    442     idsInAnnotatedCallLogNoLongerInSystemCallLog.addAll(existingAnnotatedCallLogIds);
    443     idsInAnnotatedCallLogNoLongerInSystemCallLog.removeAll(systemCallLogIds);
    444 
    445     LogUtil.i(
    446         "SystemCallLogDataSource.handleDeletes",
    447         "found %d call log entries to remove",
    448         idsInAnnotatedCallLogNoLongerInSystemCallLog.size());
    449 
    450     for (long id : idsInAnnotatedCallLogNoLongerInSystemCallLog) {
    451       mutations.delete(id);
    452     }
    453   }
    454 
    455   @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources
    456   private static Set<Long> getAnnotatedCallLogIds(Context appContext) {
    457     ArraySet<Long> ids = new ArraySet<>();
    458 
    459     try (Cursor cursor =
    460         appContext
    461             .getContentResolver()
    462             .query(
    463                 AnnotatedCallLog.CONTENT_URI,
    464                 new String[] {AnnotatedCallLog._ID},
    465                 null,
    466                 null,
    467                 null)) {
    468 
    469       if (cursor == null) {
    470         LogUtil.e("SystemCallLogDataSource.getAnnotatedCallLogIds", "null cursor");
    471         return ids;
    472       }
    473 
    474       if (cursor.moveToFirst()) {
    475         int idColumn = cursor.getColumnIndexOrThrow(AnnotatedCallLog._ID);
    476         do {
    477           ids.add(cursor.getLong(idColumn));
    478         } while (cursor.moveToNext());
    479       }
    480     }
    481     return ids;
    482   }
    483 
    484   @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources
    485   private static Set<Long> getIdsFromSystemCallLogThatMatch(
    486       Context appContext, Set<Long> matchingIds) {
    487     ArraySet<Long> ids = new ArraySet<>();
    488 
    489     // Batch the select statements into chunks of 999, the maximum size for SQLite selection args.
    490     Iterable<List<Long>> batches = Iterables.partition(matchingIds, 999);
    491     for (List<Long> idsInBatch : batches) {
    492       String[] questionMarks = new String[idsInBatch.size()];
    493       Arrays.fill(questionMarks, "?");
    494 
    495       String whereClause = (Calls._ID + " in (") + TextUtils.join(",", questionMarks) + ")";
    496       String[] whereArgs = new String[idsInBatch.size()];
    497       int i = 0;
    498       for (long id : idsInBatch) {
    499         whereArgs[i++] = String.valueOf(id);
    500       }
    501 
    502       try (Cursor cursor =
    503           appContext
    504               .getContentResolver()
    505               .query(
    506                   Calls.CONTENT_URI_WITH_VOICEMAIL,
    507                   new String[] {Calls._ID},
    508                   whereClause,
    509                   whereArgs,
    510                   null)) {
    511 
    512         if (cursor == null) {
    513           LogUtil.e("SystemCallLogDataSource.getIdsFromSystemCallLog", "null cursor");
    514           return ids;
    515         }
    516 
    517         if (cursor.moveToFirst()) {
    518           int idColumn = cursor.getColumnIndexOrThrow(Calls._ID);
    519           do {
    520             ids.add(cursor.getLong(idColumn));
    521           } while (cursor.moveToNext());
    522         }
    523       }
    524     }
    525     return ids;
    526   }
    527 }
    528