summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/calllog
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
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')
-rw-r--r--java/com/android/dialer/calllog/CallLogComponent.java2
-rw-r--r--java/com/android/dialer/calllog/CallLogFramework.java5
-rw-r--r--java/com/android/dialer/calllog/CallLogModule.java8
-rw-r--r--java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java123
-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.java331
-rw-r--r--java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java46
-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.java180
-rw-r--r--java/com/android/dialer/calllog/database/MutationApplier.java105
-rw-r--r--java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java135
-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.java (renamed from java/com/android/dialer/calllog/DataSources.java)5
-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
-rw-r--r--java/com/android/dialer/calllog/testing/FakeCallLogApplication.java53
-rw-r--r--java/com/android/dialer/calllog/ui/AndroidManifest.xml2
-rw-r--r--java/com/android/dialer/calllog/ui/AnnotatedCallLogCursorLoader.java48
-rw-r--r--java/com/android/dialer/calllog/ui/NewCallLogAdapter.java53
-rw-r--r--java/com/android/dialer/calllog/ui/NewCallLogFragment.java101
-rw-r--r--java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java43
-rw-r--r--java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml16
-rw-r--r--java/com/android/dialer/calllog/ui/res/layout/new_call_log_fragment.xml7
27 files changed, 1630 insertions, 318 deletions
diff --git a/java/com/android/dialer/calllog/CallLogComponent.java b/java/com/android/dialer/calllog/CallLogComponent.java
index 5cdd2b4d0..c7db2a1b8 100644
--- a/java/com/android/dialer/calllog/CallLogComponent.java
+++ b/java/com/android/dialer/calllog/CallLogComponent.java
@@ -25,6 +25,8 @@ public abstract class CallLogComponent {
public abstract CallLogFramework callLogFramework();
+ public abstract RefreshAnnotatedCallLogWorker getRefreshAnnotatedCallLogWorker();
+
public static CallLogComponent get(Context context) {
return ((HasComponent) ((HasRootComponent) context.getApplicationContext()).component())
.callLogComponent();
diff --git a/java/com/android/dialer/calllog/CallLogFramework.java b/java/com/android/dialer/calllog/CallLogFramework.java
index 508413b14..d3a2c638e 100644
--- a/java/com/android/dialer/calllog/CallLogFramework.java
+++ b/java/com/android/dialer/calllog/CallLogFramework.java
@@ -22,9 +22,10 @@ import android.preference.PreferenceManager;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import com.android.dialer.calllog.datasources.CallLogDataSource;
+import com.android.dialer.calllog.datasources.DataSources;
import com.android.dialer.common.Assert;
-import com.android.dialer.common.ConfigProviderBindings;
import com.android.dialer.common.LogUtil;
+import com.android.dialer.configprovider.ConfigProviderBindings;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -38,7 +39,6 @@ import javax.inject.Singleton;
public final class CallLogFramework implements CallLogDataSource.ContentObserverCallbacks {
static final String PREF_FORCE_REBUILD = "callLogFrameworkForceRebuild";
- static final String PREF_LAST_REBUILD_TIMESTAMP_MILLIS = "callLogFrameworkLastRebuild";
private final DataSources dataSources;
@@ -58,6 +58,7 @@ public final class CallLogFramework implements CallLogDataSource.ContentObserver
LogUtil.enterBlock("CallLogFramework.registerContentObservers");
if (!isNewCallLogEnabled(appContext)) {
+ LogUtil.i("CallLogFramework.registerContentObservers", "new call log not enabled");
return;
}
diff --git a/java/com/android/dialer/calllog/CallLogModule.java b/java/com/android/dialer/calllog/CallLogModule.java
index d7473a75e..2f2f16d5b 100644
--- a/java/com/android/dialer/calllog/CallLogModule.java
+++ b/java/com/android/dialer/calllog/CallLogModule.java
@@ -17,11 +17,9 @@
package com.android.dialer.calllog;
import com.android.dialer.calllog.datasources.CallLogDataSource;
+import com.android.dialer.calllog.datasources.DataSources;
import com.android.dialer.calllog.datasources.contacts.ContactsDataSource;
import com.android.dialer.calllog.datasources.systemcalllog.SystemCallLogDataSource;
-import com.android.dialer.common.concurrent.DefaultDialerExecutorFactory;
-import com.android.dialer.common.concurrent.DialerExecutorFactory;
-import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import java.util.Arrays;
@@ -32,10 +30,6 @@ import java.util.List;
@Module
public abstract class CallLogModule {
- @Binds
- abstract DialerExecutorFactory bindDialerExecutorFactory(
- DefaultDialerExecutorFactory defaultDialerExecutorFactory);
-
@Provides
static DataSources provideCallLogDataSources(
SystemCallLogDataSource systemCallLogDataSource, ContactsDataSource contactsDataSource) {
diff --git a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java
index f9f0c9935..273246649 100644
--- a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java
+++ b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java
@@ -18,91 +18,84 @@ package com.android.dialer.calllog;
import android.annotation.TargetApi;
import android.content.Context;
+import android.content.OperationApplicationException;
import android.content.SharedPreferences;
-import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
+import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.support.annotation.WorkerThread;
-import com.android.dialer.calllog.database.AnnotatedCallLog;
-import com.android.dialer.calllog.database.CallLogMutations;
+import com.android.dialer.calllog.database.CallLogDatabaseComponent;
import com.android.dialer.calllog.datasources.CallLogDataSource;
+import com.android.dialer.calllog.datasources.CallLogMutations;
+import com.android.dialer.calllog.datasources.DataSources;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DialerExecutor.Worker;
+import com.android.dialer.inject.ApplicationContext;
import javax.inject.Inject;
/**
* Worker which brings the annotated call log up to date, if necessary.
*
- * <p>Accepts a boolean which indicates if the dirty check should be skipped, and returns true if
- * the annotated call log was updated.
+ * <p>Accepts a boolean which indicates if the dirty check should be skipped.
*/
-public class RefreshAnnotatedCallLogWorker implements Worker<Boolean, Boolean> {
+public class RefreshAnnotatedCallLogWorker implements Worker<Boolean, Void> {
private final Context appContext;
private final DataSources dataSources;
@Inject
- public RefreshAnnotatedCallLogWorker(Context appContext, DataSources dataSources) {
+ RefreshAnnotatedCallLogWorker(@ApplicationContext Context appContext, DataSources dataSources) {
this.appContext = appContext;
this.dataSources = dataSources;
}
@Override
- public Boolean doInBackground(Boolean skipDirtyCheck) {
- LogUtil.enterBlock("RefreshAnnotatedCallLogWorker.doInBackgroundFallible");
+ public Void doInBackground(Boolean skipDirtyCheck)
+ throws RemoteException, OperationApplicationException {
+ LogUtil.enterBlock("RefreshAnnotatedCallLogWorker.doInBackground");
long startTime = System.currentTimeMillis();
- boolean annotatedCallLogUpdated = checkDirtyAndRebuildIfNecessary(appContext, skipDirtyCheck);
+ checkDirtyAndRebuildIfNecessary(appContext, skipDirtyCheck);
LogUtil.i(
- "RefreshAnnotatedCallLogWorker.doInBackgroundFallible",
- "updated? %s, took %dms",
- annotatedCallLogUpdated,
+ "RefreshAnnotatedCallLogWorker.doInBackground",
+ "took %dms",
System.currentTimeMillis() - startTime);
- return annotatedCallLogUpdated;
+ return null;
}
@WorkerThread
- private boolean checkDirtyAndRebuildIfNecessary(Context appContext, boolean skipDirtyCheck) {
+ private void checkDirtyAndRebuildIfNecessary(Context appContext, boolean skipDirtyCheck)
+ throws RemoteException, OperationApplicationException {
Assert.isWorkerThread();
long startTime = System.currentTimeMillis();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext);
- long lastRebuildTimeMillis =
- sharedPreferences.getLong(CallLogFramework.PREF_LAST_REBUILD_TIMESTAMP_MILLIS, 0);
- if (lastRebuildTimeMillis == 0) {
- LogUtil.i(
- "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary",
- "annotated call log has never been built, marking it dirty");
- }
+ // Default to true. If the pref doesn't exist, the annotated call log hasn't been created and
+ // we just skip isDirty checks and force a rebuild.
boolean forceRebuildPrefValue =
- sharedPreferences.getBoolean(CallLogFramework.PREF_FORCE_REBUILD, false);
+ sharedPreferences.getBoolean(CallLogFramework.PREF_FORCE_REBUILD, true);
if (forceRebuildPrefValue) {
LogUtil.i(
"RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary",
- "call log has been marked dirty");
+ "annotated call log has been marked dirty or does not exist");
}
- boolean isDirty =
- lastRebuildTimeMillis == 0
- || skipDirtyCheck
- || forceRebuildPrefValue
- || isDirty(appContext);
+ boolean isDirty = skipDirtyCheck || forceRebuildPrefValue || isDirty(appContext);
+
LogUtil.i(
"RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary",
"isDirty took: %dms",
System.currentTimeMillis() - startTime);
if (isDirty) {
startTime = System.currentTimeMillis();
- rebuild(appContext, lastRebuildTimeMillis);
+ rebuild(appContext);
LogUtil.i(
"RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary",
"rebuild took: %dms",
System.currentTimeMillis() - startTime);
- return true; // Annotated call log was updated.
}
- return false; // Annotated call log was not updated.
}
@WorkerThread
@@ -129,52 +122,58 @@ public class RefreshAnnotatedCallLogWorker implements Worker<Boolean, Boolean> {
@TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources
@WorkerThread
- private void rebuild(Context appContext, long lastRebuildTimeMillis) {
+ private void rebuild(Context appContext) throws RemoteException, OperationApplicationException {
Assert.isWorkerThread();
- // TODO: Start a transaction?
- try (SQLiteDatabase database = AnnotatedCallLog.getWritableDatabase(appContext)) {
+ CallLogMutations mutations = new CallLogMutations();
- CallLogMutations mutations = new CallLogMutations();
+ // System call log data source must go first!
+ CallLogDataSource systemCallLogDataSource = dataSources.getSystemCallLogDataSource();
+ String dataSourceName = getName(systemCallLogDataSource);
+ LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "filling %s", dataSourceName);
+ long startTime = System.currentTimeMillis();
+ systemCallLogDataSource.fill(appContext, mutations);
+ LogUtil.i(
+ "RefreshAnnotatedCallLogWorker.rebuild",
+ "%s.fill took: %dms",
+ dataSourceName,
+ System.currentTimeMillis() - startTime);
- // System call log data source must go first!
- CallLogDataSource systemCallLogDataSource = dataSources.getSystemCallLogDataSource();
- String dataSourceName = getName(systemCallLogDataSource);
+ for (CallLogDataSource dataSource : dataSources.getDataSourcesExcludingSystemCallLog()) {
+ dataSourceName = getName(dataSource);
LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "filling %s", dataSourceName);
- long startTime = System.currentTimeMillis();
- systemCallLogDataSource.fill(appContext, database, lastRebuildTimeMillis, mutations);
+ startTime = System.currentTimeMillis();
+ dataSource.fill(appContext, mutations);
LogUtil.i(
- "RefreshAnnotatedCallLogWorker.rebuild",
+ "CallLogFramework.rebuild",
"%s.fill took: %dms",
dataSourceName,
System.currentTimeMillis() - startTime);
+ }
+ LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "applying mutations to database");
+ startTime = System.currentTimeMillis();
+ CallLogDatabaseComponent.get(appContext)
+ .mutationApplier()
+ .applyToDatabase(mutations, appContext);
+ LogUtil.i(
+ "RefreshAnnotatedCallLogWorker.rebuild",
+ "applyToDatabase took: %dms",
+ System.currentTimeMillis() - startTime);
- for (CallLogDataSource dataSource : dataSources.getDataSourcesExcludingSystemCallLog()) {
- dataSourceName = getName(dataSource);
- LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "filling %s", dataSourceName);
- startTime = System.currentTimeMillis();
- dataSource.fill(appContext, database, lastRebuildTimeMillis, mutations);
- LogUtil.i(
- "CallLogFramework.rebuild",
- "%s.fill took: %dms",
- dataSourceName,
- System.currentTimeMillis() - startTime);
- }
- LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "applying mutations to database");
+ for (CallLogDataSource dataSource : dataSources.getDataSourcesIncludingSystemCallLog()) {
+ dataSourceName = getName(dataSource);
+ LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "onSuccessfulFill'ing %s", dataSourceName);
startTime = System.currentTimeMillis();
- mutations.applyToDatabase(database);
+ dataSource.onSuccessfulFill(appContext);
LogUtil.i(
- "RefreshAnnotatedCallLogWorker.rebuild",
- "applyToDatabase took: %dms",
+ "CallLogFramework.rebuild",
+ "%s.onSuccessfulFill took: %dms",
+ dataSourceName,
System.currentTimeMillis() - startTime);
}
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext);
- sharedPreferences
- .edit()
- .putBoolean(CallLogFramework.PREF_FORCE_REBUILD, false)
- .putLong(CallLogFramework.PREF_LAST_REBUILD_TIMESTAMP_MILLIS, System.currentTimeMillis())
- .commit();
+ sharedPreferences.edit().putBoolean(CallLogFramework.PREF_FORCE_REBUILD, false).apply();
}
private static String getName(CallLogDataSource dataSource) {
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;
+ }
+}
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.java b/java/com/android/dialer/calllog/datasources/DataSources.java
index 21d190167..911ca3fa3 100644
--- a/java/com/android/dialer/calllog/DataSources.java
+++ b/java/com/android/dialer/calllog/datasources/DataSources.java
@@ -14,14 +14,13 @@
* limitations under the License
*/
-package com.android.dialer.calllog;
+package com.android.dialer.calllog.datasources;
-import com.android.dialer.calllog.datasources.CallLogDataSource;
import com.android.dialer.calllog.datasources.systemcalllog.SystemCallLogDataSource;
import java.util.List;
/** Immutable lists of data sources used to populate the annotated call log. */
-interface DataSources {
+public interface DataSources {
SystemCallLogDataSource getSystemCallLogDataSource();
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;
+ }
+}
diff --git a/java/com/android/dialer/calllog/testing/FakeCallLogApplication.java b/java/com/android/dialer/calllog/testing/FakeCallLogApplication.java
new file mode 100644
index 000000000..8aee7dd56
--- /dev/null
+++ b/java/com/android/dialer/calllog/testing/FakeCallLogApplication.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.testing;
+
+import android.app.Application;
+import com.android.dialer.calllog.CallLogComponent;
+import com.android.dialer.calllog.CallLogModule;
+import com.android.dialer.calllog.database.CallLogDatabaseComponent;
+import com.android.dialer.common.concurrent.DialerExecutorComponent;
+import com.android.dialer.common.concurrent.testing.TestDialerExecutorModule;
+import com.android.dialer.inject.ContextModule;
+import com.android.dialer.inject.HasRootComponent;
+import dagger.Component;
+import javax.inject.Singleton;
+
+/**
+ * Fake application for call log robolectric tests which uses all real bindings but doesn't require
+ * tests to depend on and use all of DialerApplication.
+ */
+public final class FakeCallLogApplication extends Application implements HasRootComponent {
+ private Object rootComponent;
+
+ @Override
+ public final synchronized Object component() {
+ if (rootComponent == null) {
+ rootComponent =
+ DaggerFakeCallLogApplication_FakeComponent.builder()
+ .contextModule(new ContextModule(this))
+ .build();
+ }
+ return rootComponent;
+ }
+
+ @Singleton
+ @Component(modules = {CallLogModule.class, ContextModule.class, TestDialerExecutorModule.class})
+ interface FakeComponent
+ extends CallLogDatabaseComponent.HasComponent,
+ CallLogComponent.HasComponent,
+ DialerExecutorComponent.HasComponent {}
+}
diff --git a/java/com/android/dialer/calllog/ui/AndroidManifest.xml b/java/com/android/dialer/calllog/ui/AndroidManifest.xml
index 228167749..eaf71aba8 100644
--- a/java/com/android/dialer/calllog/ui/AndroidManifest.xml
+++ b/java/com/android/dialer/calllog/ui/AndroidManifest.xml
@@ -13,4 +13,4 @@
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
-<manifest package="com.android.dialer.calllog"/>
+<manifest package="com.android.dialer.calllog.ui"/>
diff --git a/java/com/android/dialer/calllog/ui/AnnotatedCallLogCursorLoader.java b/java/com/android/dialer/calllog/ui/AnnotatedCallLogCursorLoader.java
deleted file mode 100644
index cd8622e80..000000000
--- a/java/com/android/dialer/calllog/ui/AnnotatedCallLogCursorLoader.java
+++ /dev/null
@@ -1,48 +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.ui;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.CursorLoader;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.os.Build;
-import com.android.dialer.calllog.database.AnnotatedCallLog;
-import com.android.dialer.calllog.database.AnnotatedCallLog.Columns;
-
-/** CursorLoader which reads the annotated call log. */
-class AnnotatedCallLogCursorLoader extends CursorLoader {
-
- AnnotatedCallLogCursorLoader(Context context) {
- super(context);
- }
-
- @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources
- @Override
- public Cursor loadInBackground() {
- try (SQLiteDatabase readableDatabase = AnnotatedCallLog.getReadableDatabase(getContext())) {
- return readableDatabase.rawQuery(
- "SELECT * FROM "
- + AnnotatedCallLog.TABLE_NAME
- + " ORDER BY "
- + Columns.TIMESTAMP
- + " DESC",
- null /* selectionArgs */);
- }
- }
-}
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
new file mode 100644
index 000000000..f9ab21cb3
--- /dev/null
+++ b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.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.ui;
+
+import android.database.Cursor;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog;
+
+/** {@link RecyclerView.Adapter} for the new call log fragment. */
+final class NewCallLogAdapter extends RecyclerView.Adapter<NewCallLogViewHolder> {
+
+ private final Cursor cursor;
+ private final int timestampIndex;
+
+ NewCallLogAdapter(Cursor cursor) {
+ this.cursor = cursor;
+ timestampIndex = cursor.getColumnIndexOrThrow(CoalescedAnnotatedCallLog.TIMESTAMP);
+ }
+
+ @Override
+ public NewCallLogViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
+ return new NewCallLogViewHolder(
+ LayoutInflater.from(viewGroup.getContext())
+ .inflate(R.layout.new_call_log_entry, viewGroup, false));
+ }
+
+ @Override
+ public void onBindViewHolder(NewCallLogViewHolder viewHolder, int position) {
+ cursor.moveToPosition(position);
+ long timestamp = cursor.getLong(timestampIndex);
+ viewHolder.bind(timestamp);
+ }
+
+ @Override
+ public int getItemCount() {
+ return cursor.getCount();
+ }
+}
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
index b8f2b1326..712f7cf25 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
@@ -17,30 +17,38 @@ package com.android.dialer.calllog.ui;
import android.app.Fragment;
import android.app.LoaderManager.LoaderCallbacks;
-import android.content.Context;
+import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
import android.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.CursorAdapter;
-import android.widget.ListView;
-import android.widget.SimpleCursorAdapter;
-import android.widget.TextView;
import com.android.dialer.calllog.CallLogComponent;
import com.android.dialer.calllog.CallLogFramework;
import com.android.dialer.calllog.CallLogFramework.CallLogUi;
-import com.android.dialer.calllog.database.AnnotatedCallLog.Columns;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog;
import com.android.dialer.common.LogUtil;
-import java.text.SimpleDateFormat;
-import java.util.Locale;
+import com.android.dialer.common.concurrent.DialerExecutor;
+import com.android.dialer.common.concurrent.DialerExecutorComponent;
+import com.android.dialer.common.concurrent.DialerExecutorFactory;
/** The "new" call log fragment implementation, which is built on top of the annotated call log. */
public final class NewCallLogFragment extends Fragment
implements CallLogUi, LoaderCallbacks<Cursor> {
- private CursorAdapter cursorAdapter;
+ /*
+ * This is a reasonable time that it might take between related call log writes, that also
+ * shouldn't slow down single-writes too much. For example, when populating the database using
+ * the simulator, using this value results in ~6 refresh cycles (on a release build) to write 120
+ * call log entries.
+ */
+ private static final long WAIT_MILLIS = 100L;
+
+ private DialerExecutor<Boolean> refreshAnnotatedCallLogTask;
+ private RecyclerView recyclerView;
public NewCallLogFragment() {
LogUtil.enterBlock("NewCallLogFragment.NewCallLogFragment");
@@ -52,8 +60,27 @@ public final class NewCallLogFragment extends Fragment
LogUtil.enterBlock("NewCallLogFragment.onCreate");
- CallLogFramework callLogFramework = CallLogComponent.get(getContext()).callLogFramework();
+ CallLogComponent component = CallLogComponent.get(getContext());
+ CallLogFramework callLogFramework = component.callLogFramework();
callLogFramework.attachUi(this);
+
+ DialerExecutorFactory dialerExecutorFactory =
+ DialerExecutorComponent.get(getContext()).dialerExecutorFactory();
+
+ refreshAnnotatedCallLogTask =
+ dialerExecutorFactory
+ .createUiTaskBuilder(
+ getFragmentManager(),
+ "NewCallLogFragment.refreshAnnotatedCallLog",
+ component.getRefreshAnnotatedCallLogWorker())
+ .build();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ LogUtil.enterBlock("NewCallLogFragment.onStart");
}
@Override
@@ -64,6 +91,9 @@ public final class NewCallLogFragment extends Fragment
CallLogFramework callLogFramework = CallLogComponent.get(getContext()).callLogFramework();
callLogFramework.attachUi(this);
+
+ // TODO: Consider doing this when fragment becomes visible.
+ checkAnnotatedCallLogDirtyAndRefreshIfNecessary();
}
@Override
@@ -82,57 +112,44 @@ public final class NewCallLogFragment extends Fragment
LogUtil.enterBlock("NewCallLogFragment.onCreateView");
View view = inflater.inflate(R.layout.new_call_log_fragment, container, false);
- ListView listView = (ListView) view.findViewById(R.id.list);
+ recyclerView = view.findViewById(R.id.new_call_log_recycler_view);
- this.cursorAdapter =
- new MyCursorAdapter(
- getContext(),
- R.layout.new_call_log_entry,
- null /* cursor */,
- new String[] {Columns.TIMESTAMP, Columns.CONTACT_NAME},
- new int[] {R.id.timestamp, R.id.contact_name},
- 0);
- listView.setAdapter(cursorAdapter);
-
- getLoaderManager().initLoader(0, null, this);
+ getLoaderManager().restartLoader(0, null, this);
return view;
}
+ private void checkAnnotatedCallLogDirtyAndRefreshIfNecessary() {
+ LogUtil.enterBlock("NewCallLogFragment.checkAnnotatedCallLogDirtyAndRefreshIfNecessary");
+ refreshAnnotatedCallLogTask.executeSerialWithWait(false /* skipDirtyCheck */, WAIT_MILLIS);
+ }
+
@Override
public void invalidateUi() {
LogUtil.enterBlock("NewCallLogFragment.invalidateUi");
- // TODO: Implementation.
+ refreshAnnotatedCallLogTask.executeSerialWithWait(true /* skipDirtyCheck */, WAIT_MILLIS);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- // TODO: This is sort of weird, do we need to implement a content provider?
- return new AnnotatedCallLogCursorLoader(getContext());
+ LogUtil.enterBlock("NewCallLogFragment.onCreateLoader");
+ // CoalescedAnnotatedCallLog requires that all params be null.
+ return new CursorLoader(
+ getContext(), CoalescedAnnotatedCallLog.CONTENT_URI, null, null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor newCursor) {
- cursorAdapter.swapCursor(newCursor);
+ LogUtil.enterBlock("NewCallLogFragment.onLoadFinished");
+
+ // TODO: Handle empty cursor by showing empty view.
+ recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ recyclerView.setAdapter(new NewCallLogAdapter(newCursor));
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
- cursorAdapter.swapCursor(null);
- }
-
- private static class MyCursorAdapter extends SimpleCursorAdapter {
-
- MyCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {
- super(context, layout, c, from, to, flags);
- }
-
- @Override
- public void setViewText(TextView view, String text) {
- if (view.getId() == R.id.timestamp) {
- text = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US).format(Long.valueOf(text));
- }
- view.setText(text);
- }
+ LogUtil.enterBlock("NewCallLogFragment.onLoaderReset");
+ recyclerView.setAdapter(null);
}
}
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
new file mode 100644
index 000000000..4c459e123
--- /dev/null
+++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
@@ -0,0 +1,43 @@
+/*
+ * 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.ui;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.TextView;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/** {@link RecyclerView.ViewHolder} for the new call log. */
+final class NewCallLogViewHolder extends RecyclerView.ViewHolder {
+
+ // TODO: Format correctly using current locale.
+ private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US);
+
+ private final TextView contactNameView;
+ private final TextView timestampView;
+
+ NewCallLogViewHolder(View view) {
+ super(view);
+ contactNameView = view.findViewById(R.id.contact_name);
+ timestampView = view.findViewById(R.id.timestamp);
+ }
+
+ void bind(long timestamp) {
+ contactNameView.setText("Contact Name Placeholder");
+ timestampView.setText(dateFormat.format(timestamp));
+ }
+}
diff --git a/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml b/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml
index ee3efd002..99797fab4 100644
--- a/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml
+++ b/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml
@@ -18,16 +18,20 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="horizontal">
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:orientation="vertical">
<TextView
- android:id="@+id/timestamp"
+ android:id="@+id/contact_name"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
+ android:layout_height="wrap_content"
+ style="@style/PrimaryText"/>
<TextView
- android:id="@+id/contact_name"
+ android:id="@+id/timestamp"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
+ android:layout_height="wrap_content"
+ style="@style/SecondaryText"/>
+
</LinearLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/calllog/ui/res/layout/new_call_log_fragment.xml b/java/com/android/dialer/calllog/ui/res/layout/new_call_log_fragment.xml
index 433dbdd0f..e1d8410b6 100644
--- a/java/com/android/dialer/calllog/ui/res/layout/new_call_log_fragment.xml
+++ b/java/com/android/dialer/calllog/ui/res/layout/new_call_log_fragment.xml
@@ -15,8 +15,9 @@
~ limitations under the License
-->
-<ListView
+<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/list"
+ android:id="@+id/new_call_log_recycler_view"
android:layout_width="match_parent"
- android:layout_height="match_parent"/>
+ android:layout_height="match_parent"
+ android:background="@color/background_dialer_light"/>