Added ability to place RCS, Duo and IMS calls from new search fragment.

Bug: 37209462
Test: SearchAdapterTest + existing tests
PiperOrigin-RevId: 165210817
Change-Id: I9fb78cf7d964b97e6e95c01437780aa66405f019
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java
index 7e62065..a8b75bb 100644
--- a/java/com/android/dialer/app/DialtactsActivity.java
+++ b/java/com/android/dialer/app/DialtactsActivity.java
@@ -97,6 +97,7 @@
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.configprovider.ConfigProviderBindings;
+import com.android.dialer.constants.ActivityRequestCodes;
 import com.android.dialer.database.Database;
 import com.android.dialer.database.DialerDatabaseHelper;
 import com.android.dialer.interactions.PhoneNumberInteraction;
@@ -172,11 +173,6 @@
   /** Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}. */
   private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER";
 
-  private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1;
-  public static final int ACTIVITY_REQUEST_CODE_CALL_COMPOSE = 2;
-  public static final int ACTIVITY_REQUEST_CODE_LIGHTBRINGER = 3;
-  public static final int ACTIVITY_REQUEST_CODE_CALL_DETAILS = 4;
-
   private static final int FAB_SCALE_IN_DELAY_MS = 300;
 
   /**
@@ -723,7 +719,7 @@
       try {
         startActivityForResult(
             new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH),
-            ACTIVITY_REQUEST_CODE_VOICE_SEARCH);
+            ActivityRequestCodes.DIALTACTS_VOICE_SEARCH);
       } catch (ActivityNotFoundException e) {
         Toast.makeText(
                 DialtactsActivity.this, R.string.voice_search_not_available, Toast.LENGTH_SHORT)
@@ -769,7 +765,7 @@
         "requestCode:%d, resultCode:%d",
         requestCode,
         resultCode);
-    if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) {
+    if (requestCode == ActivityRequestCodes.DIALTACTS_VOICE_SEARCH) {
       if (resultCode == RESULT_OK) {
         final ArrayList<String> matches =
             data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
@@ -781,7 +777,7 @@
       } else {
         LogUtil.e("DialtactsActivity.onActivityResult", "voice search failed");
       }
-    } else if (requestCode == ACTIVITY_REQUEST_CODE_CALL_COMPOSE) {
+    } else if (requestCode == ActivityRequestCodes.DIALTACTS_CALL_COMPOSER) {
       if (resultCode == RESULT_FIRST_USER) {
         LogUtil.i(
             "DialtactsActivity.onActivityResult", "returned from call composer, error occurred");
@@ -793,7 +789,7 @@
       } else {
         LogUtil.i("DialtactsActivity.onActivityResult", "returned from call composer, no error");
       }
-    } else if (requestCode == ACTIVITY_REQUEST_CODE_CALL_DETAILS) {
+    } else if (requestCode == ActivityRequestCodes.DIALTACTS_CALL_DETAILS) {
       if (resultCode == RESULT_OK
           && data != null
           && data.getBooleanExtra(CallDetailsActivity.EXTRA_HAS_ENRICHED_CALL_DATA, false)) {
diff --git a/java/com/android/dialer/app/calllog/CallLogActivity.java b/java/com/android/dialer/app/calllog/CallLogActivity.java
index c9e655d..1bb894c 100644
--- a/java/com/android/dialer/app/calllog/CallLogActivity.java
+++ b/java/com/android/dialer/app/calllog/CallLogActivity.java
@@ -33,6 +33,7 @@
 import com.android.dialer.app.DialtactsActivity;
 import com.android.dialer.app.R;
 import com.android.dialer.calldetails.CallDetailsActivity;
+import com.android.dialer.constants.ActivityRequestCodes;
 import com.android.dialer.database.CallLogQueryHandler;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.logging.ScreenEvent;
@@ -234,7 +235,7 @@
 
   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-    if (requestCode == DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_DETAILS) {
+    if (requestCode == ActivityRequestCodes.DIALTACTS_CALL_DETAILS) {
       if (resultCode == RESULT_OK
           && data != null
           && data.getBooleanExtra(CallDetailsActivity.EXTRA_HAS_ENRICHED_CALL_DATA, false)) {
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
index f7ea63c..745f8b6 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -69,6 +69,7 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.CompatUtils;
 import com.android.dialer.configprovider.ConfigProviderBindings;
+import com.android.dialer.constants.ActivityRequestCodes;
 import com.android.dialer.contactphoto.ContactPhotoManager;
 import com.android.dialer.dialercontact.DialerContact;
 import com.android.dialer.dialercontact.SimDetails;
@@ -867,7 +868,7 @@
       Activity activity = (Activity) mContext;
       activity.startActivityForResult(
           CallComposerActivity.newIntent(activity, buildContact()),
-          DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_COMPOSE);
+          ActivityRequestCodes.DIALTACTS_CALL_COMPOSER);
     } else if (view.getId() == R.id.share_voicemail) {
       Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_PRESSED);
       mVoicemailPlaybackPresenter.shareVoicemail();
@@ -895,7 +896,7 @@
       } else if (CallDetailsActivity.isLaunchIntent(intent)) {
         PerformanceReport.recordClick(UiAction.Type.OPEN_CALL_DETAIL);
         ((Activity) mContext)
-            .startActivityForResult(intent, DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_DETAILS);
+            .startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_CALL_DETAILS);
       } else {
         if (Intent.ACTION_CALL.equals(intent.getAction())
             && intent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, -1)
@@ -911,7 +912,7 @@
   private void startLightbringerActivity(Intent intent) {
     try {
       Activity activity = (Activity) mContext;
-      activity.startActivityForResult(intent, DialtactsActivity.ACTIVITY_REQUEST_CODE_LIGHTBRINGER);
+      activity.startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_LIGHTBRINGER);
     } catch (ActivityNotFoundException e) {
       Toast.makeText(mContext, R.string.activity_not_available, Toast.LENGTH_SHORT).show();
     }
diff --git a/java/com/android/dialer/constants/ActivityRequestCodes.java b/java/com/android/dialer/constants/ActivityRequestCodes.java
new file mode 100644
index 0000000..da05eb7
--- /dev/null
+++ b/java/com/android/dialer/constants/ActivityRequestCodes.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.constants;
+
+/**
+ * Class containing {@link android.app.Activity#onActivityResult(int, int, android.content.Intent)}
+ * request codes.
+ */
+public final class ActivityRequestCodes {
+
+  private ActivityRequestCodes() {}
+
+  /** Request code for {@link android.speech.RecognizerIntent#ACTION_RECOGNIZE_SPEECH} intent. */
+  public static final int DIALTACTS_VOICE_SEARCH = 1;
+
+  /** Request code for {@link com.android.dialer.callcomposer.CallComposerActivity} intent. */
+  public static final int DIALTACTS_CALL_COMPOSER = 2;
+
+  /**
+   * Request code for {@link
+   * com.android.dialer.lightbringer.Lightbringer#getIntent(android.content.Context, String)}.
+   */
+  public static final int DIALTACTS_LIGHTBRINGER = 3;
+
+  /** Request code for {@link com.android.dialer.calldetails.CallDetailsActivity} intent. */
+  public static final int DIALTACTS_CALL_DETAILS = 4;
+}
diff --git a/java/com/android/dialer/searchfragment/common/Projections.java b/java/com/android/dialer/searchfragment/common/Projections.java
index 37e20d1..078c3e5 100644
--- a/java/com/android/dialer/searchfragment/common/Projections.java
+++ b/java/com/android/dialer/searchfragment/common/Projections.java
@@ -30,9 +30,10 @@
   public static final int PHONE_PHOTO_URI = 6;
   public static final int PHONE_LOOKUP_KEY = 7;
   public static final int PHONE_CARRIER_PRESENCE = 8;
