Merge "Update call log cache when annotated call log is updated."
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