From 43c978b616363bcd364693dde24209384b264319 Mon Sep 17 00:00:00 2001 From: linyuh Date: Tue, 20 Feb 2018 12:46:54 -0800 Subject: Use a broadcast receiver to refresh the annotated call log. Bug: 73347270 Test: Existing tests + RefreshAnnotatedCallLogNotifierTest PiperOrigin-RevId: 186347066 Change-Id: I5a530416bdaa9edc7131a0d5ced44f1b5ee1692b --- .../android/dialer/calllog/CallLogComponent.java | 3 + .../android/dialer/calllog/CallLogFramework.java | 70 +----------- .../calllog/RefreshAnnotatedCallLogReceiver.java | 121 +++++++++++++++++++++ .../calllog/RefreshAnnotatedCallLogWorker.java | 5 +- .../dialer/calllog/constants/IntentNames.java | 30 +++++ .../dialer/calllog/constants/SharedPrefKeys.java | 25 +++++ .../calllog/datasources/CallLogDataSource.java | 11 +- .../phonelookup/PhoneLookupDataSource.java | 18 +-- .../systemcalllog/SystemCallLogDataSource.java | 23 ++-- .../notifier/RefreshAnnotatedCallLogNotifier.java | 99 +++++++++++++++++ .../dialer/calllog/ui/NewCallLogFragment.java | 93 ++++------------ .../android/dialer/phonelookup/PhoneLookup.java | 11 +- .../DialerBlockedNumberPhoneLookup.java | 10 +- .../blockednumber/MarkDirtyObserver.java | 21 ++-- .../SystemBlockedNumberPhoneLookup.java | 10 +- .../composite/CompositePhoneLookup.java | 5 +- .../phonelookup/cp2/Cp2LocalPhoneLookup.java | 3 +- .../phonelookup/cp2/Cp2RemotePhoneLookup.java | 3 +- .../dialer/phonelookup/spam/SpamPhoneLookup.java | 6 +- .../voicemail/listui/NewVoicemailFragment.java | 107 +++++------------- 20 files changed, 387 insertions(+), 287 deletions(-) create mode 100644 java/com/android/dialer/calllog/RefreshAnnotatedCallLogReceiver.java create mode 100644 java/com/android/dialer/calllog/constants/IntentNames.java create mode 100644 java/com/android/dialer/calllog/constants/SharedPrefKeys.java create mode 100644 java/com/android/dialer/calllog/notifier/RefreshAnnotatedCallLogNotifier.java (limited to 'java/com/android/dialer') diff --git a/java/com/android/dialer/calllog/CallLogComponent.java b/java/com/android/dialer/calllog/CallLogComponent.java index bb5bfee2a..4f147f1a6 100644 --- a/java/com/android/dialer/calllog/CallLogComponent.java +++ b/java/com/android/dialer/calllog/CallLogComponent.java @@ -16,6 +16,7 @@ package com.android.dialer.calllog; import android.content.Context; +import com.android.dialer.calllog.notifier.RefreshAnnotatedCallLogNotifier; import com.android.dialer.inject.HasRootComponent; import dagger.Subcomponent; @@ -25,6 +26,8 @@ public abstract class CallLogComponent { public abstract CallLogFramework callLogFramework(); + public abstract RefreshAnnotatedCallLogNotifier getRefreshAnnotatedCallLogNotifier(); + public abstract RefreshAnnotatedCallLogWorker getRefreshAnnotatedCallLogWorker(); public abstract ClearMissedCalls getClearMissedCalls(); diff --git a/java/com/android/dialer/calllog/CallLogFramework.java b/java/com/android/dialer/calllog/CallLogFramework.java index 440055de6..7da8d9c0c 100644 --- a/java/com/android/dialer/calllog/CallLogFramework.java +++ b/java/com/android/dialer/calllog/CallLogFramework.java @@ -17,40 +17,26 @@ package com.android.dialer.calllog; import android.content.Context; -import android.content.SharedPreferences; -import android.support.annotation.MainThread; -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; import com.android.dialer.calllog.datasources.CallLogDataSource; import com.android.dialer.calllog.datasources.DataSources; -import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.configprovider.ConfigProviderBindings; -import com.android.dialer.storage.Unencrypted; import javax.inject.Inject; import javax.inject.Singleton; /** - * Coordinates work across CallLog data sources to detect if the annotated call log is out of date - * ("dirty") and update it if necessary. + * Coordinates work across {@link DataSources}. * *

All methods should be called on the main thread. */ @Singleton -public final class CallLogFramework implements CallLogDataSource.ContentObserverCallbacks { - - @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) - public static final String PREF_FORCE_REBUILD = "callLogFrameworkForceRebuild"; +public final class CallLogFramework { private final DataSources dataSources; - private final SharedPreferences sharedPreferences; - - @Nullable private CallLogUi ui; @Inject - CallLogFramework(DataSources dataSources, @Unencrypted SharedPreferences sharedPreferences) { + CallLogFramework(DataSources dataSources) { this.dataSources = dataSources; - this.sharedPreferences = sharedPreferences; } /** Registers the content observers for all data sources. */ @@ -63,58 +49,10 @@ public final class CallLogFramework implements CallLogDataSource.ContentObserver // TODO(zachh): Find a way to access Main#isNewUiEnabled without creating a circular dependency. if (ConfigProviderBindings.get(appContext).getBoolean("is_nui_shortcut_enabled", false)) { for (CallLogDataSource dataSource : dataSources.getDataSourcesIncludingSystemCallLog()) { - dataSource.registerContentObservers(appContext, this); + dataSource.registerContentObservers(appContext); } } else { LogUtil.i("CallLogFramework.registerContentObservers", "not registering content observers"); } } - - /** - * Attach a UI component to the framework so that it may be notified of changes to the annotated - * call log. - */ - public void attachUi(CallLogUi ui) { - LogUtil.enterBlock("CallLogFramework.attachUi"); - this.ui = ui; - } - - /** - * Detaches the UI from the framework. This should be called when the UI is hidden or destroyed - * and no longer needs to be notified of changes to the annotated call log. - */ - public void detachUi() { - LogUtil.enterBlock("CallLogFramework.detachUi"); - this.ui = null; - } - - /** - * Marks the call log as dirty and notifies any attached UI components. If there are no UI - * components currently attached, this is an efficient operation since it is just writing a shared - * pref. - * - *

We don't want to actually force a rebuild when there is no UI running because we don't want - * to be constantly rebuilding the database when the device is sitting on a desk and receiving a - * lot of calls, for example. - */ - @Override - @MainThread - public void markDirtyAndNotify(Context appContext) { - Assert.isMainThread(); - LogUtil.enterBlock("CallLogFramework.markDirtyAndNotify"); - - sharedPreferences.edit().putBoolean(PREF_FORCE_REBUILD, true).apply(); - - if (ui != null) { - ui.invalidateUi(); - } - } - - /** Callbacks invoked on listening UI components. */ - public interface CallLogUi { - - /** Notifies the call log UI that the annotated call log is out of date. */ - @MainThread - void invalidateUi(); - } } diff --git a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogReceiver.java b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogReceiver.java new file mode 100644 index 000000000..e0bfcd8a3 --- /dev/null +++ b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogReceiver.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.calllog; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.support.annotation.Nullable; +import com.android.dialer.calllog.constants.IntentNames; +import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.DefaultFutureCallback; +import com.android.dialer.common.concurrent.ThreadUtil; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +/** + * A {@link BroadcastReceiver} that starts/cancels refreshing the annotated call log when notified. + */ +public final class RefreshAnnotatedCallLogReceiver extends BroadcastReceiver { + + /** + * 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 REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS = 100L; + + private final RefreshAnnotatedCallLogWorker refreshAnnotatedCallLogWorker; + + @Nullable private Runnable refreshAnnotatedCallLogRunnable; + + /** Returns an {@link IntentFilter} containing all actions accepted by this broadcast receiver. */ + public static IntentFilter getIntentFilter() { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(IntentNames.ACTION_REFRESH_ANNOTATED_CALL_LOG); + intentFilter.addAction(IntentNames.ACTION_CANCEL_REFRESHING_ANNOTATED_CALL_LOG); + return intentFilter; + } + + public RefreshAnnotatedCallLogReceiver(Context context) { + refreshAnnotatedCallLogWorker = + CallLogComponent.get(context).getRefreshAnnotatedCallLogWorker(); + } + + @Override + public void onReceive(Context context, Intent intent) { + LogUtil.enterBlock("RefreshAnnotatedCallLogReceiver.onReceive"); + + String action = intent.getAction(); + + if (IntentNames.ACTION_REFRESH_ANNOTATED_CALL_LOG.equals(action)) { + boolean checkDirty = intent.getBooleanExtra(IntentNames.EXTRA_CHECK_DIRTY, false); + refreshAnnotatedCallLog(checkDirty); + } else if (IntentNames.ACTION_CANCEL_REFRESHING_ANNOTATED_CALL_LOG.equals(action)) { + cancelRefreshingAnnotatedCallLog(); + } + } + + /** + * Request a refresh of the annotated call log. + * + *

Note that the execution will be delayed by {@link #REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS}. + * Once the work begins, it can't be cancelled. + * + * @see #cancelRefreshingAnnotatedCallLog() + */ + private void refreshAnnotatedCallLog(boolean checkDirty) { + LogUtil.enterBlock("RefreshAnnotatedCallLogReceiver.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(); + Futures.addCallback( + future, new DefaultFutureCallback<>(), MoreExecutors.directExecutor()); + }; + + ThreadUtil.getUiThreadHandler() + .postDelayed(refreshAnnotatedCallLogRunnable, REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS); + } + + /** + * When a refresh is requested, its execution is delayed (see {@link + * #refreshAnnotatedCallLog(boolean)}). This method only cancels the refresh if it hasn't started. + */ + private void cancelRefreshingAnnotatedCallLog() { + LogUtil.enterBlock("RefreshAnnotatedCallLogReceiver.cancelRefreshingAnnotatedCallLog"); + + ThreadUtil.getUiThreadHandler().removeCallbacks(refreshAnnotatedCallLogRunnable); + } +} diff --git a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java index 4c5904ef1..a430d14a8 100644 --- a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java +++ b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java @@ -18,6 +18,7 @@ package com.android.dialer.calllog; import android.content.Context; import android.content.SharedPreferences; +import com.android.dialer.calllog.constants.SharedPrefKeys; import com.android.dialer.calllog.database.CallLogDatabaseComponent; import com.android.dialer.calllog.datasources.CallLogDataSource; import com.android.dialer.calllog.datasources.CallLogMutations; @@ -95,7 +96,7 @@ public class RefreshAnnotatedCallLogWorker { // 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); + sharedPreferences.getBoolean(SharedPrefKeys.FORCE_REBUILD, true); if (forceRebuildPrefValue) { LogUtil.i( "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary", @@ -183,7 +184,7 @@ public class RefreshAnnotatedCallLogWorker { return Futures.transform( onSuccessfulFillFuture, unused -> { - sharedPreferences.edit().putBoolean(CallLogFramework.PREF_FORCE_REBUILD, false).apply(); + sharedPreferences.edit().putBoolean(SharedPrefKeys.FORCE_REBUILD, false).apply(); return null; }, backgroundExecutorService); diff --git a/java/com/android/dialer/calllog/constants/IntentNames.java b/java/com/android/dialer/calllog/constants/IntentNames.java new file mode 100644 index 000000000..3912450a1 --- /dev/null +++ b/java/com/android/dialer/calllog/constants/IntentNames.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.calllog.constants; + +/** A class containing names for call log intents. */ +public final class IntentNames { + + public static final String ACTION_REFRESH_ANNOTATED_CALL_LOG = "refresh_annotated_call_log"; + + public static final String ACTION_CANCEL_REFRESHING_ANNOTATED_CALL_LOG = + "cancel_refreshing_annotated_call_log"; + + public static final String EXTRA_CHECK_DIRTY = "check_dirty"; + + private IntentNames() {} +} diff --git a/java/com/android/dialer/calllog/constants/SharedPrefKeys.java b/java/com/android/dialer/calllog/constants/SharedPrefKeys.java new file mode 100644 index 000000000..41e65bb53 --- /dev/null +++ b/java/com/android/dialer/calllog/constants/SharedPrefKeys.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.calllog.constants; + +/** Keys for shared preferences in the call log package. */ +public final class SharedPrefKeys { + + public static final String FORCE_REBUILD = "force_rebuild"; + + private SharedPrefKeys() {} +} diff --git a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java index 60654a81a..dbed1d81c 100644 --- a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java @@ -104,14 +104,5 @@ public interface CallLogDataSource { ContentValues coalesce(List individualRowsSortedByTimestampDesc); @MainThread - void registerContentObservers( - Context appContext, ContentObserverCallbacks contentObserverCallbacks); - - /** - * Methods which may optionally be called as a result of a data source's content observer firing. - */ - interface ContentObserverCallbacks { - @MainThread - void markDirtyAndNotify(Context appContext); - } + void registerContentObservers(Context appContext); } diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java index 8dec43759..40788f42a 100644 --- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java +++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java @@ -61,8 +61,7 @@ import javax.inject.Inject; * Responsible for maintaining the columns in the annotated call log which are derived from phone * numbers. */ -public final class PhoneLookupDataSource - implements CallLogDataSource, PhoneLookup.ContentObserverCallbacks { +public final class PhoneLookupDataSource implements CallLogDataSource { private final PhoneLookup phoneLookup; private final ListeningExecutorService backgroundExecutorService; @@ -85,8 +84,6 @@ public final class PhoneLookupDataSource */ private final Set phoneLookupHistoryRowsToDelete = new ArraySet<>(); - private CallLogDataSource.ContentObserverCallbacks dataSourceContentObserverCallbacks; - @Inject PhoneLookupDataSource( PhoneLookup phoneLookup, @@ -288,17 +285,8 @@ public final class PhoneLookupDataSource @MainThread @Override - public void registerContentObservers( - Context appContext, CallLogDataSource.ContentObserverCallbacks contentObserverCallbacks) { - dataSourceContentObserverCallbacks = contentObserverCallbacks; - phoneLookup.registerContentObservers(appContext, this); - } - - @MainThread - @Override - public void markDirtyAndNotify(Context appContext) { - Assert.isMainThread(); - dataSourceContentObserverCallbacks.markDirtyAndNotify(appContext); + public void registerContentObservers(Context appContext) { + phoneLookup.registerContentObservers(appContext); } private static ImmutableSet diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java index ee484d95e..e9f7c00bf 100644 --- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java +++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java @@ -47,6 +47,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.calllog.datasources.util.RowCombiner; +import com.android.dialer.calllog.notifier.RefreshAnnotatedCallLogNotifier; import com.android.dialer.calllogutils.PhoneAccountUtils; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; @@ -79,18 +80,21 @@ public class SystemCallLogDataSource implements CallLogDataSource { static final String PREF_LAST_TIMESTAMP_PROCESSED = "systemCallLogLastTimestampProcessed"; private final ListeningExecutorService backgroundExecutorService; + private final RefreshAnnotatedCallLogNotifier refreshAnnotatedCallLogNotifier; @Nullable private Long lastTimestampProcessed; @Inject - SystemCallLogDataSource(@BackgroundExecutor ListeningExecutorService backgroundExecutorService) { + SystemCallLogDataSource( + @BackgroundExecutor ListeningExecutorService backgroundExecutorService, + RefreshAnnotatedCallLogNotifier refreshAnnotatedCallLogNotifier) { this.backgroundExecutorService = backgroundExecutorService; + this.refreshAnnotatedCallLogNotifier = refreshAnnotatedCallLogNotifier; } @MainThread @Override - public void registerContentObservers( - Context appContext, ContentObserverCallbacks contentObserverCallbacks) { + public void registerContentObservers(Context appContext) { Assert.isMainThread(); LogUtil.enterBlock("SystemCallLogDataSource.registerContentObservers"); @@ -102,7 +106,7 @@ public class SystemCallLogDataSource implements CallLogDataSource { // TODO(zachh): Need to somehow register observers if user enables permission after launch? CallLogObserver callLogObserver = - new CallLogObserver(ThreadUtil.getUiThreadHandler(), appContext, contentObserverCallbacks); + new CallLogObserver(ThreadUtil.getUiThreadHandler(), refreshAnnotatedCallLogNotifier); appContext .getContentResolver() @@ -524,15 +528,14 @@ public class SystemCallLogDataSource implements CallLogDataSource { return ids; } + // TODO(a bug): Consider replacing it with MarkDirtyObserver. private static class CallLogObserver extends ContentObserver { - private final Context appContext; - private final ContentObserverCallbacks contentObserverCallbacks; + private final RefreshAnnotatedCallLogNotifier refreshAnnotatedCallLogNotifier; CallLogObserver( - Handler handler, Context appContext, ContentObserverCallbacks contentObserverCallbacks) { + Handler handler, RefreshAnnotatedCallLogNotifier refreshAnnotatedCallLogNotifier) { super(handler); - this.appContext = appContext; - this.contentObserverCallbacks = contentObserverCallbacks; + this.refreshAnnotatedCallLogNotifier = refreshAnnotatedCallLogNotifier; } @MainThread @@ -552,7 +555,7 @@ public class SystemCallLogDataSource implements CallLogDataSource { * table, which would be too slow. So, we just rely on content observers to trigger rebuilds * when any change is made to the system call log. */ - contentObserverCallbacks.markDirtyAndNotify(appContext); + refreshAnnotatedCallLogNotifier.markDirtyAndNotify(); } } } diff --git a/java/com/android/dialer/calllog/notifier/RefreshAnnotatedCallLogNotifier.java b/java/com/android/dialer/calllog/notifier/RefreshAnnotatedCallLogNotifier.java new file mode 100644 index 000000000..5b73ad778 --- /dev/null +++ b/java/com/android/dialer/calllog/notifier/RefreshAnnotatedCallLogNotifier.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.calllog.notifier; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.support.v4.content.LocalBroadcastManager; +import com.android.dialer.calllog.constants.IntentNames; +import com.android.dialer.calllog.constants.SharedPrefKeys; +import com.android.dialer.common.LogUtil; +import com.android.dialer.inject.ApplicationContext; +import com.android.dialer.storage.Unencrypted; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Notifies that a refresh of the annotated call log needs to be started/cancelled. + * + *

Methods in this class are usually invoked when the underlying data backing the annotated call + * log change. + * + *

For example, a {@link android.database.ContentObserver} for the system call log can use {@link + * #markDirtyAndNotify()} to force the annotated call log to be rebuilt. + */ +@Singleton +public class RefreshAnnotatedCallLogNotifier { + + private final Context appContext; + private final SharedPreferences sharedPreferences; + + @Inject + RefreshAnnotatedCallLogNotifier( + @ApplicationContext Context appContext, @Unencrypted SharedPreferences sharedPreferences) { + this.appContext = appContext; + this.sharedPreferences = sharedPreferences; + } + + /** + * Mark the annotated call log as "dirty" and notify that it needs to be refreshed. + * + *

This will force a rebuild by skip checking whether the annotated call log is "dirty". + */ + public void markDirtyAndNotify() { + LogUtil.enterBlock("RefreshAnnotatedCallLogNotifier.markDirtyAndNotify"); + + sharedPreferences.edit().putBoolean(SharedPrefKeys.FORCE_REBUILD, true).apply(); + notify(/* checkDirty = */ false); + } + + /** + * Notifies that the annotated call log needs to be refreshed. + * + *

Note that the notification is sent as a broadcast, which means the annotated call log might + * not be refreshed if there is no corresponding receiver registered. + * + * @param checkDirty Whether to check if the annotated call log is "dirty" before proceeding to + * rebuild it. + */ + public void notify(boolean checkDirty) { + LogUtil.i("RefreshAnnotatedCallLogNotifier.notify", "checkDirty = %s", checkDirty); + + Intent intent = new Intent(); + intent.setAction(IntentNames.ACTION_REFRESH_ANNOTATED_CALL_LOG); + intent.putExtra(IntentNames.EXTRA_CHECK_DIRTY, checkDirty); + + LocalBroadcastManager.getInstance(appContext).sendBroadcast(intent); + } + + /** + * Notifies to cancel refreshing the annotated call log. + * + *

Note that this method does not guarantee the job to be cancelled. As the notification is + * sent as a broadcast, please see the corresponding receiver for details about cancelling the + * job. + */ + public void cancel() { + LogUtil.enterBlock("RefreshAnnotatedCallLogNotifier.cancel"); + + Intent intent = new Intent(); + intent.setAction(IntentNames.ACTION_CANCEL_REFRESHING_ANNOTATED_CALL_LOG); + + LocalBroadcastManager.getInstance(appContext).sendBroadcast(intent); + } +} diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java index 5e676f072..8e8d55f3a 100644 --- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java +++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java @@ -22,44 +22,29 @@ import android.support.annotation.VisibleForTesting; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; +import android.support.v4.content.LocalBroadcastManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.android.dialer.calllog.CallLogComponent; -import com.android.dialer.calllog.CallLogFramework; -import com.android.dialer.calllog.CallLogFramework.CallLogUi; -import com.android.dialer.calllog.RefreshAnnotatedCallLogWorker; +import com.android.dialer.calllog.RefreshAnnotatedCallLogReceiver; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DefaultFutureCallback; -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.Futures; -import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.util.concurrent.TimeUnit; /** 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 REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS = 100L; +public final class NewCallLogFragment extends Fragment implements LoaderCallbacks { @VisibleForTesting static final long MARK_ALL_CALLS_READ_WAIT_MILLIS = TimeUnit.SECONDS.toMillis(3); - private RefreshAnnotatedCallLogWorker refreshAnnotatedCallLogWorker; - private UiListener refreshAnnotatedCallLogListener; + private RefreshAnnotatedCallLogReceiver refreshAnnotatedCallLogReceiver; private RecyclerView recyclerView; - @Nullable private Runnable refreshAnnotatedCallLogRunnable; private boolean shouldMarkCallsRead = false; private final Runnable setShouldMarkCallsReadTrue = () -> shouldMarkCallsRead = true; @@ -74,16 +59,7 @@ public final class NewCallLogFragment extends Fragment LogUtil.enterBlock("NewCallLogFragment.onActivityCreated"); - CallLogComponent component = CallLogComponent.get(getContext()); - CallLogFramework callLogFramework = component.callLogFramework(); - callLogFramework.attachUi(this); - - // TODO(zachh): Use support fragment manager and add support for them in executors library. - refreshAnnotatedCallLogListener = - DialerExecutorComponent.get(getContext()) - .createUiListener( - getActivity().getFragmentManager(), "NewCallLogFragment.refreshAnnotatedCallLog"); - refreshAnnotatedCallLogWorker = component.getRefreshAnnotatedCallLogWorker(); + refreshAnnotatedCallLogReceiver = new RefreshAnnotatedCallLogReceiver(getContext()); } @Override @@ -99,11 +75,12 @@ public final class NewCallLogFragment extends Fragment LogUtil.enterBlock("NewCallLogFragment.onResume"); - CallLogFramework callLogFramework = CallLogComponent.get(getContext()).callLogFramework(); - callLogFramework.attachUi(this); + registerRefreshAnnotatedCallLogReceiver(); // TODO(zachh): Consider doing this when fragment becomes visible. - refreshAnnotatedCallLog(true /* checkDirty */); + CallLogComponent.get(getContext()) + .getRefreshAnnotatedCallLogNotifier() + .notify(/* checkDirty = */ true); // There are some types of data that we show in the call log that are not represented in the // AnnotatedCallLog. For example, CP2 information for invalid numbers can sometimes only be @@ -130,11 +107,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); ThreadUtil.getUiThreadHandler().removeCallbacks(setShouldMarkCallsReadTrue); - CallLogFramework callLogFramework = CallLogComponent.get(getContext()).callLogFramework(); - callLogFramework.detachUi(); + unregisterRefreshAnnotatedCallLogReceiver(); if (shouldMarkCallsRead) { Futures.addCallback( @@ -157,42 +132,22 @@ public final class NewCallLogFragment extends Fragment return view; } - 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( - getContext(), - future, - unused -> {}, - throwable -> { - throw new RuntimeException(throwable); - }); - }; - ThreadUtil.getUiThreadHandler() - .postDelayed(refreshAnnotatedCallLogRunnable, REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS); + private void registerRefreshAnnotatedCallLogReceiver() { + LogUtil.enterBlock("NewCallLogFragment.registerRefreshAnnotatedCallLogReceiver"); + + LocalBroadcastManager.getInstance(getContext()) + .registerReceiver( + refreshAnnotatedCallLogReceiver, RefreshAnnotatedCallLogReceiver.getIntentFilter()); } - @Override - public void invalidateUi() { - LogUtil.enterBlock("NewCallLogFragment.invalidateUi"); - refreshAnnotatedCallLog(false /* checkDirty */); + private void unregisterRefreshAnnotatedCallLogReceiver() { + LogUtil.enterBlock("NewCallLogFragment.unregisterRefreshAnnotatedCallLogReceiver"); + + // Cancel pending work as we don't need it any more. + CallLogComponent.get(getContext()).getRefreshAnnotatedCallLogNotifier().cancel(); + + LocalBroadcastManager.getInstance(getContext()) + .unregisterReceiver(refreshAnnotatedCallLogReceiver); } @Override diff --git a/java/com/android/dialer/phonelookup/PhoneLookup.java b/java/com/android/dialer/phonelookup/PhoneLookup.java index 76ff98e7c..a7974ad10 100644 --- a/java/com/android/dialer/phonelookup/PhoneLookup.java +++ b/java/com/android/dialer/phonelookup/PhoneLookup.java @@ -84,14 +84,5 @@ public interface PhoneLookup { ListenableFuture onSuccessfulBulkUpdate(); @MainThread - void registerContentObservers( - Context appContext, ContentObserverCallbacks contentObserverCallbacks); - - /** - * Methods which may optionally be called as a result of a phone lookup's content observer firing. - */ - interface ContentObserverCallbacks { - @MainThread - void markDirtyAndNotify(Context appContext); - } + void registerContentObservers(Context appContext); } diff --git a/java/com/android/dialer/phonelookup/blockednumber/DialerBlockedNumberPhoneLookup.java b/java/com/android/dialer/phonelookup/blockednumber/DialerBlockedNumberPhoneLookup.java index 2271c7580..2d019c8c2 100644 --- a/java/com/android/dialer/phonelookup/blockednumber/DialerBlockedNumberPhoneLookup.java +++ b/java/com/android/dialer/phonelookup/blockednumber/DialerBlockedNumberPhoneLookup.java @@ -51,13 +51,16 @@ public final class DialerBlockedNumberPhoneLookup implements PhoneLookup @Override @MainThread - public void registerContentObservers( - Context appContext, ContentObserverCallbacks contentObserverCallbacks) { + public void registerContentObservers(Context appContext) { for (PhoneLookup phoneLookup : phoneLookups) { - phoneLookup.registerContentObservers(appContext, contentObserverCallbacks); + phoneLookup.registerContentObservers(appContext); } } } diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java index e051f473c..8db308892 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java @@ -619,8 +619,7 @@ public final class Cp2LocalPhoneLookup implements PhoneLookup { } @Override - public void registerContentObservers( - Context appContext, ContentObserverCallbacks contentObserverCallbacks) { + public void registerContentObservers(Context appContext) { // Do nothing since CP2 changes are too noisy. } diff --git a/java/com/android/dialer/phonelookup/cp2/Cp2RemotePhoneLookup.java b/java/com/android/dialer/phonelookup/cp2/Cp2RemotePhoneLookup.java index cc4fbf19f..7efe039eb 100644 --- a/java/com/android/dialer/phonelookup/cp2/Cp2RemotePhoneLookup.java +++ b/java/com/android/dialer/phonelookup/cp2/Cp2RemotePhoneLookup.java @@ -237,8 +237,7 @@ public final class Cp2RemotePhoneLookup implements PhoneLookup { } @Override - public void registerContentObservers( - Context appContext, ContentObserverCallbacks contentObserverCallbacks) { + public void registerContentObservers(Context appContext) { // No content observer needed for remote contacts } } diff --git a/java/com/android/dialer/phonelookup/spam/SpamPhoneLookup.java b/java/com/android/dialer/phonelookup/spam/SpamPhoneLookup.java index 9f0b5cf52..7661a15da 100644 --- a/java/com/android/dialer/phonelookup/spam/SpamPhoneLookup.java +++ b/java/com/android/dialer/phonelookup/spam/SpamPhoneLookup.java @@ -152,8 +152,8 @@ public final class SpamPhoneLookup implements PhoneLookup { } @Override - public void registerContentObservers( - Context appContext, ContentObserverCallbacks contentObserverCallbacks) { - // No content observer needed for spam info + public void registerContentObservers(Context appContext) { + // No content observer can be registered as Spam is not based on a content provider. + // Each Spam implementation should be responsible for notifying any data changes. } } diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java b/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java index 8b6fcbc07..8d724afe3 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailFragment.java @@ -24,18 +24,16 @@ import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; +import android.support.v4.content.LocalBroadcastManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.android.dialer.calllog.CallLogComponent; -import com.android.dialer.calllog.CallLogFramework; -import com.android.dialer.calllog.CallLogFramework.CallLogUi; -import com.android.dialer.calllog.RefreshAnnotatedCallLogWorker; +import com.android.dialer.calllog.RefreshAnnotatedCallLogReceiver; 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.android.dialer.glidephotomanager.GlidePhotoManagerComponent; import com.android.dialer.voicemail.listui.error.VoicemailStatus; @@ -48,24 +46,11 @@ import java.util.List; // TODO(uabdullah): Register content observer for VoicemailContract.Status.CONTENT_URI in onStart /** Fragment for Dialer Voicemail Tab. */ -public final class NewVoicemailFragment extends Fragment - implements LoaderCallbacks, CallLogUi { - - /* - * 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; - @Nullable private Runnable refreshAnnotatedCallLogRunnable; - - private UiListener> queryVoicemailStatusTableListener; +public final class NewVoicemailFragment extends Fragment implements LoaderCallbacks { private RecyclerView recyclerView; + private RefreshAnnotatedCallLogReceiver refreshAnnotatedCallLogReceiver; + private UiListener> queryVoicemailStatusTableListener; public NewVoicemailFragment() { LogUtil.enterBlock("NewVoicemailFragment.NewVoicemailFragment"); @@ -77,23 +62,12 @@ public final class NewVoicemailFragment extends Fragment LogUtil.enterBlock("NewVoicemailFragment.onActivityCreated"); - CallLogComponent component = CallLogComponent.get(getContext()); - CallLogFramework callLogFramework = component.callLogFramework(); - callLogFramework.attachUi(this); - - // TODO(zachh): Use support fragment manager and add support for them in executors library. - refreshAnnotatedCallLogListener = - DialerExecutorComponent.get(getContext()) - .createUiListener( - getActivity().getFragmentManager(), "NewVoicemailFragment.refreshAnnotatedCallLog"); - + refreshAnnotatedCallLogReceiver = new RefreshAnnotatedCallLogReceiver(getContext()); queryVoicemailStatusTableListener = DialerExecutorComponent.get(getContext()) .createUiListener( getActivity().getFragmentManager(), "NewVoicemailFragment.queryVoicemailStatusTable"); - - refreshAnnotatedCallLogWorker = component.getRefreshAnnotatedCallLogWorker(); } @Override @@ -108,11 +82,12 @@ public final class NewVoicemailFragment extends Fragment LogUtil.enterBlock("NewCallLogFragment.onResume"); - CallLogFramework callLogFramework = CallLogComponent.get(getContext()).callLogFramework(); - callLogFramework.attachUi(this); + registerRefreshAnnotatedCallLogReceiver(); // TODO(zachh): Consider doing this when fragment becomes visible. - refreshAnnotatedCallLog(true /* checkDirty */); + CallLogComponent.get(getContext()) + .getRefreshAnnotatedCallLogNotifier() + .notify(/* checkDirty = */ true); } @Override @@ -121,14 +96,9 @@ public final class NewVoicemailFragment extends Fragment LogUtil.enterBlock("NewVoicemailFragment.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(); + unregisterRefreshAnnotatedCallLogReceiver(); } - @Nullable @Override public View onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -139,43 +109,6 @@ public final class NewVoicemailFragment extends Fragment return view; } - private void refreshAnnotatedCallLog(boolean checkDirty) { - LogUtil.enterBlock("NewVoicemailFragment.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( - getContext(), - future, - unused -> {}, - throwable -> { - throw new RuntimeException(throwable); - }); - }; - ThreadUtil.getUiThreadHandler().postDelayed(refreshAnnotatedCallLogRunnable, WAIT_MILLIS); - } - - @Override - public void invalidateUi() { - LogUtil.enterBlock("NewVoicemailFragment.invalidateUi"); - refreshAnnotatedCallLog(false /* checkDirty */); - } - @Override public Loader onCreateLoader(int id, Bundle args) { LogUtil.enterBlock("NewVoicemailFragment.onCreateLoader"); @@ -210,6 +143,24 @@ public final class NewVoicemailFragment extends Fragment queryAndUpdateVoicemailStatusAlert(); } + private void registerRefreshAnnotatedCallLogReceiver() { + LogUtil.enterBlock("NewVoicemailFragment.registerRefreshAnnotatedCallLogReceiver"); + + LocalBroadcastManager.getInstance(getContext()) + .registerReceiver( + refreshAnnotatedCallLogReceiver, RefreshAnnotatedCallLogReceiver.getIntentFilter()); + } + + private void unregisterRefreshAnnotatedCallLogReceiver() { + LogUtil.enterBlock("NewVoicemailFragment.unregisterRefreshAnnotatedCallLogReceiver"); + + // Cancel pending work as we don't need it any more. + CallLogComponent.get(getContext()).getRefreshAnnotatedCallLogNotifier().cancel(); + + LocalBroadcastManager.getInstance(getContext()) + .unregisterReceiver(refreshAnnotatedCallLogReceiver); + } + private void queryAndUpdateVoicemailStatusAlert() { queryVoicemailStatusTableListener.listen( getContext(), -- cgit v1.2.3