From 5194036b423d455a517d06b38fd616a8bbfc4896 Mon Sep 17 00:00:00 2001 From: zachh Date: Tue, 5 Dec 2017 17:42:58 -0800 Subject: Switched CallLogDataSource interface to be Future based. Bug: 34672501 Test: existing PiperOrigin-RevId: 178038086 Change-Id: I1230992ad04bb4415f5a29bd15802d23dff88012 --- java/com/android/dialer/calllog/CallLogModule.java | 4 +- .../calllog/RefreshAnnotatedCallLogWorker.java | 266 +++++++++------------ .../dialer/calllog/database/MutationApplier.java | 29 ++- .../calllog/datasources/CallLogDataSource.java | 10 +- .../dialer/calllog/datasources/DataSources.java | 3 +- .../datasources/contacts/ContactsDataSource.java | 70 ------ .../phonelookup/PhoneLookupDataSource.java | 35 ++- .../systemcalllog/SystemCallLogDataSource.java | 39 ++- .../dialer/calllog/ui/NewCallLogFragment.java | 49 +++- .../common/concurrent/DialerFutureSerializer.java | 98 ++++++++ 10 files changed, 345 insertions(+), 258 deletions(-) delete mode 100644 java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java create mode 100644 java/com/android/dialer/common/concurrent/DialerFutureSerializer.java diff --git a/java/com/android/dialer/calllog/CallLogModule.java b/java/com/android/dialer/calllog/CallLogModule.java index 9926cebb9..6c85fd631 100644 --- a/java/com/android/dialer/calllog/CallLogModule.java +++ b/java/com/android/dialer/calllog/CallLogModule.java @@ -18,7 +18,6 @@ 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.phonelookup.PhoneLookupDataSource; import com.android.dialer.calllog.datasources.systemcalllog.SystemCallLogDataSource; import com.google.common.collect.ImmutableList; @@ -32,11 +31,10 @@ public abstract class CallLogModule { @Provides static DataSources provideCallLogDataSources( SystemCallLogDataSource systemCallLogDataSource, - ContactsDataSource contactsDataSource, PhoneLookupDataSource phoneLookupDataSource) { // System call log must be first, see getDataSourcesExcludingSystemCallLog below. ImmutableList allDataSources = - ImmutableList.of(systemCallLogDataSource, contactsDataSource, phoneLookupDataSource); + ImmutableList.of(systemCallLogDataSource, phoneLookupDataSource); return new DataSources() { @Override public SystemCallLogDataSource getSystemCallLogDataSource() { diff --git a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java index d9924b23f..de8905db8 100644 --- a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java +++ b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java @@ -16,204 +16,176 @@ package com.android.dialer.calllog; -import android.annotation.TargetApi; import android.content.Context; -import android.content.OperationApplicationException; import android.content.SharedPreferences; -import android.os.Build; -import android.os.RemoteException; -import android.support.annotation.WorkerThread; 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.Annotations.UiSerial; +import com.android.dialer.common.concurrent.Annotations.NonUiParallel; +import com.android.dialer.common.concurrent.DialerFutureSerializer; +import com.android.dialer.common.concurrent.DialerFutures; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.storage.Unencrypted; -import com.google.common.util.concurrent.ListenableScheduledFuture; -import com.google.common.util.concurrent.ListeningScheduledExecutorService; +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; import javax.inject.Inject; +import javax.inject.Singleton; /** Brings the annotated call log up to date, if necessary. */ +@Singleton public class RefreshAnnotatedCallLogWorker { - /* - * 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 final Context appContext; private final DataSources dataSources; private final SharedPreferences sharedPreferences; - private final ListeningScheduledExecutorService listeningScheduledExecutorService; - private ListenableScheduledFuture scheduledFuture; + private final ListeningExecutorService parallelUiListeningExecutorService; + // Used to ensure that only one refresh flow runs at a time. (Note that + // RefreshAnnotatedCallLogWorker is a @Singleton.) + private final DialerFutureSerializer dialerFutureSerializer = new DialerFutureSerializer(); @Inject RefreshAnnotatedCallLogWorker( @ApplicationContext Context appContext, DataSources dataSources, @Unencrypted SharedPreferences sharedPreferences, - @UiSerial ScheduledExecutorService serialUiExecutorService) { + @NonUiParallel ExecutorService parallelUiExecutorService) { this.appContext = appContext; this.dataSources = dataSources; this.sharedPreferences = sharedPreferences; - this.listeningScheduledExecutorService = - MoreExecutors.listeningDecorator(serialUiExecutorService); + + // TODO(zachh): Create and use bindings for ListeningExecutorServices. + this.parallelUiListeningExecutorService = + MoreExecutors.listeningDecorator(parallelUiExecutorService); } /** Checks if the annotated call log is dirty and refreshes it if necessary. */ - public ListenableScheduledFuture refreshWithDirtyCheck() { + public ListenableFuture refreshWithDirtyCheck() { return refresh(true); } /** Refreshes the annotated call log, bypassing dirty checks. */ - public ListenableScheduledFuture refreshWithoutDirtyCheck() { + public ListenableFuture refreshWithoutDirtyCheck() { return refresh(false); } - private ListenableScheduledFuture refresh(boolean checkDirty) { - if (scheduledFuture != null) { - LogUtil.i("RefreshAnnotatedCallLogWorker.refresh", "cancelling waiting task"); - scheduledFuture.cancel(false /* mayInterrupt */); - } - scheduledFuture = - listeningScheduledExecutorService.schedule( - () -> doInBackground(checkDirty), WAIT_MILLIS, TimeUnit.MILLISECONDS); - return scheduledFuture; + private ListenableFuture refresh(boolean checkDirty) { + LogUtil.i("RefreshAnnotatedCallLogWorker.refresh", "submitting serialized refresh request"); + // Note: directExecutor is safe to use here and throughout because all methods are async. + return dialerFutureSerializer.submitAsync( + () -> checkDirtyAndRebuildIfNecessary(appContext, checkDirty), + MoreExecutors.directExecutor()); } - @WorkerThread - private Void doInBackground(boolean checkDirty) - throws RemoteException, OperationApplicationException { - LogUtil.enterBlock("RefreshAnnotatedCallLogWorker.doInBackground"); - - long startTime = System.currentTimeMillis(); - checkDirtyAndRebuildIfNecessary(appContext, checkDirty); - LogUtil.i( - "RefreshAnnotatedCallLogWorker.doInBackground", - "took %dms", - System.currentTimeMillis() - startTime); - return null; - } - - @WorkerThread - private void checkDirtyAndRebuildIfNecessary(Context appContext, boolean checkDirty) - throws RemoteException, OperationApplicationException { - Assert.isWorkerThread(); - - long startTime = System.currentTimeMillis(); - - // 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, true); - if (forceRebuildPrefValue) { - LogUtil.i( - "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", - "annotated call log has been marked dirty or does not exist"); - } - - boolean isDirty = !checkDirty || forceRebuildPrefValue || isDirty(appContext); - - LogUtil.i( - "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", - "isDirty took: %dms", - System.currentTimeMillis() - startTime); - if (isDirty) { - startTime = System.currentTimeMillis(); - rebuild(appContext); - LogUtil.i( - "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", - "rebuild took: %dms", - System.currentTimeMillis() - startTime); - } + private ListenableFuture checkDirtyAndRebuildIfNecessary( + Context appContext, boolean checkDirty) { + ListenableFuture forceRebuildFuture = + parallelUiListeningExecutorService.submit( + () -> { + LogUtil.i( + "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", + "starting refresh flow"); + if (!checkDirty) { + return true; + } + // 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, true); + if (forceRebuildPrefValue) { + LogUtil.i( + "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", + "annotated call log has been marked dirty or does not exist"); + } + return forceRebuildPrefValue; + }); + + // After checking the "force rebuild" shared pref, conditionally call isDirty. + ListenableFuture isDirtyFuture = + Futures.transformAsync( + forceRebuildFuture, + forceRebuild -> + Preconditions.checkNotNull(forceRebuild) + ? Futures.immediateFuture(true) + : isDirty(appContext), + MoreExecutors.directExecutor()); + + // After determining isDirty, conditionally call rebuild. + return Futures.transformAsync( + isDirtyFuture, + isDirty -> + Preconditions.checkNotNull(isDirty) + ? rebuild(appContext) + : Futures.immediateFuture(null), + MoreExecutors.directExecutor()); } - @WorkerThread - private boolean isDirty(Context appContext) { - Assert.isWorkerThread(); - + private ListenableFuture isDirty(Context appContext) { + List> isDirtyFutures = new ArrayList<>(); for (CallLogDataSource dataSource : dataSources.getDataSourcesIncludingSystemCallLog()) { - String dataSourceName = getName(dataSource); - long startTime = System.currentTimeMillis(); - LogUtil.i("RefreshAnnotatedCallLogWorker.isDirty", "running isDirty for %s", dataSourceName); - boolean isDirty = dataSource.isDirty(appContext); - LogUtil.i( - "RefreshAnnotatedCallLogWorker.isDirty", - "%s.isDirty returned %b in %dms", - dataSourceName, - isDirty, - System.currentTimeMillis() - startTime); - if (isDirty) { - return true; - } + isDirtyFutures.add(dataSource.isDirty(appContext)); } - return false; + // Simultaneously invokes isDirty on all data sources, returning as soon as one returns true. + return DialerFutures.firstMatching(isDirtyFutures, Preconditions::checkNotNull, false); } - @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources - @WorkerThread - private void rebuild(Context appContext) throws RemoteException, OperationApplicationException { - Assert.isWorkerThread(); - + private ListenableFuture rebuild(Context appContext) { CallLogMutations mutations = new CallLogMutations(); - // System call log data source must go first! + // Start by filling the data sources--the 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); + ListenableFuture fillFuture = systemCallLogDataSource.fill(appContext, mutations); + // After the system call log data source is filled, call fill sequentially on each remaining + // data source. This must be done sequentially because mutations are not threadsafe and are + // passed from source to source. for (CallLogDataSource dataSource : dataSources.getDataSourcesExcludingSystemCallLog()) { - dataSourceName = getName(dataSource); - LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "filling %s", dataSourceName); - startTime = System.currentTimeMillis(); - dataSource.fill(appContext, mutations); - LogUtil.i( - "CallLogFramework.rebuild", - "%s.fill took: %dms", - dataSourceName, - System.currentTimeMillis() - startTime); + fillFuture = + Futures.transformAsync( + fillFuture, + unused -> dataSource.fill(appContext, mutations), + MoreExecutors.directExecutor()); } - 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.getDataSourcesIncludingSystemCallLog()) { - dataSourceName = getName(dataSource); - LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "onSuccessfulFill'ing %s", dataSourceName); - startTime = System.currentTimeMillis(); - dataSource.onSuccessfulFill(appContext); - LogUtil.i( - "CallLogFramework.rebuild", - "%s.onSuccessfulFill took: %dms", - dataSourceName, - System.currentTimeMillis() - startTime); - } - sharedPreferences.edit().putBoolean(CallLogFramework.PREF_FORCE_REBUILD, false).apply(); - } - private static String getName(CallLogDataSource dataSource) { - return dataSource.getClass().getSimpleName(); + // After all data sources are filled, apply mutations (at this point "fillFuture" is the result + // of filling the last data source). + ListenableFuture applyMutationsFuture = + Futures.transformAsync( + fillFuture, + unused -> + CallLogDatabaseComponent.get(appContext) + .mutationApplier() + .applyToDatabase(mutations, appContext), + MoreExecutors.directExecutor()); + + // After mutations applied, call onSuccessfulFill for each data source (in parallel). + ListenableFuture> onSuccessfulFillFuture = + Futures.transformAsync( + applyMutationsFuture, + unused -> { + List> onSuccessfulFillFutures = new ArrayList<>(); + for (CallLogDataSource dataSource : + dataSources.getDataSourcesIncludingSystemCallLog()) { + onSuccessfulFillFutures.add(dataSource.onSuccessfulFill(appContext)); + } + return Futures.allAsList(onSuccessfulFillFutures); + }, + MoreExecutors.directExecutor()); + + // After onSuccessfulFill is called for every data source, write the shared pref. + return Futures.transform( + onSuccessfulFillFuture, + unused -> { + sharedPreferences.edit().putBoolean(CallLogFramework.PREF_FORCE_REBUILD, false).apply(); + return null; + }, + parallelUiListeningExecutorService); } } diff --git a/java/com/android/dialer/calllog/database/MutationApplier.java b/java/com/android/dialer/calllog/database/MutationApplier.java index 21c8a507d..720daec54 100644 --- a/java/com/android/dialer/calllog/database/MutationApplier.java +++ b/java/com/android/dialer/calllog/database/MutationApplier.java @@ -28,27 +28,44 @@ import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.Ann import com.android.dialer.calllog.datasources.CallLogMutations; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.NonUiParallel; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.Arrays; import java.util.Map.Entry; +import java.util.concurrent.ExecutorService; import javax.inject.Inject; /** Applies {@link CallLogMutations} to the annotated call log. */ public class MutationApplier { + private final ListeningExecutorService executorService; + @Inject - MutationApplier() {} + MutationApplier(@NonUiParallel ExecutorService executorService) { + this.executorService = MoreExecutors.listeningDecorator(executorService); + } /** Applies the provided {@link CallLogMutations} to the annotated call log. */ + public ListenableFuture applyToDatabase(CallLogMutations mutations, Context appContext) { + if (mutations.isEmpty()) { + return Futures.immediateFuture(null); + } + return executorService.submit( + () -> { + applyToDatabaseInternal(mutations, appContext); + return null; + }); + } + @WorkerThread - public void applyToDatabase(CallLogMutations mutations, Context appContext) + private void applyToDatabaseInternal(CallLogMutations mutations, Context appContext) throws RemoteException, OperationApplicationException { Assert.isWorkerThread(); - if (mutations.isEmpty()) { - return; - } - ArrayList operations = new ArrayList<>(); if (!mutations.getInserts().isEmpty()) { diff --git a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java index 3fff3ba53..60654a81a 100644 --- a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java @@ -21,6 +21,7 @@ import android.content.Context; import android.support.annotation.MainThread; import android.support.annotation.WorkerThread; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract; +import com.google.common.util.concurrent.ListenableFuture; import java.util.List; /** @@ -64,8 +65,7 @@ public interface CallLogDataSource { * * @see CallLogDataSource class doc for complete lifecyle information */ - @WorkerThread - boolean isDirty(Context appContext); + ListenableFuture isDirty(Context appContext); /** * Computes the set of mutations necessary to update the annotated call log with respect to this @@ -76,8 +76,7 @@ public interface CallLogDataSource { * contain inserts from the system call log, and these inserts should be modified by each data * source. */ - @WorkerThread - void fill(Context appContext, CallLogMutations mutations); + ListenableFuture fill(Context appContext, CallLogMutations mutations); /** * Called after database mutations have been applied to all data sources. This is useful for @@ -86,8 +85,7 @@ public interface CallLogDataSource { * * @see CallLogDataSource class doc for complete lifecyle information */ - @WorkerThread - void onSuccessfulFill(Context appContext); + ListenableFuture onSuccessfulFill(Context appContext); /** * Combines raw annotated call log rows into a single coalesced row. diff --git a/java/com/android/dialer/calllog/datasources/DataSources.java b/java/com/android/dialer/calllog/datasources/DataSources.java index 113a9f7b1..9fe6c1db3 100644 --- a/java/com/android/dialer/calllog/datasources/DataSources.java +++ b/java/com/android/dialer/calllog/datasources/DataSources.java @@ -16,13 +16,12 @@ package com.android.dialer.calllog.datasources; -import com.android.dialer.calllog.datasources.systemcalllog.SystemCallLogDataSource; import com.google.common.collect.ImmutableList; /** Immutable lists of data sources used to populate the annotated call log. */ public interface DataSources { - SystemCallLogDataSource getSystemCallLogDataSource(); + CallLogDataSource getSystemCallLogDataSource(); ImmutableList getDataSourcesIncludingSystemCallLog(); diff --git a/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java b/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java deleted file mode 100644 index f0384b09a..000000000 --- a/java/com/android/dialer/calllog/datasources/contacts/ContactsDataSource.java +++ /dev/null @@ -1,70 +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.datasources.contacts; - -import android.content.ContentValues; -import android.content.Context; -import android.support.annotation.MainThread; -import android.support.annotation.WorkerThread; -import com.android.dialer.calllog.datasources.CallLogDataSource; -import com.android.dialer.calllog.datasources.CallLogMutations; -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. */ -public final class ContactsDataSource implements CallLogDataSource { - - @Inject - public ContactsDataSource() {} - - @WorkerThread - @Override - public boolean isDirty(Context appContext) { - Assert.isWorkerThread(); - - // TODO(zachh): Implementation. - return false; - } - - @WorkerThread - @Override - public void fill( - Context appContext, - CallLogMutations mutations) { - Assert.isWorkerThread(); - // TODO(zachh): Implementation. - } - - @Override - public void onSuccessfulFill(Context appContext) { - // TODO(zachh): Implementation. - } - - @Override - public ContentValues coalesce(List individualRowsSortedByTimestampDesc) { - // TODO(zachh): Implementation. - return new ContentValues(); - } - - @MainThread - @Override - public void registerContentObservers( - Context appContext, ContentObserverCallbacks contentObserverCallbacks) { - // TODO(zachh): Guard against missing permissions during callback registration. - } -} diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java index 010cb8541..41eaf2bae 100644 --- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java +++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java @@ -29,6 +29,7 @@ import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.Ann import com.android.dialer.calllog.datasources.CallLogDataSource; import com.android.dialer.calllog.datasources.CallLogMutations; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.NonUiParallel; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; import com.android.dialer.phonelookup.PhoneLookupSelector; @@ -37,6 +38,9 @@ import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.protobuf.InvalidProtocolBufferException; import java.util.Arrays; @@ -45,6 +49,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; import javax.inject.Inject; /** @@ -54,15 +59,31 @@ import javax.inject.Inject; public final class PhoneLookupDataSource implements CallLogDataSource { private final PhoneLookup phoneLookup; + private final ListeningExecutorService executorService; @Inject - PhoneLookupDataSource(PhoneLookup phoneLookup) { + PhoneLookupDataSource(PhoneLookup phoneLookup, @NonUiParallel ExecutorService executorService) { this.phoneLookup = phoneLookup; + this.executorService = MoreExecutors.listeningDecorator(executorService); + } + + @Override + public ListenableFuture isDirty(Context appContext) { + return executorService.submit(() -> isDirtyInternal(appContext)); + } + + @Override + public ListenableFuture fill(Context appContext, CallLogMutations mutations) { + return executorService.submit(() -> fillInternal(appContext, mutations)); } - @WorkerThread @Override - public boolean isDirty(Context appContext) { + public ListenableFuture onSuccessfulFill(Context appContext) { + return executorService.submit(this::onSuccessfulFillInternal); + } + + @WorkerThread + private boolean isDirtyInternal(Context appContext) { ImmutableSet uniqueDialerPhoneNumbers = queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(appContext); @@ -102,8 +123,7 @@ public final class PhoneLookupDataSource implements CallLogDataSource { * */ @WorkerThread - @Override - public void fill(Context appContext, CallLogMutations mutations) { + private Void fillInternal(Context appContext, CallLogMutations mutations) { Map> annotatedCallLogIdsByNumber = queryIdAndNumberFromAnnotatedCallLog(appContext); ImmutableMap originalPhoneLookupInfosByNumber = @@ -137,12 +157,13 @@ public final class PhoneLookupDataSource implements CallLogDataSource { } } updateMutations(rowsToUpdate.build(), mutations); + return null; } @WorkerThread - @Override - public void onSuccessfulFill(Context appContext) { + private Void onSuccessfulFillInternal() { // TODO(zachh): Update PhoneLookupHistory. + return null; } @WorkerThread diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java index ef40c308e..dfc768c0a 100644 --- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java @@ -45,15 +45,20 @@ import com.android.dialer.calllog.datasources.util.RowCombiner; import com.android.dialer.calllogutils.PhoneAccountUtils; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.NonUiParallel; import com.android.dialer.common.concurrent.ThreadUtil; import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.android.dialer.storage.StorageComponent; import com.android.dialer.theme.R; import com.android.dialer.util.PermissionsUtil; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import com.google.i18n.phonenumbers.PhoneNumberUtil; import java.util.Arrays; import java.util.List; import java.util.Set; +import java.util.concurrent.ExecutorService; import javax.inject.Inject; /** @@ -66,10 +71,14 @@ public class SystemCallLogDataSource implements CallLogDataSource { @VisibleForTesting static final String PREF_LAST_TIMESTAMP_PROCESSED = "systemCallLogLastTimestampProcessed"; + private final ListeningExecutorService executorService; + @Nullable private Long lastTimestampProcessed; @Inject - public SystemCallLogDataSource() {} + SystemCallLogDataSource(@NonUiParallel ExecutorService executorService) { + this.executorService = MoreExecutors.listeningDecorator(executorService); + } @MainThread @Override @@ -94,9 +103,23 @@ public class SystemCallLogDataSource implements CallLogDataSource { ThreadUtil.getUiThreadHandler(), appContext, contentObserverCallbacks)); } - @WorkerThread @Override - public boolean isDirty(Context appContext) { + public ListenableFuture isDirty(Context appContext) { + return executorService.submit(() -> isDirtyInternal(appContext)); + } + + @Override + public ListenableFuture fill(Context appContext, CallLogMutations mutations) { + return executorService.submit(() -> fillInternal(appContext, mutations)); + } + + @Override + public ListenableFuture onSuccessfulFill(Context appContext) { + return executorService.submit(() -> onSuccessfulFillInternal(appContext)); + } + + @WorkerThread + private boolean isDirtyInternal(Context appContext) { Assert.isWorkerThread(); /* @@ -113,15 +136,14 @@ public class SystemCallLogDataSource implements CallLogDataSource { } @WorkerThread - @Override - public void fill(Context appContext, CallLogMutations mutations) { + private Void fillInternal(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; + return null; } // This data source should always run first so the mutations should always be empty. @@ -136,11 +158,11 @@ public class SystemCallLogDataSource implements CallLogDataSource { handleInsertsAndUpdates(appContext, mutations, annotatedCallLogIds); handleDeletes(appContext, annotatedCallLogIds, mutations); + return null; } @WorkerThread - @Override - public void onSuccessfulFill(Context appContext) { + private Void onSuccessfulFillInternal(Context appContext) { // If a fill operation was a no-op, lastTimestampProcessed could still be null. if (lastTimestampProcessed != null) { StorageComponent.get(appContext) @@ -149,6 +171,7 @@ public class SystemCallLogDataSource implements CallLogDataSource { .putLong(PREF_LAST_TIMESTAMP_PROCESSED, lastTimestampProcessed) .apply(); } + return null; } @Override diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java index 6833452c6..a5dccaf69 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java @@ -17,6 +17,7 @@ package com.android.dialer.calllog.ui; import android.database.Cursor; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; @@ -31,16 +32,26 @@ import com.android.dialer.calllog.CallLogFramework.CallLogUi; import com.android.dialer.calllog.RefreshAnnotatedCallLogWorker; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutorComponent; +import com.android.dialer.common.concurrent.ThreadUtil; import com.android.dialer.common.concurrent.UiListener; -import com.google.common.util.concurrent.ListenableScheduledFuture; +import com.google.common.util.concurrent.ListenableFuture; /** 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 { + /* + * 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 RefreshAnnotatedCallLogWorker refreshAnnotatedCallLogWorker; private UiListener refreshAnnotatedCallLogListener; private RecyclerView recyclerView; + @Nullable private Runnable refreshAnnotatedCallLogRunnable; public NewCallLogFragment() { LogUtil.enterBlock("NewCallLogFragment.NewCallLogFragment"); @@ -81,7 +92,7 @@ public final class NewCallLogFragment extends Fragment callLogFramework.attachUi(this); // TODO(zachh): Consider doing this when fragment becomes visible. - checkAnnotatedCallLogDirtyAndRefreshIfNecessary(); + refreshAnnotatedCallLog(true /* checkDirty */); } @Override @@ -90,6 +101,9 @@ public final class NewCallLogFragment extends Fragment LogUtil.enterBlock("NewCallLogFragment.onPause"); + // This is pending work that we don't actually need to follow through with. + ThreadUtil.getUiThreadHandler().removeCallbacks(refreshAnnotatedCallLogRunnable); + CallLogFramework callLogFramework = CallLogComponent.get(getContext()).callLogFramework(); callLogFramework.detachUi(); } @@ -107,18 +121,35 @@ public final class NewCallLogFragment extends Fragment return view; } - private void checkAnnotatedCallLogDirtyAndRefreshIfNecessary() { - LogUtil.enterBlock("NewCallLogFragment.checkAnnotatedCallLogDirtyAndRefreshIfNecessary"); - ListenableScheduledFuture future = refreshAnnotatedCallLogWorker.refreshWithDirtyCheck(); - refreshAnnotatedCallLogListener.listen(future, unused -> {}, RuntimeException::new); + private void refreshAnnotatedCallLog(boolean checkDirty) { + LogUtil.enterBlock("NewCallLogFragment.refreshAnnotatedCallLog"); + + // If we already scheduled a refresh, cancel it and schedule a new one so that repeated requests + // in quick succession don't result in too much work. For example, if we get 10 requests in + // 10ms, and a complete refresh takes a constant 200ms, the refresh will take 300ms (100ms wait + // and 1 iteration @200ms) instead of 2 seconds (10 iterations @ 200ms) since the work requests + // are serialized in RefreshAnnotatedCallLogWorker. + // + // We might get many requests in quick succession, for example, when the simulator inserts + // hundreds of rows into the system call log, or when the data for a new call is incrementally + // written to different columns as it becomes available. + ThreadUtil.getUiThreadHandler().removeCallbacks(refreshAnnotatedCallLogRunnable); + + refreshAnnotatedCallLogRunnable = + () -> { + ListenableFuture future = + checkDirty + ? refreshAnnotatedCallLogWorker.refreshWithDirtyCheck() + : refreshAnnotatedCallLogWorker.refreshWithoutDirtyCheck(); + refreshAnnotatedCallLogListener.listen(future, unused -> {}, RuntimeException::new); + }; + ThreadUtil.getUiThreadHandler().postDelayed(refreshAnnotatedCallLogRunnable, WAIT_MILLIS); } @Override public void invalidateUi() { LogUtil.enterBlock("NewCallLogFragment.invalidateUi"); - ListenableScheduledFuture future = - refreshAnnotatedCallLogWorker.refreshWithoutDirtyCheck(); - refreshAnnotatedCallLogListener.listen(future, unused -> {}, RuntimeException::new); + refreshAnnotatedCallLog(false /* checkDirty */); } @Override diff --git a/java/com/android/dialer/common/concurrent/DialerFutureSerializer.java b/java/com/android/dialer/common/concurrent/DialerFutureSerializer.java new file mode 100644 index 000000000..2629abbbe --- /dev/null +++ b/java/com/android/dialer/common/concurrent/DialerFutureSerializer.java @@ -0,0 +1,98 @@ +/* + * 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.common.concurrent; + +import static com.google.common.util.concurrent.Futures.immediateCancelledFuture; +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + +import com.google.common.util.concurrent.AsyncCallable; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Serializes execution of a set of operations. This class guarantees that a submitted callable will + * not be called before previously submitted callables have completed. + */ +public final class DialerFutureSerializer { + /** This reference acts as a pointer tracking the head of a linked list of ListenableFutures. */ + private final AtomicReference> ref = + new AtomicReference<>(immediateFuture(null)); + + /** Enqueues a task to run when the previous task (if any) completes. */ + public ListenableFuture submit(final Callable callable, Executor executor) { + return submitAsync(() -> immediateFuture(callable.call()), executor); + } + + /** + * Enqueues a task to run when the previous task (if any) completes. + * + *

Cancellation does not propagate from the output future to the future returned from {@code + * callable}, but if the output future is cancelled before {@link AsyncCallable#call()} is + * invoked, {@link AsyncCallable#call()} will not be invoked. + */ + public ListenableFuture submitAsync(final AsyncCallable callable, Executor executor) { + AtomicBoolean wasCancelled = new AtomicBoolean(false); + final AsyncCallable task = + () -> { + if (wasCancelled.get()) { + return immediateCancelledFuture(); + } + return callable.call(); + }; + /* + * Three futures are at play here: + * taskFuture is the future that comes from the callable. + * newFuture is the future we use to track the serialization of our task. + * oldFuture is the previous task's newFuture. + * + * newFuture is guaranteed to only complete once all tasks previously submitted to this instance + * once the futures returned from those submissions have completed. + */ + final SettableFuture newFuture = SettableFuture.create(); + + final ListenableFuture oldFuture = ref.getAndSet(newFuture); + + // Invoke our task once the previous future completes. + final ListenableFuture taskFuture = + Futures.nonCancellationPropagating( + Futures.submitAsync(task, runnable -> oldFuture.addListener(runnable, executor))); + // newFuture's lifetime is determined by taskFuture, unless taskFuture is cancelled, in which + // case it falls back to oldFuture's. This is to ensure that if the future we return is + // cancelled, we don't begin execution of the next task until after oldFuture completes. + taskFuture.addListener( + () -> { + if (taskFuture.isCancelled()) { + // Since the value of oldFuture can only ever be immediateFuture(null) or setFuture of a + // future that eventually came from immediateFuture(null), this doesn't leak throwables + // or completion values. + wasCancelled.set(true); + newFuture.setFuture(oldFuture); + } else { + newFuture.set(null); + } + }, + directExecutor()); + + return taskFuture; + } +} -- cgit v1.2.3 From 7772ec071f5ed2e2283e9a00215e8cf1fca9168a Mon Sep 17 00:00:00 2001 From: erfanian Date: Tue, 5 Dec 2017 17:43:20 -0800 Subject: Add Assisted Dialing Call Details Implementation. This includes all of the work specified in the UX layouts, with the exception of the parenthetical country code. Because the canonical mapping of dial prefixes to country codes is not contained in this package, some thought needs to be given on the best way to implement this part of the spec. This change also introduces a fallback error text, in case the client experiences bad data from the call log. Test: new unit tests PiperOrigin-RevId: 178038123 Change-Id: I88eec72a73820e092f24c5f53ee9520a42486ada --- .../dialer/calldetails/CallDetailsActivity.java | 59 ++++++++++++-- .../dialer/calldetails/CallDetailsAdapter.java | 12 +-- .../calldetails/CallDetailsHeaderViewHolder.java | 93 +++++++++++++++++++--- .../calldetails/res/layout/contact_container.xml | 61 +++++++++++--- .../dialer/calldetails/res/values/dimens.xml | 12 ++- .../dialer/calldetails/res/values/strings.xml | 6 ++ .../compat/telephony/TelephonyManagerCompat.java | 6 ++ 7 files changed, 213 insertions(+), 36 deletions(-) diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java index b51d833dc..c29f9e9ae 100644 --- a/java/com/android/dialer/calldetails/CallDetailsActivity.java +++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java @@ -33,13 +33,20 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; +import android.view.View; import android.widget.Toast; +import com.android.dialer.DialerPhoneNumber; +import com.android.dialer.assisteddialing.ui.AssistedDialingSettingActivity; import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; import com.android.dialer.callintent.CallInitiationType; import com.android.dialer.callintent.CallIntentBuilder; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.AsyncTaskExecutors; +import com.android.dialer.common.concurrent.DialerExecutor.FailureListener; +import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener; +import com.android.dialer.common.concurrent.DialerExecutor.Worker; +import com.android.dialer.common.concurrent.DialerExecutorComponent; import com.android.dialer.constants.ActivityRequestCodes; import com.android.dialer.dialercontact.DialerContact; import com.android.dialer.duo.Duo; @@ -51,10 +58,12 @@ import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.Logger; import com.android.dialer.logging.UiAction; import com.android.dialer.performancereport.PerformanceReport; +import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.android.dialer.postcall.PostCall; import com.android.dialer.precall.PreCall; import com.android.dialer.protos.ProtoParsers; import com.google.common.base.Preconditions; +import com.google.i18n.phonenumbers.PhoneNumberUtil; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.List; @@ -70,8 +79,8 @@ public class CallDetailsActivity extends AppCompatActivity { public static final String EXTRA_CAN_REPORT_CALLER_ID = "can_report_caller_id"; private static final String EXTRA_CAN_SUPPORT_ASSISTED_DIALING = "can_support_assisted_dialing"; - private final CallDetailsHeaderViewHolder.CallbackActionListener callbackActionListener = - new CallbackActionListener(this); + private final CallDetailsHeaderViewHolder.CallDetailsHeaderListener callDetailsHeaderListener = + new CallDetailsHeaderListener(this); private final CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener = new DeleteCallDetailsListener(this); private final CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener = @@ -166,7 +175,7 @@ public class CallDetailsActivity extends AppCompatActivity { this /* context */, contact, entries.getEntriesList(), - callbackActionListener, + callDetailsHeaderListener, reportCallIdListener, deleteCallDetailsListener); @@ -248,11 +257,11 @@ public class CallDetailsActivity extends AppCompatActivity { } } - private static final class CallbackActionListener - implements CallDetailsHeaderViewHolder.CallbackActionListener { - private final WeakReference activityWeakReference; + private static final class CallDetailsHeaderListener + implements CallDetailsHeaderViewHolder.CallDetailsHeaderListener { + private final WeakReference activityWeakReference; - CallbackActionListener(Activity activity) { + CallDetailsHeaderListener(CallDetailsActivity activity) { this.activityWeakReference = new WeakReference<>(activity); } @@ -303,9 +312,43 @@ public class CallDetailsActivity extends AppCompatActivity { PreCall.start(getActivity(), callIntentBuilder); } - private Activity getActivity() { + private CallDetailsActivity getActivity() { return Preconditions.checkNotNull(activityWeakReference.get()); } + + @Override + public void openAssistedDialingSettings(View unused) { + Intent intent = new Intent(getActivity(), AssistedDialingSettingActivity.class); + getActivity().startActivity(intent); + } + + @Override + public void createAssistedDialerNumberParserTask( + AssistedDialingNumberParseWorker worker, + SuccessListener successListener, + FailureListener failureListener) { + DialerExecutorComponent.get(getActivity().getApplicationContext()) + .dialerExecutorFactory() + .createUiTaskBuilder( + getActivity().getFragmentManager(), + "CallDetailsActivity.createAssistedDialerNumberParserTask", + new AssistedDialingNumberParseWorker()) + .onSuccess(successListener) + .onFailure(failureListener) + .build() + .executeParallel(getActivity().contact.getNumber()); + } + } + + static class AssistedDialingNumberParseWorker implements Worker { + + @Override + public Integer doInBackground(@NonNull String phoneNumber) { + DialerPhoneNumberUtil dialerPhoneNumberUtil = + new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance()); + DialerPhoneNumber parsedNumber = dialerPhoneNumberUtil.parse(phoneNumber, null); + return parsedNumber.getDialerInternalPhoneNumber().getCountryCode(); + } } private static final class DeleteCallDetailsListener diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapter.java b/java/com/android/dialer/calldetails/CallDetailsAdapter.java index 07590597e..9095b86ea 100644 --- a/java/com/android/dialer/calldetails/CallDetailsAdapter.java +++ b/java/com/android/dialer/calldetails/CallDetailsAdapter.java @@ -24,7 +24,7 @@ import android.view.LayoutInflater; import android.view.ViewGroup; import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener; -import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallbackActionListener; +import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener; import com.android.dialer.calllogutils.CallTypeHelper; import com.android.dialer.calllogutils.CallbackActionHelper; import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction; @@ -41,7 +41,7 @@ final class CallDetailsAdapter extends RecyclerView.Adapter callDetailsEntries, - CallbackActionListener callbackActionListener, + CallDetailsHeaderListener callDetailsHeaderListener, CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener, DeleteCallDetailsListener deleteCallDetailsListener) { this.contact = Assert.isNotNull(contact); this.callDetailsEntries = callDetailsEntries; - this.callbackActionListener = callbackActionListener; + this.callDetailsHeaderListener = callDetailsHeaderListener; this.reportCallIdListener = reportCallIdListener; this.deleteCallDetailsListener = deleteCallDetailsListener; callTypeHelper = new CallTypeHelper(context.getResources(), DuoComponent.get(context).getDuo()); @@ -68,7 +68,7 @@ final class CallDetailsAdapter extends RecyclerView.Adapter onSuccess, + FailureListener onFailure); } } diff --git a/java/com/android/dialer/calldetails/res/layout/contact_container.xml b/java/com/android/dialer/calldetails/res/layout/contact_container.xml index b01a6cc13..5f531ab43 100644 --- a/java/com/android/dialer/calldetails/res/layout/contact_container.xml +++ b/java/com/android/dialer/calldetails/res/layout/contact_container.xml @@ -19,49 +19,54 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/call_details_top_margin" - android:gravity="center_vertical" android:paddingTop="@dimen/contact_container_padding_top_start" - android:paddingStart="@dimen/contact_container_padding_top_start" android:paddingBottom="@dimen/contact_container_padding_bottom_end" - android:paddingEnd="@dimen/contact_container_padding_bottom_end"> + android:paddingStart="@dimen/contact_container_padding_top_start" + android:paddingEnd="@dimen/contact_container_padding_bottom_end" + android:gravity="center_vertical" + android:orientation="vertical"> + android:gravity="center_vertical" + android:minHeight="@dimen/call_details_contact_photo_size" + android:orientation="vertical"> + android:layout_marginStart="@dimen/photo_text_margin"/> + android:layout_marginStart="@dimen/photo_text_margin"/> + android:visibility="gone"/> + + + + + + + + + + + + \ No newline at end of file diff --git a/java/com/android/dialer/calldetails/res/values/dimens.xml b/java/com/android/dialer/calldetails/res/values/dimens.xml index 694c8f47c..8c84b1b9b 100644 --- a/java/com/android/dialer/calldetails/res/values/dimens.xml +++ b/java/com/android/dialer/calldetails/res/values/dimens.xml @@ -18,7 +18,7 @@ 6dp - 16dp + 8dp 12dp 48dp 4dp @@ -34,4 +34,14 @@ 48dp 40dp 8dp + + + 48dp + 18dp + 16dp + 8dp + + 2dp + 60dp + \ No newline at end of file diff --git a/java/com/android/dialer/calldetails/res/values/strings.xml b/java/com/android/dialer/calldetails/res/values/strings.xml index 74ac71c32..f81696034 100644 --- a/java/com/android/dialer/calldetails/res/values/strings.xml +++ b/java/com/android/dialer/calldetails/res/values/strings.xml @@ -49,4 +49,10 @@ Number reported + + + Assisted dialing: Used country code +%1$s + + + Assisted dialing was used diff --git a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java index cadce4d48..6bed818da 100644 --- a/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java +++ b/java/com/android/dialer/compat/telephony/TelephonyManagerCompat.java @@ -76,6 +76,12 @@ public class TelephonyManagerCompat { public static final String EXTRA_IS_REFRESH = BuildCompat.isAtLeastOMR1() ? "android.telephony.extra.IS_REFRESH" : "is_refresh"; + /** + * Indicates the call underwent Assisted Dialing; typically set as a feature available from the + * CallLog. + */ + public static final Integer FEATURES_ASSISTED_DIALING = 0x10; + /** * Returns the number of phones available. Returns 1 for Single standby mode (Single SIM * functionality) Returns 2 for Dual standby mode.(Dual SIM functionality) -- cgit v1.2.3 From 32632f8cbed8ec7651e7c831b4defdfd4d81e70d Mon Sep 17 00:00:00 2001 From: erfanian Date: Wed, 6 Dec 2017 11:01:51 -0800 Subject: Use explicit version constant for AD ceiling. Test: tap PiperOrigin-RevId: 178121918 Change-Id: Ia60c844181b63b527ce9753a9bef96d7b7076c40 --- java/com/android/dialer/assisteddialing/ConcreteCreator.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/java/com/android/dialer/assisteddialing/ConcreteCreator.java b/java/com/android/dialer/assisteddialing/ConcreteCreator.java index 73817d7fc..5236ea8eb 100644 --- a/java/com/android/dialer/assisteddialing/ConcreteCreator.java +++ b/java/com/android/dialer/assisteddialing/ConcreteCreator.java @@ -41,8 +41,7 @@ public final class ConcreteCreator { // Floor set at N due to use of Optional. protected static final int BUILD_CODE_FLOOR = Build.VERSION_CODES.N; // Ceiling set at O_MR1 because this feature will ship as part of the framework in P. - // TODO(erfanian): Switch to public build constant when 27 is available in public master. - @VisibleForTesting public static final int BUILD_CODE_CEILING = 27; + @VisibleForTesting public static final int BUILD_CODE_CEILING = Build.VERSION_CODES.O_MR1; /** * Creates a new AssistedDialingMediator -- cgit v1.2.3 From 0c0c0a318d72e4b6f24514c4c6b3a173e249297d Mon Sep 17 00:00:00 2001 From: zachh Date: Wed, 6 Dec 2017 18:06:06 -0800 Subject: Added bindings for ListeningExecutorServices. Use them where appropriate. Bug: 34672501 Test: existing PiperOrigin-RevId: 178182298 Change-Id: If454225e0d636c7cb14b5af02d46780d7732abf0 --- .../calllog/RefreshAnnotatedCallLogWorker.java | 35 +++++++++---------- .../dialer/calllog/database/MutationApplier.java | 12 +++---- .../phonelookup/PhoneLookupDataSource.java | 18 +++++----- .../systemcalllog/SystemCallLogDataSource.java | 16 ++++----- .../dialer/common/concurrent/Annotations.java | 8 +++++ .../concurrent/DefaultDialerExecutorFactory.java | 4 +-- .../common/concurrent/DialerExecutorModule.java | 23 +++++++++++-- .../dialer/phonelookup/PhoneLookupModule.java | 9 +++-- .../composite/CompositePhoneLookup.java | 17 +++++++--- .../dialer/phonelookup/cp2/Cp2PhoneLookup.java | 39 ++++++++++++---------- 10 files changed, 107 insertions(+), 74 deletions(-) diff --git a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java index de8905db8..e5cc3eb89 100644 --- a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java +++ b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java @@ -23,7 +23,8 @@ 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.LogUtil; -import com.android.dialer.common.concurrent.Annotations.NonUiParallel; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; +import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; import com.android.dialer.common.concurrent.DialerFutureSerializer; import com.android.dialer.common.concurrent.DialerFutures; import com.android.dialer.inject.ApplicationContext; @@ -32,10 +33,8 @@ import com.google.common.base.Preconditions; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutorService; import javax.inject.Inject; import javax.inject.Singleton; @@ -46,7 +45,8 @@ public class RefreshAnnotatedCallLogWorker { private final Context appContext; private final DataSources dataSources; private final SharedPreferences sharedPreferences; - private final ListeningExecutorService parallelUiListeningExecutorService; + private final ListeningExecutorService backgroundExecutorService; + private final ListeningExecutorService lightweightExecutorService; // Used to ensure that only one refresh flow runs at a time. (Note that // RefreshAnnotatedCallLogWorker is a @Singleton.) private final DialerFutureSerializer dialerFutureSerializer = new DialerFutureSerializer(); @@ -56,14 +56,13 @@ public class RefreshAnnotatedCallLogWorker { @ApplicationContext Context appContext, DataSources dataSources, @Unencrypted SharedPreferences sharedPreferences, - @NonUiParallel ExecutorService parallelUiExecutorService) { + @BackgroundExecutor ListeningExecutorService backgroundExecutorService, + @LightweightExecutor ListeningExecutorService lightweightExecutorService) { this.appContext = appContext; this.dataSources = dataSources; this.sharedPreferences = sharedPreferences; - - // TODO(zachh): Create and use bindings for ListeningExecutorServices. - this.parallelUiListeningExecutorService = - MoreExecutors.listeningDecorator(parallelUiExecutorService); + this.backgroundExecutorService = backgroundExecutorService; + this.lightweightExecutorService = lightweightExecutorService; } /** Checks if the annotated call log is dirty and refreshes it if necessary. */ @@ -78,16 +77,14 @@ public class RefreshAnnotatedCallLogWorker { private ListenableFuture refresh(boolean checkDirty) { LogUtil.i("RefreshAnnotatedCallLogWorker.refresh", "submitting serialized refresh request"); - // Note: directExecutor is safe to use here and throughout because all methods are async. return dialerFutureSerializer.submitAsync( - () -> checkDirtyAndRebuildIfNecessary(appContext, checkDirty), - MoreExecutors.directExecutor()); + () -> checkDirtyAndRebuildIfNecessary(appContext, checkDirty), lightweightExecutorService); } private ListenableFuture checkDirtyAndRebuildIfNecessary( Context appContext, boolean checkDirty) { ListenableFuture forceRebuildFuture = - parallelUiListeningExecutorService.submit( + backgroundExecutorService.submit( () -> { LogUtil.i( "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", @@ -115,7 +112,7 @@ public class RefreshAnnotatedCallLogWorker { Preconditions.checkNotNull(forceRebuild) ? Futures.immediateFuture(true) : isDirty(appContext), - MoreExecutors.directExecutor()); + lightweightExecutorService); // After determining isDirty, conditionally call rebuild. return Futures.transformAsync( @@ -124,7 +121,7 @@ public class RefreshAnnotatedCallLogWorker { Preconditions.checkNotNull(isDirty) ? rebuild(appContext) : Futures.immediateFuture(null), - MoreExecutors.directExecutor()); + lightweightExecutorService); } private ListenableFuture isDirty(Context appContext) { @@ -151,7 +148,7 @@ public class RefreshAnnotatedCallLogWorker { Futures.transformAsync( fillFuture, unused -> dataSource.fill(appContext, mutations), - MoreExecutors.directExecutor()); + lightweightExecutorService); } // After all data sources are filled, apply mutations (at this point "fillFuture" is the result @@ -163,7 +160,7 @@ public class RefreshAnnotatedCallLogWorker { CallLogDatabaseComponent.get(appContext) .mutationApplier() .applyToDatabase(mutations, appContext), - MoreExecutors.directExecutor()); + lightweightExecutorService); // After mutations applied, call onSuccessfulFill for each data source (in parallel). ListenableFuture> onSuccessfulFillFuture = @@ -177,7 +174,7 @@ public class RefreshAnnotatedCallLogWorker { } return Futures.allAsList(onSuccessfulFillFutures); }, - MoreExecutors.directExecutor()); + lightweightExecutorService); // After onSuccessfulFill is called for every data source, write the shared pref. return Futures.transform( @@ -186,6 +183,6 @@ public class RefreshAnnotatedCallLogWorker { sharedPreferences.edit().putBoolean(CallLogFramework.PREF_FORCE_REBUILD, false).apply(); return null; }, - parallelUiListeningExecutorService); + backgroundExecutorService); } } diff --git a/java/com/android/dialer/calllog/database/MutationApplier.java b/java/com/android/dialer/calllog/database/MutationApplier.java index 720daec54..eee810eb8 100644 --- a/java/com/android/dialer/calllog/database/MutationApplier.java +++ b/java/com/android/dialer/calllog/database/MutationApplier.java @@ -28,25 +28,23 @@ import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.Ann import com.android.dialer.calllog.datasources.CallLogMutations; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; -import com.android.dialer.common.concurrent.Annotations.NonUiParallel; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.Arrays; import java.util.Map.Entry; -import java.util.concurrent.ExecutorService; import javax.inject.Inject; /** Applies {@link CallLogMutations} to the annotated call log. */ public class MutationApplier { - private final ListeningExecutorService executorService; + private final ListeningExecutorService backgroundExecutorService; @Inject - MutationApplier(@NonUiParallel ExecutorService executorService) { - this.executorService = MoreExecutors.listeningDecorator(executorService); + MutationApplier(@BackgroundExecutor ListeningExecutorService backgroundExecutorService) { + this.backgroundExecutorService = backgroundExecutorService; } /** Applies the provided {@link CallLogMutations} to the annotated call log. */ @@ -54,7 +52,7 @@ public class MutationApplier { if (mutations.isEmpty()) { return Futures.immediateFuture(null); } - return executorService.submit( + return backgroundExecutorService.submit( () -> { applyToDatabaseInternal(mutations, appContext); return null; diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java index 41eaf2bae..9b90ad5cc 100644 --- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java +++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java @@ -29,7 +29,7 @@ import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.Ann import com.android.dialer.calllog.datasources.CallLogDataSource; import com.android.dialer.calllog.datasources.CallLogMutations; import com.android.dialer.common.LogUtil; -import com.android.dialer.common.concurrent.Annotations.NonUiParallel; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; import com.android.dialer.phonelookup.PhoneLookupSelector; @@ -40,7 +40,6 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.protobuf.InvalidProtocolBufferException; import java.util.Arrays; @@ -49,7 +48,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; import javax.inject.Inject; /** @@ -59,27 +57,29 @@ import javax.inject.Inject; public final class PhoneLookupDataSource implements CallLogDataSource { private final PhoneLookup phoneLookup; - private final ListeningExecutorService executorService; + private final ListeningExecutorService backgroundExecutorService; @Inject - PhoneLookupDataSource(PhoneLookup phoneLookup, @NonUiParallel ExecutorService executorService) { + PhoneLookupDataSource( + PhoneLookup phoneLookup, + @BackgroundExecutor ListeningExecutorService backgroundExecutorService) { this.phoneLookup = phoneLookup; - this.executorService = MoreExecutors.listeningDecorator(executorService); + this.backgroundExecutorService = backgroundExecutorService; } @Override public ListenableFuture isDirty(Context appContext) { - return executorService.submit(() -> isDirtyInternal(appContext)); + return backgroundExecutorService.submit(() -> isDirtyInternal(appContext)); } @Override public ListenableFuture fill(Context appContext, CallLogMutations mutations) { - return executorService.submit(() -> fillInternal(appContext, mutations)); + return backgroundExecutorService.submit(() -> fillInternal(appContext, mutations)); } @Override public ListenableFuture onSuccessfulFill(Context appContext) { - return executorService.submit(this::onSuccessfulFillInternal); + return backgroundExecutorService.submit(this::onSuccessfulFillInternal); } @WorkerThread diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java index dfc768c0a..91db915ef 100644 --- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java @@ -45,7 +45,7 @@ import com.android.dialer.calllog.datasources.util.RowCombiner; import com.android.dialer.calllogutils.PhoneAccountUtils; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; -import com.android.dialer.common.concurrent.Annotations.NonUiParallel; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; import com.android.dialer.common.concurrent.ThreadUtil; import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.android.dialer.storage.StorageComponent; @@ -53,12 +53,10 @@ import com.android.dialer.theme.R; import com.android.dialer.util.PermissionsUtil; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import com.google.i18n.phonenumbers.PhoneNumberUtil; import java.util.Arrays; import java.util.List; import java.util.Set; -import java.util.concurrent.ExecutorService; import javax.inject.Inject; /** @@ -71,13 +69,13 @@ public class SystemCallLogDataSource implements CallLogDataSource { @VisibleForTesting static final String PREF_LAST_TIMESTAMP_PROCESSED = "systemCallLogLastTimestampProcessed"; - private final ListeningExecutorService executorService; + private final ListeningExecutorService backgroundExecutorService; @Nullable private Long lastTimestampProcessed; @Inject - SystemCallLogDataSource(@NonUiParallel ExecutorService executorService) { - this.executorService = MoreExecutors.listeningDecorator(executorService); + SystemCallLogDataSource(@BackgroundExecutor ListeningExecutorService backgroundExecutorService) { + this.backgroundExecutorService = backgroundExecutorService; } @MainThread @@ -105,17 +103,17 @@ public class SystemCallLogDataSource implements CallLogDataSource { @Override public ListenableFuture isDirty(Context appContext) { - return executorService.submit(() -> isDirtyInternal(appContext)); + return backgroundExecutorService.submit(() -> isDirtyInternal(appContext)); } @Override public ListenableFuture fill(Context appContext, CallLogMutations mutations) { - return executorService.submit(() -> fillInternal(appContext, mutations)); + return backgroundExecutorService.submit(() -> fillInternal(appContext, mutations)); } @Override public ListenableFuture onSuccessfulFill(Context appContext) { - return executorService.submit(() -> onSuccessfulFillInternal(appContext)); + return backgroundExecutorService.submit(() -> onSuccessfulFillInternal(appContext)); } @WorkerThread diff --git a/java/com/android/dialer/common/concurrent/Annotations.java b/java/com/android/dialer/common/concurrent/Annotations.java index 5e3954cf9..62d5b318e 100644 --- a/java/com/android/dialer/common/concurrent/Annotations.java +++ b/java/com/android/dialer/common/concurrent/Annotations.java @@ -39,4 +39,12 @@ public class Annotations { /** Annotation for retrieving the UI serial executor. */ @Qualifier public @interface UiSerial {} + + /** Annotation for retrieving the lightweight executor. */ + @Qualifier + public @interface LightweightExecutor {} + + /** Annotation for retrieving the background executor. */ + @Qualifier + public @interface BackgroundExecutor {} } diff --git a/java/com/android/dialer/common/concurrent/DefaultDialerExecutorFactory.java b/java/com/android/dialer/common/concurrent/DefaultDialerExecutorFactory.java index ab01654aa..317807b1d 100644 --- a/java/com/android/dialer/common/concurrent/DefaultDialerExecutorFactory.java +++ b/java/com/android/dialer/common/concurrent/DefaultDialerExecutorFactory.java @@ -40,14 +40,14 @@ import javax.inject.Inject; public class DefaultDialerExecutorFactory implements DialerExecutorFactory { private final ExecutorService nonUiThreadPool; private final ScheduledExecutorService nonUiSerialExecutor; - private final Executor uiThreadPool; + private final ExecutorService uiThreadPool; private final ScheduledExecutorService uiSerialExecutor; @Inject DefaultDialerExecutorFactory( @NonUiParallel ExecutorService nonUiThreadPool, @NonUiSerial ScheduledExecutorService nonUiSerialExecutor, - @UiParallel Executor uiThreadPool, + @UiParallel ExecutorService uiThreadPool, @UiSerial ScheduledExecutorService uiSerialExecutor) { this.nonUiThreadPool = nonUiThreadPool; this.nonUiSerialExecutor = nonUiSerialExecutor; diff --git a/java/com/android/dialer/common/concurrent/DialerExecutorModule.java b/java/com/android/dialer/common/concurrent/DialerExecutorModule.java index 5e0190e8d..98738ed37 100644 --- a/java/com/android/dialer/common/concurrent/DialerExecutorModule.java +++ b/java/com/android/dialer/common/concurrent/DialerExecutorModule.java @@ -17,16 +17,18 @@ package com.android.dialer.common.concurrent; import android.os.AsyncTask; 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.Annotations.NonUiParallel; import com.android.dialer.common.concurrent.Annotations.NonUiSerial; import com.android.dialer.common.concurrent.Annotations.Ui; import com.android.dialer.common.concurrent.Annotations.UiParallel; import com.android.dialer.common.concurrent.Annotations.UiSerial; import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import dagger.Binds; import dagger.Module; import dagger.Provides; -import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -85,8 +87,8 @@ public abstract class DialerExecutorModule { @Provides @UiParallel - static Executor provideUiThreadPool() { - return AsyncTask.THREAD_POOL_EXECUTOR; + static ExecutorService provideUiThreadPool() { + return (ExecutorService) AsyncTask.THREAD_POOL_EXECUTOR; } @Provides @@ -105,4 +107,19 @@ public abstract class DialerExecutorModule { } }); } + + @Provides + @Singleton + @LightweightExecutor + static ListeningExecutorService provideLightweightExecutor(@UiParallel ExecutorService delegate) { + return MoreExecutors.listeningDecorator(delegate); + } + + @Provides + @Singleton + @BackgroundExecutor + static ListeningExecutorService provideBackgroundExecutor( + @NonUiParallel ExecutorService delegate) { + return MoreExecutors.listeningDecorator(delegate); + } } diff --git a/java/com/android/dialer/phonelookup/PhoneLookupModule.java b/java/com/android/dialer/phonelookup/PhoneLookupModule.java index 400caff3a..39b0a5083 100644 --- a/java/com/android/dialer/phonelookup/PhoneLookupModule.java +++ b/java/com/android/dialer/phonelookup/PhoneLookupModule.java @@ -27,7 +27,12 @@ import dagger.Provides; public abstract class PhoneLookupModule { @Provides - static PhoneLookup providePhoneLookup(Cp2PhoneLookup cp2PhoneLookup) { - return new CompositePhoneLookup(ImmutableList.of(cp2PhoneLookup)); + static ImmutableList providePhoneLookupList(Cp2PhoneLookup cp2PhoneLookup) { + return ImmutableList.of(cp2PhoneLookup); + } + + @Provides + static PhoneLookup providePhoneLookup(CompositePhoneLookup compositePhoneLookup) { + return compositePhoneLookup; } } diff --git a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java index f432e27ae..abc0c74fd 100644 --- a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java +++ b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java @@ -20,6 +20,7 @@ import android.support.annotation.NonNull; import android.telecom.Call; import com.android.dialer.DialerPhoneNumber; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; import com.android.dialer.common.concurrent.DialerFutures; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; @@ -29,9 +30,10 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.ListeningExecutorService; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; /** * {@link PhoneLookup} which delegates to a configured set of {@link PhoneLookup PhoneLookups}, @@ -40,9 +42,14 @@ import java.util.List; public final class CompositePhoneLookup implements PhoneLookup { private final ImmutableList phoneLookups; + private final ListeningExecutorService lightweightExecutorService; - public CompositePhoneLookup(ImmutableList phoneLookups) { + @Inject + CompositePhoneLookup( + ImmutableList phoneLookups, + @LightweightExecutor ListeningExecutorService lightweightExecutorService) { this.phoneLookups = phoneLookups; + this.lightweightExecutorService = lightweightExecutorService; } /** @@ -68,7 +75,7 @@ public final class CompositePhoneLookup implements PhoneLookup { } return mergedInfo.build(); }, - MoreExecutors.directExecutor()); + lightweightExecutorService); } @Override @@ -117,7 +124,7 @@ public final class CompositePhoneLookup implements PhoneLookup { } return combinedMap.build(); }, - MoreExecutors.directExecutor()); + lightweightExecutorService); } @Override @@ -127,6 +134,6 @@ public final class CompositePhoneLookup implements PhoneLookup { futures.add(phoneLookup.onSuccessfulBulkUpdate()); } return Futures.transform( - Futures.allAsList(futures), unused -> null, MoreExecutors.directExecutor()); + Futures.allAsList(futures), unused -> null, lightweightExecutorService); } } diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java index cfb8fb7b8..cd645a447 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java @@ -30,6 +30,7 @@ import android.telecom.Call; import android.text.TextUtils; import com.android.dialer.DialerPhoneNumber; import com.android.dialer.common.Assert; +import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; @@ -39,7 +40,7 @@ import com.android.dialer.storage.Unencrypted; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.ListeningExecutorService; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -71,25 +72,29 @@ public final class Cp2PhoneLookup implements PhoneLookup { private final Context appContext; private final SharedPreferences sharedPreferences; + private final ListeningExecutorService backgroundExecutorService; + @Nullable private Long currentLastTimestampProcessed; @Inject Cp2PhoneLookup( - @ApplicationContext Context appContext, @Unencrypted SharedPreferences sharedPreferences) { + @ApplicationContext Context appContext, + @Unencrypted SharedPreferences sharedPreferences, + @BackgroundExecutor ListeningExecutorService backgroundExecutorService) { this.appContext = appContext; this.sharedPreferences = sharedPreferences; + this.backgroundExecutorService = backgroundExecutorService; } @Override public ListenableFuture lookup(@NonNull Call call) { // TODO(zachh): Implementation. - return MoreExecutors.newDirectExecutorService().submit(PhoneLookupInfo::getDefaultInstance); + return backgroundExecutorService.submit(PhoneLookupInfo::getDefaultInstance); } @Override public ListenableFuture isDirty(ImmutableSet phoneNumbers) { - // TODO(calderwoodra): consider a different thread pool - return MoreExecutors.newDirectExecutorService().submit(() -> isDirtyInternal(phoneNumbers)); + return backgroundExecutorService.submit(() -> isDirtyInternal(phoneNumbers)); } private boolean isDirtyInternal(ImmutableSet phoneNumbers) { @@ -185,8 +190,7 @@ public final class Cp2PhoneLookup implements PhoneLookup { @Override public ListenableFuture> bulkUpdate( ImmutableMap existingInfoMap) { - return MoreExecutors.newDirectExecutorService() - .submit(() -> bulkUpdateInternal(existingInfoMap)); + return backgroundExecutorService.submit(() -> bulkUpdateInternal(existingInfoMap)); } private ImmutableMap bulkUpdateInternal( @@ -234,17 +238,16 @@ public final class Cp2PhoneLookup implements PhoneLookup { @Override public ListenableFuture onSuccessfulBulkUpdate() { - return MoreExecutors.newDirectExecutorService() - .submit( - () -> { - if (currentLastTimestampProcessed != null) { - sharedPreferences - .edit() - .putLong(PREF_LAST_TIMESTAMP_PROCESSED, currentLastTimestampProcessed) - .apply(); - } - return null; - }); + return backgroundExecutorService.submit( + () -> { + if (currentLastTimestampProcessed != null) { + sharedPreferences + .edit() + .putLong(PREF_LAST_TIMESTAMP_PROCESSED, currentLastTimestampProcessed) + .apply(); + } + return null; + }); } /** -- cgit v1.2.3 From d28981d41c3691fc9fa5204e1d02fa4f2d818b43 Mon Sep 17 00:00:00 2001 From: zachh Date: Wed, 6 Dec 2017 19:25:22 -0800 Subject: Renamed PhoneLookup#bulkUpdate to #getMostRecentPhoneLookupInfo. This is just to more clearly convey what the method does. Bug: 34672501 Test: existing PiperOrigin-RevId: 178188575 Change-Id: Id02f34b1d79346ecd8ca9eebc043fe9b3063264b --- .../phonelookup/PhoneLookupDataSource.java | 20 +++++++------------- java/com/android/dialer/phonelookup/PhoneLookup.java | 20 ++++++++++---------- .../phonelookup/composite/CompositePhoneLookup.java | 7 ++++--- .../dialer/phonelookup/cp2/Cp2PhoneLookup.java | 5 +++-- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java index 9b90ad5cc..9606b8e77 100644 --- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java +++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java @@ -83,18 +83,10 @@ public final class PhoneLookupDataSource implements CallLogDataSource { } @WorkerThread - private boolean isDirtyInternal(Context appContext) { + private boolean isDirtyInternal(Context appContext) throws Exception { ImmutableSet uniqueDialerPhoneNumbers = queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(appContext); - - try { - // TODO(zachh): Would be good to rework call log architecture to properly use futures. - // TODO(zachh): Consider how individual lookups should behave wrt timeouts/exceptions and - // handle appropriately here. - return phoneLookup.isDirty(uniqueDialerPhoneNumbers).get(); - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException(e); - } + return phoneLookup.isDirty(uniqueDialerPhoneNumbers).get(); } /** @@ -113,8 +105,9 @@ public final class PhoneLookupDataSource implements CallLogDataSource { *
  • For inserts, uses the contents of PhoneLookupHistory to populate the fields of the * provided mutations. (Note that at this point, data may not be fully up-to-date, but the * next steps will take care of that.) - *
  • Uses all of the numbers from AnnotatedCallLog to invoke CompositePhoneLookup:bulkUpdate - *
  • Looks through the results of bulkUpdate + *
  • Uses all of the numbers from AnnotatedCallLog to invoke (composite) {@link + * PhoneLookup#getMostRecentPhoneLookupInfo(ImmutableMap)} + *
  • Looks through the results of getMostRecentPhoneLookupInfo *
      *
    • For each number, checks if the original PhoneLookupInfo differs from the new one *
    • If so, it applies the update to the mutations and (in onSuccessfulFill) writes the @@ -142,7 +135,8 @@ public final class PhoneLookupDataSource implements CallLogDataSource { ImmutableMap updatedInfoMap; try { - updatedInfoMap = phoneLookup.bulkUpdate(originalPhoneLookupInfosByNumber).get(); + updatedInfoMap = + phoneLookup.getMostRecentPhoneLookupInfo(originalPhoneLookupInfosByNumber).get(); } catch (InterruptedException | ExecutionException e) { throw new IllegalStateException(e); } diff --git a/java/com/android/dialer/phonelookup/PhoneLookup.java b/java/com/android/dialer/phonelookup/PhoneLookup.java index 183277569..eeab4dadd 100644 --- a/java/com/android/dialer/phonelookup/PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/PhoneLookup.java @@ -27,8 +27,8 @@ import com.google.common.util.concurrent.ListenableFuture; * Provides operations related to retrieving information about phone numbers. * *

      Some operations defined by this interface are generally targeted towards specific use cases; - * for example {@link #isDirty(ImmutableSet)}, {@link #bulkUpdate(ImmutableMap)}, and {@link - * #onSuccessfulBulkUpdate()} are generally intended to be used by the call log. + * for example {@link #isDirty(ImmutableSet)}, {@link #getMostRecentPhoneLookupInfo(ImmutableMap)}, + * and {@link #onSuccessfulBulkUpdate()} are generally intended to be used by the call log. */ public interface PhoneLookup { @@ -48,9 +48,9 @@ public interface PhoneLookup { ListenableFuture isDirty(ImmutableSet phoneNumbers); /** - * Performs a bulk update of this {@link PhoneLookup}. The returned map must contain the exact - * same keys as the provided map. Most implementations will rely on last modified timestamps to - * efficiently only update the data which needs to be updated. + * Get the most recent phone lookup information for this {@link PhoneLookup}. The returned map + * must contain the exact same keys as the provided map. Most implementations will rely on last + * modified timestamps to efficiently only update the data which needs to be updated. * *

      If there are no changes required, it is valid for this method to simply return the provided * {@code existingInfoMap}. @@ -58,16 +58,16 @@ public interface PhoneLookup { *

      If there is no longer information associated with a number (for example, a local contact was * deleted) the returned map should contain an empty {@link PhoneLookupInfo} for that number. */ - ListenableFuture> bulkUpdate( + ListenableFuture> getMostRecentPhoneLookupInfo( ImmutableMap existingInfoMap); /** - * Called when the results of the {@link #bulkUpdate(ImmutableMap)} have been applied by the - * caller. + * Called when the results of the {@link #getMostRecentPhoneLookupInfo(ImmutableMap)} have been + * applied by the caller. * *

      Typically implementations will use this to store a "last processed" timestamp so that future - * invocations of {@link #isDirty(ImmutableSet)} and {@link #bulkUpdate(ImmutableMap)} can be - * efficiently implemented. + * invocations of {@link #isDirty(ImmutableSet)} and {@link + * #getMostRecentPhoneLookupInfo(ImmutableMap)} can be efficiently implemented. */ ListenableFuture onSuccessfulBulkUpdate(); } diff --git a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java index abc0c74fd..ee2244615 100644 --- a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java +++ b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java @@ -97,12 +97,13 @@ public final class CompositePhoneLookup implements PhoneLookup { * the dependent lookups does not complete, the returned future will also not complete. */ @Override - public ListenableFuture> bulkUpdate( - ImmutableMap existingInfoMap) { + public ListenableFuture> + getMostRecentPhoneLookupInfo( + ImmutableMap existingInfoMap) { List>> futures = new ArrayList<>(); for (PhoneLookup phoneLookup : phoneLookups) { - futures.add(phoneLookup.bulkUpdate(existingInfoMap)); + futures.add(phoneLookup.getMostRecentPhoneLookupInfo(existingInfoMap)); } return Futures.transform( Futures.allAsList(futures), diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java index cd645a447..03e05b563 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2PhoneLookup.java @@ -188,8 +188,9 @@ public final class Cp2PhoneLookup implements PhoneLookup { } @Override - public ListenableFuture> bulkUpdate( - ImmutableMap existingInfoMap) { + public ListenableFuture> + getMostRecentPhoneLookupInfo( + ImmutableMap existingInfoMap) { return backgroundExecutorService.submit(() -> bulkUpdateInternal(existingInfoMap)); } -- cgit v1.2.3 From 17edc22de11f772a1efe91a9c6b7b4fd7e5d3416 Mon Sep 17 00:00:00 2001 From: zachh Date: Wed, 6 Dec 2017 20:33:23 -0800 Subject: Made PhoneLookupDataSource implementation async. We were previously calling get() which can cause deadlocks. Bug: 34672501 Test: existing PiperOrigin-RevId: 178192772 Change-Id: Id9088b12b765307c778d101d847cb1016ea828d1 --- .../phonelookup/PhoneLookupDataSource.java | 144 +++++++++++++-------- 1 file changed, 87 insertions(+), 57 deletions(-) diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java index 9606b8e77..17a09ce47 100644 --- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java +++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java @@ -30,6 +30,7 @@ import com.android.dialer.calllog.datasources.CallLogDataSource; 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.common.concurrent.Annotations.LightweightExecutor; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; import com.android.dialer.phonelookup.PhoneLookupSelector; @@ -38,6 +39,7 @@ import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.i18n.phonenumbers.PhoneNumberUtil; @@ -47,7 +49,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.concurrent.ExecutionException; +import java.util.concurrent.Callable; import javax.inject.Inject; /** @@ -58,35 +60,24 @@ public final class PhoneLookupDataSource implements CallLogDataSource { private final PhoneLookup phoneLookup; private final ListeningExecutorService backgroundExecutorService; + private final ListeningExecutorService lightweightExecutorService; @Inject PhoneLookupDataSource( PhoneLookup phoneLookup, - @BackgroundExecutor ListeningExecutorService backgroundExecutorService) { + @BackgroundExecutor ListeningExecutorService backgroundExecutorService, + @LightweightExecutor ListeningExecutorService lightweightExecutorService) { this.phoneLookup = phoneLookup; this.backgroundExecutorService = backgroundExecutorService; + this.lightweightExecutorService = lightweightExecutorService; } @Override public ListenableFuture isDirty(Context appContext) { - return backgroundExecutorService.submit(() -> isDirtyInternal(appContext)); - } - - @Override - public ListenableFuture fill(Context appContext, CallLogMutations mutations) { - return backgroundExecutorService.submit(() -> fillInternal(appContext, mutations)); - } - - @Override - public ListenableFuture onSuccessfulFill(Context appContext) { - return backgroundExecutorService.submit(this::onSuccessfulFillInternal); - } - - @WorkerThread - private boolean isDirtyInternal(Context appContext) throws Exception { - ImmutableSet uniqueDialerPhoneNumbers = - queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(appContext); - return phoneLookup.isDirty(uniqueDialerPhoneNumbers).get(); + ListenableFuture> phoneNumbers = + backgroundExecutorService.submit( + () -> queryDistinctDialerPhoneNumbersFromAnnotatedCallLog(appContext)); + return Futures.transformAsync(phoneNumbers, phoneLookup::isDirty, lightweightExecutorService); } /** @@ -115,43 +106,82 @@ public final class PhoneLookupDataSource implements CallLogDataSource { *

    * */ - @WorkerThread - private Void fillInternal(Context appContext, CallLogMutations mutations) { - Map> annotatedCallLogIdsByNumber = - queryIdAndNumberFromAnnotatedCallLog(appContext); - ImmutableMap originalPhoneLookupInfosByNumber = - queryPhoneLookupHistoryForNumbers(appContext, annotatedCallLogIdsByNumber.keySet()); - ImmutableMap.Builder originalPhoneLookupHistoryDataByAnnotatedCallLogId = - ImmutableMap.builder(); - for (Entry entry : - originalPhoneLookupInfosByNumber.entrySet()) { - DialerPhoneNumber dialerPhoneNumber = entry.getKey(); - PhoneLookupInfo phoneLookupInfo = entry.getValue(); - for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { - originalPhoneLookupHistoryDataByAnnotatedCallLogId.put(id, phoneLookupInfo); - } - } - populateInserts(originalPhoneLookupHistoryDataByAnnotatedCallLogId.build(), mutations); - - ImmutableMap updatedInfoMap; - try { - updatedInfoMap = - phoneLookup.getMostRecentPhoneLookupInfo(originalPhoneLookupInfosByNumber).get(); - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException(e); - } - ImmutableMap.Builder rowsToUpdate = ImmutableMap.builder(); - for (Entry entry : updatedInfoMap.entrySet()) { - DialerPhoneNumber dialerPhoneNumber = entry.getKey(); - PhoneLookupInfo upToDateInfo = entry.getValue(); - if (!originalPhoneLookupInfosByNumber.get(dialerPhoneNumber).equals(upToDateInfo)) { - for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { - rowsToUpdate.put(id, upToDateInfo); - } - } - } - updateMutations(rowsToUpdate.build(), mutations); - return null; + @Override + public ListenableFuture fill(Context appContext, CallLogMutations mutations) { + // First query information from annotated call log. + ListenableFuture>> annotatedCallLogIdsByNumberFuture = + backgroundExecutorService.submit(() -> queryIdAndNumberFromAnnotatedCallLog(appContext)); + + // Use it to create the original info map. + ListenableFuture> originalInfoMapFuture = + Futures.transform( + annotatedCallLogIdsByNumberFuture, + annotatedCallLogIdsByNumber -> + queryPhoneLookupHistoryForNumbers(appContext, annotatedCallLogIdsByNumber.keySet()), + backgroundExecutorService); + + // Use the original info map to generate the updated info map by delegating to phoneLookup. + ListenableFuture> updatedInfoMapFuture = + Futures.transformAsync( + originalInfoMapFuture, + phoneLookup::getMostRecentPhoneLookupInfo, + lightweightExecutorService); + + // This is the computation that will use the result of all of the above. + Callable> computeRowsToUpdate = + () -> { + // These get() calls are safe because we are using whenAllSucceed below. + Map> annotatedCallLogIdsByNumber = + annotatedCallLogIdsByNumberFuture.get(); + ImmutableMap originalInfoMap = + originalInfoMapFuture.get(); + ImmutableMap updatedInfoMap = + updatedInfoMapFuture.get(); + + // First populate the insert mutations + ImmutableMap.Builder + originalPhoneLookupHistoryDataByAnnotatedCallLogId = ImmutableMap.builder(); + for (Entry entry : originalInfoMap.entrySet()) { + DialerPhoneNumber dialerPhoneNumber = entry.getKey(); + PhoneLookupInfo phoneLookupInfo = entry.getValue(); + for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { + originalPhoneLookupHistoryDataByAnnotatedCallLogId.put(id, phoneLookupInfo); + } + } + populateInserts(originalPhoneLookupHistoryDataByAnnotatedCallLogId.build(), mutations); + + // Now compute the rows to update. + ImmutableMap.Builder rowsToUpdate = ImmutableMap.builder(); + for (Entry entry : updatedInfoMap.entrySet()) { + DialerPhoneNumber dialerPhoneNumber = entry.getKey(); + PhoneLookupInfo upToDateInfo = entry.getValue(); + if (!originalInfoMap.get(dialerPhoneNumber).equals(upToDateInfo)) { + for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { + rowsToUpdate.put(id, upToDateInfo); + } + } + } + return rowsToUpdate.build(); + }; + + ListenableFuture> rowsToUpdateFuture = + Futures.whenAllSucceed( + annotatedCallLogIdsByNumberFuture, updatedInfoMapFuture, originalInfoMapFuture) + .call(computeRowsToUpdate, lightweightExecutorService); + + // Finally apply the computed rows to update to mutations. + return Futures.transform( + rowsToUpdateFuture, + rowsToUpdate -> { + updateMutations(rowsToUpdate, mutations); + return null; + }, + lightweightExecutorService); + } + + @Override + public ListenableFuture onSuccessfulFill(Context appContext) { + return backgroundExecutorService.submit(this::onSuccessfulFillInternal); } @WorkerThread -- cgit v1.2.3 From 25f45be4e54aa5aec3f20fae3bab9e5f803079ca Mon Sep 17 00:00:00 2001 From: Android Dialer Date: Thu, 7 Dec 2017 10:57:50 -0800 Subject: Bug: 68953167 Test: Bubble.show PiperOrigin-RevId: 178265418 Change-Id: Iad3b85b10aae736953a64824bac1631d506892c8 --- java/com/android/bubble/Bubble.java | 144 ++++++++++++++------- java/com/android/bubble/res/layout/bubble_base.xml | 67 +++++++++- 2 files changed, 166 insertions(+), 45 deletions(-) diff --git a/java/com/android/bubble/Bubble.java b/java/com/android/bubble/Bubble.java index 392daaf28..a25316f5b 100644 --- a/java/com/android/bubble/Bubble.java +++ b/java/com/android/bubble/Bubble.java @@ -64,6 +64,7 @@ import android.widget.ViewAnimator; import com.android.bubble.BubbleInfo.Action; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; import java.util.List; /** @@ -96,7 +97,7 @@ public class Bubble { private BubbleInfo currentInfo; @Visibility private int visibility; - private boolean expanded; + @VisibleForTesting boolean expanded; private boolean textShowing; private boolean hideAfterText; private CharSequence textAfterShow; @@ -104,7 +105,7 @@ public class Bubble { @VisibleForTesting ViewHolder viewHolder; private ViewPropertyAnimator collapseAnimation; - private Integer overrideGravity; + @VisibleForTesting Integer overrideGravity; private ViewPropertyAnimator exitAnimator; private final Runnable collapseRunnable = @@ -497,13 +498,6 @@ public class Bubble { ViewGroup.LayoutParams layoutParams = primaryContainer.getLayoutParams(); ((FrameLayout.LayoutParams) layoutParams).gravity = onRight ? Gravity.RIGHT : Gravity.LEFT; primaryContainer.setLayoutParams(layoutParams); - - viewHolder - .getExpandedView() - .setBackgroundResource( - onRight - ? R.drawable.bubble_background_pill_rtl - : R.drawable.bubble_background_pill_ltr); } LayoutParams getWindowParams() { @@ -570,20 +564,23 @@ public class Bubble { backgroundRipple.getDrawable(0).setTint(primaryTint); viewHolder.getPrimaryButton().setBackground(backgroundRipple); - setBackgroundDrawable(viewHolder.getFirstButton(), primaryTint); - setBackgroundDrawable(viewHolder.getSecondButton(), primaryTint); - setBackgroundDrawable(viewHolder.getThirdButton(), primaryTint); + for (CheckableImageButton button : viewHolder.getActionButtons()) { + setBackgroundDrawable(button, primaryTint); + } int numButtons = currentInfo.getActions().size(); - viewHolder.getThirdButton().setVisibility(numButtons < 3 ? View.GONE : View.VISIBLE); - viewHolder.getSecondButton().setVisibility(numButtons < 2 ? View.GONE : View.VISIBLE); + for (CheckableImageButton button : viewHolder.getThirdButtons()) { + button.setVisibility(numButtons < 3 ? View.GONE : View.VISIBLE); + } + for (CheckableImageButton button : viewHolder.getSecondButtons()) { + button.setVisibility(numButtons < 2 ? View.GONE : View.VISIBLE); + } viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon()); updatePrimaryIconAnimation(); - - viewHolder - .getExpandedView() - .setBackgroundTintList(ColorStateList.valueOf(currentInfo.getPrimaryColor())); + for (View expandedView : viewHolder.getExpandedViews()) { + expandedView.setBackgroundTintList(ColorStateList.valueOf(currentInfo.getPrimaryColor())); + } updateButtonStates(); } @@ -613,11 +610,17 @@ public class Bubble { int numButtons = currentInfo.getActions().size(); if (numButtons >= 1) { - configureButton(currentInfo.getActions().get(0), viewHolder.getFirstButton()); + for (CheckableImageButton button : viewHolder.getFirstButtons()) { + configureButton(currentInfo.getActions().get(0), button); + } if (numButtons >= 2) { - configureButton(currentInfo.getActions().get(1), viewHolder.getSecondButton()); + for (CheckableImageButton button : viewHolder.getSecondButtons()) { + configureButton(currentInfo.getActions().get(1), button); + } if (numButtons >= 3) { - configureButton(currentInfo.getActions().get(2), viewHolder.getThirdButton()); + for (CheckableImageButton button : viewHolder.getThirdButtons()) { + configureButton(currentInfo.getActions().get(2), button); + } } } } @@ -788,10 +791,15 @@ public class Bubble { private final TextView primaryText; private final CheckableImageButton firstButton; + private final CheckableImageButton firstButtonRtl; private final CheckableImageButton secondButton; + private final CheckableImageButton secondButtonRtl; private final CheckableImageButton thirdButton; + private final CheckableImageButton thirdButtonRtl; private final View expandedView; + private final View expandedViewRtl; private final View shadowProvider; + private final View shadowProviderRtl; public ViewHolder(Context context) { // Window root is not in the layout file so that the inflater has a view to inflate into @@ -799,14 +807,19 @@ public class Bubble { LayoutInflater inflater = LayoutInflater.from(root.getContext()); View contentView = inflater.inflate(R.layout.bubble_base, root, true); expandedView = contentView.findViewById(R.id.bubble_expanded_layout); + expandedViewRtl = contentView.findViewById(R.id.bubble_expanded_layout_rtl); primaryButton = contentView.findViewById(R.id.bubble_button_primary); primaryIcon = contentView.findViewById(R.id.bubble_icon_primary); primaryText = contentView.findViewById(R.id.bubble_text); shadowProvider = contentView.findViewById(R.id.bubble_drawer_shadow_provider); + shadowProviderRtl = contentView.findViewById(R.id.bubble_drawer_shadow_provider_rtl); firstButton = contentView.findViewById(R.id.bubble_icon_first); + firstButtonRtl = contentView.findViewById(R.id.bubble_icon_first_rtl); secondButton = contentView.findViewById(R.id.bubble_icon_second); + secondButtonRtl = contentView.findViewById(R.id.bubble_icon_second_rtl); thirdButton = contentView.findViewById(R.id.bubble_icon_third); + thirdButtonRtl = contentView.findViewById(R.id.bubble_icon_third_rtl); root.setOnBackPressedListener( () -> { @@ -839,27 +852,34 @@ public class Bubble { int parentOffset = ((MarginLayoutParams) ((ViewGroup) expandedView.getParent()).getLayoutParams()) .leftMargin; - if (isDrawingFromRight()) { - int maxLeft = - shadowProvider.getRight() - - context.getResources().getDimensionPixelSize(R.dimen.bubble_size); - shadowProvider.setLeft( - Math.min(maxLeft, expandedView.getLeft() + translationX + parentOffset)); - } else { - int minRight = - shadowProvider.getLeft() - + context.getResources().getDimensionPixelSize(R.dimen.bubble_size); - shadowProvider.setRight( - Math.max(minRight, expandedView.getRight() + translationX + parentOffset)); - } + int minRight = + shadowProvider.getLeft() + + context.getResources().getDimensionPixelSize(R.dimen.bubble_size); + shadowProvider.setRight( + Math.max(minRight, expandedView.getRight() + translationX + parentOffset)); + }); + expandedViewRtl + .getViewTreeObserver() + .addOnDrawListener( + () -> { + int translationX = (int) expandedViewRtl.getTranslationX(); + int parentOffset = + ((MarginLayoutParams) + ((ViewGroup) expandedViewRtl.getParent()).getLayoutParams()) + .leftMargin; + int maxLeft = + shadowProviderRtl.getRight() + - context.getResources().getDimensionPixelSize(R.dimen.bubble_size); + shadowProviderRtl.setLeft( + Math.min(maxLeft, expandedViewRtl.getLeft() + translationX + parentOffset)); }); moveHandler = new MoveHandler(primaryButton, Bubble.this); } private void setChildClickable(boolean clickable) { - firstButton.setClickable(clickable); - secondButton.setClickable(clickable); - thirdButton.setClickable(clickable); + for (CheckableImageButton button : getActionButtons()) { + button.setClickable(clickable); + } primaryButton.setOnTouchListener(clickable ? moveHandler : null); } @@ -880,29 +900,65 @@ public class Bubble { return primaryText; } + /** Get list of all the action buttons from both LTR/RTL drawers. */ + public List getActionButtons() { + return Arrays.asList( + firstButton, firstButtonRtl, secondButton, secondButtonRtl, thirdButton, thirdButtonRtl); + } + + /** Get the first action button used in the current orientation drawer. */ public CheckableImageButton getFirstButton() { - return firstButton; + return isDrawingFromRight() ? firstButtonRtl : firstButton; + } + + /** Get both of the first action buttons from both LTR/RTL drawers. */ + public List getFirstButtons() { + return Arrays.asList(firstButton, firstButtonRtl); } + /** Get the second action button used in the current orientation drawer. */ public CheckableImageButton getSecondButton() { - return secondButton; + return isDrawingFromRight() ? secondButtonRtl : secondButton; } + /** Get both of the second action buttons from both LTR/RTL drawers. */ + public List getSecondButtons() { + return Arrays.asList(secondButton, secondButtonRtl); + } + + /** Get the third action button used in the current orientation drawer. */ public CheckableImageButton getThirdButton() { - return thirdButton; + return isDrawingFromRight() ? thirdButtonRtl : thirdButton; + } + + /** Get both of the third action buttons from both LTR/RTL drawers. */ + public List getThirdButtons() { + return Arrays.asList(thirdButton, thirdButtonRtl); } + /** Get the correct expanded view used in current bubble orientation. */ public View getExpandedView() { - return expandedView; + return isDrawingFromRight() ? expandedViewRtl : expandedView; } + /** Get both views of the LTR and RTL drawers. */ + public List getExpandedViews() { + return Arrays.asList(expandedView, expandedViewRtl); + } + + /** Get the correct shadow provider view used in current bubble orientation. */ public View getShadowProvider() { - return shadowProvider; + return isDrawingFromRight() ? shadowProviderRtl : shadowProvider; } public void setDrawerVisibility(int visibility) { - expandedView.setVisibility(visibility); - shadowProvider.setVisibility(visibility); + if (isDrawingFromRight()) { + expandedViewRtl.setVisibility(visibility); + shadowProviderRtl.setVisibility(visibility); + } else { + expandedView.setVisibility(visibility); + shadowProvider.setVisibility(visibility); + } } public boolean isMoving() { diff --git a/java/com/android/bubble/res/layout/bubble_base.xml b/java/com/android/bubble/res/layout/bubble_base.xml index 3b5735cd0..0712db603 100644 --- a/java/com/android/bubble/res/layout/bubble_base.xml +++ b/java/com/android/bubble/res/layout/bubble_base.xml @@ -33,6 +33,19 @@ android:elevation="10dp" android:visibility="invisible" /> + + + + + + + + Date: Thu, 7 Dec 2017 14:09:24 -0800 Subject: Add isActivated check to Duo interface Bug: 70034799 Test: GoogleCallLogAdapterTest PiperOrigin-RevId: 178292533 Change-Id: I9769a7fbe0edcc42eff99d636334a0c2eaf14779 --- java/com/android/dialer/duo/Duo.java | 6 ++++++ java/com/android/dialer/duo/stub/DuoStub.java | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/java/com/android/dialer/duo/Duo.java b/java/com/android/dialer/duo/Duo.java index 839c1d3a8..ff694c053 100644 --- a/java/com/android/dialer/duo/Duo.java +++ b/java/com/android/dialer/duo/Duo.java @@ -31,6 +31,12 @@ public interface Duo { boolean isEnabled(); + /** + * @return true if Duo is installed and the user has gone through the set-up flow confirming their + * phone number. + */ + boolean isActivated(@NonNull Context context); + @MainThread boolean isReachable(@NonNull Context context, @Nullable String number); diff --git a/java/com/android/dialer/duo/stub/DuoStub.java b/java/com/android/dialer/duo/stub/DuoStub.java index 82b9c79e3..7cc8f7808 100644 --- a/java/com/android/dialer/duo/stub/DuoStub.java +++ b/java/com/android/dialer/duo/stub/DuoStub.java @@ -40,6 +40,11 @@ public class DuoStub implements Duo { return false; } + @Override + public boolean isActivated(@NonNull Context context) { + return false; + } + @MainThread @Override public boolean isReachable(@NonNull Context context, @Nullable String number) { -- cgit v1.2.3 From 36a384e04fd9b8ba1d53500afb7e0055e4e4bb99 Mon Sep 17 00:00:00 2001 From: zachh Date: Thu, 7 Dec 2017 17:54:53 -0800 Subject: Fixed compile error in AOSP due to use of guava 23 API. Futures.submitAsync is V23 and AOSP is currently stuck at V20. This is just a completely non-sensical dummy implementation of DialerFutureSerializer until we can figure out what to do. Test: tap PiperOrigin-RevId: 178323108 Change-Id: I1dc2c8ddfa7ef3e15170543dadefbe4fc2e19226 --- .../common/concurrent/DialerFutureSerializer.java | 73 +++------------------- 1 file changed, 7 insertions(+), 66 deletions(-) diff --git a/java/com/android/dialer/common/concurrent/DialerFutureSerializer.java b/java/com/android/dialer/common/concurrent/DialerFutureSerializer.java index 2629abbbe..de37a2915 100644 --- a/java/com/android/dialer/common/concurrent/DialerFutureSerializer.java +++ b/java/com/android/dialer/common/concurrent/DialerFutureSerializer.java @@ -16,83 +16,24 @@ package com.android.dialer.common.concurrent; -import static com.google.common.util.concurrent.Futures.immediateCancelledFuture; -import static com.google.common.util.concurrent.Futures.immediateFuture; -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; - import com.google.common.util.concurrent.AsyncCallable; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import java.util.concurrent.Callable; import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; /** * Serializes execution of a set of operations. This class guarantees that a submitted callable will * not be called before previously submitted callables have completed. */ public final class DialerFutureSerializer { - /** This reference acts as a pointer tracking the head of a linked list of ListenableFutures. */ - private final AtomicReference> ref = - new AtomicReference<>(immediateFuture(null)); /** Enqueues a task to run when the previous task (if any) completes. */ - public ListenableFuture submit(final Callable callable, Executor executor) { - return submitAsync(() -> immediateFuture(callable.call()), executor); - } - - /** - * Enqueues a task to run when the previous task (if any) completes. - * - *

    Cancellation does not propagate from the output future to the future returned from {@code - * callable}, but if the output future is cancelled before {@link AsyncCallable#call()} is - * invoked, {@link AsyncCallable#call()} will not be invoked. - */ public ListenableFuture submitAsync(final AsyncCallable callable, Executor executor) { - AtomicBoolean wasCancelled = new AtomicBoolean(false); - final AsyncCallable task = - () -> { - if (wasCancelled.get()) { - return immediateCancelledFuture(); - } - return callable.call(); - }; - /* - * Three futures are at play here: - * taskFuture is the future that comes from the callable. - * newFuture is the future we use to track the serialization of our task. - * oldFuture is the previous task's newFuture. - * - * newFuture is guaranteed to only complete once all tasks previously submitted to this instance - * once the futures returned from those submissions have completed. - */ - final SettableFuture newFuture = SettableFuture.create(); - - final ListenableFuture oldFuture = ref.getAndSet(newFuture); - - // Invoke our task once the previous future completes. - final ListenableFuture taskFuture = - Futures.nonCancellationPropagating( - Futures.submitAsync(task, runnable -> oldFuture.addListener(runnable, executor))); - // newFuture's lifetime is determined by taskFuture, unless taskFuture is cancelled, in which - // case it falls back to oldFuture's. This is to ensure that if the future we return is - // cancelled, we don't begin execution of the next task until after oldFuture completes. - taskFuture.addListener( - () -> { - if (taskFuture.isCancelled()) { - // Since the value of oldFuture can only ever be immediateFuture(null) or setFuture of a - // future that eventually came from immediateFuture(null), this doesn't leak throwables - // or completion values. - wasCancelled.set(true); - newFuture.setFuture(oldFuture); - } else { - newFuture.set(null); - } - }, - directExecutor()); - - return taskFuture; + // TODO(zachh): This is just a dummy implementation until we fix guava API level issues. + try { + return callable.call(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; } } -- cgit v1.2.3 From 81a7b490ebbf2d9a4213a79b52e7b999aa076b7f Mon Sep 17 00:00:00 2001 From: zachh Date: Thu, 7 Dec 2017 17:59:20 -0800 Subject: Implemented PhoneLookupDataSource#onSuccesfulFill. Required adding applyBatch functionality to PhoneLookupHistoryContentProvider so that the updates can be performed in a transaction. This code was just copied and modified from AnnotatedCallLogContentProvider. Also removed the trigger which limited the size of the PhoneLookupHistory, since we now delete rows from PhoneLookupHistory when the last occurrence of a number is deleted from AnnotatedCallLog. Since AnnotatedCallLog is bounded to 1000 rows PhoneLookupHistory is now indirectly bounded by that as well. Bug: 34672501 Test: unit PiperOrigin-RevId: 178323464 Change-Id: I233163fe70641b0e4b1d4c5c0e8970ad0b4b167d --- .../database/AnnotatedCallLogContentProvider.java | 40 ++++--- .../phonelookup/PhoneLookupDataSource.java | 117 ++++++++++++++++++++- .../PhoneLookupHistoryContentProvider.java | 90 +++++++++++++--- .../database/PhoneLookupHistoryDatabaseHelper.java | 29 +---- 4 files changed, 214 insertions(+), 62 deletions(-) diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java index 825e84f91..2427624a4 100644 --- a/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java +++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java @@ -245,12 +245,12 @@ public class AnnotatedCallLogContentProvider extends ContentProvider { throw new IllegalArgumentException("Unknown uri: " + uri); } int rows = database.delete(AnnotatedCallLog.TABLE, selection, selectionArgs); - if (rows > 0) { - if (!isApplyingBatch()) { - notifyChange(uri); - } - } else { + if (rows == 0) { LogUtil.w("AnnotatedCallLogContentProvider.delete", "no rows deleted"); + return rows; + } + if (!isApplyingBatch()) { + notifyChange(uri); } return rows; } @@ -268,7 +268,15 @@ public class AnnotatedCallLogContentProvider extends ContentProvider { int match = uriMatcher.match(uri); switch (match) { case ANNOTATED_CALL_LOG_TABLE_CODE: - break; + int rows = database.update(AnnotatedCallLog.TABLE, values, selection, selectionArgs); + if (rows == 0) { + LogUtil.w("AnnotatedCallLogContentProvider.update", "no rows updated"); + return rows; + } + if (!isApplyingBatch()) { + notifyChange(uri); + } + return rows; case ANNOTATED_CALL_LOG_TABLE_ID_CODE: Assert.checkArgument( !values.containsKey(AnnotatedCallLog._ID), "Do not specify _ID when updating by ID"); @@ -276,23 +284,21 @@ public class AnnotatedCallLogContentProvider extends ContentProvider { Assert.checkArgument( selectionArgs == null, "Do not specify selection args when updating by ID"); selection = getSelectionWithId(ContentUris.parseId(uri)); - break; + rows = database.update(AnnotatedCallLog.TABLE, values, selection, selectionArgs); + if (rows == 0) { + LogUtil.w("AnnotatedCallLogContentProvider.update", "no rows updated"); + return rows; + } + if (!isApplyingBatch()) { + notifyChange(uri); + } + return rows; case ANNOTATED_CALL_LOG_TABLE_DISTINCT_NUMBER_CODE: - throw new UnsupportedOperationException(); case COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE: throw new UnsupportedOperationException(); 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; } /** diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java index 17a09ce47..fa7d3be16 100644 --- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java +++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java @@ -16,9 +16,13 @@ package com.android.dialer.calllog.datasources.phonelookup; +import android.content.ContentProviderOperation; import android.content.ContentValues; import android.content.Context; +import android.content.OperationApplicationException; import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; import android.support.annotation.MainThread; import android.support.annotation.WorkerThread; import android.text.TextUtils; @@ -34,6 +38,7 @@ import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; import com.android.dialer.phonelookup.PhoneLookup; import com.android.dialer.phonelookup.PhoneLookupInfo; import com.android.dialer.phonelookup.PhoneLookupSelector; +import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract; import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; import com.google.common.collect.ImmutableMap; @@ -44,6 +49,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.protobuf.InvalidProtocolBufferException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -62,6 +68,23 @@ public final class PhoneLookupDataSource implements CallLogDataSource { private final ListeningExecutorService backgroundExecutorService; private final ListeningExecutorService lightweightExecutorService; + /** + * Keyed by normalized number (the primary key for PhoneLookupHistory). + * + *

    This is state saved between the {@link #fill(Context, CallLogMutations)} and {@link + * #onSuccessfulFill(Context)} operations. + */ + private final Map phoneLookupHistoryRowsToUpdate = new ArrayMap<>(); + + /** + * Normalized numbers (the primary key for PhoneLookupHistory) which should be deleted from + * PhoneLookupHistory. + * + *

    This is state saved between the {@link #fill(Context, CallLogMutations)} and {@link + * #onSuccessfulFill(Context)} operations. + */ + private final Set phoneLookupHistoryRowsToDelete = new ArraySet<>(); + @Inject PhoneLookupDataSource( PhoneLookup phoneLookup, @@ -108,6 +131,11 @@ public final class PhoneLookupDataSource implements CallLogDataSource { */ @Override public ListenableFuture fill(Context appContext, CallLogMutations mutations) { + // Clear state saved since the last call to fill. This is necessary in case fill is called but + // onSuccessfulFill is not called during a previous flow. + phoneLookupHistoryRowsToUpdate.clear(); + phoneLookupHistoryRowsToDelete.clear(); + // First query information from annotated call log. ListenableFuture>> annotatedCallLogIdsByNumberFuture = backgroundExecutorService.submit(() -> queryIdAndNumberFromAnnotatedCallLog(appContext)); @@ -150,6 +178,13 @@ public final class PhoneLookupDataSource implements CallLogDataSource { } populateInserts(originalPhoneLookupHistoryDataByAnnotatedCallLogId.build(), mutations); + // Compute and save the PhoneLookupHistory rows which can be deleted in onSuccessfulFill. + DialerPhoneNumberUtil dialerPhoneNumberUtil = + new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance()); + phoneLookupHistoryRowsToDelete.addAll( + computePhoneLookupHistoryRowsToDelete( + annotatedCallLogIdsByNumber, mutations, dialerPhoneNumberUtil)); + // Now compute the rows to update. ImmutableMap.Builder rowsToUpdate = ImmutableMap.builder(); for (Entry entry : updatedInfoMap.entrySet()) { @@ -159,6 +194,10 @@ public final class PhoneLookupDataSource implements CallLogDataSource { for (Long id : annotatedCallLogIdsByNumber.get(dialerPhoneNumber)) { rowsToUpdate.put(id, upToDateInfo); } + // Also save the updated information so that it can be written to PhoneLookupHistory + // in onSuccessfulFill. + String normalizedNumber = dialerPhoneNumberUtil.formatToE164(dialerPhoneNumber); + phoneLookupHistoryRowsToUpdate.put(normalizedNumber, upToDateInfo); } } return rowsToUpdate.build(); @@ -167,9 +206,11 @@ public final class PhoneLookupDataSource implements CallLogDataSource { ListenableFuture> rowsToUpdateFuture = Futures.whenAllSucceed( annotatedCallLogIdsByNumberFuture, updatedInfoMapFuture, originalInfoMapFuture) - .call(computeRowsToUpdate, lightweightExecutorService); + .call( + computeRowsToUpdate, + backgroundExecutorService /* PhoneNumberUtil may do disk IO */); - // Finally apply the computed rows to update to mutations. + // Finally update the mutations with the computed rows. return Futures.transform( rowsToUpdateFuture, rowsToUpdate -> { @@ -181,12 +222,38 @@ public final class PhoneLookupDataSource implements CallLogDataSource { @Override public ListenableFuture onSuccessfulFill(Context appContext) { - return backgroundExecutorService.submit(this::onSuccessfulFillInternal); + // First update and/or delete the appropriate rows in PhoneLookupHistory. + ListenableFuture writePhoneLookupHistory = + backgroundExecutorService.submit(() -> writePhoneLookupHistory(appContext)); + + // If that succeeds, delegate to the composite PhoneLookup to notify all PhoneLookups that both + // the AnnotatedCallLog and PhoneLookupHistory have been successfully updated. + return Futures.transformAsync( + writePhoneLookupHistory, + unused -> phoneLookup.onSuccessfulBulkUpdate(), + lightweightExecutorService); } @WorkerThread - private Void onSuccessfulFillInternal() { - // TODO(zachh): Update PhoneLookupHistory. + private Void writePhoneLookupHistory(Context appContext) + throws RemoteException, OperationApplicationException { + ArrayList operations = new ArrayList<>(); + long currentTimestamp = System.currentTimeMillis(); + for (Entry entry : phoneLookupHistoryRowsToUpdate.entrySet()) { + String normalizedNumber = entry.getKey(); + PhoneLookupInfo phoneLookupInfo = entry.getValue(); + ContentValues contentValues = new ContentValues(); + contentValues.put(PhoneLookupHistory.PHONE_LOOKUP_INFO, phoneLookupInfo.toByteArray()); + contentValues.put(PhoneLookupHistory.LAST_MODIFIED, currentTimestamp); + operations.add( + ContentProviderOperation.newUpdate(numberUri(normalizedNumber)) + .withValues(contentValues) + .build()); + } + for (String normalizedNumber : phoneLookupHistoryRowsToDelete) { + operations.add(ContentProviderOperation.newDelete(numberUri(normalizedNumber)).build()); + } + appContext.getContentResolver().applyBatch(PhoneLookupHistoryContract.AUTHORITY, operations); return null; } @@ -422,7 +489,47 @@ public final class PhoneLookupDataSource implements CallLogDataSource { } } + private Set computePhoneLookupHistoryRowsToDelete( + Map> annotatedCallLogIdsByNumber, + CallLogMutations mutations, + DialerPhoneNumberUtil dialerPhoneNumberUtil) { + if (mutations.getDeletes().isEmpty()) { + return ImmutableSet.of(); + } + // First convert the dialer phone numbers to normalized numbers; we need to combine entries + // because different DialerPhoneNumbers can map to the same normalized number. + Map> idsByNormalizedNumber = new ArrayMap<>(); + for (Entry> entry : annotatedCallLogIdsByNumber.entrySet()) { + DialerPhoneNumber dialerPhoneNumber = entry.getKey(); + Set idsForDialerPhoneNumber = entry.getValue(); + String normalizedNumber = dialerPhoneNumberUtil.formatToE164(dialerPhoneNumber); + Set idsForNormalizedNumber = idsByNormalizedNumber.get(normalizedNumber); + if (idsForNormalizedNumber == null) { + idsForNormalizedNumber = new ArraySet<>(); + idsByNormalizedNumber.put(normalizedNumber, idsForNormalizedNumber); + } + idsForNormalizedNumber.addAll(idsForDialerPhoneNumber); + } + // Now look through and remove all IDs that were scheduled for delete; after doing that, if + // there are no remaining IDs left for a normalized number, the number can be deleted from + // PhoneLookupHistory. + Set normalizedNumbersToDelete = new ArraySet<>(); + for (Entry> entry : idsByNormalizedNumber.entrySet()) { + String normalizedNumber = entry.getKey(); + Set idsForNormalizedNumber = entry.getValue(); + idsForNormalizedNumber.removeAll(mutations.getDeletes()); + if (idsForNormalizedNumber.isEmpty()) { + normalizedNumbersToDelete.add(normalizedNumber); + } + } + return normalizedNumbersToDelete; + } + private static String selectName(PhoneLookupInfo phoneLookupInfo) { return PhoneLookupSelector.selectName(phoneLookupInfo); } + + private static Uri numberUri(String number) { + return PhoneLookupHistory.CONTENT_URI.buildUpon().appendEncodedPath(number).build(); + } } diff --git a/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java b/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java index e85654e99..5c0c00f81 100644 --- a/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java +++ b/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryContentProvider.java @@ -17,7 +17,10 @@ package com.android.dialer.phonelookup.database; import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; import android.content.ContentValues; +import android.content.OperationApplicationException; import android.content.UriMatcher; import android.database.Cursor; import android.database.DatabaseUtils; @@ -31,6 +34,7 @@ import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract; import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; +import java.util.ArrayList; /** * {@link ContentProvider} for the PhoneLookupHistory. @@ -50,13 +54,6 @@ import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContra */ public class PhoneLookupHistoryContentProvider extends ContentProvider { - /** - * We sometimes run queries where we potentially pass numbers 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; - // For operations against: content://com.android.dialer.phonelookuphistory/PhoneLookupHistory private static final int PHONE_LOOKUP_HISTORY_TABLE_CODE = 1; // For operations against: content://com.android.dialer.phonelookuphistory/PhoneLookupHistory/+123 @@ -77,9 +74,16 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { private PhoneLookupHistoryDatabaseHelper databaseHelper; + private final ThreadLocal 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 PhoneLookupHistoryDatabaseHelper(getContext(), MAX_ROWS); + databaseHelper = new PhoneLookupHistoryDatabaseHelper(getContext()); return true; } @@ -168,7 +172,9 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { .buildUpon() .appendEncodedPath(values.getAsString(PhoneLookupHistory.NORMALIZED_NUMBER)) .build(); - notifyChange(insertedUri); + if (!isApplyingBatch()) { + notifyChange(insertedUri); + } return insertedUri; } @@ -197,7 +203,9 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { LogUtil.w("PhoneLookupHistoryContentProvider.delete", "no rows deleted"); return rows; } - notifyChange(uri); + if (!isApplyingBatch()) { + notifyChange(uri); + } return rows; } @@ -206,6 +214,9 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { * "content://com.android.dialer.phonelookuphistory/PhoneLookupHistory/+123") then the update * operation will actually be a "replace" operation, inserting a new row if one does not already * exist. + * + *

    All columns in an existing row will be replaced which means you must specify all required + * columns in {@code values} when using this method. */ @Override public int update( @@ -225,7 +236,9 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { LogUtil.w("PhoneLookupHistoryContentProvider.update", "no rows updated"); return rows; } - notifyChange(uri); + if (!isApplyingBatch()) { + notifyChange(uri); + } return rows; case PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: Assert.checkArgument( @@ -237,14 +250,65 @@ public class PhoneLookupHistoryContentProvider extends ContentProvider { String normalizedNumber = uri.getLastPathSegment(); values.put(PhoneLookupHistory.NORMALIZED_NUMBER, normalizedNumber); - database.replace(PhoneLookupHistory.TABLE, null, values); - notifyChange(uri); + long result = database.replace(PhoneLookupHistory.TABLE, null, values); + Assert.checkArgument(result != -1, "replacing PhoneLookupHistory row failed"); + if (!isApplyingBatch()) { + notifyChange(uri); + } return 1; default: throw new IllegalArgumentException("Unknown uri: " + uri); } } + /** + * {@inheritDoc} + * + *

    Note: When applyBatch is used with the PhoneLookupHistory, only a single notification for + * the content URI is generated, not individual notifications for each affected URI. + */ + @NonNull + @Override + public ContentProviderResult[] applyBatch(@NonNull ArrayList 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 PHONE_LOOKUP_HISTORY_TABLE_CODE: + case PHONE_LOOKUP_HISTORY_TABLE_ID_CODE: + ContentProviderResult result = operation.apply(this, results, i); + if (operation.isInsert()) { + if (result.uri == null) { + throw new OperationApplicationException("error inserting row"); + } + } else if (result.count == 0) { + throw new OperationApplicationException("error applying operation"); + } + results[i] = result; + break; + default: + throw new IllegalArgumentException("Unknown uri: " + operation.getUri()); + } + } + database.setTransactionSuccessful(); + } finally { + applyingBatch.set(false); + database.endTransaction(); + } + notifyChange(PhoneLookupHistory.CONTENT_URI); + return results; + } + private void notifyChange(Uri uri) { getContext().getContentResolver().notifyChange(uri, null); } diff --git a/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryDatabaseHelper.java b/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryDatabaseHelper.java index 70d88cee4..43b6f102c 100644 --- a/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryDatabaseHelper.java +++ b/java/com/android/dialer/phonelookup/database/PhoneLookupHistoryDatabaseHelper.java @@ -22,17 +22,15 @@ import android.database.sqlite.SQLiteOpenHelper; import android.os.SystemClock; import com.android.dialer.common.LogUtil; import com.android.dialer.phonelookup.database.contract.PhoneLookupHistoryContract.PhoneLookupHistory; -import java.util.Locale; /** {@link SQLiteOpenHelper} for the PhoneLookupHistory database. */ class PhoneLookupHistoryDatabaseHelper extends SQLiteOpenHelper { - private final int maxRows; - PhoneLookupHistoryDatabaseHelper(Context appContext, int maxRows) { + PhoneLookupHistoryDatabaseHelper(Context appContext) { super(appContext, "phone_lookup_history.db", null, 1); - this.maxRows = maxRows; } + // TODO(zachh): LAST_MODIFIED is no longer read and can be deleted. private static final String CREATE_TABLE_SQL = "create table if not exists " + PhoneLookupHistory.TABLE @@ -42,28 +40,6 @@ class PhoneLookupHistoryDatabaseHelper extends SQLiteOpenHelper { + (PhoneLookupHistory.LAST_MODIFIED + " long not null") + ");"; - /** 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 " - + PhoneLookupHistory.TABLE - + " when (select count(*) from " - + PhoneLookupHistory.TABLE - + ") > %d" - + " begin delete from " - + PhoneLookupHistory.TABLE - + " where " - + PhoneLookupHistory.NORMALIZED_NUMBER - + " in (select " - + PhoneLookupHistory.NORMALIZED_NUMBER - + " from " - + PhoneLookupHistory.TABLE - + " order by " - + PhoneLookupHistory.LAST_MODIFIED - + " limit (select count(*)-%d" - + " from " - + PhoneLookupHistory.TABLE - + " )); end;"; - private static final String CREATE_INDEX_ON_LAST_MODIFIED_SQL = "create index last_modified_index on " + PhoneLookupHistory.TABLE @@ -76,7 +52,6 @@ class PhoneLookupHistoryDatabaseHelper extends SQLiteOpenHelper { LogUtil.enterBlock("PhoneLookupHistoryDatabaseHelper.onCreate"); long startTime = SystemClock.uptimeMillis(); db.execSQL(CREATE_TABLE_SQL); - db.execSQL(String.format(Locale.US, CREATE_TRIGGER_SQL, maxRows, maxRows)); db.execSQL(CREATE_INDEX_ON_LAST_MODIFIED_SQL); // TODO(zachh): Consider logging impression. LogUtil.i( -- cgit v1.2.3