diff options
author | twyen <twyen@google.com> | 2018-06-06 12:26:26 -0700 |
---|---|---|
committer | Copybara-Service <copybara-piper@google.com> | 2018-06-06 14:02:32 -0700 |
commit | d8f2a8c619cadf50875b47f499f972c5494d9e5a (patch) | |
tree | f003de388990154f153b99220f6d1fc71215fc0a /java | |
parent | 6b41e3a0c6024b4afb7cef10973d95d6d0daf294 (diff) |
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
Diffstat (limited to 'java')
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 |