Merge changes Ib360d3bc,Iae40d0ab,I486f7b1a,I709a1e30

* changes:
  Mark calls as read in new call log.
  Restored work profile contacts to Dialer search.
  Clicking on a missed call in the call log no longer crashes the app.
  Add bottom sheet options for blocked and/or spam numbers in the new call log.
diff --git a/assets/quantum/res/drawable/quantum_ic_report_off_vd_theme_24.xml b/assets/quantum/res/drawable/quantum_ic_report_off_vd_theme_24.xml
new file mode 100644
index 0000000..8af3a72
--- /dev/null
+++ b/assets/quantum/res/drawable/quantum_ic_report_off_vd_theme_24.xml
@@ -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
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M11,7h2v2.92l6.91,6.91 1.09,-1.1L21,8.27L15.73,3L8.27,3L7.18,4.1 11,7.92zM22.27,21.73l-20,-20.01L1,2.99l3.64,3.64L3,8.27v7.46L8.27,21h7.46l1.64,-1.63L21,23l1.27,-1.27zM12,17.3c-0.72,0 -1.3,-0.58 -1.3,-1.3s0.58,-1.3 1.3,-1.3 1.3,0.58 1.3,1.3 -0.58,1.3 -1.3,1.3z"/>
+</vector>
diff --git a/java/com/android/contacts/common/compat/DirectoryCompat.java b/java/com/android/contacts/common/compat/DirectoryCompat.java
index 85f4a42..e670876 100644
--- a/java/com/android/contacts/common/compat/DirectoryCompat.java
+++ b/java/com/android/contacts/common/compat/DirectoryCompat.java
@@ -48,4 +48,8 @@
   public static boolean isEnterpriseDirectoryId(long directoryId) {
     return VERSION.SDK_INT >= VERSION_CODES.N && Directory.isEnterpriseDirectoryId(directoryId);
   }
+
+  public static boolean isOnlyEnterpriseDirectoryId(long directoryId) {
+    return isEnterpriseDirectoryId(directoryId) && !isRemoteDirectoryId(directoryId);
+  }
 }
diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java
index 51df702..baca590 100644
--- a/java/com/android/dialer/app/calllog/CallLogAdapter.java
+++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java
@@ -54,6 +54,7 @@
 import com.android.contacts.common.ContactsUtils;
 import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
 import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.dialer.app.DialtactsActivity;
 import com.android.dialer.app.R;
 import com.android.dialer.app.calllog.CallLogFragment.CallLogFragmentListener;
 import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator;
@@ -381,9 +382,7 @@
             if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) {
               CallLogAsyncTaskUtil.markCallAsRead(activity, viewHolder.callIds);
               if (activityType == ACTIVITY_TYPE_DIALTACTS) {
-                if (v.getContext() instanceof CallLogFragmentListener) {
-                  ((CallLogFragmentListener) v.getContext()).updateTabUnreadCounts();
-                } else if (v.getContext() instanceof MainActivityPeer.PeerSupplier) {
+                if (v.getContext() instanceof MainActivityPeer.PeerSupplier) {
                   // This is really bad, but we must do this to prevent a dependency cycle, enforce
                   // best practices in new code, and avoid refactoring DialtactsActivity.
                   ((FragmentUtilListener)
@@ -391,8 +390,7 @@
                       .getImpl(CallLogFragmentListener.class)
                       .updateTabUnreadCounts();
                 } else {
-                  throw Assert.createIllegalStateFailException(
-                      "View parent does not implement CallLogFragmentListener");
+                  ((DialtactsActivity) v.getContext()).updateTabUnreadCounts();
                 }
               }
             }
diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
index 5949141..10e30ff 100644
--- a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
+++ b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java
@@ -31,6 +31,7 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
 import com.android.dialer.common.concurrent.DialerExecutorComponent;
+import com.android.dialer.notification.missedcalls.MissedCallNotificationCanceller;
 import com.android.dialer.telecom.TelecomUtil;
 import com.android.dialer.util.PermissionsUtil;
 
@@ -87,14 +88,6 @@
     context.startService(serviceIntent);
   }
 
-  public static void markSingleNewVoicemailAsOld(Context context, @Nullable Uri voicemailUri) {
-    LogUtil.enterBlock("CallLogNotificationsService.markSingleNewVoicemailAsOld");
-    Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
-    serviceIntent.setAction(CallLogNotificationsService.ACTION_MARK_SINGLE_NEW_VOICEMAIL_AS_OLD);
-    serviceIntent.setData(voicemailUri);
-    context.startService(serviceIntent);
-  }
-
   public static void cancelAllMissedCalls(Context context) {
     LogUtil.enterBlock("CallLogNotificationsService.cancelAllMissedCalls");
     DialerExecutorComponent.get(context)
@@ -175,7 +168,7 @@
       case ACTION_CANCEL_SINGLE_MISSED_CALL:
         Uri callUri = intent.getData();
         CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(this, callUri);
-        MissedCallNotifier.cancelSingleMissedCallNotification(this, callUri);
+        MissedCallNotificationCanceller.cancelSingle(this, callUri);
         TelecomUtil.cancelMissedCallsNotification(this);
         break;
       case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION:
@@ -196,7 +189,7 @@
     LogUtil.enterBlock("CallLogNotificationsService.cancelAllMissedCallsBackground");
     Assert.isWorkerThread();
     CallLogNotificationsQueryHelper.markAllMissedCallsInCallLogAsRead(context);
-    MissedCallNotifier.cancelAllMissedCallNotifications(context);
+    MissedCallNotificationCanceller.cancelAll(context);
     TelecomUtil.cancelMissedCallsNotification(context);
   }
 
diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
index 417f8f0..14bbdfa 100644
--- a/java/com/android/dialer/app/calllog/MissedCallNotifier.java
+++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java
@@ -58,7 +58,9 @@
 import com.android.dialer.enrichedcall.FuzzyPhoneNumberMatcher;
 import com.android.dialer.notification.DialerNotificationManager;
 import com.android.dialer.notification.NotificationChannelId;
-import com.android.dialer.notification.NotificationManagerUtils;
+import com.android.dialer.notification.missedcalls.MissedCallConstants;
+import com.android.dialer.notification.missedcalls.MissedCallNotificationCanceller;
+import com.android.dialer.notification.missedcalls.MissedCallNotificationTags;
 import com.android.dialer.phonenumbercache.ContactInfo;
 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
 import com.android.dialer.precall.PreCall;
@@ -71,18 +73,6 @@
 /** Creates a notification for calls that the user missed (neither answered nor rejected). */
 public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {
 
-  /** Prefix used to generate a unique tag for each missed call notification. */
-  private static final String NOTIFICATION_TAG_PREFIX = "MissedCall_";
-  /** Common ID for all missed call notifications. */
-  private static final int NOTIFICATION_ID = 1;
-  /** Tag for the group summary notification. */
-  private static final String GROUP_SUMMARY_NOTIFICATION_TAG = "GroupSummary_MissedCall";
-  /**
-   * Key used to associate all missed call notifications and the summary as belonging to a single
-   * group.
-   */
-  private static final String GROUP_KEY = "MissedCallGroup";
-
   private final Context context;
   private final CallLogNotificationsQueryHelper callLogNotificationsQueryHelper;
 
@@ -126,7 +116,7 @@
     if ((newCalls != null && newCalls.isEmpty()) || count == 0) {
       // No calls to notify about: clear the notification.
       CallLogNotificationsQueryHelper.markAllMissedCallsInCallLogAsRead(context);
-      cancelAllMissedCallNotifications(context);
+      MissedCallNotificationCanceller.cancelAll(context);
       return;
     }
 
@@ -226,7 +216,10 @@
 
     LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification");
     DialerNotificationManager.notify(
-        context, GROUP_SUMMARY_NOTIFICATION_TAG, NOTIFICATION_ID, notification);
+        context,
+        MissedCallConstants.GROUP_SUMMARY_NOTIFICATION_TAG,
+        MissedCallConstants.NOTIFICATION_ID,
+        notification);
 
     if (useCallList) {
       // Do not repost active notifications to prevent erasing post call notes.
@@ -240,7 +233,10 @@
         String callTag = getNotificationTagForCall(call);
         if (!activeTags.contains(callTag)) {
           DialerNotificationManager.notify(
-              context, callTag, NOTIFICATION_ID, getNotificationForCall(call, null));
+              context,
+              callTag,
+              MissedCallConstants.NOTIFICATION_ID,
+              getNotificationForCall(call, null));
         }
       }
     }
