Use a broadcast receiver to refresh the annotated call log.

Bug: 73347270
Test: Existing tests + RefreshAnnotatedCallLogNotifierTest
PiperOrigin-RevId: 186347066
Change-Id: I5a530416bdaa9edc7131a0d5ced44f1b5ee1692b
diff --git a/java/com/android/dialer/calllog/CallLogComponent.java b/java/com/android/dialer/calllog/CallLogComponent.java
index bb5bfee..4f147f1 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 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 440055d..7da8d9c 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}.
  *
  * <p>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 @@
     // 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.
-   *
-   * <p>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 0000000..e0bfcd8
--- /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.
+   *
+   * <p>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<Void> 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 4c5904e..a430d14 100644
--- a/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java
+++ b/java/com/android/dialer/calllog/RefreshAnnotatedCallLogWorker.java
@@ -18,6 +18,7 @@
 
 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 @@
               // 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 @@
     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 0000000..3912450
--- /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 0000000..41e65bb
--- /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 60654a8..dbed1d8 100644
--- a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java
@@ -104,14 +104,5 @@
   ContentValues coalesce(List<ContentValues> 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 8dec437..40788f4 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 @@
  * 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<PhoneLookupInfo> phoneLookup;
   private final ListeningExecutorService backgroundExecutorService;
@@ -85,8 +84,6 @@
    */
   private final Set<String> phoneLookupHistoryRowsToDelete = new ArraySet<>();
 
-  private CallLogDataSource.ContentObserverCallbacks dataSourceContentObserverCallbacks;
-
   @Inject
   PhoneLookupDataSource(
       PhoneLookup<PhoneLookupInfo> phoneLookup,
@@ -288,17 +285,8 @@
 
   @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<DialerPhoneNumber>
diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
index ee484d9..e9f7c00 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.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 @@
   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 @@
     // 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 @@
     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 @@
        * 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 0000000..5b73ad7
--- /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.
+ *
+ * <p>Methods in this class are usually invoked when the underlying data backing the annotated call
+ * log change.
+ *
+ * <p>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.
+   *
+   * <p>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.
+   *
+   * <p>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.
+   *
+   * <p>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 5e676f0..8e8d55f 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.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<Cursor> {
-
-  /*
-   * 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<Cursor> {
 
   @VisibleForTesting
   static final long MARK_ALL_CALLS_READ_WAIT_MILLIS = TimeUnit.SECONDS.toMillis(3);
 
-  private RefreshAnnotatedCallLogWorker refreshAnnotatedCallLogWorker;
-  private UiListener<Void> 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 @@
 
     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 @@
 
     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 @@
     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 @@
     return view;
   }
 
-  private void refreshAnnotatedCallLog(boolean checkDirty) {
-    LogUtil.enterBlock("NewCallLogFragment.refreshAnnotatedCallLog");
+  private void registerRefreshAnnotatedCallLogReceiver() {
+    LogUtil.enterBlock("NewCallLogFragment.registerRefreshAnnotatedCallLogReceiver");
 
-    // 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<Void> 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);
+    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 76ff98e..a7974ad 100644
--- a/java/com/android/dialer/phonelookup/PhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/PhoneLookup.java
@@ -84,14 +84,5 @@
   ListenableFuture<Void> 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 2271c75..2d019c8 100644
--- a/java/com/android/dialer/phonelookup/blockednumber/DialerBlockedNumberPhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/blockednumber/DialerBlockedNumberPhoneLookup.java
@@ -51,13 +51,16 @@
 
   private final Context appContext;
   private final ListeningExecutorService executorService;
+  private final MarkDirtyObserver markDirtyObserver;
 
   @Inject
   DialerBlockedNumberPhoneLookup(
       @ApplicationContext Context appContext,
-      @BackgroundExecutor ListeningExecutorService executorService) {
+      @BackgroundExecutor ListeningExecutorService executorService,
+      MarkDirtyObserver markDirtyObserver) {
     this.appContext = appContext;
     this.executorService = executorService;
+    this.markDirtyObserver = markDirtyObserver;
   }
 
   @Override
@@ -165,13 +168,12 @@
   }
 
   @Override
-  public void registerContentObservers(
-      Context appContext, ContentObserverCallbacks contentObserverCallbacks) {
+  public void registerContentObservers(Context appContext) {
     appContext
         .getContentResolver()
         .registerContentObserver(
             FilteredNumber.CONTENT_URI,
             true, // FilteredNumberProvider notifies on the item
-            new MarkDirtyObserver(appContext, contentObserverCallbacks));
+            markDirtyObserver);
   }
 }
diff --git a/java/com/android/dialer/phonelookup/blockednumber/MarkDirtyObserver.java b/java/com/android/dialer/phonelookup/blockednumber/MarkDirtyObserver.java
index 1c41d8f..9f72ba4 100644
--- a/java/com/android/dialer/phonelookup/blockednumber/MarkDirtyObserver.java
+++ b/java/com/android/dialer/phonelookup/blockednumber/MarkDirtyObserver.java
@@ -16,25 +16,28 @@
 
 package com.android.dialer.phonelookup.blockednumber;
 
-import android.content.Context;
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.support.annotation.MainThread;
+import com.android.dialer.calllog.notifier.RefreshAnnotatedCallLogNotifier;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.ThreadUtil;
-import com.android.dialer.phonelookup.PhoneLookup.ContentObserverCallbacks;
+import javax.inject.Inject;
 
-/** Calls {@link ContentObserverCallbacks#markDirtyAndNotify(Context)} when the content changed */
+/**
+ * Mark the annotated call log as dirty and notify that a refresh is in order when the content
+ * changes.
+ */
+// TODO(a bug): Consider making this class available to all data sources and PhoneLookups.
 class MarkDirtyObserver extends ContentObserver {
 
-  private final Context appContext;
-  private final ContentObserverCallbacks contentObserverCallbacks;
+  private final RefreshAnnotatedCallLogNotifier refreshAnnotatedCallLogNotifier;
 
-  MarkDirtyObserver(Context appContext, ContentObserverCallbacks contentObserverCallbacks) {
+  @Inject
+  MarkDirtyObserver(RefreshAnnotatedCallLogNotifier refreshAnnotatedCallLogNotifier) {
     super(ThreadUtil.getUiThreadHandler());
-    this.appContext = appContext;
-    this.contentObserverCallbacks = contentObserverCallbacks;
+    this.refreshAnnotatedCallLogNotifier = refreshAnnotatedCallLogNotifier;
   }
 
   @MainThread
@@ -42,6 +45,6 @@
   public void onChange(boolean selfChange, Uri uri) {
     Assert.isMainThread();
     LogUtil.enterBlock("SystemBlockedNumberPhoneLookup.FilteredNumberObserver.onChange");
-    contentObserverCallbacks.markDirtyAndNotify(appContext);
+    refreshAnnotatedCallLogNotifier.markDirtyAndNotify();
   }
 }
diff --git a/java/com/android/dialer/phonelookup/blockednumber/SystemBlockedNumberPhoneLookup.java b/java/com/android/dialer/phonelookup/blockednumber/SystemBlockedNumberPhoneLookup.java
index e0ff995..f35b3e1 100644
--- a/java/com/android/dialer/phonelookup/blockednumber/SystemBlockedNumberPhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/blockednumber/SystemBlockedNumberPhoneLookup.java
@@ -54,13 +54,16 @@
 
   private final Context appContext;
   private final ListeningExecutorService executorService;
+  private final MarkDirtyObserver markDirtyObserver;
 
   @Inject
   SystemBlockedNumberPhoneLookup(
       @ApplicationContext Context appContext,
-      @BackgroundExecutor ListeningExecutorService executorService) {
+      @BackgroundExecutor ListeningExecutorService executorService,
+      MarkDirtyObserver markDirtyObserver) {
     this.appContext = appContext;
     this.executorService = executorService;
+    this.markDirtyObserver = markDirtyObserver;
   }
 
   @Override
@@ -166,8 +169,7 @@
   }
 
   @Override
-  public void registerContentObservers(
-      Context appContext, ContentObserverCallbacks contentObserverCallbacks) {
+  public void registerContentObservers(Context appContext) {
     if (VERSION.SDK_INT < VERSION_CODES.N) {
       return;
     }
@@ -176,6 +178,6 @@
         .registerContentObserver(
             BlockedNumbers.CONTENT_URI,
             true, // BlockedNumbers notifies on the item
-            new MarkDirtyObserver(appContext, contentObserverCallbacks));
+            markDirtyObserver);
   }
 }
diff --git a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java
index b77a86c..abe18f7 100644
--- a/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/composite/CompositePhoneLookup.java
@@ -179,10 +179,9 @@
 
   @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 e051f47..8db3088 100644
--- a/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/cp2/Cp2LocalPhoneLookup.java
@@ -619,8 +619,7 @@
   }
 
   @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 cc4fbf1..7efe039 100644
--- a/java/com/android/dialer/phonelookup/cp2/Cp2RemotePhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/cp2/Cp2RemotePhoneLookup.java
@@ -237,8 +237,7 @@
   }
 
   @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 9f0b5cf..7661a15 100644
