summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/calllog/datasources
diff options
context:
space:
mode:
authorEric Erfanian <erfanian@google.com>2017-06-19 11:26:01 -0700
committerEric Erfanian <erfanian@google.com>2017-06-19 11:30:45 -0700
commit2f1c7586bcce334ca69022eb8dc6d8965ceb6a05 (patch)
treebf00ada449ee3de31ec983a14e84159200aa18c2 /java/com/android/dialer/calllog/datasources
parent3d0ca68e466482971a4cf46576c50cb2bd42bcb5 (diff)
Update AOSP Dialer source from internal google3 repository at
cl/159428781. Test: make, treehugger This CL updates the AOSP Dialer source with all the changes that have gone into the private google3 repository. This includes all the changes from cl/152373142 (4/06/2017) to cl/159428781 (6/19/2017). This goal of these drops is to keep the AOSP source in sync with the internal google3 repository. Currently these sync are done by hand with very minor modifications to the internal source code. See the Android.mk file for list of modifications. Our current goal is to do frequent drops (daily if possible) and eventually switched to an automated process. Change-Id: Ie60a84b3936efd0ea3d95d7c86bf96d2b1663030
Diffstat (limited to 'java/com/android/dialer/calllog/datasources')
-rw-r--r--java/com/android/dialer/calllog/datasources/CallLogDataSource.java67
-rw-r--r--java/com/android/dialer/calllog/datasources/CallLogMutations.java110
-rw-r--r--java/com/android/dialer/calllog/datasources/DataSources.java30
-rw-r--r--java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java25
-rw-r--r--java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java251
-rw-r--r--java/com/android/dialer/calllog/datasources/util/RowCombiner.java53
6 files changed, 515 insertions, 21 deletions
diff --git a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java
index 13d0b842d..3fff3ba53 100644
--- a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java
@@ -16,13 +16,39 @@
package com.android.dialer.calllog.datasources;
+import android.content.ContentValues;
import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.MainThread;
import android.support.annotation.WorkerThread;
-import com.android.dialer.calllog.database.CallLogMutations;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract;
+import java.util.List;
-/** A source of data for one or more columns in the annotated call log. */
+/**
+ * A source of data for one or more columns in the annotated call log.
+ *
+ * <p>Data sources have three lifecycle operations, which are always called on the same thread and
+ * in the same order for a particular "checkDirtyAndRebuild" cycle. However, not all operations are
+ * always invoked.
+ *
+ * <ol>
+ * <li>{@link #isDirty(Context)}: Invoked only if the framework doesn't yet know if a rebuild is
+ * necessary.
+ * <li>{@link #fill(Context, CallLogMutations)}: Invoked only if the framework determined a
+ * rebuild is necessary.
+ * <li>{@link #onSuccessfulFill(Context)}: Invoked if and only if fill was previously called and
+ * the mutations provided by the previous fill operation succeeded in being applied.
+ * </ol>
+ *
+ * <p>Because {@link #isDirty(Context)} is not always invoked, {@link #fill(Context,
+ * CallLogMutations)} shouldn't rely on any state saved during {@link #isDirty(Context)}. It
+ * <em>is</em> safe to assume that {@link #onSuccessfulFill(Context)} refers to the previous fill
+ * operation.
+ *
+ * <p>The same data source objects may be reused across multiple checkDirtyAndRebuild cycles, so
+ * implementors should take care to clear any internal state at the start of a new cycle.
+ *
+ * <p>{@link #coalesce(List)} may be called from any worker thread at any time.
+ */
public interface CallLogDataSource {
/**
@@ -35,6 +61,8 @@ public interface CallLogDataSource {
* <p>Most implementations of this method will rely on some sort of last modified timestamp. If it
* is impossible for a data source to be modified without the dialer application being notified,
* this method may immediately return false.
+ *
+ * @see CallLogDataSource class doc for complete lifecyle information
*/
@WorkerThread
boolean isDirty(Context appContext);
@@ -43,16 +71,39 @@ public interface CallLogDataSource {
* Computes the set of mutations necessary to update the annotated call log with respect to this
* data source.
*
+ * @see CallLogDataSource class doc for complete lifecyle information
* @param mutations the set of mutations which this method should contribute to. Note that it may
* contain inserts from the system call log, and these inserts should be modified by each data
* source.
*/
@WorkerThread
- void fill(
- Context appContext,
- SQLiteDatabase readableDatabase,
- long lastRebuildTimeMillis,
- CallLogMutations mutations);
+ void fill(Context appContext, CallLogMutations mutations);
+
+ /**
+ * Called after database mutations have been applied to all data sources. This is useful for
+ * saving state such as the timestamp of the last row processed in an underlying database. Note
+ * that all mutations across all data sources are applied in a single transaction.
+ *
+ * @see CallLogDataSource class doc for complete lifecyle information
+ */
+ @WorkerThread
+ void onSuccessfulFill(Context appContext);
+
+ /**
+ * Combines raw annotated call log rows into a single coalesced row.
+ *
+ * <p>May be called by any worker thread at any time so implementations should take care to be
+ * threadsafe. (Ideally no state should be required to implement this.)
+ *
+ * @param individualRowsSortedByTimestampDesc group of fully populated rows from {@link
+ * AnnotatedCallLogContract.AnnotatedCallLog} which need to be combined for display purposes.
+ * This method should not modify this list.
+ * @return a partial {@link AnnotatedCallLogContract.CoalescedAnnotatedCallLog} row containing
+ * only columns which this data source is responsible for, which is the result of aggregating
+ * {@code individualRowsSortedByTimestampDesc}.
+ */
+ @WorkerThread
+ ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc);
@MainThread
void registerContentObservers(
diff --git a/java/com/android/dialer/calllog/datasources/CallLogMutations.java b/java/com/android/dialer/calllog/datasources/CallLogMutations.java
new file mode 100644
index 000000000..148601d68
--- /dev/null
+++ b/java/com/android/dialer/calllog/datasources/CallLogMutations.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.calllog.datasources;
+
+import android.content.ContentValues;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import com.android.dialer.common.Assert;
+
+/** A collection of mutations to the annotated call log. */
+public final class CallLogMutations {
+
+ private final ArrayMap<Long, ContentValues> inserts = new ArrayMap<>();
+ private final ArrayMap<Long, ContentValues> updates = new ArrayMap<>();
+ private final ArraySet<Long> deletes = new ArraySet<>();
+
+ /**
+ * @param contentValues an entire row not including the ID
+ * @throws IllegalStateException if this {@link CallLogMutations} already contains an insert,
+ * update, or delete with the provided id
+ */
+ public void insert(long id, ContentValues contentValues) {
+ Assert.checkArgument(!inserts.containsKey(id), "Can't insert row already scheduled for insert");
+ Assert.checkArgument(!updates.containsKey(id), "Can't insert row scheduled for update");
+ Assert.checkArgument(!deletes.contains(id), "Can't insert row scheduled for delete");
+
+ inserts.put(id, contentValues);
+ }
+
+ /**
+ * Stores a database update using the provided ID and content values. If this {@link
+ * CallLogMutations} object already contains an update with the specified ID, the existing content
+ * values are merged with the provided ones, with the provided ones overwriting the existing ones
+ * for values with the same key.
+ *
+ * @param contentValues the specific columns to update, not including the ID.
+ * @throws IllegalStateException if this {@link CallLogMutations} already contains an insert or
+ * delete with the provided id
+ */
+ public void update(long id, ContentValues contentValues) {
+ Assert.checkArgument(!inserts.containsKey(id), "Can't update row scheduled for insert");
+ Assert.checkArgument(!deletes.contains(id), "Can't delete row scheduled for delete");
+
+ ContentValues existingContentValues = updates.get(id);
+ if (existingContentValues != null) {
+ existingContentValues.putAll(contentValues);
+ } else {
+ updates.put(id, contentValues);
+ }
+ }
+
+ /**
+ * @throws IllegalStateException if this {@link CallLogMutations} already contains an insert,
+ * update, or delete with the provided id
+ */
+ public void delete(long id) {
+ Assert.checkArgument(!inserts.containsKey(id), "Can't delete row scheduled for insert");
+ Assert.checkArgument(!updates.containsKey(id), "Can't delete row scheduled for update");
+ Assert.checkArgument(!deletes.contains(id), "Can't delete row already scheduled for delete");
+
+ deletes.add(id);
+ }
+
+ public boolean isEmpty() {
+ return inserts.isEmpty() && updates.isEmpty() && deletes.isEmpty();
+ }
+
+ /**
+ * Get the pending inserts.
+ *
+ * @return the pending inserts where the key is the annotated call log database ID and the values
+ * are values to be inserted (not including the ID)
+ */
+ public ArrayMap<Long, ContentValues> getInserts() {
+ return inserts;
+ }
+
+ /**
+ * Get the pending updates.
+ *
+ * @return the pending updates where the key is the annotated call log database ID and the values
+ * are values to be updated (not including the ID)
+ */
+ public ArrayMap<Long, ContentValues> getUpdates() {
+ return updates;
+ }
+
+ /**
+ * Get the pending deletes.
+ *
+ * @return the annotated call log database IDs corresponding to the rows to be deleted
+ */
+ public ArraySet<Long> getDeletes() {
+ return deletes;
+ }
+}
diff --git a/java/com/android/dialer/calllog/datasources/DataSources.java b/java/com/android/dialer/calllog/datasources/DataSources.java
new file mode 100644
index 000000000..911ca3fa3
--- /dev/null
+++ b/java/com/android/dialer/calllog/datasources/DataSources.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.calllog.datasources;
+
+import com.android.dialer.calllog.datasources.systemcalllog.SystemCallLogDataSource;
+import java.util.List;
+
+/** Immutable lists of data sources used to populate the annotated call log. */
+public interface DataSources {
+
+ SystemCallLogDataSource getSystemCallLogDataSource();
+
+ List<CallLogDataSource> getDataSourcesIncludingSystemCallLog();
+
+ List<CallLogDataSource> getDataSourcesExcludingSystemCallLog();
+}
diff --git a/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java b/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java
index 355940f6a..82a85235b 100644
--- a/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java
@@ -16,13 +16,16 @@
package com.android.dialer.calllog.datasources.contacts;
+import android.content.ContentValues;
import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.MainThread;
import android.support.annotation.WorkerThread;
-import com.android.dialer.calllog.database.CallLogMutations;
+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 java.util.List;
import javax.inject.Inject;
/** Responsible for maintaining the contacts related columns in the annotated call log. */
@@ -44,11 +47,25 @@ public final class ContactsDataSource implements CallLogDataSource {
@Override
public void fill(
Context appContext,
- SQLiteDatabase readableDatabase,
- long lastRebuildTimeMillis,
CallLogMutations mutations) {
Assert.isWorkerThread();
// TODO: Implementation.
+ for (ContentValues contentValues : mutations.getInserts().values()) {
+ contentValues.put(AnnotatedCallLog.CONTACT_NAME, "Placeholder name");
+ }
+ }
+
+ @Override
+ public void onSuccessfulFill(Context appContext) {
+ // TODO: Implementation.
+ }
+
+ @Override
+ public ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc) {
+ // TODO: Implementation.
+ return new RowCombiner(individualRowsSortedByTimestampDesc)
+ .useSingleValueString(AnnotatedCallLog.CONTACT_NAME)
+ .combine();
}
@MainThread
diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
index ea6663fbe..f2063283f 100644
--- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
@@ -16,28 +16,54 @@
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.DialerPhoneNumber;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog;
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.phonenumberproto.DialerPhoneNumberUtil;
import com.android.dialer.util.PermissionsUtil;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.protobuf.InvalidProtocolBufferException;
+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 +73,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 +105,222 @@ 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)
+ .apply();
+ }
+ }
+
+ @Override
+ public ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc) {
+ // TODO: Complete implementation.
+ ContentValues coalescedValues =
+ new RowCombiner(individualRowsSortedByTimestampDesc)
+ .useMostRecentLong(AnnotatedCallLog.TIMESTAMP)
+ .combine();
+
+ // All phone numbers in the provided group should be equivalent (but could be formatted
+ // differently). Arbitrarily show the raw phone number of the most recent call.
+ DialerPhoneNumber mostRecentPhoneNumber =
+ getMostRecentPhoneNumber(individualRowsSortedByTimestampDesc);
+ coalescedValues.put(
+ CoalescedAnnotatedCallLog.FORMATTED_NUMBER,
+ mostRecentPhoneNumber.getRawInput().getNumber());
+ return coalescedValues;
+ }
+
+ private static DialerPhoneNumber getMostRecentPhoneNumber(
+ List<ContentValues> individualRowsSortedByTimestampDesc) {
+ DialerPhoneNumber dialerPhoneNumber;
+ byte[] protoBytes =
+ individualRowsSortedByTimestampDesc.get(0).getAsByteArray(AnnotatedCallLog.NUMBER);
+ try {
+ dialerPhoneNumber = DialerPhoneNumber.parseFrom(protoBytes);
+ } catch (InvalidProtocolBufferException e) {
+ throw Assert.createAssertionFailException("couldn't parse DialerPhoneNumber", e);
+ }
+ return dialerPhoneNumber;
+ }
+
+ @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);
+
+ DialerPhoneNumberUtil dialerPhoneNumberUtil =
+ new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance());
+
+ // TODO: Really should be getting last 1000 by timestamp, not by last modified.
+ try (Cursor cursor =
+ appContext
+ .getContentResolver()
+ .query(
+ Calls.CONTENT_URI, // Excludes voicemail
+ new String[] {
+ Calls._ID, Calls.DATE, Calls.LAST_MODIFIED, Calls.NUMBER, Calls.COUNTRY_ISO
+ },
+ Calls.LAST_MODIFIED + " > ?",
+ new String[] {String.valueOf(previousTimestampProcessed)},
+ Calls.LAST_MODIFIED + " DESC LIMIT 1000")) {
- // TODO: Implementation.
+ if (cursor == null) {
+ LogUtil.e("SystemCallLogDataSource.handleInsertsAndUpdates", "null cursor");
+ return;
+ }
+
+ LogUtil.i(
+ "SystemCallLogDataSource.handleInsertsAndUpdates",
+ "found %d entries to insert/update",
+ cursor.getCount());
+
+ if (cursor.moveToFirst()) {
+ int idColumn = cursor.getColumnIndexOrThrow(Calls._ID);
+ int dateColumn = cursor.getColumnIndexOrThrow(Calls.DATE);
+ int lastModifiedColumn = cursor.getColumnIndexOrThrow(Calls.LAST_MODIFIED);
+ int numberColumn = cursor.getColumnIndexOrThrow(Calls.NUMBER);
+ int countryIsoColumn = cursor.getColumnIndexOrThrow(Calls.COUNTRY_ISO);
+
+ // 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);
+ String numberAsStr = cursor.getString(numberColumn);
+ String countryIso = cursor.getString(countryIsoColumn);
+
+ byte[] numberAsProtoBytes =
+ dialerPhoneNumberUtil.parse(numberAsStr, countryIso).toByteArray();
+
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(AnnotatedCallLog.TIMESTAMP, date);
+ contentValues.put(AnnotatedCallLog.NUMBER, numberAsProtoBytes);
+
+ 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 matching 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 {
diff --git a/java/com/android/dialer/calllog/datasources/util/RowCombiner.java b/java/com/android/dialer/calllog/datasources/util/RowCombiner.java
new file mode 100644
index 000000000..0c7be1e27
--- /dev/null
+++ b/java/com/android/dialer/calllog/datasources/util/RowCombiner.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.dialer.calllog.datasources.util;
+
+import android.content.ContentValues;
+import com.android.dialer.common.Assert;
+import java.util.Iterator;
+import java.util.List;
+
+/** Convenience class for aggregating row values. */
+public class RowCombiner {
+ private final List<ContentValues> individualRowsSortedByTimestampDesc;
+ private final ContentValues combinedRow = new ContentValues();
+
+ public RowCombiner(List<ContentValues> individualRowsSortedByTimestampDesc) {
+ Assert.checkArgument(!individualRowsSortedByTimestampDesc.isEmpty());
+ this.individualRowsSortedByTimestampDesc = individualRowsSortedByTimestampDesc;
+ }
+
+ /** Use the most recent value for the specified column. */
+ public RowCombiner useMostRecentLong(String columnName) {
+ combinedRow.put(columnName, individualRowsSortedByTimestampDesc.get(0).getAsLong(columnName));
+ return this;
+ }
+
+ /** Asserts that all column values for the given column name are the same, and uses it. */
+ public RowCombiner useSingleValueString(String columnName) {
+ Iterator<ContentValues> iterator = individualRowsSortedByTimestampDesc.iterator();
+ String singleValue = iterator.next().getAsString(columnName);
+ while (iterator.hasNext()) {
+ Assert.checkState(iterator.next().getAsString(columnName).equals(singleValue));
+ }
+ combinedRow.put(columnName, singleValue);
+ return this;
+ }
+
+ public ContentValues combine() {
+ return combinedRow;
+ }
+}