Update call log cache when annotated call log is updated.
If the NumberAttribute has changed the new data will be cached back to the call log. Also updated TestCallLogProvider to support selection with ID based URI.
Note: currently the write will trigger an extra refresh, the next CL will address that.
TEST=TAP
Bug: 77292040
Test: TAP
PiperOrigin-RevId: 199509348
Change-Id: I49c43adb5bcec96128d5ec36676c4569bf536490
diff --git a/java/com/android/dialer/calllog/CallLogCacheUpdater.java b/java/com/android/dialer/calllog/CallLogCacheUpdater.java
new file mode 100644
index 0000000..a7b2b3d
--- /dev/null
+++ b/java/com/android/dialer/calllog/CallLogCacheUpdater.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2018 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;
+
+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.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import com.android.dialer.DialerPhoneNumber;
+import com.android.dialer.NumberAttributes;
+import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
+import com.android.dialer.calllog.datasources.CallLogMutations;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
+import com.android.dialer.inject.ApplicationContext;
+import com.android.dialer.protos.ProtoParsers;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import javax.inject.Inject;
+
+/**
+ * Update {@link Calls#CACHED_NAME} and other cached columns after the annotated call log has been
+ * updated. Dialer does not read these columns but other apps relies on it.
+ */
+@SuppressWarnings("AndroidApiChecker")
+public final class CallLogCacheUpdater {
+
+ private final Context appContext;
+ private final ListeningExecutorService backgroundExecutor;
+
+ @Inject
+ CallLogCacheUpdater(
+ @ApplicationContext Context appContext,
+ @BackgroundExecutor ListeningExecutorService backgroundExecutor) {
+ this.appContext = appContext;
+ this.backgroundExecutor = backgroundExecutor;
+ }
+
+ /**
+ * Extracts inserts and updates from {@code mutations} to update the 'cached' columns in the
+ * system call log.
+ *
+ * <p>If the cached columns are non-empty, it will only be updated if {@link Calls#CACHED_NAME}
+ * has changed
+ */
+ public ListenableFuture<Void> updateCache(CallLogMutations mutations) {
+ return backgroundExecutor.submit(
+ () -> {
+ updateCacheInternal(mutations);
+ return null;
+ });
+ }
+
+ private void updateCacheInternal(CallLogMutations mutations) {
+ ArrayList<ContentProviderOperation> operations = new ArrayList<>();
+ Stream.concat(
+ mutations.getInserts().entrySet().stream(), mutations.getUpdates().entrySet().stream())
+ .forEach(
+ entry -> {
+ ContentValues values = entry.getValue();
+ if (!values.containsKey(AnnotatedCallLog.NUMBER_ATTRIBUTES)
+ || !values.containsKey(AnnotatedCallLog.NUMBER)) {
+ return;
+ }
+ DialerPhoneNumber dialerPhoneNumber =
+ ProtoParsers.getTrusted(
+ values, AnnotatedCallLog.NUMBER, DialerPhoneNumber.getDefaultInstance());
+ NumberAttributes numberAttributes =
+ ProtoParsers.getTrusted(
+ values,
+ AnnotatedCallLog.NUMBER_ATTRIBUTES,
+ NumberAttributes.getDefaultInstance());
+ operations.add(
+ ContentProviderOperation.newUpdate(
+ ContentUris.withAppendedId(Calls.CONTENT_URI, entry.getKey()))
+ .withValue(
+ Calls.CACHED_FORMATTED_NUMBER,
+ values.getAsString(AnnotatedCallLog.FORMATTED_NUMBER))
+ .withValue(Calls.CACHED_LOOKUP_URI, numberAttributes.getLookupUri())
+ // Calls.CACHED_MATCHED_NUMBER is not available.
+ .withValue(Calls.CACHED_NAME, numberAttributes.getName())
+ .withValue(
+ Calls.CACHED_NORMALIZED_NUMBER, dialerPhoneNumber.getNormalizedNumber())
+ .withValue(Calls.CACHED_NUMBER_LABEL, numberAttributes.getNumberTypeLabel())
+ // NUMBER_TYPE is lost in NumberAttributes when it is converted to a string
+ // label, Use TYPE_CUSTOM so the label will be displayed.
+ .withValue(Calls.CACHED_NUMBER_TYPE, Phone.TYPE_CUSTOM)
+ .withValue(Calls.CACHED_PHOTO_ID, numberAttributes.getPhotoId())
+ .withValue(Calls.CACHED_PHOTO_URI, numberAttributes.getPhotoUri())
+ // Avoid writing to the call log for insignificant changes to avoid triggering
+ // other content observers such as the voicemail client.
+ .withSelection(
+ Calls.CACHED_NAME + " IS NOT ?",
+ new String[] {numberAttributes.getName()})
+ .build());
+ });
+ try {
+ int count =
+ Arrays.stream(appContext.getContentResolver().applyBatch(CallLog.AUTHORITY, operations))
+ .mapToInt(result -> result.count)
+ .sum();
+ LogUtil.i("CallLogCacheUpdater.updateCache", "updated %d rows", count);
+ } catch (OperationApplicationException | RemoteException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java
index fb3700e..7d6a000 100644
--- a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java
+++ b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java
@@ -26,6 +26,7 @@
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
import com.android.dialer.common.concurrent.Annotations.LightweightExecutor;
+import com.android.dialer.common.concurrent.DefaultFutureCallback;
import com.android.dialer.common.concurrent.DialerFutureSerializer;
import com.android.dialer.common.concurrent.DialerFutures;
import com.android.dialer.inject.ApplicationContext;
@@ -53,6 +54,7 @@
private final MutationApplier mutationApplier;
private final FutureTimer futureTimer;
private final CallLogState callLogState;
+ private final CallLogCacheUpdater callLogCacheUpdater;
private final ListeningExecutorService backgroundExecutorService;
private final ListeningExecutorService lightweightExecutorService;
// Used to ensure that only one refresh flow runs at a time. (Note that
@@ -67,6 +69,7 @@
MutationApplier mutationApplier,
FutureTimer futureTimer,
CallLogState callLogState,
+ CallLogCacheUpdater callLogCacheUpdater,
@BackgroundExecutor ListeningExecutorService backgroundExecutorService,
@LightweightExecutor ListeningExecutorService lightweightExecutorService) {
this.appContext = appContext;
@@ -75,6 +78,7 @@
this.mutationApplier = mutationApplier;
this.futureTimer = futureTimer;
this.callLogState = callLogState;
+ this.callLogCacheUpdater = callLogCacheUpdater;
this.backgroundExecutorService = backgroundExecutorService;
this.lightweightExecutorService = lightweightExecutorService;
}
@@ -206,6 +210,14 @@
},
lightweightExecutorService);
+ Futures.addCallback(
+ Futures.transformAsync(
+ applyMutationsFuture,
+ unused -> callLogCacheUpdater.updateCache(mutations),
+ MoreExecutors.directExecutor()),
+ new DefaultFutureCallback<>(),
+ MoreExecutors.directExecutor());
+
// After mutations applied, call onSuccessfulFill for each data source (in parallel).
ListenableFuture<List<Void>> onSuccessfulFillFuture =
Futures.transformAsync(
diff --git a/java/com/android/dialer/protos/ProtoParsers.java b/java/com/android/dialer/protos/ProtoParsers.java
index e529206..00d5a26 100644
--- a/java/com/android/dialer/protos/ProtoParsers.java
+++ b/java/com/android/dialer/protos/ProtoParsers.java
@@ -16,6 +16,7 @@
package com.android.dialer.protos;
+import android.content.ContentValues;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
@@ -43,9 +44,26 @@
}
/**
+ * Retrieve a proto from a ContentValues which was not created within the current
+ * executable/version.
+ */
+ @SuppressWarnings("unchecked") // We want to eventually optimize away parser classes, so cast
+ public static <T extends MessageLite> T get(
+ @NonNull ContentValues contentValues, @NonNull String key, @NonNull T defaultInstance)
+ throws InvalidProtocolBufferException {
+
+ Assert.isNotNull(contentValues);
+ Assert.isNotNull(key);
+ Assert.isNotNull(defaultInstance);
+
+ byte[] bytes = contentValues.getAsByteArray(key);
+ return (T) mergeFrom(bytes, defaultInstance.getDefaultInstanceForType());
+ }
+
+ /**
* Retrieve a proto from a trusted bundle which was created within the current executable/version.
*
- * @throws RuntimeException if the proto cannot be parsed
+ * @throws IllegalStateException if the proto cannot be parsed
*/
public static <T extends MessageLite> T getTrusted(
@NonNull Bundle bundle, @NonNull String key, @NonNull T defaultInstance) {
@@ -57,6 +75,21 @@
}
/**
+ * Retrieve a proto from a trusted ContentValues which was created within the current
+ * executable/version.
+ *
+ * @throws IllegalStateException if the proto cannot be parsed
+ */
+ public static <T extends MessageLite> T getTrusted(
+ @NonNull ContentValues contentValues, @NonNull String key, @NonNull T defaultInstance) {
+ try {
+ return get(contentValues, key, defaultInstance);
+ } catch (InvalidProtocolBufferException e) {
+ throw Assert.createIllegalStateFailException(e.toString());
+ }
+ }
+
+ /**
* Retrieve a proto from a trusted bundle which was created within the current executable/version.
*
* @throws RuntimeException if the proto cannot be parsed