@@ -286,29 +282,8 @@
     }
   }
 
-  public static void cancelAllMissedCallNotifications(@NonNull Context context) {
-    NotificationManagerUtils.cancelAllInGroup(context, GROUP_KEY);
-  }
-
-  public static void cancelSingleMissedCallNotification(
-      @NonNull Context context, @Nullable Uri callUri) {
-    if (callUri == null) {
-      LogUtil.e(
-          "MissedCallNotifier.cancelSingleMissedCallNotification",
-          "unable to cancel notification, uri is null");
-      return;
-    }
-    // This will also dismiss the group summary if there are no more missed call notifications.
-    DialerNotificationManager.cancel(
-        context, getNotificationTagForCallUri(callUri), NOTIFICATION_ID);
-  }
-
   private static String getNotificationTagForCall(@NonNull NewCall call) {
-    return getNotificationTagForCallUri(call.callsUri);
-  }
-
-  private static String getNotificationTagForCallUri(@NonNull Uri callUri) {
-    return NOTIFICATION_TAG_PREFIX + callUri;
+    return MissedCallNotificationTags.getNotificationTagForCallUri(call.callsUri);
   }
 
   @WorkerThread
@@ -324,7 +299,7 @@
           DialerNotificationManager.notify(
               context,
               getNotificationTagForCall(call),
-              NOTIFICATION_ID,
+              MissedCallConstants.NOTIFICATION_ID,
               getNotificationForCall(call, note));
           return;
         }