+  public static final int PHONE_CONTACT_ID = 9;
 
   @SuppressWarnings("unused")
-  public static final int PHONE_SORT_KEY = 9;
+  public static final int PHONE_SORT_KEY = 10;
 
   public static final String[] PHONE_PROJECTION =
       new String[] {
@@ -45,6 +46,7 @@
         Phone.PHOTO_THUMBNAIL_URI, // 6
         Phone.LOOKUP_KEY, // 7
         Phone.CARRIER_PRESENCE, // 8
-        Phone.SORT_KEY_PRIMARY // 9
+        Phone.CONTACT_ID, // 9
+        Phone.SORT_KEY_PRIMARY // 10
       };
 }
diff --git a/java/com/android/dialer/searchfragment/common/RowClickListener.java b/java/com/android/dialer/searchfragment/common/RowClickListener.java
new file mode 100644
index 0000000..e82f3f7
--- /dev/null
+++ b/java/com/android/dialer/searchfragment/common/RowClickListener.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.searchfragment.common;
+
+import com.android.dialer.dialercontact.DialerContact;
+
+/** Interface of possible actions that can be performed by search elements. */
+public interface RowClickListener {
+
+  /**
+   * Places a traditional voice call.
+   *
+   * @param ranking position in the list relative to the other elements
+   */
+  void placeVoiceCall(String phoneNumber, int ranking);
+
+  /**
+   * Places an IMS video call.
+   *
+   * @param ranking position in the list relative to the other elements
+   */
+  void placeVideoCall(String phoneNumber, int ranking);
+
+  /** Places a Duo video call. */
+  void placeDuoCall(String phoneNumber);
+
+  /** Opens the enriched calling/call composer interface. */
+  void openCallAndShare(DialerContact dialerContact);
+}
diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java b/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java
index 1e8224d..327fe53 100644
--- a/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java
+++ b/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java
@@ -23,6 +23,7 @@
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.Contacts;
 import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView.ViewHolder;
 import android.text.TextUtils;
 import android.view.View;
