diff options
author | Eric Erfanian <erfanian@google.com> | 2017-06-19 11:26:01 -0700 |
---|---|---|
committer | Eric Erfanian <erfanian@google.com> | 2017-06-19 11:30:45 -0700 |
commit | 2f1c7586bcce334ca69022eb8dc6d8965ceb6a05 (patch) | |
tree | bf00ada449ee3de31ec983a14e84159200aa18c2 /java/com/android/dialer/calllog/database | |
parent | 3d0ca68e466482971a4cf46576c50cb2bd42bcb5 (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/database')
9 files changed, 853 insertions, 123 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..30aa2bff5 --- /dev/null +++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java @@ -0,0 +1,331 @@ +/* + * 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 == null, "projection not 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..887dfcbb6 100644 --- a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java +++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java @@ -16,36 +16,58 @@ 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 + " (") + .append(AnnotatedCallLog._ID + " integer primary key, ") + .append(AnnotatedCallLog.TIMESTAMP + " integer, ") + .append(AnnotatedCallLog.CONTACT_NAME + " string, ") + .append(AnnotatedCallLog.NUMBER + " blob") .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); + db.execSQL(CREATE_TABLE_SQL); + db.execSQL(String.format(Locale.US, CREATE_TRIGGER_SQL, maxRows, maxRows)); // TODO: Consider logging impression. LogUtil.i( "AnnotatedCallLogDatabaseHelper.onCreate", 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..5683687fd --- /dev/null +++ b/java/com/android/dialer/calllog/database/Coalescer.java @@ -0,0 +1,180 @@ +/* + * 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 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) { + // TODO: Real implementation. + DialerPhoneNumber number1; + DialerPhoneNumber number2; + try { + number1 = DialerPhoneNumber.parseFrom(row1.getAsByteArray(AnnotatedCallLog.NUMBER)); + number2 = DialerPhoneNumber.parseFrom(row2.getAsByteArray(AnnotatedCallLog.NUMBER)); + } 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/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java new file mode 100644 index 000000000..7f314e37c --- /dev/null +++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java @@ -0,0 +1,135 @@ +/* + * 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"; + + /** + * Name to display for the entry. + * + * <p>Type: TEXT + */ + String CONTACT_NAME = "contact_name"; + + String[] ALL_COMMON_COLUMNS = new String[] {_ID, TIMESTAMP, CONTACT_NAME}; + } + + /** + * 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"; + } + + /** + * 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 phone number formatted in a way suitable for display to the user. This value is generated + * on the fly when the {@link CoalescedAnnotatedCallLog} is generated. + * + * <p>Type: TEXT + */ + public static final String FORMATTED_NUMBER = "formatted_number"; + + /** + * 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, FORMATTED_NUMBER}; + + /** 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; + } +} |