@@ -408,7 +383,7 @@
 
   private Notification.Builder createNotificationBuilder() {
     return new Notification.Builder(context)
-        .setGroup(GROUP_KEY)
+        .setGroup(MissedCallConstants.GROUP_KEY)
         .setSmallIcon(android.R.drawable.stat_notify_missed_call)
         .setColor(context.getResources().getColor(R.color.dialer_theme_color, null))
         .setAutoCancel(true)
@@ -437,7 +412,7 @@
   public void callBackFromMissedCall(String number, Uri callUri) {
     closeSystemDialogs(context);
     CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri);
-    cancelSingleMissedCallNotification(context, callUri);
+    MissedCallNotificationCanceller.cancelSingle(context, callUri);
     DialerUtils.startActivityWithErrorToast(
         context,
         PreCall.getIntent(
@@ -450,7 +425,7 @@
   public void sendSmsFromMissedCall(String number, Uri callUri) {
     closeSystemDialogs(context);
     CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri);
-    cancelSingleMissedCallNotification(context, callUri);
+    MissedCallNotificationCanceller.cancelSingle(context, callUri);
     DialerUtils.startActivityWithErrorToast(
         context, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
   }
diff --git a/java/com/android/dialer/calllog/CallLogComponent.java b/java/com/android/dialer/calllog/CallLogComponent.java
index c7db2a1..bb5bfee 100644
--- a/java/com/android/dialer/calllog/CallLogComponent.java
+++ b/java/com/android/dialer/calllog/CallLogComponent.java
@@ -27,6 +27,8 @@
 
   public abstract RefreshAnnotatedCallLogWorker getRefreshAnnotatedCallLogWorker();
 
+  public abstract ClearMissedCalls getClearMissedCalls();
+
   public static CallLogComponent get(Context context) {
     return ((HasComponent) ((HasRootComponent) context.getApplicationContext()).component())
         .callLogComponent();
diff --git a/java/com/android/dialer/calllog/ClearMissedCalls.java b/java/com/android/dialer/calllog/ClearMissedCalls.java
new file mode 100644
index 0000000..d216e7b
--- /dev/null
+++ b/java/com/android/dialer/calllog/ClearMissedCalls.java
@@ -0,0 +1,166 @@
+/*
+ * 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.annotation.SuppressLint;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.support.v4.os.UserManagerCompat;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
+import com.android.dialer.common.concurrent.Annotations.Ui;
+import com.android.dialer.common.database.Selection;
+import com.android.dialer.inject.ApplicationContext;
+import com.android.dialer.notification.missedcalls.MissedCallNotificationCanceller;
+import com.android.dialer.util.PermissionsUtil;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.Collection;
+import javax.inject.Inject;
+
+/**
+ * Clears missed calls. This includes cancelling notifications and updating the "NEW" status in the
+ * system call log.
+ */
+public final class ClearMissedCalls {
+
+  private final Context appContext;
+  private final ListeningExecutorService backgroundExecutor;
+  private final ListeningExecutorService uiThreadExecutor;
+
+  @Inject
+  ClearMissedCalls(
+      @ApplicationContext Context appContext,
+      @BackgroundExecutor ListeningExecutorService backgroundExecutor,
+      @Ui ListeningExecutorService uiThreadExecutor) {
+    this.appContext = appContext;
+    this.backgroundExecutor = backgroundExecutor;
+    this.uiThreadExecutor = uiThreadExecutor;
+  }
+
+  /**
+   * Cancels all missed call notifications and marks all "new" missed calls in the system call log
+   * as "not new".
+   */
+  public ListenableFuture<Void> clearAll() {
+    ListenableFuture<Void> markNewFuture = markNotNew(ImmutableSet.of());
+    ListenableFuture<Void> cancelNotificationsFuture =
+        uiThreadExecutor.submit(
+            () -> {
+              MissedCallNotificationCanceller.cancelAll(appContext);
+              return null;
+            });
+
+    // Note on this usage of whenAllComplete:
+    //   -The returned future completes when all sub-futures complete (whether they fail or not)
+    //   -The returned future fails if any sub-future fails
+    return Futures.whenAllComplete(markNewFuture, cancelNotificationsFuture)
+        .call(
+            () -> {
+              // Calling get() is necessary to propagate failures.
+              markNewFuture.get();
+              cancelNotificationsFuture.get();
+              return null;
+            },
+            MoreExecutors.directExecutor());
+  }
+
+  /**
+   * For the provided set of IDs from the system call log, cancels their missed call notifications
+   * and marks them "not new".
+   *
+   * @param ids IDs from the system call log (see {@link Calls#_ID}}.
+   */
+  public ListenableFuture<Void> clearBySystemCallLogId(Collection<Long> ids) {
+    ListenableFuture<Void> markNewFuture = markNotNew(ids);
+    ListenableFuture<Void> cancelNotificationsFuture =
+        uiThreadExecutor.submit(
+            () -> {
+              for (long id : ids) {
+                Uri callUri = Calls.CONTENT_URI.buildUpon().appendPath(Long.toString(id)).build();
+                MissedCallNotificationCanceller.cancelSingle(appContext, callUri);
+              }
+              return null;
+            });
+
+    // Note on this usage of whenAllComplete:
+    //   -The returned future completes when all sub-futures complete (whether they fail or not)
+    //   -The returned future fails if any sub-future fails
+    return Futures.whenAllComplete(markNewFuture, cancelNotificationsFuture)
+        .call(
+            () -> {
+              // Calling get() is necessary to propagate failures.
+              markNewFuture.get();
+              cancelNotificationsFuture.get();
+              return null;
+            },
+            MoreExecutors.directExecutor());
+  }
+
+  /**
+   * Marks all provided system call log IDs as not new, or if the provided collection is empty,
+   * marks all calls as not new.
+   */
+  @SuppressLint("MissingPermission")
+  private ListenableFuture<Void> markNotNew(Collection<Long> ids) {
+    return backgroundExecutor.submit(
+        () -> {
+          if (!UserManagerCompat.isUserUnlocked(appContext)) {
+            LogUtil.e("ClearMissedCalls.markNotNew", "locked");
+            return null;
+          }
+          if (!PermissionsUtil.hasCallLogWritePermissions(appContext)) {
+            LogUtil.e("ClearMissedCalls.markNotNew", "no permission");
+            return null;
+          }
+
+          ContentValues values = new ContentValues();
+          values.put(Calls.NEW, 0);
+
+          Selection.Builder selectionBuilder =
+              Selection.builder()
+                  .and(Selection.column(Calls.NEW).is("=", 1))
+                  .and(Selection.column(Calls.TYPE).is("=", Calls.MISSED_TYPE));
+          if (!ids.isEmpty()) {
+            selectionBuilder.and(Selection.column(Calls._ID).in(toStrings(ids)));
+          }
+          Selection selection = selectionBuilder.build();
+          appContext
+              .getContentResolver()
+              .update(
+                  Calls.CONTENT_URI,
+                  values,
+                  selection.getSelection(),
+                  selection.getSelectionArgs());
+          return null;
+        });
+  }
+
+  private static String[] toStrings(Collection<Long> longs) {
+    String[] strings = new String[longs.size()];
+    int i = 0;
+    for (long value : longs) {
+      strings[i++] = Long.toString(value);
+    }
+    return strings;
+  }
+}
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
index 10f75ef..5e676f0 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
@@ -18,6 +18,7 @@
 import android.database.Cursor;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.Loader;
@@ -31,10 +32,14 @@
 import com.android.dialer.calllog.CallLogFramework.CallLogUi;
 import com.android.dialer.calllog.RefreshAnnotatedCallLogWorker;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.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
@@ -46,13 +51,19 @@
    * 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 static final long REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS = 100L;
+
+  @VisibleForTesting
+  static final long MARK_ALL_CALLS_READ_WAIT_MILLIS = TimeUnit.SECONDS.toMillis(3);
 
   private RefreshAnnotatedCallLogWorker refreshAnnotatedCallLogWorker;
   private UiListener<Void> refreshAnnotatedCallLogListener;
   private RecyclerView recyclerView;
   @Nullable private Runnable refreshAnnotatedCallLogRunnable;
 
+  private boolean shouldMarkCallsRead = false;
+  private final Runnable setShouldMarkCallsReadTrue = () -> shouldMarkCallsRead = true;
+
   public NewCallLogFragment() {
     LogUtil.enterBlock("NewCallLogFragment.NewCallLogFragment");
   }
@@ -103,6 +114,13 @@
       ((NewCallLogAdapter) recyclerView.getAdapter()).clearCache();
       recyclerView.getAdapter().notifyDataSetChanged();
     }
+
+    // We shouldn't mark the calls as read immediately when the 3 second timer expires because we
+    // don't want to disrupt the UI; instead we set a bit indicating to mark them read when the user
+    // leaves the fragment (in onPause).
+    shouldMarkCallsRead = false;
+    ThreadUtil.getUiThreadHandler()
+        .postDelayed(setShouldMarkCallsReadTrue, MARK_ALL_CALLS_READ_WAIT_MILLIS);
   }
 
   @Override
@@ -113,9 +131,17 @@
 
     // 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();
+
+    if (shouldMarkCallsRead) {
+      Futures.addCallback(
+          CallLogComponent.get(getContext()).getClearMissedCalls().clearAll(),
+          new DefaultFutureCallback<>(),
+          MoreExecutors.directExecutor());
+    }
   }
 
   @Override
@@ -159,7 +185,8 @@
                 throw new RuntimeException(throwable);
               });
         };
-    ThreadUtil.getUiThreadHandler().postDelayed(refreshAnnotatedCallLogRunnable, WAIT_MILLIS);
+    ThreadUtil.getUiThreadHandler()
+        .postDelayed(refreshAnnotatedCallLogRunnable, REFRESH_ANNOTATED_CALL_LOG_WAIT_MILLIS);
   }
 
   @Override