@@ -30,16 +31,19 @@
 import android.widget.ImageView;
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
-import com.android.dialer.callintent.CallInitiationType.Type;
-import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.common.Assert;
 import com.android.dialer.contactphoto.ContactPhotoManager;
+import com.android.dialer.dialercontact.DialerContact;
+import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
 import com.android.dialer.lettertile.LetterTileDrawable;
+import com.android.dialer.lightbringer.LightbringerComponent;
 import com.android.dialer.searchfragment.common.Projections;
 import com.android.dialer.searchfragment.common.QueryBoldingUtil;
 import com.android.dialer.searchfragment.common.R;
+import com.android.dialer.searchfragment.common.RowClickListener;
 import com.android.dialer.searchfragment.common.SearchCursor;
-import com.android.dialer.telecom.TelecomUtil;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -48,24 +52,34 @@
 
   /** IntDef for the different types of actions that can be shown. */
   @Retention(RetentionPolicy.SOURCE)
-  @IntDef({CallToAction.NONE, CallToAction.VIDEO_CALL, CallToAction.SHARE_AND_CALL})
+  @IntDef({
+    CallToAction.NONE,
+    CallToAction.VIDEO_CALL,
+    CallToAction.DUO_CALL,
+    CallToAction.SHARE_AND_CALL
+  })
   @interface CallToAction {
     int NONE = 0;
     int VIDEO_CALL = 1;
-    int SHARE_AND_CALL = 2;
+    int DUO_CALL = 2;
+    int SHARE_AND_CALL = 3;
   }
 
+  private final RowClickListener listener;
   private final QuickContactBadge photo;
   private final TextView nameOrNumberView;
   private final TextView numberView;
   private final ImageView callToActionView;
   private final Context context;
 
+  private int position;
   private String number;
+  private DialerContact dialerContact;
   private @CallToAction int currentAction;
 