--- a/java/com/android/dialer/phonelookup/spam/SpamPhoneLookup.java
+++ b/java/com/android/dialer/phonelookup/spam/SpamPhoneLookup.java
@@ -152,8 +152,8 @@
   }
 
   @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 8b6fcbc..8d724af 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.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 @@
 
 // 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<Cursor>, 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<Void> refreshAnnotatedCallLogListener;
-  @Nullable private Runnable refreshAnnotatedCallLogRunnable;
-
-  private UiListener<ImmutableList<VoicemailStatus>> queryVoicemailStatusTableListener;
+public final class NewVoicemailFragment extends Fragment implements LoaderCallbacks<Cursor> {
 
   private RecyclerView recyclerView;
+  private RefreshAnnotatedCallLogReceiver refreshAnnotatedCallLogReceiver;
+  private UiListener<ImmutableList<VoicemailStatus>> queryVoicemailStatusTableListener;
 
   public NewVoicemailFragment() {
     LogUtil.enterBlock("NewVoicemailFragment.NewVoicemailFragment");
@@ -77,23 +62,12 @@
 
     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 @@
 
     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 @@
 
     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 @@
     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<Void> 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<Cursor> onCreateLoader(int id, Bundle args) {
     LogUtil.enterBlock("NewVoicemailFragment.onCreateLoader");
@@ -210,6 +143,24 @@
     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(),