diff --git a/java/com/android/dialer/calllog/ui/menu/Modules.java b/java/com/android/dialer/calllog/ui/menu/Modules.java
index d591558..c85a9fd 100644
--- a/java/com/android/dialer/calllog/ui/menu/Modules.java
+++ b/java/com/android/dialer/calllog/ui/menu/Modules.java
@@ -52,15 +52,8 @@
 
     if (canPlaceCalls) {
       addModuleForVideoOrAudioCall(context, modules, row, normalizedNumber);
-
-      SharedModules.maybeAddModuleForAddingToContacts(
-          context,
-          modules,
-          row.number(),
-          row.numberAttributes().getName(),
-          row.numberAttributes().getLookupUri());
-
-      SharedModules.maybeAddModuleForSendingTextMessage(context, modules, normalizedNumber);
+      SharedModules.maybeAddModuleForSendingTextMessage(
+          context, modules, normalizedNumber, row.numberAttributes().getIsBlocked());
     }
 
     if (!modules.isEmpty()) {
@@ -68,10 +61,23 @@
     }
 
 
-    // TODO(zachh): Module for blocking/unblocking spam.
     // TODO(zachh): Module for CallComposer.
 
     if (canPlaceCalls) {
+      SharedModules.maybeAddModuleForAddingToContacts(
+          context,
+          modules,
+          row.number(),
+          row.numberAttributes().getName(),
+          row.numberAttributes().getLookupUri(),
+          row.numberAttributes().getIsBlocked(),
+          row.numberAttributes().getIsSpam());
+      SharedModules.addModulesHandlingBlockedOrSpamNumber(
+          context,
+          modules,
+          normalizedNumber,
+          row.numberAttributes().getIsBlocked(),
+          row.numberAttributes().getIsSpam());
       SharedModules.maybeAddModuleForCopyingNumber(context, modules, normalizedNumber);
     }
 