-  public SearchContactViewHolder(View view) {
+  public SearchContactViewHolder(View view, RowClickListener listener) {
     super(view);
+    this.listener = listener;
     view.setOnClickListener(this);
     photo = view.findViewById(R.id.photo);
     nameOrNumberView = view.findViewById(R.id.primary);
@@ -79,6 +93,8 @@
    * at the cursors set position.
    */
   public void bind(SearchCursor cursor, String query) {
+    dialerContact = getDialerContact(context, cursor);
+    position = cursor.getPosition();
     number = cursor.getString(Projections.PHONE_NUMBER);
     String name = cursor.getString(Projections.PHONE_DISPLAY_NAME);
     String label = getLabel(context.getResources(), cursor);
@@ -90,7 +106,7 @@
 
     nameOrNumberView.setText(QueryBoldingUtil.getNameWithQueryBolded(query, name));
     numberView.setText(QueryBoldingUtil.getNumberWithQueryBolded(query, secondaryInfo));
-    setCallToAction(cursor);
+    setCallToAction(cursor, query);
 
     if (shouldShowPhoto(cursor)) {
       nameOrNumberView.setVisibility(View.VISIBLE);
@@ -144,8 +160,8 @@
     return (String) Phone.getTypeLabel(resources, numberType, numberLabel);
   }
 
-  private void setCallToAction(Cursor cursor) {
-    currentAction = getCallToAction(cursor);
+  private void setCallToAction(SearchCursor cursor, String query) {
+    currentAction = getCallToAction(context, cursor, query);
     switch (currentAction) {
       case CallToAction.NONE:
         callToActionView.setVisibility(View.GONE);
@@ -157,6 +173,7 @@
             context.getDrawable(com.android.contacts.common.R.drawable.ic_phone_attach));
         callToActionView.setOnClickListener(this);
         break;
+      case CallToAction.DUO_CALL:
       case CallToAction.VIDEO_CALL:
         callToActionView.setVisibility(View.VISIBLE);
         callToActionView.setImageDrawable(
@@ -169,31 +186,69 @@
     }
   }
 
-  private static @CallToAction int getCallToAction(Cursor cursor) {
+  private static @CallToAction int getCallToAction(
+      Context context, SearchCursor cursor, String query) {
     int carrierPresence = cursor.getInt(Projections.PHONE_CARRIER_PRESENCE);
+    String number = cursor.getString(Projections.PHONE_NUMBER);
     if ((carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) == 1) {
       return CallToAction.VIDEO_CALL;
     }
 
-    // TODO(calderwoodra): enriched calling
+    if (LightbringerComponent.get(context).getLightbringer().isReachable(context, number)) {
+      return CallToAction.DUO_CALL;
+    }
+
+    EnrichedCallManager manager = EnrichedCallComponent.get(context).getEnrichedCallManager();
+    EnrichedCallCapabilities capabilities = manager.getCapabilities(number);
+    if (capabilities != null && capabilities.isCallComposerCapable()) {
+      return CallToAction.SHARE_AND_CALL;
+    } else if (shouldRequestCapabilities(cursor, capabilities, query)) {
+      manager.requestCapabilities(number);
+    }
     return CallToAction.NONE;
   }
 
+  /**
+   * An RPC is initiated for each number we request capabilities for, so to limit the network load
+   * and latency on slow networks, we only want to request capabilities for potential contacts the
+   * user is interested in calling. The requirements are that:
+   *
+   * <ul>
+   *   <li>The search query must be 3 or more characters; OR
+   *   <li>There must be 4 or fewer contacts listed in the cursor.
+   * </ul>
+   */
+  private static boolean shouldRequestCapabilities(
+      SearchCursor cursor,
+      @Nullable EnrichedCallCapabilities capabilities,
+      @Nullable String query) {
+    if (capabilities != null) {
+      return false;
+    }
+
+    if (query != null && query.length() >= 3) {
+      return true;
+    }
+
+    // TODO(calderwoodra): implement SearchCursor#getHeaderCount
+    if (cursor.getCount() <= 5) { // 4 contacts + 1 header row element
+      return true;
+    }
+    return false;
+  }
+
   @Override
   public void onClick(View view) {
     if (view == callToActionView) {
       switch (currentAction) {
         case CallToAction.SHARE_AND_CALL:
-          callToActionView.setVisibility(View.VISIBLE);
-          callToActionView.setImageDrawable(
-              context.getDrawable(com.android.contacts.common.R.drawable.ic_phone_attach));
-          // TODO(calderwoodra): open call composer.
+          listener.openCallAndShare(dialerContact);
           break;
         case CallToAction.VIDEO_CALL:
-          callToActionView.setVisibility(View.VISIBLE);
-          callToActionView.setImageDrawable(
-              context.getDrawable(R.drawable.quantum_ic_videocam_white_24));
-          // TODO(calderwoodra): place a video call
+          listener.placeVideoCall(number, position);
+          break;
+        case CallToAction.DUO_CALL:
+          listener.placeDuoCall(number);
           break;
         case CallToAction.NONE:
         default:
@@ -201,8 +256,44 @@
               "Invalid Call to action type: " + currentAction);
       }
     } else {
-      // TODO(calderwoodra): set the correct call initiation type.
-      TelecomUtil.placeCall(context, new CallIntentBuilder(number, Type.REGULAR_SEARCH).build());
+      listener.placeVoiceCall(number, position);
     }
   }
+
+  private static DialerContact getDialerContact(Context context, Cursor cursor) {
+    DialerContact.Builder contact = DialerContact.newBuilder();
+    String displayName = cursor.getString(Projections.PHONE_DISPLAY_NAME);
+    String number = cursor.getString(Projections.PHONE_NUMBER);
+    Uri contactUri =
+        Contacts.getLookupUri(
+            cursor.getLong(Projections.PHONE_CONTACT_ID),
+            cursor.getString(Projections.PHONE_LOOKUP_KEY));
+
+    contact
+        .setNumber(number)
+        .setPhotoId(cursor.getLong(Projections.PHONE_PHOTO_ID))
+        .setContactType(LetterTileDrawable.TYPE_DEFAULT)
+        .setNameOrNumber(displayName)
+        .setNumberLabel(
+            Phone.getTypeLabel(
+                    context.getResources(),
+                    cursor.getInt(Projections.PHONE_TYPE),
+                    cursor.getString(Projections.PHONE_LABEL))
+                .toString());
+
+    String photoUri = cursor.getString(Projections.PHONE_PHOTO_URI);
+    if (photoUri != null) {
+      contact.setPhotoUri(photoUri);
+    }
+
+    if (contactUri != null) {
+      contact.setContactUri(contactUri.toString());
+    }
+
+    if (!TextUtils.isEmpty(displayName)) {
+      contact.setDisplayNumber(number);
+    }
+
+    return contact.build();
+  }
 }
diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
index 2c02815..7d355c9 100644
--- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
+++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
@@ -37,6 +37,8 @@
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.ThreadUtil;
+import com.android.dialer.enrichedcall.EnrichedCallComponent;
+import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener;
 import com.android.dialer.searchfragment.common.SearchCursor;
 import com.android.dialer.searchfragment.cp2.SearchContactsCursorLoader;
 import com.android.dialer.searchfragment.nearbyplaces.NearbyPlacesCursorLoader;
@@ -53,11 +55,16 @@
 
 /** Fragment used for searching contacts. */
 public final class NewSearchFragment extends Fragment
-    implements LoaderCallbacks<Cursor>, OnEmptyViewActionButtonClickedListener {
+    implements LoaderCallbacks<Cursor>,
+        OnEmptyViewActionButtonClickedListener,
+        CapabilitiesListener {
 
   // Since some of our queries can generate network requests, we should delay them until the user
   // stops typing to prevent generating too much network traffic.
   private static final int NETWORK_SEARCH_DELAY_MILLIS = 300;
+  // To prevent constant capabilities updates refreshing the adapter, we want to add a delay between
+  // updates so they are bundled together
+  private static final int ENRICHED_CALLING_CAPABILITIES_UPDATED_DELAY = 400;
 
   @VisibleForTesting public static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
 
@@ -77,6 +84,7 @@
       () -> getLoaderManager().restartLoader(NEARBY_PLACES_LOADER_ID, null, this);
   private final Runnable loadRemoteContactsRunnable =
       () -> getLoaderManager().restartLoader(REMOTE_CONTACTS_LOADER_ID, null, this);
+  private final Runnable capabilitiesUpdatedRunnable = () -> adapter.notifyDataSetChanged();
 
   private Runnable updatePositionRunnable;
 
@@ -85,7 +93,7 @@
   public View onCreateView(
       LayoutInflater inflater, @Nullable ViewGroup parent, @Nullable Bundle bundle) {
     View view = inflater.inflate(R.layout.fragment_search, parent, false);
-    adapter = new SearchAdapter(getContext(), new SearchCursorManager());
+    adapter = new SearchAdapter(getActivity(), new SearchCursorManager());
     emptyContentView = view.findViewById(R.id.empty_view);
     recyclerView = view.findViewById(R.id.recycler_view);
     recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
@@ -192,6 +200,7 @@
     super.onDestroy();
     ThreadUtil.getUiThreadHandler().removeCallbacks(loadNearbyPlacesRunnable);
     ThreadUtil.getUiThreadHandler().removeCallbacks(loadRemoteContactsRunnable);
+    ThreadUtil.getUiThreadHandler().removeCallbacks(capabilitiesUpdatedRunnable);
   }
 
   private void loadNearbyPlacesCursor() {
@@ -249,6 +258,29 @@
         .postDelayed(loadRemoteContactsRunnable, NETWORK_SEARCH_DELAY_MILLIS);
   }
 
+  @Override
+  public void onResume() {
+    super.onResume();
+    EnrichedCallComponent.get(getContext())
+        .getEnrichedCallManager()
+        .registerCapabilitiesListener(this);
+  }
+
+  @Override
+  public void onPause() {
+    super.onPause();
+    EnrichedCallComponent.get(getContext())
+        .getEnrichedCallManager()
+        .unregisterCapabilitiesListener(this);
+  }
+
+  @Override
+  public void onCapabilitiesUpdated() {
+    ThreadUtil.getUiThreadHandler().removeCallbacks(capabilitiesUpdatedRunnable);
+    ThreadUtil.getUiThreadHandler()
+        .postDelayed(capabilitiesUpdatedRunnable, ENRICHED_CALLING_CAPABILITIES_UPDATED_DELAY);
+  }
+
   // Currently, setting up multiple FakeContentProviders doesn't work and results in this fragment
   // being untestable while it can query multiple datasources. This is a temporary fix.
   // TODO(b/64099602): Remove this method and test this fragment with multiple data sources
diff --git a/java/com/android/dialer/searchfragment/list/SearchAdapter.java b/java/com/android/dialer/searchfragment/list/SearchAdapter.java
index 81e8e38..4cb44a2 100644
--- a/java/com/android/dialer/searchfragment/list/SearchAdapter.java
+++ b/java/com/android/dialer/searchfragment/list/SearchAdapter.java
@@ -16,28 +16,45 @@
 
 package com.android.dialer.searchfragment.list;
 
-import android.content.Context;
+import android.app.Activity;
+import android.content.Intent;
+import android.support.annotation.VisibleForTesting;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerView.ViewHolder;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
+import com.android.dialer.callcomposer.CallComposerActivity;
+import com.android.dialer.callintent.CallInitiationType;
+import com.android.dialer.callintent.CallInitiationType.Type;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.CallSpecificAppData;
 import com.android.dialer.common.Assert;
+import com.android.dialer.constants.ActivityRequestCodes;
+import com.android.dialer.dialercontact.DialerContact;
+import com.android.dialer.lightbringer.LightbringerComponent;
+import com.android.dialer.logging.DialerImpression;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.searchfragment.common.RowClickListener;
 import com.android.dialer.searchfragment.common.SearchCursor;
 import com.android.dialer.searchfragment.cp2.SearchContactViewHolder;
 import com.android.dialer.searchfragment.list.SearchCursorManager.RowType;
 import com.android.dialer.searchfragment.nearbyplaces.NearbyPlaceViewHolder;
 import com.android.dialer.searchfragment.remote.RemoteContactViewHolder;
+import com.android.dialer.util.DialerUtils;
 
 /** RecyclerView adapter for {@link NewSearchFragment}. */
-class SearchAdapter extends RecyclerView.Adapter<ViewHolder> {
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+public final class SearchAdapter extends RecyclerView.Adapter<ViewHolder>
+    implements RowClickListener {
 
   private final SearchCursorManager searchCursorManager;
-  private final Context context;
+  private final Activity activity;
 
   private String query;
 
-  SearchAdapter(Context context, SearchCursorManager searchCursorManager) {
-    this.context = context;
+  @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+  public SearchAdapter(Activity activity, SearchCursorManager searchCursorManager) {
+    this.activity = activity;
     this.searchCursorManager = searchCursorManager;
   }
 
@@ -46,18 +63,18 @@
     switch (rowType) {
       case RowType.CONTACT_ROW:
         return new SearchContactViewHolder(
-            LayoutInflater.from(context).inflate(R.layout.search_contact_row, root, false));
+            LayoutInflater.from(activity).inflate(R.layout.search_contact_row, root, false), this);
       case RowType.NEARBY_PLACES_ROW:
         return new NearbyPlaceViewHolder(
-            LayoutInflater.from(context).inflate(R.layout.search_contact_row, root, false));
+            LayoutInflater.from(activity).inflate(R.layout.search_contact_row, root, false));
       case RowType.CONTACT_HEADER:
       case RowType.DIRECTORY_HEADER:
       case RowType.NEARBY_PLACES_HEADER:
         return new HeaderViewHolder(
-            LayoutInflater.from(context).inflate(R.layout.header_layout, root, false));
+            LayoutInflater.from(activity).inflate(R.layout.header_layout, root, false));
       case RowType.DIRECTORY_ROW:
         return new RemoteContactViewHolder(
-            LayoutInflater.from(context).inflate(R.layout.search_contact_row, root, false));
+            LayoutInflater.from(activity).inflate(R.layout.search_contact_row, root, false));
       case RowType.INVALID:
       default:
         throw Assert.createIllegalStateFailException("Invalid RowType: " + rowType);
@@ -86,7 +103,7 @@
     }
   }
 
-  void setContactsCursor(SearchCursor cursor) {
+  public void setContactsCursor(SearchCursor cursor) {
     searchCursorManager.setContactsCursor(cursor);
     notifyDataSetChanged();
   }
@@ -118,4 +135,46 @@
       notifyDataSetChanged();
     }
   }
+
+  @Override
+  public void placeVoiceCall(String phoneNumber, int ranking) {
+    placeCall(phoneNumber, ranking, false);
+  }
+
+  @Override
+  public void placeVideoCall(String phoneNumber, int ranking) {
+    placeCall(phoneNumber, ranking, true);
+  }
+
+  private void placeCall(String phoneNumber, int position, boolean isVideoCall) {
+    CallSpecificAppData callSpecificAppData =
+        CallSpecificAppData.newBuilder()
+            .setCallInitiationType(getCallInitiationType())
+            .setPositionOfSelectedSearchResult(position)
+            .setCharactersInSearchString(query == null ? 0 : query.length())
+            .build();
+    Intent intent =
+        new CallIntentBuilder(phoneNumber, callSpecificAppData).setIsVideoCall(isVideoCall).build();
+    DialerUtils.startActivityWithErrorToast(activity, intent);
+  }
+
+  @Override
+  public void placeDuoCall(String phoneNumber) {
+    Logger.get(activity)
+        .logImpression(DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FROM_SEARCH);
+    Intent intent =
+        LightbringerComponent.get(activity).getLightbringer().getIntent(activity, phoneNumber);
+    activity.startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_LIGHTBRINGER);
+  }
+
+  @Override
+  public void openCallAndShare(DialerContact contact) {
+    Intent intent = CallComposerActivity.newIntent(activity, contact);
+    DialerUtils.startActivityWithErrorToast(activity, intent);
+  }
+
+  private CallInitiationType.Type getCallInitiationType() {
+    // TODO(calderwoodra): add correct initiation type
+    return Type.REGULAR_SEARCH;
+  }
 }
