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.ContentObserver;
     24 import android.database.Cursor;
     25 import android.net.Uri;
     26 import android.os.Build;
     27 import android.os.Handler;
     28 import android.preference.PreferenceManager;
     29 import android.provider.CallLog;
     30 import android.provider.CallLog.Calls;
     31 import android.support.annotation.MainThread;
     32 import android.support.annotation.Nullable;
     33 import android.support.annotation.VisibleForTesting;
     34 import android.support.annotation.WorkerThread;
     35 import android.text.TextUtils;
     36 import android.util.ArraySet;
     37 import com.android.dialer.DialerPhoneNumber;
     38 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
     39 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog;
     40 import com.android.dialer.calllog.datasources.CallLogDataSource;
     41 import com.android.dialer.calllog.datasources.CallLogMutations;
     42 import com.android.dialer.calllog.datasources.util.RowCombiner;
     43 import com.android.dialer.common.Assert;
     44 import com.android.dialer.common.LogUtil;
     45 import com.android.dialer.common.concurrent.ThreadUtil;
     46 import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
     47 import com.android.dialer.util.PermissionsUtil;
     48 import com.google.i18n.phonenumbers.PhoneNumberUtil;
     49 import com.google.protobuf.InvalidProtocolBufferException;
     50 import java.util.Arrays;
     51 import java.util.List;
     52 import java.util.Set;
     53 import javax.inject.Inject;
     54 
     55 /**
     56  * Responsible for defining the rows in the annotated call log and maintaining the columns in it
     57  * which are derived from the system call log.
     58  */
     59 @SuppressWarnings("MissingPermission")
     60 public class SystemCallLogDataSource implements CallLogDataSource {
     61 
     62   @VisibleForTesting
     63   static final String PREF_LAST_TIMESTAMP_PROCESSED = "systemCallLogLastTimestampProcessed";
     64 
     65   @Nullable private Long lastTimestampProcessed;
     66 
     67   @Inject
     68   public SystemCallLogDataSource() {}
     69 
     70   @MainThread
     71   @Override
     72   public void registerContentObservers(
     73       Context appContext, ContentObserverCallbacks contentObserverCallbacks) {
     74     Assert.isMainThread();
     75 
     76     LogUtil.enterBlock("SystemCallLogDataSource.registerContentObservers");
     77 
     78     if (!PermissionsUtil.hasCallLogReadPermissions(appContext)) {
     79       LogUtil.i("SystemCallLogDataSource.registerContentObservers", "no call log permissions");
     80       return;
     81     }
     82 
     83     appContext
     84         .getContentResolver()
     85         .registerContentObserver(
     86             CallLog.Calls.CONTENT_URI,
     87             true,
     88             new CallLogObserver(
     89                 ThreadUtil.getUiThreadHandler(), appContext, contentObserverCallbacks));
     90   }
     91 
     92   @WorkerThread
     93   @Override
     94   public boolean isDirty(Context appContext) {
     95     Assert.isWorkerThread();
     96 
     97     /*
     98      * The system call log has a last updated timestamp, but deletes are physical (the "deleted"
     99      * column is unused). This means that we can't detect deletes without scanning the entire table,
    100      * which would be too slow. So, we just rely on content observers to trigger rebuilds when any
    101      * change is made to the system call log.
    102      */
    103     return false;
    104   }
    105 
    106   @WorkerThread
    107   @Override
    108   public void fill(Context appContext, CallLogMutations mutations) {
    109     Assert.isWorkerThread();
    110 
    111     lastTimestampProcessed = null;
    112 
    113     if (!PermissionsUtil.hasPermission(appContext, permission.READ_CALL_LOG)) {
    114       LogUtil.i("SystemCallLogDataSource.fill", "no call log permissions");
    115       return;
    116     }
    117 
    118     // This data source should always run first so the mutations should always be empty.
    119     Assert.checkArgument(mutations.isEmpty());
    120 
    121     Set<Long> annotatedCallLogIds = getAnnotatedCallLogIds(appContext);
    122 
    123     LogUtil.i(
    124         "SystemCallLogDataSource.fill",
    125         "found %d existing annotated call log ids",
    126         annotatedCallLogIds.size());
    127 
    128     handleInsertsAndUpdates(appContext, mutations, annotatedCallLogIds);
    129     handleDeletes(appContext, annotatedCallLogIds, mutations);
    130   }
    131 
    132   @WorkerThread
    133   @Override
    134   public void onSuccessfulFill(Context appContext) {
    135     // If a fill operation was a no-op, lastTimestampProcessed could still be null.
    136     if (lastTimestampProcessed != null) {
    137       PreferenceManager.getDefaultSharedPreferences(appContext)
    138           .edit()
    139           .putLong(PREF_LAST_TIMESTAMP_PROCESSED, lastTimestampProcessed)
    140           .apply();
    141     }
    142   }
    143 
    144   @Override
    145   public ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc) {
    146     // TODO: Complete implementation.
    147     ContentValues coalescedValues =
    148         new RowCombiner(individualRowsSortedByTimestampDesc)
    149             .useMostRecentLong(AnnotatedCallLog.TIMESTAMP)
    150             .combine();
    151 
    152     // All phone numbers in the provided group should be equivalent (but could be formatted
    153     // differently). Arbitrarily show the raw phone number of the most recent call.
    154     DialerPhoneNumber mostRecentPhoneNumber =
    155         getMostRecentPhoneNumber(individualRowsSortedByTimestampDesc);
    156     coalescedValues.put(
    157         CoalescedAnnotatedCallLog.FORMATTED_NUMBER,
    158         mostRecentPhoneNumber.getRawInput().getNumber());
    159     return coalescedValues;
    160   }
    161 
    162   private static DialerPhoneNumber getMostRecentPhoneNumber(
    163       List<ContentValues> individualRowsSortedByTimestampDesc) {
    164     DialerPhoneNumber dialerPhoneNumber;
    165     byte[] protoBytes =
    166         individualRowsSortedByTimestampDesc.get(0).getAsByteArray(AnnotatedCallLog.NUMBER);
    167     try {
    168       dialerPhoneNumber = DialerPhoneNumber.parseFrom(protoBytes);
    169     } catch (InvalidProtocolBufferException e) {
    170       throw Assert.createAssertionFailException("couldn't parse DialerPhoneNumber", e);
    171     }
    172     return dialerPhoneNumber;
    173   }
    174 
    175   @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources
    176   private void handleInsertsAndUpdates(
    177       Context appContext, CallLogMutations mutations, Set<Long> existingAnnotatedCallLogIds) {
    178     long previousTimestampProcessed =
    179         PreferenceManager.getDefaultSharedPreferences(appContext)
    180             .getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L);
    181 
    182     DialerPhoneNumberUtil dialerPhoneNumberUtil =
    183         new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance());
    184 
    185     // TODO: Really should be getting last 1000 by timestamp, not by last modified.
    186     try (Cursor cursor =
    187         appContext
    188             .getContentResolver()
    189             .query(
    190                 Calls.CONTENT_URI, // Excludes voicemail
    191                 new String[] {
    192                   Calls._ID, Calls.DATE, Calls.LAST_MODIFIED, Calls.NUMBER, Calls.COUNTRY_ISO
    193                 },
    194                 Calls.LAST_MODIFIED + " > ?",
    195                 new String[] {String.valueOf(previousTimestampProcessed)},
    196                 Calls.LAST_MODIFIED + " DESC LIMIT 1000")) {
    197 
    198       if (cursor == null) {
    199         LogUtil.e("SystemCallLogDataSource.handleInsertsAndUpdates", "null cursor");
    200         return;
    201       }
    202 
    203       LogUtil.i(
    204           "SystemCallLogDataSource.handleInsertsAndUpdates",
    205           "found %d entries to insert/update",
    206           cursor.getCount());
    207 
    208       if (cursor.moveToFirst()) {
    209         int idColumn = cursor.getColumnIndexOrThrow(Calls._ID);
    210         int dateColumn = cursor.getColumnIndexOrThrow(Calls.DATE);
    211         int lastModifiedColumn = cursor.getColumnIndexOrThrow(Calls.LAST_MODIFIED);
    212         int numberColumn = cursor.getColumnIndexOrThrow(Calls.NUMBER);
    213         int countryIsoColumn = cursor.getColumnIndexOrThrow(Calls.COUNTRY_ISO);
    214 
    215         // The cursor orders by LAST_MODIFIED DESC, so the first result is the most recent timestamp
    216         // processed.
    217         lastTimestampProcessed = cursor.getLong(lastModifiedColumn);
    218         do {
    219           long id = cursor.getLong(idColumn);
    220           long date = cursor.getLong(dateColumn);
    221           String numberAsStr = cursor.getString(numberColumn);
    222           String countryIso = cursor.getString(countryIsoColumn);
    223 
    224           byte[] numberAsProtoBytes =
    225               dialerPhoneNumberUtil.parse(numberAsStr, countryIso).toByteArray();
    226 
    227           ContentValues contentValues = new ContentValues();
    228           contentValues.put(AnnotatedCallLog.TIMESTAMP, date);
    229           contentValues.put(AnnotatedCallLog.NUMBER, numberAsProtoBytes);
    230 
    231           if (existingAnnotatedCallLogIds.contains(id)) {
    232             mutations.update(id, contentValues);
    233           } else {
    234             mutations.insert(id, contentValues);
    235           }
    236         } while (cursor.moveToNext());
    237       } // else no new results, do nothing.
    238     }
    239   }
    240 
    241   private static void handleDeletes(
    242       Context appContext, Set<Long> existingAnnotatedCallLogIds, CallLogMutations mutations) {
    243     Set<Long> systemCallLogIds =
    244         getIdsFromSystemCallLogThatMatch(appContext, existingAnnotatedCallLogIds);
    245     LogUtil.i(
    246         "SystemCallLogDataSource.handleDeletes",
    247         "found %d matching entries in system call log",
    248         systemCallLogIds.size());
    249     Set<Long> idsInAnnotatedCallLogNoLongerInSystemCallLog = new ArraySet<>();
    250     idsInAnnotatedCallLogNoLongerInSystemCallLog.addAll(existingAnnotatedCallLogIds);
    251     idsInAnnotatedCallLogNoLongerInSystemCallLog.removeAll(systemCallLogIds);
    252 
    253     LogUtil.i(
    254         "SystemCallLogDataSource.handleDeletes",
    255         "found %d call log entries to remove",
    256         idsInAnnotatedCallLogNoLongerInSystemCallLog.size());
    257 
    258     for (long id : idsInAnnotatedCallLogNoLongerInSystemCallLog) {
    259       mutations.delete(id);
    260     }
    261   }
    262 
    263   @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources
    264   private static Set<Long> getAnnotatedCallLogIds(Context appContext) {
    265     ArraySet<Long> ids = new ArraySet<>();
    266 
    267     try (Cursor cursor =
    268         appContext
    269             .getContentResolver()
    270             .query(
    271                 AnnotatedCallLog.CONTENT_URI,
    272                 new String[] {AnnotatedCallLog._ID},
    273                 null,
    274                 null,
    275                 null)) {
    276 
    277       if (cursor == null) {
    278         LogUtil.e("SystemCallLogDataSource.getAnnotatedCallLogIds", "null cursor");
    279         return ids;
    280       }
    281 
    282       if (cursor.moveToFirst()) {
    283         int idColumn = cursor.getColumnIndexOrThrow(AnnotatedCallLog._ID);
    284         do {
    285           ids.add(cursor.getLong(idColumn));
    286         } while (cursor.moveToNext());
    287       }
    288     }
    289     return ids;
    290   }
    291 
    292   @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources
    293   private static Set<Long> getIdsFromSystemCallLogThatMatch(
    294       Context appContext, Set<Long> matchingIds) {
    295     ArraySet<Long> ids = new ArraySet<>();
    296 
    297     String[] questionMarks = new String[matchingIds.size()];
    298     Arrays.fill(questionMarks, "?");
    299     String whereClause = (Calls._ID + " in (") + TextUtils.join(",", questionMarks) + ")";
    300     String[] whereArgs = new String[matchingIds.size()];
    301     int i = 0;
    302     for (long id : matchingIds) {
    303       whereArgs[i++] = String.valueOf(id);
    304     }
    305 
    306     try (Cursor cursor =
    307         appContext
    308             .getContentResolver()
    309             .query(Calls.CONTENT_URI, new String[] {Calls._ID}, whereClause, whereArgs, null)) {
    310 
    311       if (cursor == null) {
    312         LogUtil.e("SystemCallLogDataSource.getIdsFromSystemCallLog", "null cursor");
    313         return ids;
    314       }
    315 
    316       if (cursor.moveToFirst()) {
    317         int idColumn = cursor.getColumnIndexOrThrow(Calls._ID);
    318         do {
    319           ids.add(cursor.getLong(idColumn));
    320         } while (cursor.moveToNext());
    321       }
    322       return ids;
    323     }
    324   }
    325 
    326   private static class CallLogObserver extends ContentObserver {
    327     private final Context appContext;
    328     private final ContentObserverCallbacks contentObserverCallbacks;
    329 
    330     CallLogObserver(
    331         Handler handler, Context appContext, ContentObserverCallbacks contentObserverCallbacks) {
    332       super(handler);
    333       this.appContext = appContext;
    334       this.contentObserverCallbacks = contentObserverCallbacks;
    335     }
    336 
    337     @MainThread
    338     @Override
    339     public void onChange(boolean selfChange, Uri uri) {
    340       Assert.isMainThread();
    341       LogUtil.enterBlock("SystemCallLogDataSource.CallLogObserver.onChange");
    342       super.onChange(selfChange, uri);
    343 
    344       /*
    345        * The system call log has a last updated timestamp, but deletes are physical (the "deleted"
    346        * column is unused). This means that we can't detect deletes without scanning the entire
    347        * table, which would be too slow. So, we just rely on content observers to trigger rebuilds
    348        * when any change is made to the system call log.
    349        */
    350       contentObserverCallbacks.markDirtyAndNotify(appContext);
    351     }
    352   }
    353 }
    354