diff options
3 files changed, 175 insertions, 1 deletions
diff --git a/java/com/android/dialer/calllog/CallLogCacheUpdater.java b/java/com/android/dialer/calllog/CallLogCacheUpdater.java new file mode 100644 index 000000000..a7b2b3d0d --- /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 fb3700efe..7d6a00097 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.calllog.datasources.DataSources; 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 @@ public class RefreshAnnotatedCallLogWorker { 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 @@ public class RefreshAnnotatedCallLogWorker { MutationApplier mutationApplier, FutureTimer futureTimer, CallLogState callLogState, + CallLogCacheUpdater callLogCacheUpdater, @BackgroundExecutor ListeningExecutorService backgroundExecutorService, @LightweightExecutor ListeningExecutorService lightweightExecutorService) { this.appContext = appContext; @@ -75,6 +78,7 @@ public class RefreshAnnotatedCallLogWorker { this.mutationApplier = mutationApplier; this.futureTimer = futureTimer; this.callLogState = callLogState; + this.callLogCacheUpdater = callLogCacheUpdater; this.backgroundExecutorService = backgroundExecutorService; this.lightweightExecutorService = lightweightExecutorService; } @@ -206,6 +210,14 @@ public class RefreshAnnotatedCallLogWorker { }, 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 e5292061b..00d5a26d2 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 @@ public final class ProtoParsers { } /** + * 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 @@ public final class ProtoParsers { } /** + * 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 |