diff --git a/java/com/android/dialer/searchfragment/list/SearchCursorManager.java b/java/com/android/dialer/searchfragment/list/SearchCursorManager.java
index b385aa3..a303425 100644
--- a/java/com/android/dialer/searchfragment/list/SearchCursorManager.java
+++ b/java/com/android/dialer/searchfragment/list/SearchCursorManager.java
@@ -17,6 +17,8 @@
 package com.android.dialer.searchfragment.list;
 
 import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
 import com.android.dialer.common.Assert;
 import com.android.dialer.searchfragment.common.SearchCursor;
 import java.lang.annotation.Retention;
@@ -42,7 +44,8 @@
  *   <li>{@link #getRowType(int)}
  * </ul>
  */
-final class SearchCursorManager {
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+public final class SearchCursorManager {
 
   /** IntDef for the different types of rows that can be shown when searching. */
   @Retention(RetentionPolicy.SOURCE)
@@ -77,7 +80,7 @@
   private SearchCursor corpDirectoryCursor = null;
 
   /** Returns true if the cursor changed. */
-  boolean setContactsCursor(SearchCursor cursor) {
+  boolean setContactsCursor(@Nullable SearchCursor cursor) {
     if (cursor == contactsCursor) {
       return false;
     }
@@ -95,7 +98,7 @@
   }
 
   /** Returns true if the cursor changed. */
-  boolean setNearbyPlacesCursor(SearchCursor cursor) {
+  boolean setNearbyPlacesCursor(@Nullable SearchCursor cursor) {
     if (cursor == nearbyPlacesCursor) {
       return false;
     }
@@ -113,7 +116,7 @@
   }
 
   /** Returns true if a cursor changed. */
-  boolean setCorpDirectoryCursor(SearchCursor cursor) {
+  boolean setCorpDirectoryCursor(@Nullable SearchCursor cursor) {
     if (cursor == corpDirectoryCursor) {
       return false;
     }