summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/calllog/database
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/dialer/calllog/database')
-rw-r--r--java/com/android/dialer/calllog/database/AndroidManifest.xml28
-rw-r--r--java/com/android/dialer/calllog/database/AnnotatedCallLog.java53
-rw-r--r--java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java333
-rw-r--r--java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java64
-rw-r--r--java/com/android/dialer/calllog/database/CallLogDatabaseComponent.java40
-rw-r--r--java/com/android/dialer/calllog/database/CallLogMutations.java58
-rw-r--r--java/com/android/dialer/calllog/database/Coalescer.java194
-rw-r--r--java/com/android/dialer/calllog/database/MutationApplier.java105
-rw-r--r--java/com/android/dialer/calllog/database/annotated_call_log.proto15
-rw-r--r--java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java259
10 files changed, 1025 insertions, 124 deletions
diff --git a/java/com/android/dialer/calllog/database/AndroidManifest.xml b/java/com/android/dialer/calllog/database/AndroidManifest.xml
new file mode 100644
index 000000000..396a6d9a1
--- /dev/null
+++ b/java/com/android/dialer/calllog/database/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ 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
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.calllog.database">
+
+ <application>
+
+ <provider
+ android:authorities="com.android.dialer.annotatedcalllog"
+ android:exported="false"
+ android:multiprocess="false"
+ android:name=".AnnotatedCallLogContentProvider"/>
+
+ </application>
+</manifest>
diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLog.java b/java/com/android/dialer/calllog/database/AnnotatedCallLog.java
deleted file mode 100644
index 7dca44a60..000000000
--- a/java/com/android/dialer/calllog/database/AnnotatedCallLog.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.database;
-
-import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
-import android.support.annotation.WorkerThread;
-import com.android.dialer.common.Assert;
-
-/** Static methods and constants for interacting with the annotated call log table. */
-public final class AnnotatedCallLog {
-
- private static final String DATABASE_NAME = "annotated_call_log.db";
-
- public static final String TABLE_NAME = "AnnotatedCallLog";
-
- /** Column names for the annotated call log table. */
- public static final class Columns {
- public static final String ID = "_id";
- public static final String TIMESTAMP = "timestamp";
- public static final String CONTACT_NAME = "contact_name";
- }
-
- private AnnotatedCallLog() {}
-
- @WorkerThread
- public static SQLiteDatabase getWritableDatabase(Context appContext) {
- Assert.isWorkerThread();
-
- return new AnnotatedCallLogDatabaseHelper(appContext, DATABASE_NAME).getWritableDatabase();
- }
-
- @WorkerThread
- public static SQLiteDatabase getReadableDatabase(Context appContext) {
- Assert.isWorkerThread();
-
- return new AnnotatedCallLogDatabaseHelper(appContext, DATABASE_NAME).getReadableDatabase();
- }
-}
diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java
new file mode 100644
index 000000000..9a3d2e20f
--- /dev/null
+++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java
@@ -0,0 +1,333 @@
+/*
+ * 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.database;
+
+import android.annotation.TargetApi;
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+
+/** {@link ContentProvider} for the annotated call log. */
+public class AnnotatedCallLogContentProvider extends ContentProvider {
+
+ /**
+ * We sometimes run queries where we potentially pass every ID into a where clause using the
+ * (?,?,?,...) syntax. The maximum number of host parameters is 999, so that's the maximum size
+ * this table can be. See https://www.sqlite.org/limits.html for more details.
+ */
+ private static final int MAX_ROWS = 999;
+
+ private static final int ANNOTATED_CALL_LOG_TABLE_CODE = 1;
+ private static final int ANNOTATED_CALL_LOG_TABLE_ID_CODE = 2;
+ private static final int COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE = 3;
+
+ private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ static {
+ uriMatcher.addURI(
+ AnnotatedCallLogContract.AUTHORITY, AnnotatedCallLog.TABLE, ANNOTATED_CALL_LOG_TABLE_CODE);
+ uriMatcher.addURI(
+ AnnotatedCallLogContract.AUTHORITY,
+ AnnotatedCallLog.TABLE + "/#",
+ ANNOTATED_CALL_LOG_TABLE_ID_CODE);
+ uriMatcher.addURI(
+ AnnotatedCallLogContract.AUTHORITY,
+ CoalescedAnnotatedCallLog.TABLE,
+ COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE);
+ }
+
+ private AnnotatedCallLogDatabaseHelper databaseHelper;
+ private Coalescer coalescer;
+
+ private final ThreadLocal<Boolean> applyingBatch = new ThreadLocal<>();
+
+ /** Ensures that only a single notification is generated from {@link #applyBatch(ArrayList)}. */
+ private boolean isApplyingBatch() {
+ return applyingBatch.get() != null && applyingBatch.get();
+ }
+
+ @Override
+ public boolean onCreate() {
+ databaseHelper = new AnnotatedCallLogDatabaseHelper(getContext(), MAX_ROWS);
+ coalescer = CallLogDatabaseComponent.get(getContext()).coalescer();
+ return true;
+ }
+
+ @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources
+ @Nullable
+ @Override
+ public Cursor query(
+ @NonNull Uri uri,
+ @Nullable String[] projection,
+ @Nullable String selection,
+ @Nullable String[] selectionArgs,
+ @Nullable String sortOrder) {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
+ queryBuilder.setTables(AnnotatedCallLog.TABLE);
+ int match = uriMatcher.match(uri);
+ switch (match) {
+ case ANNOTATED_CALL_LOG_TABLE_ID_CODE:
+ queryBuilder.appendWhere(AnnotatedCallLog._ID + "=" + ContentUris.parseId(uri));
+ // fall through
+ case ANNOTATED_CALL_LOG_TABLE_CODE:
+ Cursor cursor =
+ queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
+ if (cursor != null) {
+ cursor.setNotificationUri(
+ getContext().getContentResolver(), AnnotatedCallLog.CONTENT_URI);
+ } else {
+ LogUtil.w("AnnotatedCallLogContentProvider.query", "cursor was null");
+ }
+ return cursor;
+ case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE:
+ Assert.checkArgument(
+ projection == CoalescedAnnotatedCallLog.ALL_COLUMNS,
+ "only ALL_COLUMNS projection supported for coalesced call log");
+ Assert.checkArgument(selection == null, "selection not supported for coalesced call log");
+ Assert.checkArgument(
+ selectionArgs == null, "selection args not supported for coalesced call log");
+ Assert.checkArgument(sortOrder == null, "sort order not supported for coalesced call log");
+ try (Cursor allAnnotatedCallLogRows =
+ queryBuilder.query(
+ db, null, null, null, null, null, AnnotatedCallLog.TIMESTAMP + " DESC")) {
+ Cursor coalescedRows = coalescer.coalesce(allAnnotatedCallLogRows);
+ coalescedRows.setNotificationUri(
+ getContext().getContentResolver(), CoalescedAnnotatedCallLog.CONTENT_URI);
+ return coalescedRows;
+ }
+ default:
+ throw new IllegalArgumentException("Unknown uri: " + uri);
+ }
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ return AnnotatedCallLog.CONTENT_ITEM_TYPE;
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+ // Javadoc states values is not nullable, even though it is annotated as such (b/38123194)!
+ Assert.checkArgument(values != null);
+
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+ int match = uriMatcher.match(uri);
+ switch (match) {
+ case ANNOTATED_CALL_LOG_TABLE_CODE:
+ Assert.checkArgument(
+ values.get(AnnotatedCallLog._ID) != null, "You must specify an _ID when inserting");
+ break;
+ case ANNOTATED_CALL_LOG_TABLE_ID_CODE:
+ Long idFromUri = ContentUris.parseId(uri);
+ Long idFromValues = values.getAsLong(AnnotatedCallLog._ID);
+ Assert.checkArgument(
+ idFromValues == null || idFromValues.equals(idFromUri),
+ "_ID from values %d does not match ID from URI: %s",
+ idFromValues,
+ uri);
+ if (idFromValues == null) {
+ values.put(AnnotatedCallLog._ID, idFromUri);
+ }
+ break;
+ case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE:
+ throw new UnsupportedOperationException("coalesced call log does not support inserting");
+ default:
+ throw new IllegalArgumentException("Unknown uri: " + uri);
+ }
+ long id = database.insert(AnnotatedCallLog.TABLE, null, values);
+ if (id < 0) {
+ LogUtil.w(
+ "AnnotatedCallLogContentProvider.insert",
+ "error inserting row with id: %d",
+ values.get(AnnotatedCallLog._ID));
+ return null;
+ }
+ Uri insertedUri = ContentUris.withAppendedId(AnnotatedCallLog.CONTENT_URI, id);
+ if (!isApplyingBatch()) {
+ notifyChange(insertedUri);
+ }
+ return insertedUri;
+ }
+
+ @Override
+ public int delete(
+ @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+ final int match = uriMatcher.match(uri);
+ switch (match) {
+ case ANNOTATED_CALL_LOG_TABLE_CODE:
+ break;
+ case ANNOTATED_CALL_LOG_TABLE_ID_CODE:
+ Assert.checkArgument(selection == null, "Do not specify selection when deleting by ID");
+ Assert.checkArgument(
+ selectionArgs == null, "Do not specify selection args when deleting by ID");
+ long id = ContentUris.parseId(uri);
+ Assert.checkArgument(id != -1, "error parsing id from uri %s", uri);
+ selection = getSelectionWithId(id);
+ break;
+ case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE:
+ throw new UnsupportedOperationException("coalesced call log does not support deleting");
+ default:
+ throw new IllegalArgumentException("Unknown uri: " + uri);
+ }
+ int rows = database.delete(AnnotatedCallLog.TABLE, selection, selectionArgs);
+ if (rows > 0) {
+ if (!isApplyingBatch()) {
+ notifyChange(uri);
+ }
+ } else {
+ LogUtil.w("AnnotatedCallLogContentProvider.delete", "no rows deleted");
+ }
+ return rows;
+ }
+
+ @Override
+ public int update(
+ @NonNull Uri uri,
+ @Nullable ContentValues values,
+ @Nullable String selection,
+ @Nullable String[] selectionArgs) {
+ // Javadoc states values is not nullable, even though it is annotated as such (b/38123194)!
+ Assert.checkArgument(values != null);
+
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+ int match = uriMatcher.match(uri);
+ switch (match) {
+ case ANNOTATED_CALL_LOG_TABLE_CODE:
+ break;
+ case ANNOTATED_CALL_LOG_TABLE_ID_CODE:
+ Assert.checkArgument(
+ !values.containsKey(AnnotatedCallLog._ID), "Do not specify _ID when updating by ID");
+ Assert.checkArgument(selection == null, "Do not specify selection when updating by ID");
+ Assert.checkArgument(
+ selectionArgs == null, "Do not specify selection args when updating by ID");
+ selection = getSelectionWithId(ContentUris.parseId(uri));
+ break;
+ case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE:
+ throw new UnsupportedOperationException("coalesced call log does not support updating");
+ default:
+ throw new IllegalArgumentException("Unknown uri: " + uri);
+ }
+ int rows = database.update(AnnotatedCallLog.TABLE, values, selection, selectionArgs);
+ if (rows > 0) {
+ if (!isApplyingBatch()) {
+ notifyChange(uri);
+ }
+ } else {
+ LogUtil.w("AnnotatedCallLogContentProvider.update", "no rows updated");
+ }
+ return rows;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Note: When applyBatch is used with the AnnotatedCallLog, only a single notification for the
+ * content URI is generated, not individual notifications for each affected URI.
+ */
+ @NonNull
+ @Override
+ public ContentProviderResult[] applyBatch(@NonNull ArrayList<ContentProviderOperation> operations)
+ throws OperationApplicationException {
+ ContentProviderResult[] results = new ContentProviderResult[operations.size()];
+ if (operations.isEmpty()) {
+ return results;
+ }
+
+ SQLiteDatabase database = databaseHelper.getWritableDatabase();
+ try {
+ applyingBatch.set(true);
+ database.beginTransaction();
+ for (int i = 0; i < operations.size(); i++) {
+ ContentProviderOperation operation = operations.get(i);
+ int match = uriMatcher.match(operation.getUri());
+ switch (match) {
+ case ANNOTATED_CALL_LOG_TABLE_CODE:
+ case ANNOTATED_CALL_LOG_TABLE_ID_CODE:
+ // These are allowed values, continue.
+ break;
+ case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE:
+ throw new UnsupportedOperationException(
+ "coalesced call log does not support applyBatch");
+ default:
+ throw new IllegalArgumentException("Unknown uri: " + operation.getUri());
+ }
+ ContentProviderResult result = operation.apply(this, results, i);
+ if (operations.get(i).isInsert()) {
+ if (result.uri == null) {
+ throw new OperationApplicationException("error inserting row");
+ }
+ } else if (result.count == 0) {
+ /*
+ * The batches built by MutationApplier happen to contain operations in order of:
+ *
+ * 1. Inserts
+ * 2. Updates
+ * 3. Deletes
+ *
+ * Let's say the last row in the table is row Z, and MutationApplier wishes to update it,
+ * as well as insert row A. When row A gets inserted, row Z will be deleted via the
+ * trigger if the table is full. Then later, when we try to process the update for row Z,
+ * it won't exist.
+ */
+ LogUtil.w(
+ "AnnotatedCallLogContentProvider.applyBatch",
+ "update or delete failed, possibly because row got cleaned up");
+ }
+ results[i] = result;
+ }
+ database.setTransactionSuccessful();
+ } finally {
+ applyingBatch.set(false);
+ database.endTransaction();
+ }
+ notifyChange(AnnotatedCallLog.CONTENT_URI);
+ return results;
+ }
+
+ private String getSelectionWithId(long id) {
+ return AnnotatedCallLog._ID + "=" + id;
+ }
+
+ private void notifyChange(Uri uri) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ // Any time the annotated call log changes, we need to also notify observers of the
+ // CoalescedAnnotatedCallLog, since that is just a massaged in-memory view of the real annotated
+ // call log table.
+ getContext().getContentResolver().notifyChange(CoalescedAnnotatedCallLog.CONTENT_URI, null);
+ }
+}
diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
index 7b28e5505..e1ec0f6b1 100644
--- a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
+++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
@@ -16,37 +16,75 @@
package com.android.dialer.calllog.database;
-import static com.android.dialer.calllog.database.AnnotatedCallLog.Columns.CONTACT_NAME;
-import static com.android.dialer.calllog.database.AnnotatedCallLog.Columns.ID;
-import static com.android.dialer.calllog.database.AnnotatedCallLog.Columns.TIMESTAMP;
-
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
import com.android.dialer.common.LogUtil;
+import java.util.Locale;
/** {@link SQLiteOpenHelper} for the AnnotatedCallLog database. */
class AnnotatedCallLogDatabaseHelper extends SQLiteOpenHelper {
+ private final int maxRows;
- AnnotatedCallLogDatabaseHelper(Context appContext, String databaseName) {
- super(appContext, databaseName, null, 1);
+ AnnotatedCallLogDatabaseHelper(Context appContext, int maxRows) {
+ super(appContext, "annotated_call_log.db", null, 1);
+ this.maxRows = maxRows;
}
- private static final String CREATE_SQL =
+ private static final String CREATE_TABLE_SQL =
new StringBuilder()
- .append("create table if not exists " + AnnotatedCallLog.TABLE_NAME + " (")
- .append(ID + " integer primary key, ")
- .append(TIMESTAMP + " integer, ")
- .append(CONTACT_NAME + " string")
+ .append("create table if not exists " + AnnotatedCallLog.TABLE + " (")
+ // Common columns.
+ .append(AnnotatedCallLog._ID + " integer primary key, ")
+ .append(AnnotatedCallLog.TIMESTAMP + " integer, ")
+ .append(AnnotatedCallLog.NAME + " string, ")
+ .append(AnnotatedCallLog.FORMATTED_NUMBER + " string, ")
+ .append(AnnotatedCallLog.PHOTO_URI + " string, ")
+ .append(AnnotatedCallLog.PHOTO_ID + " integer, ")
+ .append(AnnotatedCallLog.LOOKUP_URI + " string, ")
+ .append(AnnotatedCallLog.NUMBER_TYPE_LABEL + " string, ")
+ .append(AnnotatedCallLog.IS_READ + " integer, ")
+ .append(AnnotatedCallLog.NEW + " integer, ")
+ .append(AnnotatedCallLog.GEOCODED_LOCATION + " string, ")
+ .append(AnnotatedCallLog.PHONE_ACCOUNT_LABEL + " string, ")
+ .append(AnnotatedCallLog.PHONE_ACCOUNT_COLOR + " integer, ")
+ .append(AnnotatedCallLog.FEATURES + " integer, ")
+ .append(AnnotatedCallLog.IS_BUSINESS + " integer, ")
+ .append(AnnotatedCallLog.IS_VOICEMAIL + " integer, ")
+ // Columns only in AnnotatedCallLog
+ .append(AnnotatedCallLog.NUMBER + " blob, ")
+ .append(AnnotatedCallLog.TYPE + " integer")
.append(");")
.toString();
+ /** Deletes all but the first maxRows rows (by timestamp) to keep the table a manageable size. */
+ private static final String CREATE_TRIGGER_SQL =
+ "create trigger delete_old_rows after insert on "
+ + AnnotatedCallLog.TABLE
+ + " when (select count(*) from "
+ + AnnotatedCallLog.TABLE
+ + ") > %d"
+ + " begin delete from "
+ + AnnotatedCallLog.TABLE
+ + " where "
+ + AnnotatedCallLog._ID
+ + " in (select "
+ + AnnotatedCallLog._ID
+ + " from "
+ + AnnotatedCallLog.TABLE
+ + " order by timestamp limit (select count(*)-%d"
+ + " from "
+ + AnnotatedCallLog.TABLE
+ + " )); end;";
+
@Override
public void onCreate(SQLiteDatabase db) {
LogUtil.enterBlock("AnnotatedCallLogDatabaseHelper.onCreate");
long startTime = System.currentTimeMillis();
- db.execSQL(CREATE_SQL);
- // TODO: Consider logging impression.
+ db.execSQL(CREATE_TABLE_SQL);
+ db.execSQL(String.format(Locale.US, CREATE_TRIGGER_SQL, maxRows, maxRows));
+ // TODO(zachh): Consider logging impression.
LogUtil.i(
"AnnotatedCallLogDatabaseHelper.onCreate",
"took: %dms",
diff --git a/java/com/android/dialer/calllog/database/CallLogDatabaseComponent.java b/java/com/android/dialer/calllog/database/CallLogDatabaseComponent.java
new file mode 100644
index 000000000..ede46911c
--- /dev/null
+++ b/java/com/android/dialer/calllog/database/CallLogDatabaseComponent.java
@@ -0,0 +1,40 @@
+/*
+ * 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.database;
+
+import android.content.Context;
+import com.android.dialer.inject.HasRootComponent;
+import dagger.Subcomponent;
+
+/** Dagger component for database package. */
+@Subcomponent
+public abstract class CallLogDatabaseComponent {
+
+ public abstract Coalescer coalescer();
+
+ public abstract MutationApplier mutationApplier();
+
+ public static CallLogDatabaseComponent get(Context context) {
+ return ((CallLogDatabaseComponent.HasComponent)
+ ((HasRootComponent) context.getApplicationContext()).component())
+ .callLogDatabaseComponent();
+ }
+
+ /** Used to refer to the root application component. */
+ public interface HasComponent {
+ CallLogDatabaseComponent callLogDatabaseComponent();
+ }
+}
diff --git a/java/com/android/dialer/calllog/database/CallLogMutations.java b/java/com/android/dialer/calllog/database/CallLogMutations.java
deleted file mode 100644
index ec020c6af..000000000
--- a/java/com/android/dialer/calllog/database/CallLogMutations.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.database;
-
-import android.content.ContentValues;
-import android.database.sqlite.SQLiteDatabase;
-import android.support.annotation.WorkerThread;
-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<Integer, ContentValues> inserts = new ArrayMap<>();
- private final ArrayMap<Integer, ContentValues> updates = new ArrayMap<>();
- private final ArraySet<Integer> deletes = new ArraySet<>();
-
- /** @param contentValues an entire row not including the ID */
- public void insert(int id, ContentValues contentValues) {
- inserts.put(id, contentValues);
- }
-
- /** @param contentValues the specific columns to update, not including the ID. */
- public void update(int id, ContentValues contentValues) {
- // TODO: Consider merging automatically.
- updates.put(id, contentValues);
- }
-
- public void delete(int id) {
- deletes.add(id);
- }
-
- public boolean isEmpty() {
- return inserts.isEmpty() && updates.isEmpty() && deletes.isEmpty();
- }
-
- @WorkerThread
- public void applyToDatabase(SQLiteDatabase writableDatabase) {
- Assert.isWorkerThread();
-
- // TODO: Implementation.
- }
-}
diff --git a/java/com/android/dialer/calllog/database/Coalescer.java b/java/com/android/dialer/calllog/database/Coalescer.java
new file mode 100644
index 000000000..63fa9f828
--- /dev/null
+++ b/java/com/android/dialer/calllog/database/Coalescer.java
@@ -0,0 +1,194 @@
+/*
+ * 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.database;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+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.DataSources;
+import com.android.dialer.common.Assert;
+import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import javax.inject.Inject;
+
+/**
+ * Coalesces call log rows by combining some adjacent rows.
+ *
+ * <p>Applies the business which logic which determines which adjacent rows should be coalasced, and
+ * then delegates to each data source to determine how individual columns should be aggregated.
+ */
+public class Coalescer {
+
+ private final DataSources dataSources;
+
+ @Inject
+ Coalescer(DataSources dataSources) {
+ this.dataSources = dataSources;
+ }
+
+ /**
+ * Reads the entire {@link AnnotatedCallLog} database into memory from the provided {@code
+ * allAnnotatedCallLog} parameter and then builds and returns a new {@link MatrixCursor} which is
+ * the result of combining adjacent rows which should be collapsed for display purposes.
+ *
+ * @param allAnnotatedCallLogRowsSortedByTimestampDesc all {@link AnnotatedCallLog} rows, sorted
+ * by timestamp descending
+ * @return a new {@link MatrixCursor} containing the {@link CoalescedAnnotatedCallLog} rows to
+ * display
+ */
+ @WorkerThread
+ @NonNull
+ Cursor coalesce(@NonNull Cursor allAnnotatedCallLogRowsSortedByTimestampDesc) {
+ Assert.isWorkerThread();
+
+ // Note: This method relies on rowsShouldBeCombined to determine which rows should be combined,
+ // but delegates to data sources to actually aggregate column values.
+
+ DialerPhoneNumberUtil dialerPhoneNumberUtil =
+ new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance());
+
+ MatrixCursor allCoalescedRowsMatrixCursor =
+ new MatrixCursor(
+ CoalescedAnnotatedCallLog.ALL_COLUMNS,
+ Assert.isNotNull(allAnnotatedCallLogRowsSortedByTimestampDesc).getCount());
+
+ if (allAnnotatedCallLogRowsSortedByTimestampDesc.moveToFirst()) {
+ int coalescedRowId = 0;
+
+ List<ContentValues> currentRowGroup = new ArrayList<>();
+
+ do {
+ ContentValues currentRow =
+ cursorRowToContentValues(allAnnotatedCallLogRowsSortedByTimestampDesc);
+
+ if (currentRowGroup.isEmpty()) {
+ currentRowGroup.add(currentRow);
+ continue;
+ }
+
+ ContentValues previousRow = currentRowGroup.get(currentRowGroup.size() - 1);
+
+ if (!rowsShouldBeCombined(dialerPhoneNumberUtil, previousRow, currentRow)) {
+ ContentValues coalescedRow = coalesceRowsForAllDataSources(currentRowGroup);
+ coalescedRow.put(CoalescedAnnotatedCallLog.NUMBER_CALLS, currentRowGroup.size());
+ addContentValuesToMatrixCursor(
+ coalescedRow, allCoalescedRowsMatrixCursor, coalescedRowId++);
+ currentRowGroup.clear();
+ }
+ currentRowGroup.add(currentRow);
+ } while (allAnnotatedCallLogRowsSortedByTimestampDesc.moveToNext());
+
+ // Deal with leftover rows.
+ ContentValues coalescedRow = coalesceRowsForAllDataSources(currentRowGroup);
+ coalescedRow.put(CoalescedAnnotatedCallLog.NUMBER_CALLS, currentRowGroup.size());
+ addContentValuesToMatrixCursor(coalescedRow, allCoalescedRowsMatrixCursor, coalescedRowId);
+ }
+ return allCoalescedRowsMatrixCursor;
+ }
+
+ private static ContentValues cursorRowToContentValues(Cursor cursor) {
+ ContentValues values = new ContentValues();
+ String[] columns = cursor.getColumnNames();
+ int length = columns.length;
+ for (int i = 0; i < length; i++) {
+ if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) {
+ values.put(columns[i], cursor.getBlob(i));
+ } else {
+ values.put(columns[i], cursor.getString(i));
+ }
+ }
+ return values;
+ }
+
+ /**
+ * @param row1 a row from {@link AnnotatedCallLog}
+ * @param row2 a row from {@link AnnotatedCallLog}
+ */
+ private static boolean rowsShouldBeCombined(
+ DialerPhoneNumberUtil dialerPhoneNumberUtil, ContentValues row1, ContentValues row2) {
+ // Don't combine rows which don't use the same phone account.
+ if (!Objects.equals(
+ row1.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_LABEL),
+ row2.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_LABEL))) {
+ return false;
+ }
+ DialerPhoneNumber number1;
+ DialerPhoneNumber number2;
+ try {
+ byte[] number1Bytes = row1.getAsByteArray(AnnotatedCallLog.NUMBER);
+ byte[] number2Bytes = row2.getAsByteArray(AnnotatedCallLog.NUMBER);
+
+ if (number1Bytes == null || number2Bytes == null) {
+ // Empty numbers should not be combined.
+ return false;
+ }
+
+ number1 = DialerPhoneNumber.parseFrom(number1Bytes);
+ number2 = DialerPhoneNumber.parseFrom(number2Bytes);
+ } catch (InvalidProtocolBufferException e) {
+ throw Assert.createAssertionFailException("error parsing DialerPhoneNumber proto", e);
+ }
+
+ if (!number1.hasDialerInternalPhoneNumber() && !number2.hasDialerInternalPhoneNumber()) {
+ // Empty numbers should not be combined.
+ return false;
+ }
+
+ if (!number1.hasDialerInternalPhoneNumber() || !number2.hasDialerInternalPhoneNumber()) {
+ // An empty number should not be combined with a non-empty number.
+ return false;
+ }
+ return dialerPhoneNumberUtil.isExactMatch(number1, number2);
+ }
+
+ /**
+ * Delegates to data sources to aggregate individual columns to create a new coalesced row.
+ *
+ * @param individualRows {@link AnnotatedCallLog} rows sorted by timestamp descending
+ * @return a {@link CoalescedAnnotatedCallLog} row
+ */
+ private ContentValues coalesceRowsForAllDataSources(List<ContentValues> individualRows) {
+ ContentValues coalescedValues = new ContentValues();
+ for (CallLogDataSource dataSource : dataSources.getDataSourcesIncludingSystemCallLog()) {
+ coalescedValues.putAll(dataSource.coalesce(individualRows));
+ }
+ return coalescedValues;
+ }
+
+ /**
+ * @param contentValues a {@link CoalescedAnnotatedCallLog} row
+ * @param matrixCursor represents {@link CoalescedAnnotatedCallLog}
+ */
+ private static void addContentValuesToMatrixCursor(
+ ContentValues contentValues, MatrixCursor matrixCursor, int rowId) {
+ MatrixCursor.RowBuilder rowBuilder = matrixCursor.newRow();
+ rowBuilder.add(CoalescedAnnotatedCallLog._ID, rowId);
+ for (Map.Entry<String, Object> entry : contentValues.valueSet()) {
+ rowBuilder.add(entry.getKey(), entry.getValue());
+ }
+ }
+}
diff --git a/java/com/android/dialer/calllog/database/MutationApplier.java b/java/com/android/dialer/calllog/database/MutationApplier.java
new file mode 100644
index 000000000..21c8a507d
--- /dev/null
+++ b/java/com/android/dialer/calllog/database/MutationApplier.java
@@ -0,0 +1,105 @@
+/*
+ * 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.database;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.os.RemoteException;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
+import com.android.dialer.calllog.datasources.CallLogMutations;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map.Entry;
+import javax.inject.Inject;
+
+/** Applies {@link CallLogMutations} to the annotated call log. */
+public class MutationApplier {
+
+ @Inject
+ MutationApplier() {}
+
+ /** Applies the provided {@link CallLogMutations} to the annotated call log. */
+ @WorkerThread
+ public void applyToDatabase(CallLogMutations mutations, Context appContext)
+ throws RemoteException, OperationApplicationException {
+ Assert.isWorkerThread();
+
+ if (mutations.isEmpty()) {
+ return;
+ }
+
+ ArrayList<ContentProviderOperation> operations = new ArrayList<>();
+
+ if (!mutations.getInserts().isEmpty()) {
+ LogUtil.i(
+ "CallLogMutations.applyToDatabase", "inserting %d rows", mutations.getInserts().size());
+ for (Entry<Long, ContentValues> entry : mutations.getInserts().entrySet()) {
+ long id = entry.getKey();
+ ContentValues contentValues = entry.getValue();
+ operations.add(
+ ContentProviderOperation.newInsert(
+ ContentUris.withAppendedId(AnnotatedCallLog.CONTENT_URI, id))
+ .withValues(contentValues)
+ .build());
+ }
+ }
+
+ if (!mutations.getUpdates().isEmpty()) {
+ LogUtil.i(
+ "CallLogMutations.applyToDatabase", "updating %d rows", mutations.getUpdates().size());
+ for (Entry<Long, ContentValues> entry : mutations.getUpdates().entrySet()) {
+ long id = entry.getKey();
+ ContentValues contentValues = entry.getValue();
+ operations.add(
+ ContentProviderOperation.newUpdate(
+ ContentUris.withAppendedId(AnnotatedCallLog.CONTENT_URI, id))
+ .withValues(contentValues)
+ .build());
+ }
+ }
+
+ if (!mutations.getDeletes().isEmpty()) {
+ LogUtil.i(
+ "CallLogMutations.applyToDatabase", "deleting %d rows", mutations.getDeletes().size());
+ String[] questionMarks = new String[mutations.getDeletes().size()];
+ Arrays.fill(questionMarks, "?");
+
+ String whereClause =
+ (AnnotatedCallLog._ID + " in (") + TextUtils.join(",", questionMarks) + ")";
+
+ String[] whereArgs = new String[mutations.getDeletes().size()];
+ int i = 0;
+ for (long id : mutations.getDeletes()) {
+ whereArgs[i++] = String.valueOf(id);
+ }
+
+ operations.add(
+ ContentProviderOperation.newDelete(AnnotatedCallLog.CONTENT_URI)
+ .withSelection(whereClause, whereArgs)
+ .build());
+ }
+
+ appContext.getContentResolver().applyBatch(AnnotatedCallLogContract.AUTHORITY, operations);
+ }
+}
diff --git a/java/com/android/dialer/calllog/database/annotated_call_log.proto b/java/com/android/dialer/calllog/database/annotated_call_log.proto
new file mode 100644
index 000000000..de2bc5f14
--- /dev/null
+++ b/java/com/android/dialer/calllog/database/annotated_call_log.proto
@@ -0,0 +1,15 @@
+syntax = "proto2";
+
+option java_package = "com.android.dialer";
+option java_multiple_files = true;
+option optimize_for = LITE_RUNTIME;
+
+// DIALER_SCRUB.UNCOMMENT_IN_OPEN_SOURCE option optimize_for = LITE_RUNTIME;
+
+package com.android.dialer;
+
+// A list of android.provider.CallLog.Calls.TYPE values ordered from newest to
+// oldest.
+message CallTypes {
+ repeated int32 type = 1;
+}
diff --git a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
new file mode 100644
index 000000000..25950f6b9
--- /dev/null
+++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
@@ -0,0 +1,259 @@
+/*
+ * 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.database.contract;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+import com.android.dialer.constants.Constants;
+import java.util.Arrays;
+
+/** Contract for the AnnotatedCallLog content provider. */
+public class AnnotatedCallLogContract {
+ public static final String AUTHORITY = Constants.get().getAnnotatedCallLogProviderAuthority();
+
+ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
+
+ /**
+ * Columns shared by {@link AnnotatedCallLog} and {@link CoalescedAnnotatedCallLog}.
+ *
+ * <p>When adding columns be sure to update {@link #ALL_COMMON_COLUMNS}.
+ */
+ interface CommonColumns extends BaseColumns {
+
+ /**
+ * Timestamp of the entry, in milliseconds.
+ *
+ * <p>Type: INTEGER (long)
+ */
+ String TIMESTAMP = "timestamp";
+
+ /**
+ * Copied from {@link android.provider.CallLog.Calls#CACHED_NAME}.
+ *
+ * <p>This is exactly how it should appear to the user. If the user's locale or name display
+ * preferences change, this column should be rewritten.
+ *
+ * <p>Type: TEXT
+ */
+ String NAME = "name";
+
+ /**
+ * Copied from {@link android.provider.CallLog.Calls#CACHED_FORMATTED_NUMBER}.
+ *
+ * <p>Type: TEXT
+ */
+ String FORMATTED_NUMBER = "formatted_number";
+
+ /**
+ * Copied from {@link android.provider.CallLog.Calls#CACHED_PHOTO_URI}.
+ *
+ * <p>TYPE: TEXT
+ */
+ String PHOTO_URI = "photo_uri";
+
+ /**
+ * Copied from {@link android.provider.CallLog.Calls#CACHED_PHOTO_ID}.
+ *
+ * <p>Type: INTEGER (long)
+ */
+ String PHOTO_ID = "photo_id";
+
+ /**
+ * Copied from {@link android.provider.CallLog.Calls#CACHED_LOOKUP_URI}.
+ *
+ * <p>TYPE: TEXT
+ */
+ String LOOKUP_URI = "lookup_uri";
+
+ // TODO(zachh): If we need to support photos other than local contacts', add a (blob?) column.
+
+ /**
+ * The number type as a string to be displayed to the user, for example "Home" or "Mobile".
+ *
+ * <p>This column should be updated for the appropriate language when the locale changes.
+ *
+ * <p>TYPE: TEXT
+ */
+ String NUMBER_TYPE_LABEL = "number_type_label";
+
+ /**
+ * See {@link android.provider.CallLog.Calls#IS_READ}.
+ *
+ * <p>TYPE: INTEGER (boolean)
+ */
+ String IS_READ = "is_read";
+
+ /**
+ * See {@link android.provider.CallLog.Calls#NEW}.
+ *
+ * <p>Type: INTEGER (boolean)
+ */
+ String NEW = "new";
+
+ /**
+ * See {@link android.provider.CallLog.Calls#GEOCODED_LOCATION}.
+ *
+ * <p>TYPE: TEXT
+ */
+ String GEOCODED_LOCATION = "geocoded_location";
+
+ /**
+ * String suitable for display which indicates the phone account used to make the call.
+ *
+ * <p>TYPE: TEXT
+ */
+ String PHONE_ACCOUNT_LABEL = "phone_account_label";
+
+ /**
+ * The color int for the phone account.
+ *
+ * <p>TYPE: INTEGER (int)
+ */
+ String PHONE_ACCOUNT_COLOR = "phone_account_color";
+
+ /**
+ * See {@link android.provider.CallLog.Calls#FEATURES}.
+ *
+ * <p>TYPE: INTEGER (int)
+ */
+ String FEATURES = "features";
+
+ /**
+ * True if a caller ID data source informed us that this is a business number. This is used to
+ * determine if a generic business avatar should be shown vs. a generic person avatar.
+ *
+ * <p>TYPE: INTEGER (boolean)
+ */
+ String IS_BUSINESS = "is_business";
+
+ /**
+ * True if this was a call to voicemail. This is used to determine if the voicemail avatar
+ * should be displayed.
+ *
+ * <p>TYPE: INTEGER (boolean)
+ */
+ String IS_VOICEMAIL = "is_voicemail";
+
+ String[] ALL_COMMON_COLUMNS =
+ new String[] {
+ _ID,
+ TIMESTAMP,
+ NAME,
+ FORMATTED_NUMBER,
+ PHOTO_URI,
+ PHOTO_ID,
+ LOOKUP_URI,
+ NUMBER_TYPE_LABEL,
+ IS_READ,
+ NEW,
+ GEOCODED_LOCATION,
+ PHONE_ACCOUNT_LABEL,
+ PHONE_ACCOUNT_COLOR,
+ FEATURES,
+ IS_BUSINESS,
+ IS_VOICEMAIL
+ };
+ }
+
+ /**
+ * AnnotatedCallLog table.
+ *
+ * <p>This contains all of the non-coalesced call log entries.
+ */
+ public static final class AnnotatedCallLog implements CommonColumns {
+
+ public static final String TABLE = "AnnotatedCallLog";
+
+ /** The content URI for this table. */
+ public static final Uri CONTENT_URI =
+ Uri.withAppendedPath(AnnotatedCallLogContract.CONTENT_URI, TABLE);
+
+ /** The MIME type of a {@link android.content.ContentProvider#getType(Uri)} single entry. */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/annotated_call_log";
+
+ /**
+ * The phone number called or number the call came from, encoded as a {@link
+ * com.android.dialer.DialerPhoneNumber} proto. The number may be empty if it was an incoming
+ * call and the number was unknown.
+ *
+ * <p>This column is only present in the annotated call log, and not the coalesced annotated
+ * call log. The coalesced version uses a formatted number string rather than proto bytes.
+ *
+ * <p>Type: BLOB
+ */
+ public static final String NUMBER = "number";
+
+ /**
+ * Copied from {@link android.provider.CallLog.Calls#TYPE}.
+ *
+ * <p>Type: INTEGER (int)
+ */
+ public static final String TYPE = "type";
+ }
+
+ /**
+ * Coalesced view of the AnnotatedCallLog table.
+ *
+ * <p>This is an in-memory view of the {@link AnnotatedCallLog} with some adjacent entries
+ * collapsed.
+ *
+ * <p>When adding columns be sure to update {@link #COLUMNS_ONLY_IN_COALESCED_CALL_LOG}.
+ */
+ public static final class CoalescedAnnotatedCallLog implements CommonColumns {
+
+ public static final String TABLE = "CoalescedAnnotatedCallLog";
+
+ /** The content URI for this table. */
+ public static final Uri CONTENT_URI =
+ Uri.withAppendedPath(AnnotatedCallLogContract.CONTENT_URI, TABLE);
+
+ /** The MIME type of a {@link android.content.ContentProvider#getType(Uri)} single entry. */
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/coalesced_annotated_call_log";
+
+ /**
+ * Number of AnnotatedCallLog rows represented by this CoalescedAnnotatedCallLog row.
+ *
+ * <p>Type: INTEGER
+ */
+ public static final String NUMBER_CALLS = "number_calls";
+
+ /**
+ * The call types of the most recent 3 calls, encoded as a CallTypes proto.
+ *
+ * <p>TYPE: BLOB
+ */
+ public static final String CALL_TYPES = "call_types";
+
+ /**
+ * Columns that are only in the {@link CoalescedAnnotatedCallLog} but not the {@link
+ * AnnotatedCallLog}.
+ */
+ private static final String[] COLUMNS_ONLY_IN_COALESCED_CALL_LOG =
+ new String[] {NUMBER_CALLS, CALL_TYPES};
+
+ /** All columns in the {@link CoalescedAnnotatedCallLog}. */
+ public static final String[] ALL_COLUMNS =
+ concat(ALL_COMMON_COLUMNS, COLUMNS_ONLY_IN_COALESCED_CALL_LOG);
+ }
+
+ private static String[] concat(String[] first, String[] second) {
+ String[] result = Arrays.copyOf(first, first.length + second.length);
+ System.arraycopy(second, 0, result, first.length, second.length);
+ return result;
+ }
+}