@@ -89,10 +95,23 @@
       List<ContactActionModule> modules,
       CoalescedRow row,
       String normalizedNumber) {
+    // If a number is blocked, skip this menu item.
+    if (row.numberAttributes().getIsBlocked()) {
+      return;
+    }
+
     PhoneAccountHandle phoneAccountHandle =
         TelecomUtil.composePhoneAccountHandle(
             row.phoneAccountComponentName(), row.phoneAccountId());
 
+    // For a spam number, only audio calls are allowed.
+    if (row.numberAttributes().getIsSpam()) {
+      modules.add(
+          IntentModule.newCallModule(
+              context, normalizedNumber, phoneAccountHandle, CallInitiationType.Type.CALL_LOG));
+      return;
+    }
+
     if ((row.features() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
       // Add an audio call item for video calls. Clicking the top entry on the bottom sheet will
       // trigger a video call.
diff --git a/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java b/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java
index 81c0513..02724e6 100644
--- a/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java
+++ b/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java
@@ -17,10 +17,15 @@
 package com.android.dialer.calllog.ui.menu;
 
 import android.content.Context;
+import android.provider.CallLog.Calls;
 import android.view.View;
+import com.android.dialer.calllog.CallLogComponent;
 import com.android.dialer.calllog.model.CoalescedRow;
+import com.android.dialer.common.concurrent.DefaultFutureCallback;
 import com.android.dialer.contactactions.ContactActionBottomSheet;
 import com.android.dialer.glidephotomanager.GlidePhotoManager;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
 
 /** Handles configuration of the bottom sheet menus for call log entries. */
 public final class NewCallLogMenu {
@@ -28,11 +33,23 @@
   /** Creates and returns the OnClickListener which opens the menu for the provided row. */
   public static View.OnClickListener createOnClickListener(
       Context context, CoalescedRow row, GlidePhotoManager glidePhotoManager) {
-    return (view) ->
-        ContactActionBottomSheet.show(
-            context,
-            PrimaryAction.fromRow(context, row),
-            Modules.fromRow(context, row),
-            glidePhotoManager);
+    return view -> {
+      ContactActionBottomSheet.show(
+          context,
+          PrimaryAction.fromRow(context, row),
+          Modules.fromRow(context, row),
+          glidePhotoManager);
+
+      // If the user opens the bottom sheet for a new call, clear the notifications and make the row
+      // not bold immediately. To do this, mark all of the calls in group as not new.
+      if (row.isNew() && row.callType() == Calls.MISSED_TYPE) {
+        Futures.addCallback(
+            CallLogComponent.get(context)
+                .getClearMissedCalls()
+                .clearBySystemCallLogId(row.coalescedIds().getCoalescedIdList()),
+            new DefaultFutureCallback<>(),
+            MoreExecutors.directExecutor());
+      }
+    };
   }
 }
diff --git a/java/com/android/dialer/common/concurrent/DefaultFutureCallback.java b/java/com/android/dialer/common/concurrent/DefaultFutureCallback.java
new file mode 100644
index 0000000..93ad0fa
--- /dev/null
+++ b/java/com/android/dialer/common/concurrent/DefaultFutureCallback.java
@@ -0,0 +1,44 @@
+/*
+ * 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.common.concurrent;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.MoreExecutors;
+
+/**
+ * Returns a {@link FutureCallback} which does nothing on success and crashes the application on
+ * failure.
+ *
+ * <p>You generally shouldn't use this for futures which should be tied to UI, for those use {@link
+ * UiListener}.
+ *
+ * <p>Can be safely used with {@link MoreExecutors#directExecutor()}
+ */
+public final class DefaultFutureCallback<T> implements FutureCallback<T> {
+
+  @Override
+  public void onSuccess(T unused) {}
+
+  @Override
+  public void onFailure(Throwable throwable) {
+    ThreadUtil.getUiThreadHandler()
+        .post(
+            () -> {
+              throw new RuntimeException(throwable);
+            });
+  }
+}
diff --git a/java/com/android/dialer/common/concurrent/UiThreadExecutor.java b/java/com/android/dialer/common/concurrent/UiThreadExecutor.java
index ec51ed3..8378d69 100644
--- a/java/com/android/dialer/common/concurrent/UiThreadExecutor.java
+++ b/java/com/android/dialer/common/concurrent/UiThreadExecutor.java
@@ -31,7 +31,7 @@
 public class UiThreadExecutor extends AbstractListeningExecutorService {
 
   @Inject
-  UiThreadExecutor() {}
+  public UiThreadExecutor() {}
 
   @Override
   public void shutdown() {
diff --git a/java/com/android/dialer/contactactions/IntentModule.java b/java/com/android/dialer/contactactions/IntentModule.java
index aa7fd25..9a345c6 100644
--- a/java/com/android/dialer/contactactions/IntentModule.java
+++ b/java/com/android/dialer/contactactions/IntentModule.java
@@ -72,7 +72,7 @@
             context,
             new CallIntentBuilder(number, initiationType)
                 .setPhoneAccountHandle(phoneAccountHandle)),
-        R.string.call,
+        R.string.voice_call,
         R.drawable.quantum_ic_call_white_24);
   }
 
diff --git a/java/com/android/dialer/contactactions/SharedModules.java b/java/com/android/dialer/contactactions/SharedModules.java
index 7e72863..6d97fcb 100644
--- a/java/com/android/dialer/contactactions/SharedModules.java
+++ b/java/com/android/dialer/contactactions/SharedModules.java
@@ -20,14 +20,15 @@
 import android.content.Intent;
 import android.net.Uri;
 import android.provider.ContactsContract;
-import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
+import android.widget.Toast;
 import com.android.dialer.DialerPhoneNumber;
 import com.android.dialer.clipboard.ClipboardUtils;
 import com.android.dialer.util.IntentUtil;
 import com.android.dialer.util.UriUtils;
 import java.util.List;
+import java.util.Locale;
 
 /**
  * Modules for the bottom sheet that are shared between NewVoicemailFragment and NewCallLogFragment
@@ -37,10 +38,15 @@
   public static void maybeAddModuleForAddingToContacts(
       Context context,
       List<ContactActionModule> modules,
-      @NonNull DialerPhoneNumber number,
-      @Nullable String name,
-      @Nullable String lookupUri) {
-    // TODO(zachh): Only show this for non-spam/blocked numbers.
+      DialerPhoneNumber number,
+      String name,
+      String lookupUri,
+      boolean isBlocked,
+      boolean isSpam) {
+    // Skip showing the menu item for a spam/blocked number.
+    if (isBlocked || isSpam) {
+      return;
+    }
 
     // Skip showing the menu item for existing contacts.
     if (isExistingContact(lookupUri)) {
@@ -83,22 +89,148 @@
   }
 
   public static void maybeAddModuleForSendingTextMessage(
-      Context context, List<ContactActionModule> modules, String originalNumber) {
+      Context context,
+      List<ContactActionModule> modules,
+      String normalizedNumber,
+      boolean isBlocked) {
+    // Don't show the option to send a text message if the number is blocked.
+    if (isBlocked) {
+      return;
+    }
+
     // TODO(zachh): There are some conditions where this module should not be shown; consider
-    // voicemail, business numbers, blocked numbers, spam numbers, etc.
-    if (!TextUtils.isEmpty(originalNumber)) {
+    // voicemail, business numbers, etc.
+
+    if (!TextUtils.isEmpty(normalizedNumber)) {
       modules.add(
           new IntentModule(
               context,
-              IntentUtil.getSendSmsIntent(originalNumber),
+              IntentUtil.getSendSmsIntent(normalizedNumber),
               R.string.send_a_message,
               R.drawable.quantum_ic_message_vd_theme_24));
     }
   }
 
+  public static void addModulesHandlingBlockedOrSpamNumber(
+      Context context,
+      List<ContactActionModule> modules,
+      String normalizedNumber,
+      boolean isBlocked,
+      boolean isSpam) {
+    // For a spam number, add two options:
+    // (1) "Not spam" and "Block", or
+    // (2) "Not spam" and "Unblock".
+    if (isSpam) {
+      addModuleForMarkingNumberAsNonSpam(context, modules, normalizedNumber);
+      addModuleForBlockingOrUnblockingNumber(context, modules, normalizedNumber, isBlocked);
+      return;
+    }
+
+    // For a blocked non-spam number, add "Unblock" option.
+    if (isBlocked) {
+      addModuleForBlockingOrUnblockingNumber(context, modules, normalizedNumber, isBlocked);
+      return;
+    }
+
+    // For a number that is neither a spam number nor blocked, add "Block/Report spam" option.
+    addModuleForBlockingNumberAndOptionallyReportingSpam(context, modules, normalizedNumber);
+  }
+
+  private static void addModuleForMarkingNumberAsNonSpam(
+      Context context, List<ContactActionModule> modules, String normalizedNumber) {
+    modules.add(
+        new ContactActionModule() {
+          @Override
+          public int getStringId() {
+            return R.string.not_spam;
+          }
+
+          @Override
+          public int getDrawableId() {
+            return R.drawable.quantum_ic_report_off_vd_theme_24;
+          }
+
+          @Override
+          public boolean onClick() {
+            // TODO(a bug): implement this method.
+            Toast.makeText(
+                    context,
+                    String.format(Locale.ENGLISH, "TODO: Report %s as non-spam", normalizedNumber),
+                    Toast.LENGTH_SHORT)
+                .show();
+            return true; // Close the bottom sheet.
+          }
+        });
+  }
+
+  private static void addModuleForBlockingOrUnblockingNumber(
+      Context context,
+      List<ContactActionModule> modules,
+      String normalizedNumber,
+      boolean isBlocked) {
+    modules.add(
+        new ContactActionModule() {
+          @Override
+          public int getStringId() {
+            return isBlocked ? R.string.unblock_number : R.string.block_number;
+          }
+
+          @Override
+          public int getDrawableId() {
+            return isBlocked
+                ? R.drawable.ic_unblock // TODO(a bug): use a vector icon
+                : R.drawable.quantum_ic_block_vd_theme_24;
+          }
+
+          @Override
+          public boolean onClick() {
+            // TODO(a bug): implement this method.
+            Toast.makeText(
+                    context,
+                    String.format(
+                        Locale.ENGLISH,
+                        "TODO: " + (isBlocked ? "Unblock " : "Block ") + " number %s.",
+                        normalizedNumber),
+                    Toast.LENGTH_SHORT)
+                .show();
+            return true; // Close the bottom sheet.
+          }
+        });
+  }
+
+  private static void addModuleForBlockingNumberAndOptionallyReportingSpam(
+      Context context, List<ContactActionModule> modules, String normalizedNumber) {
+    modules.add(
+        new ContactActionModule() {
+          @Override
+          public int getStringId() {
+            return R.string.block_and_optionally_report_spam;
+          }
+
+          @Override
+          public int getDrawableId() {
+            return R.drawable.quantum_ic_block_vd_theme_24;
+          }
+
+          @Override
+          public boolean onClick() {
+            // TODO(a bug): implement this method.
+            Toast.makeText(
+                    context,
+                    String.format(
+                        Locale.ENGLISH,
+                        "TODO: Block and optionally report as spam %s.",
+                        normalizedNumber),
+                    Toast.LENGTH_SHORT)
+                .show();
+            return true; // Close the bottom sheet.
+          }
+        });
+  }
+
   public static void maybeAddModuleForCopyingNumber(
-      Context context, List<ContactActionModule> modules, String originalNumber) {
-    if (TextUtils.isEmpty(originalNumber)) {
+      Context context, List<ContactActionModule> modules, String normalizedNumber) {
+    if (TextUtils.isEmpty(normalizedNumber)) {
       return;
     }
     modules.add(
@@ -115,7 +247,7 @@
 
           @Override
           public boolean onClick() {
-            ClipboardUtils.copyText(context, null, originalNumber, true);
+            ClipboardUtils.copyText(context, null, normalizedNumber, true);
             return false;
           }
         });
diff --git a/java/com/android/dialer/contactactions/res/drawable-xxxhdpi/ic_unblock.png b/java/com/android/dialer/contactactions/res/drawable-xxxhdpi/ic_unblock.png
new file mode 100644
index 0000000..01551e2
--- /dev/null
+++ b/java/com/android/dialer/contactactions/res/drawable-xxxhdpi/ic_unblock.png
Binary files differ
diff --git a/java/com/android/dialer/contactactions/res/values/strings.xml b/java/com/android/dialer/contactactions/res/values/strings.xml
index 0e953a5..4d598a9 100644
--- a/java/com/android/dialer/contactactions/res/values/strings.xml
+++ b/java/com/android/dialer/contactactions/res/values/strings.xml
@@ -16,13 +16,31 @@
   -->
 
 <resources>
+  <!-- Option shown in call log/voicemail menu to make a voice call [CHAR LIMIT=30] -->
+  <string name="voice_call">Voice call</string>
+
+  <!-- Option shown in a call log/voicemail menu to make a video call [CHAR LIMIT=30] -->
+  <string name="video_call">Video call</string>
+
   <!-- Option shown in call log menu/voicemail to add the phone number from an entry to an existing contact
        (also provides option to create a new contact from the number). [CHAR LIMIT=30] -->
-  <string name="add_to_contacts">Add to contacts</string>
+  <string name="add_to_contacts">Add contact</string>
 
   <!-- Options shown in call log/voicemail menu to send a SMS to the number represented by the call log/voicemailentry.
      [CHAR LIMIT=30] -->
-  <string name="send_a_message">Send a message</string>
+  <string name="send_a_message">Message</string>
+
+  <!-- Options shown in call log/voicemail menu to mark a number as non-spam. [CHAR LIMIT=30] -->
+  <string name="not_spam">Not spam</string>
+
+  <!-- Options shown in a call log/voicemail menu to block a number. [CHAR LIMIT=30] -->
+  <string name="block_number">Block</string>
+
+  <!-- Options shown in a call log/voicemail menu to unblock a number. [CHAR LIMIT=30] -->
+  <string name="unblock_number">Unblock</string>
+
+  <!-- Options shown in a call log/voicemail menu to block a number and/or report it as spam. [CHAR LIMIT=30] -->
+  <string name="block_and_optionally_report_spam">Block/Report spam</string>
 
   <!-- Option displayed in call log/voicemail menu to copy phone number. [CHAR LIMIT=30] -->
   <string name="copy_number">Copy number</string>
diff --git a/java/com/android/dialer/main/impl/NewMainActivityPeer.java b/java/com/android/dialer/main/impl/NewMainActivityPeer.java
index 7896489..0a85667 100644
--- a/java/com/android/dialer/main/impl/NewMainActivityPeer.java
+++ b/java/com/android/dialer/main/impl/NewMainActivityPeer.java
@@ -16,16 +16,22 @@
 
 package com.android.dialer.main.impl;
 
+import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
+import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.FragmentTransaction;
+import com.android.dialer.calllog.CallLogComponent;
 import com.android.dialer.calllog.ui.NewCallLogFragment;
+import com.android.dialer.common.concurrent.DefaultFutureCallback;
 import com.android.dialer.main.MainActivityPeer;
 import com.android.dialer.main.impl.bottomnav.BottomNavBar;
 import com.android.dialer.main.impl.bottomnav.BottomNavBar.OnBottomNavTabSelectedListener;
 import com.android.dialer.main.impl.bottomnav.BottomNavBar.TabIndex;
 import com.android.dialer.voicemail.listui.NewVoicemailFragment;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
 
 /** MainActivityPeer that implements the new fragments. */
 public class NewMainActivityPeer implements MainActivityPeer {
@@ -40,7 +46,8 @@
   public void onActivityCreate(Bundle saveInstanceState) {
     mainActivity.setContentView(R.layout.main_activity);
     MainBottomNavBarBottomNavTabListener bottomNavBarBottomNavTabListener =
-        new MainBottomNavBarBottomNavTabListener(mainActivity.getSupportFragmentManager());
+        new MainBottomNavBarBottomNavTabListener(
+            mainActivity.getSupportFragmentManager(), mainActivity.getApplicationContext());
     BottomNavBar bottomNav = mainActivity.findViewById(R.id.bottom_nav_bar);
     bottomNav.addOnTabSelectedListener(bottomNavBarBottomNavTabListener);
     bottomNav.selectTab(TabIndex.SPEED_DIAL);
@@ -77,9 +84,12 @@
     private static final String VOICEMAIL_TAG = "voicemail";
 
     private final FragmentManager supportFragmentManager;
+    private final Context appContext;
 
-    private MainBottomNavBarBottomNavTabListener(FragmentManager supportFragmentManager) {
+    private MainBottomNavBarBottomNavTabListener(
+        FragmentManager supportFragmentManager, Context appContext) {
       this.supportFragmentManager = supportFragmentManager;
+      this.appContext = appContext;
     }
 
     @Override
@@ -126,8 +136,18 @@
 
     private void hideAllFragments() {
       FragmentTransaction supportTransaction = supportFragmentManager.beginTransaction();
-      if (supportFragmentManager.findFragmentByTag(CALL_LOG_TAG) != null) {
-        supportTransaction.hide(supportFragmentManager.findFragmentByTag(CALL_LOG_TAG));
+      Fragment callLogFragment = supportFragmentManager.findFragmentByTag(CALL_LOG_TAG);
+      if (callLogFragment != null) {
+        if (callLogFragment.isVisible()) {
+          // If the user taps any bottom nav button and the call log is showing, immediately cancel
+          // missed calls (unbold them and clear their notifications).
+          Futures.addCallback(
+              // TODO(zachh): Use dagger to create Peer and MainBottomNavBarBottomNavTabListener.
+              CallLogComponent.get(appContext).getClearMissedCalls().clearAll(),
+              new DefaultFutureCallback<>(),
+              MoreExecutors.directExecutor());
+        }
+        supportTransaction.hide(callLogFragment);
       }
       if (supportFragmentManager.findFragmentByTag(VOICEMAIL_TAG) != null) {
         supportTransaction.hide(supportFragmentManager.findFragmentByTag(VOICEMAIL_TAG));
diff --git a/java/com/android/dialer/main/impl/bottomnav/BottomNavBar.java b/java/com/android/dialer/main/impl/bottomnav/BottomNavBar.java
index a580aee..2945e39 100644
--- a/java/com/android/dialer/main/impl/bottomnav/BottomNavBar.java
+++ b/java/com/android/dialer/main/impl/bottomnav/BottomNavBar.java
@@ -162,6 +162,7 @@
     }
   }
 
+  @TabIndex
   public int getSelectedTab() {
     return selectedTab;
   }
diff --git a/java/com/android/dialer/notification/missedcalls/MissedCallConstants.java b/java/com/android/dialer/notification/missedcalls/MissedCallConstants.java
new file mode 100644
index 0000000..8553ea5
--- /dev/null
+++ b/java/com/android/dialer/notification/missedcalls/MissedCallConstants.java
@@ -0,0 +1,36 @@
+/*
+ * 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.notification.missedcalls;
+
+/** Constants related to missed call notifications. */
+public final class MissedCallConstants {
+
+  /** Prefix used to generate a unique tag for each missed call notification. */
+  public static final String NOTIFICATION_TAG_PREFIX = "MissedCall_";
+
+  /** Common ID for all missed call notifications. */
+  public static final int NOTIFICATION_ID = 1;
+
+  /** Tag for the group summary notification. */
+  public static final String GROUP_SUMMARY_NOTIFICATION_TAG = "GroupSummary_MissedCall";
+
+  /**
+   * Key used to associate all missed call notifications and the summary as belonging to a single
+   * group.
+   */
+  public static final String GROUP_KEY = "MissedCallGroup";
+}
diff --git a/java/com/android/dialer/notification/missedcalls/MissedCallNotificationCanceller.java b/java/com/android/dialer/notification/missedcalls/MissedCallNotificationCanceller.java
new file mode 100644
index 0000000..8798c21
--- /dev/null
+++ b/java/com/android/dialer/notification/missedcalls/MissedCallNotificationCanceller.java
@@ -0,0 +1,48 @@
+/*
+ * 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.notification.missedcalls;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.notification.DialerNotificationManager;
+import com.android.dialer.notification.NotificationManagerUtils;
+
+/** Cancels missed calls notifications. */
+public final class MissedCallNotificationCanceller {
+
+  /** Cancels all missed call notifications. */
+  public static void cancelAll(@NonNull Context context) {
+    NotificationManagerUtils.cancelAllInGroup(context, MissedCallConstants.GROUP_KEY);
+  }
+
+  /** Cancels a missed call notification for a single call. */
+  public static void cancelSingle(@NonNull Context context, @Nullable Uri callUri) {
+    if (callUri == null) {
+      LogUtil.e(
+          "MissedCallNotificationCanceller.cancelSingle",
+          "unable to cancel notification, uri is null");
+      return;
+    }
+    // This will also dismiss the group summary if there are no more missed call notifications.
+    DialerNotificationManager.cancel(
+        context,
+        MissedCallNotificationTags.getNotificationTagForCallUri(callUri),
+        MissedCallConstants.NOTIFICATION_ID);
+  }
+}
diff --git a/java/com/android/dialer/notification/missedcalls/MissedCallNotificationTags.java b/java/com/android/dialer/notification/missedcalls/MissedCallNotificationTags.java
new file mode 100644
index 0000000..64f28ee
--- /dev/null
+++ b/java/com/android/dialer/notification/missedcalls/MissedCallNotificationTags.java
@@ -0,0 +1,29 @@
+/*
+ * 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.notification.missedcalls;
+
+import android.net.Uri;
+import android.support.annotation.NonNull;
+
+/** Static methods related to missed call notification tags. */
+public final class MissedCallNotificationTags {
+
+  /** Gets the notification tag for a single call. */
+  public static String getNotificationTagForCallUri(@NonNull Uri callUri) {
+    return MissedCallConstants.NOTIFICATION_TAG_PREFIX + callUri;
+  }
+}
diff --git a/java/com/android/dialer/searchfragment/common/res/layout/search_contact_row.xml b/java/com/android/dialer/searchfragment/common/res/layout/search_contact_row.xml
index 407207a..9be7fa0 100644
--- a/java/com/android/dialer/searchfragment/common/res/layout/search_contact_row.xml
+++ b/java/com/android/dialer/searchfragment/common/res/layout/search_contact_row.xml
@@ -65,4 +65,15 @@
       android:tint="@color/dialer_secondary_text_color"
       android:visibility="gone"
       android:scaleType="center"/>
+
+  <ImageView
+      android:id="@+id/work_icon"
+      android:layout_width="@dimen/search_row_height"
+      android:layout_height="@dimen/search_row_height"
+      android:layout_alignParentEnd="true"
+      android:layout_centerVertical="true"
+      android:padding="@dimen/call_to_action_padding"
+      android:src="@drawable/ic_work_profile"
+      android:scaleType="centerInside"
+      android:visibility="gone"/>
 </RelativeLayout>
\ No newline at end of file
diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java b/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java
index 9d36900..4be96fe 100644
--- a/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java
+++ b/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java
@@ -26,8 +26,10 @@
 import android.support.v7.widget.RecyclerView;
 import android.text.TextUtils;
 import android.view.View;
+import android.widget.ImageView;
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
+import com.android.contacts.common.compat.DirectoryCompat;
 import com.android.dialer.callintent.CallInitiationType;
 import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.contactphoto.ContactPhotoManager;
@@ -46,6 +48,7 @@
   private final TextView nameView;
   private final TextView numberView;
   private final QuickContactBadge photo;
+  private final ImageView workBadge;
 
   private String number;
 
@@ -55,6 +58,7 @@
     photo = view.findViewById(R.id.photo);
     nameView = view.findViewById(R.id.primary);
     numberView = view.findViewById(R.id.secondary);
+    workBadge = view.findViewById(R.id.work_icon);
     context = view.getContext();
   }
 
@@ -74,6 +78,10 @@
 
     nameView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name, context));
     numberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, secondaryInfo, context));
+    workBadge.setVisibility(
+        DirectoryCompat.isOnlyEnterpriseDirectoryId(cursor.getDirectoryId())
+            ? View.VISIBLE
+            : View.GONE);
 
     if (shouldShowPhoto(cursor)) {
       nameView.setVisibility(View.VISIBLE);
diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java
index 9510443..653c670 100644
--- a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java
+++ b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java
@@ -22,6 +22,7 @@
 import android.database.MergeCursor;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
+import com.android.contacts.common.compat.DirectoryCompat;
 import com.android.dialer.common.Assert;
 import com.android.dialer.searchfragment.common.SearchCursor;
 import com.android.dialer.searchfragment.directories.DirectoriesCursorLoader;
@@ -101,7 +102,12 @@
 
   private static MatrixCursor createHeaderCursor(Context context, String name, long id) {
     MatrixCursor headerCursor = new MatrixCursor(PROJECTION, 1);
-    headerCursor.addRow(new Object[] {context.getString(R.string.directory, name), id});
+    if (DirectoryCompat.isOnlyEnterpriseDirectoryId(id)) {
+      headerCursor.addRow(
+          new Object[] {context.getString(R.string.directory_search_label_work), id});
+    } else {
+      headerCursor.addRow(new Object[] {context.getString(R.string.directory, name), id});
+    }
     return headerCursor;
   }
 
diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
index 9feeb7e..cf495e4 100644
--- a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
+++ b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
@@ -27,6 +27,7 @@
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.support.annotation.NonNull;
 import android.support.annotation.VisibleForTesting;
+import com.android.contacts.common.compat.DirectoryCompat;
 import com.android.dialer.searchfragment.common.Projections;
 import com.android.dialer.searchfragment.directories.DirectoriesCursorLoader.Directory;
 import java.util.ArrayList;
@@ -71,7 +72,14 @@
       Directory directory = directories.get(i);
 
       // Filter out local directories
-      if (!isRemoteDirectory(directory.getId())) {
+      if (!DirectoryCompat.isRemoteDirectoryId(directory.getId())
+          && !DirectoryCompat.isEnterpriseDirectoryId(directory.getId())) {
+        cursors[i] = null;
+        continue;
+      }
+
+      // Filter out invisible directories
+      if (DirectoryCompat.isInvisibleDirectory(directory.getId())) {
         cursors[i] = null;
         continue;
       }
@@ -93,17 +101,6 @@
     return RemoteContactsCursor.newInstance(getContext(), cursors, directories);
   }
 
-  private static boolean isRemoteDirectory(long directoryId) {
-    return VERSION.SDK_INT >= VERSION_CODES.N
-        ? ContactsContract.Directory.isRemoteDirectoryId(directoryId)
-        : (directoryId != ContactsContract.Directory.DEFAULT
-            && directoryId != ContactsContract.Directory.LOCAL_INVISIBLE
-            // Directory.ENTERPRISE_DEFAULT is the default work profile directory for locally stored
-            // contacts
-            && directoryId != ContactsContract.Directory.ENTERPRISE_DEFAULT
-            && directoryId != ContactsContract.Directory.ENTERPRISE_LOCAL_INVISIBLE);
-  }
-
   private MatrixCursor createMatrixCursorFilteringNullNumbers(Cursor cursor) {
     if (cursor == null) {
       return null;
diff --git a/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java
index 00899fd..f28393c 100644
--- a/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java
+++ b/java/com/android/dialer/simulator/impl/SimulatorSimCallManager.java
@@ -29,6 +29,7 @@
 import android.telephony.TelephonyManager;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.strictmode.StrictModeUtils;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Random;
@@ -53,17 +54,23 @@
   static void register(@NonNull Context context) {
     LogUtil.enterBlock("SimulatorSimCallManager.register");
     Assert.isNotNull(context);
-    TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
-    telecomManager.registerPhoneAccount(buildSimCallManagerAccount(context));
-    telecomManager.registerPhoneAccount(buildVideoProviderAccount(context));
+    StrictModeUtils.bypass(
+        () -> {
+          TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
+          telecomManager.registerPhoneAccount(buildSimCallManagerAccount(context));
+          telecomManager.registerPhoneAccount(buildVideoProviderAccount(context));
+        });
   }
 
   static void unregister(@NonNull Context context) {
     LogUtil.enterBlock("SimulatorSimCallManager.unregister");
     Assert.isNotNull(context);
-    TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
-    telecomManager.unregisterPhoneAccount(getSimCallManagerHandle(context));
-    telecomManager.unregisterPhoneAccount(getVideoProviderHandle(context));
+    StrictModeUtils.bypass(
+        () -> {
+          TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
+          telecomManager.unregisterPhoneAccount(getSimCallManagerHandle(context));
+          telecomManager.unregisterPhoneAccount(getVideoProviderHandle(context));
+        });
   }
 
   @NonNull
diff --git a/java/com/android/dialer/theme/res/values/strings.xml b/java/com/android/dialer/theme/res/values/strings.xml
index 74cabad..a14693f 100644
--- a/java/com/android/dialer/theme/res/values/strings.xml
+++ b/java/com/android/dialer/theme/res/values/strings.xml
@@ -30,9 +30,6 @@
        used in the Launcher icon. -->
   <string name="launcherActivityLabel">Phone</string>
 
-  <!-- text on a button, Video call, as in to place a video call. -->
-  <string name="video_call">Video call</string>
-
   <!-- Label shown on the 'positive' button for the dialog. Indicates that the call will proceed -->
   <string name="call">Call</string>
 </resources>
diff --git a/java/com/android/dialer/voicemail/listui/menu/Modules.java b/java/com/android/dialer/voicemail/listui/menu/Modules.java
index 665031a..7254ad6 100644
--- a/java/com/android/dialer/voicemail/listui/menu/Modules.java
+++ b/java/com/android/dialer/voicemail/listui/menu/Modules.java
@@ -41,16 +41,25 @@
         modules,
         voicemailEntry.number(),
         voicemailEntry.numberAttributes().getName(),
-        voicemailEntry.numberAttributes().getLookupUri());
+        voicemailEntry.numberAttributes().getLookupUri(),
+        voicemailEntry.numberAttributes().getIsBlocked(),
+        voicemailEntry.numberAttributes().getIsSpam());
 
     String normalizedNumber = voicemailEntry.number().getNormalizedNumber();
-    SharedModules.maybeAddModuleForSendingTextMessage(context, modules, normalizedNumber);
+    SharedModules.maybeAddModuleForSendingTextMessage(
+        context, modules, normalizedNumber, voicemailEntry.numberAttributes().getIsBlocked());
 
     if (!modules.isEmpty()) {
       modules.add(new DividerModule());
     }
 
-    // TODO(zachh): Module for blocking/unblocking spam.
+    SharedModules.addModulesHandlingBlockedOrSpamNumber(
+        context,
+        modules,
+        normalizedNumber,
+        voicemailEntry.numberAttributes().getIsBlocked(),
+        voicemailEntry.numberAttributes().getIsSpam());
+
     // TODO(zachh): Module for CallComposer.
     SharedModules.maybeAddModuleForCopyingNumber(context, modules, normalizedNumber);