diff options
Diffstat (limited to 'java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java')
-rw-r--r-- | java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java | 209 |
1 files changed, 200 insertions, 9 deletions
diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java index ea6663fbe..be2df6043 100644 --- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java @@ -16,28 +16,49 @@ package com.android.dialer.calllog.datasources.systemcalllog; +import android.Manifest.permission; +import android.annotation.TargetApi; +import android.content.ContentValues; import android.content.Context; import android.database.ContentObserver; -import android.database.sqlite.SQLiteDatabase; +import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.os.Handler; +import android.preference.PreferenceManager; import android.provider.CallLog; +import android.provider.CallLog.Calls; import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; -import com.android.dialer.calllog.database.CallLogMutations; +import android.text.TextUtils; +import android.util.ArraySet; +import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; import com.android.dialer.calllog.datasources.CallLogDataSource; +import com.android.dialer.calllog.datasources.CallLogMutations; +import com.android.dialer.calllog.datasources.util.RowCombiner; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.ThreadUtil; import com.android.dialer.util.PermissionsUtil; +import java.util.Arrays; +import java.util.List; +import java.util.Set; import javax.inject.Inject; /** * Responsible for defining the rows in the annotated call log and maintaining the columns in it * which are derived from the system call log. */ +@SuppressWarnings("MissingPermission") public class SystemCallLogDataSource implements CallLogDataSource { + @VisibleForTesting + static final String PREF_LAST_TIMESTAMP_PROCESSED = "systemCallLogLastTimestampProcessed"; + + @Nullable private Long lastTimestampProcessed; + @Inject public SystemCallLogDataSource() {} @@ -47,6 +68,8 @@ public class SystemCallLogDataSource implements CallLogDataSource { Context appContext, ContentObserverCallbacks contentObserverCallbacks) { Assert.isMainThread(); + LogUtil.enterBlock("SystemCallLogDataSource.registerContentObservers"); + if (!PermissionsUtil.hasCallLogReadPermissions(appContext)) { LogUtil.i("SystemCallLogDataSource.registerContentObservers", "no call log permissions"); return; @@ -77,17 +100,185 @@ public class SystemCallLogDataSource implements CallLogDataSource { @WorkerThread @Override - public void fill( - Context appContext, - SQLiteDatabase readableDatabase, - long lastRebuildTimeMillis, - CallLogMutations mutations) { + public void fill(Context appContext, CallLogMutations mutations) { Assert.isWorkerThread(); + lastTimestampProcessed = null; + + if (!PermissionsUtil.hasPermission(appContext, permission.READ_CALL_LOG)) { + LogUtil.i("SystemCallLogDataSource.fill", "no call log permissions"); + return; + } + // This data source should always run first so the mutations should always be empty. - Assert.checkState(mutations.isEmpty()); + Assert.checkArgument(mutations.isEmpty()); + + Set<Long> annotatedCallLogIds = getAnnotatedCallLogIds(appContext); + + LogUtil.i( + "SystemCallLogDataSource.fill", + "found %d existing annotated call log ids", + annotatedCallLogIds.size()); + + handleInsertsAndUpdates(appContext, mutations, annotatedCallLogIds); + handleDeletes(appContext, annotatedCallLogIds, mutations); + } + + @WorkerThread + @Override + public void onSuccessfulFill(Context appContext) { + // If a fill operation was a no-op, lastTimestampProcessed could still be null. + if (lastTimestampProcessed != null) { + PreferenceManager.getDefaultSharedPreferences(appContext) + .edit() + .putLong(PREF_LAST_TIMESTAMP_PROCESSED, lastTimestampProcessed) + .commit(); + } + } + + @Override + public ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc) { + // TODO: Complete implementation. + return new RowCombiner(individualRowsSortedByTimestampDesc) + .useMostRecentLong(AnnotatedCallLog.TIMESTAMP) + .combine(); + } + + @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources + private void handleInsertsAndUpdates( + Context appContext, CallLogMutations mutations, Set<Long> existingAnnotatedCallLogIds) { + long previousTimestampProcessed = + PreferenceManager.getDefaultSharedPreferences(appContext) + .getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L); + + try (Cursor cursor = + appContext + .getContentResolver() + .query( + Calls.CONTENT_URI, // Excludes voicemail + new String[] {Calls._ID, Calls.DATE, Calls.LAST_MODIFIED}, + Calls.LAST_MODIFIED + " > ?", + new String[] {String.valueOf(previousTimestampProcessed)}, + Calls.LAST_MODIFIED + " DESC LIMIT 1000")) { + + if (cursor == null) { + LogUtil.e("SystemCallLogDataSource.handleInsertsAndUpdates", "null cursor"); + return; + } + + LogUtil.i( + "SystemCallLogDataSource.handleInsertsAndUpdates", + "found %d entries to insert/update", + cursor.getCount()); - // TODO: Implementation. + if (cursor.moveToFirst()) { + int idColumn = cursor.getColumnIndexOrThrow(Calls._ID); + int dateColumn = cursor.getColumnIndexOrThrow(Calls.DATE); + int lastModifiedColumn = cursor.getColumnIndexOrThrow(Calls.LAST_MODIFIED); + + // The cursor orders by LAST_MODIFIED DESC, so the first result is the most recent timestamp + // processed. + lastTimestampProcessed = cursor.getLong(lastModifiedColumn); + do { + long id = cursor.getLong(idColumn); + long date = cursor.getLong(dateColumn); + + ContentValues contentValues = new ContentValues(); + contentValues.put(AnnotatedCallLog.TIMESTAMP, date); + + if (existingAnnotatedCallLogIds.contains(id)) { + mutations.update(id, contentValues); + } else { + mutations.insert(id, contentValues); + } + } while (cursor.moveToNext()); + } // else no new results, do nothing. + } + } + + private static void handleDeletes( + Context appContext, Set<Long> existingAnnotatedCallLogIds, CallLogMutations mutations) { + Set<Long> systemCallLogIds = + getIdsFromSystemCallLogThatMatch(appContext, existingAnnotatedCallLogIds); + LogUtil.i( + "SystemCallLogDataSource.handleDeletes", + "found %d entries in system call log", + systemCallLogIds.size()); + Set<Long> idsInAnnotatedCallLogNoLongerInSystemCallLog = new ArraySet<>(); + idsInAnnotatedCallLogNoLongerInSystemCallLog.addAll(existingAnnotatedCallLogIds); + idsInAnnotatedCallLogNoLongerInSystemCallLog.removeAll(systemCallLogIds); + + LogUtil.i( + "SystemCallLogDataSource.handleDeletes", + "found %d call log entries to remove", + idsInAnnotatedCallLogNoLongerInSystemCallLog.size()); + + for (long id : idsInAnnotatedCallLogNoLongerInSystemCallLog) { + mutations.delete(id); + } + } + + @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources + private static Set<Long> getAnnotatedCallLogIds(Context appContext) { + ArraySet<Long> ids = new ArraySet<>(); + + try (Cursor cursor = + appContext + .getContentResolver() + .query( + AnnotatedCallLog.CONTENT_URI, + new String[] {AnnotatedCallLog._ID}, + null, + null, + null)) { + + if (cursor == null) { + LogUtil.e("SystemCallLogDataSource.getAnnotatedCallLogIds", "null cursor"); + return ids; + } + + if (cursor.moveToFirst()) { + int idColumn = cursor.getColumnIndexOrThrow(AnnotatedCallLog._ID); + do { + ids.add(cursor.getLong(idColumn)); + } while (cursor.moveToNext()); + } + } + return ids; + } + + @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources + private static Set<Long> getIdsFromSystemCallLogThatMatch( + Context appContext, Set<Long> matchingIds) { + ArraySet<Long> ids = new ArraySet<>(); + + String[] questionMarks = new String[matchingIds.size()]; + Arrays.fill(questionMarks, "?"); + String whereClause = (Calls._ID + " in (") + TextUtils.join(",", questionMarks) + ")"; + String[] whereArgs = new String[matchingIds.size()]; + int i = 0; + for (long id : matchingIds) { + whereArgs[i++] = String.valueOf(id); + } + + try (Cursor cursor = + appContext + .getContentResolver() + .query(Calls.CONTENT_URI, new String[] {Calls._ID}, whereClause, whereArgs, null)) { + + if (cursor == null) { + LogUtil.e("SystemCallLogDataSource.getIdsFromSystemCallLog", "null cursor"); + return ids; + } + + if (cursor.moveToFirst()) { + int idColumn = cursor.getColumnIndexOrThrow(Calls._ID); + do { + ids.add(cursor.getLong(idColumn)); + } while (cursor.moveToNext()); + } + return ids; + } } private static class CallLogObserver extends ContentObserver { |