Merge "Implemented adding a new favorites contact flow NUI."
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java
index 2d82b6f..48796db 100644
--- a/java/com/android/dialer/app/DialtactsActivity.java
+++ b/java/com/android/dialer/app/DialtactsActivity.java
@@ -32,6 +32,7 @@
 import android.os.SystemClock;
 import android.os.Trace;
 import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.QuickContact;
 import android.speech.RecognizerIntent;
 import android.support.annotation.MainThread;
 import android.support.annotation.NonNull;
@@ -59,6 +60,7 @@
 import android.widget.AbsListView.OnScrollListener;
 import android.widget.EditText;
 import android.widget.ImageButton;
+import android.widget.ImageView;
 import android.widget.PopupMenu;
 import android.widget.TextView;
 import android.widget.Toast;
@@ -101,6 +103,7 @@
 import com.android.dialer.configprovider.ConfigProviderBindings;
 import com.android.dialer.constants.ActivityRequestCodes;
 import com.android.dialer.contactsfragment.ContactsFragment;
+import com.android.dialer.contactsfragment.ContactsFragment.OnContactSelectedListener;
 import com.android.dialer.database.Database;
 import com.android.dialer.database.DialerDatabaseHelper;
 import com.android.dialer.dialpadview.DialpadFragment;
@@ -109,6 +112,7 @@
 import com.android.dialer.interactions.PhoneNumberInteraction;
 import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorCode;
 import com.android.dialer.logging.DialerImpression;
+import com.android.dialer.logging.InteractionEvent;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.logging.LoggingBindings;
 import com.android.dialer.logging.ScreenEvent;
@@ -166,7 +170,8 @@
         PhoneNumberInteraction.DisambigDialogDismissedListener,
         ActivityCompat.OnRequestPermissionsResultCallback,
         DialpadListener,
-        SearchFragmentListener {
+        SearchFragmentListener,
+        OnContactSelectedListener {
 
   public static final boolean DEBUG = false;
   @VisibleForTesting public static final String TAG_DIALPAD_FRAGMENT = "dialpad";
@@ -1695,6 +1700,14 @@
     return mPreviouslySelectedTabIndex;
   }
 
+  @Override
+  public void onContactSelected(ImageView photo, Uri contactUri, long contactId) {
+    Logger.get(this)
+        .logInteraction(InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_CONTACTS_FRAGMENT_ITEM);
+    QuickContact.showQuickContact(
+        this, photo, contactUri, QuickContact.MODE_LARGE, null /* excludeMimes */);
+  }
+
   /** Popup menu accessible from the search bar */
   protected class OptionsPopupMenu extends PopupMenu {
 
diff --git a/java/com/android/dialer/app/list/DialtactsPagerAdapter.java b/java/com/android/dialer/app/list/DialtactsPagerAdapter.java
index 484ab0b..d272932 100644
--- a/java/com/android/dialer/app/list/DialtactsPagerAdapter.java
+++ b/java/com/android/dialer/app/list/DialtactsPagerAdapter.java
@@ -28,7 +28,6 @@
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.configprovider.ConfigProviderBindings;
 import com.android.dialer.contactsfragment.ContactsFragment;
-import com.android.dialer.contactsfragment.ContactsFragment.ClickAction;
 import com.android.dialer.contactsfragment.ContactsFragment.Header;
 import com.android.dialer.database.CallLogQueryHandler;
 import com.android.dialer.speeddial.SpeedDialFragment;
@@ -108,8 +107,7 @@
       case TAB_INDEX_ALL_CONTACTS:
         if (useNewContactsTab) {
           if (contactsFragment == null) {
-            contactsFragment =
-                ContactsFragment.newInstance(Header.ADD_CONTACT, ClickAction.OPEN_CONTACT_CARD);
+            contactsFragment = ContactsFragment.newInstance(Header.ADD_CONTACT);
           }
           return contactsFragment;
         } else {
diff --git a/java/com/android/dialer/contactsfragment/ContactViewHolder.java b/java/com/android/dialer/contactsfragment/ContactViewHolder.java
index 0597c2a..2730c0d 100644
--- a/java/com/android/dialer/contactsfragment/ContactViewHolder.java
+++ b/java/com/android/dialer/contactsfragment/ContactViewHolder.java
@@ -18,7 +18,6 @@
 
 import android.content.Context;
 import android.net.Uri;
-import android.provider.ContactsContract.QuickContact;
 import android.support.v7.widget.RecyclerView;
 import android.text.TextUtils;
 import android.view.View;
@@ -26,7 +25,7 @@
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
 import com.android.dialer.common.Assert;
-import com.android.dialer.contactsfragment.ContactsFragment.ClickAction;
+import com.android.dialer.contactsfragment.ContactsFragment.OnContactSelectedListener;
 import com.android.dialer.logging.InteractionEvent;
 import com.android.dialer.logging.Logger;
 
@@ -37,20 +36,20 @@
   private final TextView name;
   private final QuickContactBadge photo;
   private final Context context;
-  private final @ClickAction int clickAction;
+  private final OnContactSelectedListener onContactSelectedListener;
 
   private String headerText;
   private Uri contactUri;
+  private long contactId;
 
-  ContactViewHolder(View itemView, @ClickAction int clickAction) {
+  ContactViewHolder(View itemView, OnContactSelectedListener onContactSelectedListener) {
     super(itemView);
-    Assert.checkArgument(clickAction != ClickAction.INVALID, "Invalid click action.");
+    this.onContactSelectedListener = Assert.isNotNull(onContactSelectedListener);
     context = itemView.getContext();
     itemView.findViewById(R.id.click_target).setOnClickListener(this);
     header = itemView.findViewById(R.id.header);
     name = itemView.findViewById(R.id.contact_name);
     photo = itemView.findViewById(R.id.photo);
-    this.clickAction = clickAction;
   }
 
   /**
@@ -61,9 +60,11 @@
    * @param contactUri to be shown by the contact card on photo click.
    * @param showHeader if header view should be shown {@code True}, {@code False} otherwise.
    */
-  public void bind(String headerText, String displayName, Uri contactUri, boolean showHeader) {
+  public void bind(
+      String headerText, String displayName, Uri contactUri, long contactId, boolean showHeader) {
     Assert.checkArgument(!TextUtils.isEmpty(displayName));
     this.contactUri = contactUri;
+    this.contactId = contactId;
     this.headerText = headerText;
 
     name.setText(displayName);
@@ -89,20 +90,6 @@
 
   @Override
   public void onClick(View v) {
-    switch (clickAction) {
-      case ClickAction.OPEN_CONTACT_CARD:
-        Logger.get(context)
-            .logInteraction(InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_CONTACTS_FRAGMENT_ITEM);
-        QuickContact.showQuickContact(
-            photo.getContext(),
-            photo,
-            contactUri,
-            QuickContact.MODE_LARGE,
-            null /* excludeMimes */);
-        break;
-      case ClickAction.INVALID:
-      default:
-        throw Assert.createIllegalStateFailException("Invalid click action.");
-    }
+    onContactSelectedListener.onContactSelected(photo, contactUri, contactId);
   }
 }
diff --git a/java/com/android/dialer/contactsfragment/ContactsAdapter.java b/java/com/android/dialer/contactsfragment/ContactsAdapter.java
index 8f2120c..44abe29 100644
--- a/java/com/android/dialer/contactsfragment/ContactsAdapter.java
+++ b/java/com/android/dialer/contactsfragment/ContactsAdapter.java
@@ -29,8 +29,8 @@
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.contactphoto.ContactPhotoManager;
-import com.android.dialer.contactsfragment.ContactsFragment.ClickAction;
 import com.android.dialer.contactsfragment.ContactsFragment.Header;
+import com.android.dialer.contactsfragment.ContactsFragment.OnContactSelectedListener;
 import com.android.dialer.lettertile.LetterTileDrawable;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -50,7 +50,7 @@
   private final ArrayMap<ContactViewHolder, Integer> holderMap = new ArrayMap<>();
   private final Context context;
   private final @Header int header;
-  private final @ClickAction int clickAction;
+  private final OnContactSelectedListener onContactSelectedListener;
 
   // List of contact sublist headers
   private String[] headers = new String[0];
@@ -59,10 +59,11 @@
   // Cursor with list of contacts
   private Cursor cursor;
 
-  ContactsAdapter(Context context, @Header int header, @ClickAction int clickAction) {
+  ContactsAdapter(
+      Context context, @Header int header, OnContactSelectedListener onContactSelectedListener) {
     this.context = context;
     this.header = header;
-    this.clickAction = clickAction;
+    this.onContactSelectedListener = Assert.isNotNull(onContactSelectedListener);
   }
 
   void updateCursor(Cursor cursor) {
@@ -92,7 +93,8 @@
             LayoutInflater.from(context).inflate(R.layout.add_contact_row, parent, false));
       case CONTACT_VIEW_TYPE:
         return new ContactViewHolder(
-            LayoutInflater.from(context).inflate(R.layout.contact_row, parent, false), clickAction);
+            LayoutInflater.from(context).inflate(R.layout.contact_row, parent, false),
+            onContactSelectedListener);
       case UNKNOWN_VIEW_TYPE:
       default:
         throw Assert.createIllegalStateFailException("Invalid view type: " + viewType);
@@ -133,7 +135,7 @@
     // it to the previous element and only show the anchored header if the row elements fall into
     // the same sublists.
     boolean showHeader = position == 0 || !header.equals(getHeaderString(position - 1));
-    contactViewHolder.bind(header, name, contactUri, showHeader);
+    contactViewHolder.bind(header, name, contactUri, getContactId(cursor), showHeader);
   }
 
   /**
@@ -190,11 +192,15 @@
   }
 
   private static Uri getContactUri(Cursor cursor) {
-    long contactId = cursor.getLong(ContactsCursorLoader.CONTACT_ID);
+    long contactId = getContactId(cursor);
     String lookupKey = cursor.getString(ContactsCursorLoader.CONTACT_LOOKUP_KEY);
     return Contacts.getLookupUri(contactId, lookupKey);
   }
 
+  private static long getContactId(Cursor cursor) {
+    return cursor.getLong(ContactsCursorLoader.CONTACT_ID);
+  }
+
   String getHeaderString(int position) {
     if (header != Header.NONE) {
       if (position == 0) {
diff --git a/java/com/android/dialer/contactsfragment/ContactsCursorLoader.java b/java/com/android/dialer/contactsfragment/ContactsCursorLoader.java
index a22f7eb..e55f951 100644
--- a/java/com/android/dialer/contactsfragment/ContactsCursorLoader.java
+++ b/java/com/android/dialer/contactsfragment/ContactsCursorLoader.java
@@ -18,7 +18,10 @@
 
 import android.content.Context;
 import android.content.CursorLoader;
+import android.net.Uri;
 import android.provider.ContactsContract.Contacts;
+import android.text.TextUtils;
+import com.android.contacts.common.preference.ContactsPreferences;
 
 /** Cursor Loader for {@link ContactsFragment}. */
 final class ContactsCursorLoader extends CursorLoader {
@@ -47,26 +50,52 @@
         Contacts.LOOKUP_KEY, // 4
       };
 
-  private ContactsCursorLoader(Context context, String[] contactProjection, String sortKey) {
+  ContactsCursorLoader(Context context, boolean hasPhoneNumbers) {
     super(
         context,
-        Contacts.CONTENT_URI
-            .buildUpon()
-            .appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true")
-            .build(),
-        contactProjection,
-        contactProjection[CONTACT_DISPLAY_NAME] + " IS NOT NULL",
+        buildUri(""),
+        getProjection(context),
+        getWhere(context, hasPhoneNumbers),
         null,
-        sortKey + " ASC");
+        getSortKey(context) + " ASC");
   }
 
-  public static ContactsCursorLoader createInstanceDisplayNamePrimary(
-      Context context, String sortKey) {
-    return new ContactsCursorLoader(context, CONTACTS_PROJECTION_DISPLAY_NAME_PRIMARY, sortKey);
+  private static String[] getProjection(Context context) {
+    ContactsPreferences contactsPrefs = new ContactsPreferences(context);
+    boolean displayOrderPrimary =
+        (contactsPrefs.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY);
+    return displayOrderPrimary
+        ? CONTACTS_PROJECTION_DISPLAY_NAME_PRIMARY
+        : CONTACTS_PROJECTION_DISPLAY_NAME_ALTERNATIVE;
   }
 
-  public static ContactsCursorLoader createInstanceDisplayNameAlternative(
-      Context context, String sortKey) {
-    return new ContactsCursorLoader(context, CONTACTS_PROJECTION_DISPLAY_NAME_ALTERNATIVE, sortKey);
+  private static String getWhere(Context context, boolean hasPhoneNumbers) {
+    String where = getProjection(context)[CONTACT_DISPLAY_NAME] + " IS NOT NULL";
+    if (hasPhoneNumbers) {
+      where += " AND " + Contacts.HAS_PHONE_NUMBER + "=1";
+    }
+    return where;
+  }
+
+  private static String getSortKey(Context context) {
+    ContactsPreferences contactsPrefs = new ContactsPreferences(context);
+    boolean sortOrderPrimary =
+        (contactsPrefs.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY);
+    return sortOrderPrimary ? Contacts.SORT_KEY_PRIMARY : Contacts.SORT_KEY_ALTERNATIVE;
+  }
+
+  /** Update cursor loader to filter contacts based on the provided query. */
+  public void setQuery(String query) {
+    setUri(buildUri(query));
+  }
+
+  private static Uri buildUri(String query) {
+    Uri.Builder baseUri;
+    if (TextUtils.isEmpty(query)) {
+      baseUri = Contacts.CONTENT_URI.buildUpon();
+    } else {
+      baseUri = Contacts.CONTENT_FILTER_URI.buildUpon().appendPath(query);
+    }
+    return baseUri.appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true").build();
   }
 }
diff --git a/java/com/android/dialer/contactsfragment/ContactsFragment.java b/java/com/android/dialer/contactsfragment/ContactsFragment.java
index 82b68b8..7147393 100644
--- a/java/com/android/dialer/contactsfragment/ContactsFragment.java
+++ b/java/com/android/dialer/contactsfragment/ContactsFragment.java
@@ -26,8 +26,8 @@
 import android.content.Loader;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
+import android.net.Uri;
 import android.os.Bundle;
-import android.provider.ContactsContract.Contacts;
 import android.support.annotation.IntDef;
 import android.support.annotation.Nullable;
 import android.support.v13.app.FragmentCompat;
@@ -39,12 +39,14 @@
 import android.view.View;
 import android.view.View.OnScrollChangeListener;
 import android.view.ViewGroup;
+import android.widget.ImageView;
 import android.widget.TextView;
 import com.android.contacts.common.preference.ContactsPreferences;
 import com.android.contacts.common.preference.ContactsPreferences.ChangeListener;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.FragmentUtils;
 import com.android.dialer.common.LogUtil;
+import com.android.dialer.contactsfragment.ContactsFragment.OnContactSelectedListener;
 import com.android.dialer.performancereport.PerformanceReport;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.IntentUtil;
@@ -62,15 +64,6 @@
         OnEmptyViewActionButtonClickedListener,
         ChangeListener {
 
-  /** IntDef to define the OnClick action for contact rows. */
-  @Retention(RetentionPolicy.SOURCE)
-  @IntDef({ClickAction.INVALID, ClickAction.OPEN_CONTACT_CARD})
-  public @interface ClickAction {
-    int INVALID = 0;
-    /** Open contact card on click. */
-    int OPEN_CONTACT_CARD = 1;
-  }
-
   /** An enum for the different types of headers that be inserted at position 0 in the list. */
   @Retention(RetentionPolicy.SOURCE)
   @IntDef({Header.NONE, Header.ADD_CONTACT})
@@ -83,7 +76,7 @@
   public static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
 
   private static final String EXTRA_HEADER = "extra_header";
-  private static final String EXTRA_CLICK_ACTION = "extra_click_action";
+  private static final String EXTRA_HAS_PHONE_NUMBERS = "extra_has_phone_numbers";
 
   /**
    * Listen to broadcast events about permissions in order to be notified if the READ_CONTACTS
@@ -104,14 +97,11 @@
   private ContactsAdapter adapter;
   private EmptyContentView emptyContentView;
 
-  private ContactsPreferences contactsPrefs;
   private @Header int header;
-  private @ClickAction int clickAction;
 
-  /** Listener for contacts list scroll state. */
-  public interface OnContactsListScrolledListener {
-    void onContactsListScrolled(boolean isDragging);
-  }
+  private ContactsPreferences contactsPrefs;
+  private boolean hasPhoneNumbers;
+  private String query;
 
   /**
    * Used to get a configured instance of ContactsFragment.
@@ -121,26 +111,42 @@
    *
    * <ul>
    *   <li>{@link Header#ADD_CONTACT} to insert a header that allows users to add a contact
-   *   <li>{@link ClickAction#OPEN_CONTACT_CARD} to open contact cards on click
+   *   <li>Open contact cards on click
    * </ul>
    *
    * And for the add favorite contact screen we might use:
    *
    * <ul>
    *   <li>{@link Header#NONE} so that all rows are contacts (i.e. no header inserted)
-   *   <li>{@link ClickAction#SET_RESULT_AND_FINISH} to send a selected contact to the previous
-   *       activity.
+   *   <li>Send a selected contact to the parent activity.
    * </ul>
    *
    * @param header determines the type of header inserted at position 0 in the contacts list
-   * @param clickAction defines the on click actions on rows that represent contacts
    */
-  public static ContactsFragment newInstance(@Header int header, @ClickAction int clickAction) {
-    Assert.checkArgument(clickAction != ClickAction.INVALID, "Invalid click action");
+  public static ContactsFragment newInstance(@Header int header) {
     ContactsFragment fragment = new ContactsFragment();
     Bundle args = new Bundle();
     args.putInt(EXTRA_HEADER, header);
-    args.putInt(EXTRA_CLICK_ACTION, clickAction);
+    fragment.setArguments(args);
+    return fragment;
+  }
+
+  /**
+   * Returns {@link ContactsFragment} with a list of contacts such that:
+   *
+   * <ul>
+   *   <li>Each contact has a phone number
+   *   <li>Contacts are filterable via {@link #updateQuery(String)}
+   *   <li>There is no list header (i.e. {@link Header#NONE}
+   *   <li>Clicking on a contact notifies the parent activity via {@link
+   *       OnContactSelectedListener#onContactSelected(ImageView, Uri, long)}.
+   * </ul>
+   */
+  public static ContactsFragment newAddFavoritesInstance() {
+    ContactsFragment fragment = new ContactsFragment();
+    Bundle args = new Bundle();
+    args.putInt(EXTRA_HEADER, Header.NONE);
+    args.putBoolean(EXTRA_HAS_PHONE_NUMBERS, true);
     fragment.setArguments(args);
     return fragment;
   }
@@ -152,7 +158,21 @@
     contactsPrefs = new ContactsPreferences(getContext());
     contactsPrefs.registerChangeListener(this);
     header = getArguments().getInt(EXTRA_HEADER);
-    clickAction = getArguments().getInt(EXTRA_CLICK_ACTION);
+    hasPhoneNumbers = getArguments().getBoolean(EXTRA_HAS_PHONE_NUMBERS);
+  }
+
+  @Override
+  public void onStart() {
+    super.onStart();
+    PermissionsUtil.registerPermissionReceiver(
+        getActivity(), readContactsPermissionGrantedReceiver, READ_CONTACTS);
+  }
+
+  @Override
+  public void onStop() {
+    PermissionsUtil.unregisterPermissionReceiver(
+        getActivity(), readContactsPermissionGrantedReceiver);
+    super.onStop();
   }
 
   @Nullable
@@ -163,7 +183,9 @@
     fastScroller = view.findViewById(R.id.fast_scroller);
     anchoredHeader = view.findViewById(R.id.header);
     recyclerView = view.findViewById(R.id.recycler_view);
-    adapter = new ContactsAdapter(getContext(), header, clickAction);
+    adapter =
+        new ContactsAdapter(
+            getContext(), header, FragmentUtils.getParent(this, OnContactSelectedListener.class));
     recyclerView.setAdapter(adapter);
     manager =
         new LinearLayoutManager(getContext()) {
@@ -208,15 +230,14 @@
   /** @return a loader according to sort order and display order. */
   @Override
   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-    boolean sortOrderPrimary =
-        (contactsPrefs.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY);
-    boolean displayOrderPrimary =
-        (contactsPrefs.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY);
+    ContactsCursorLoader cursorLoader = new ContactsCursorLoader(getContext(), hasPhoneNumbers);
+    cursorLoader.setQuery(query);
+    return cursorLoader;
+  }
 
-    String sortKey = sortOrderPrimary ? Contacts.SORT_KEY_PRIMARY : Contacts.SORT_KEY_ALTERNATIVE;
-    return displayOrderPrimary
-        ? ContactsCursorLoader.createInstanceDisplayNamePrimary(getContext(), sortKey)
-        : ContactsCursorLoader.createInstanceDisplayNameAlternative(getContext(), sortKey);
+  public void updateQuery(String query) {
+    this.query = query;
+    getLoaderManager().restartLoader(0, null, this);
   }
 
   @Override
@@ -266,10 +287,13 @@
     }
     String anchoredHeaderString = adapter.getHeaderString(firstCompletelyVisible);
 
-    FragmentUtils.getParentUnsafe(this, OnContactsListScrolledListener.class)
-        .onContactsListScrolled(
-            recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING
-                || fastScroller.isDragStarted());
+    OnContactsListScrolledListener listener =
+        FragmentUtils.getParent(this, OnContactsListScrolledListener.class);
+    if (listener != null) {
+      listener.onContactsListScrolled(
+          recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING
+              || fastScroller.isDragStarted());
+    }
 
     // If the user swipes to the top of the list very quickly, there is some strange behavior
     // between this method updating headers and adapter#onBindViewHolder updating headers.
@@ -331,17 +355,15 @@
     }
   }
 
-  @Override
-  public void onStart() {
-    super.onStart();
-    PermissionsUtil.registerPermissionReceiver(
-        getActivity(), readContactsPermissionGrantedReceiver, READ_CONTACTS);
+  /** Listener for contacts list scroll state. */
+  public interface OnContactsListScrolledListener {
+    void onContactsListScrolled(boolean isDragging);
   }
 
-  @Override
-  public void onStop() {
-    PermissionsUtil.unregisterPermissionReceiver(
-        getActivity(), readContactsPermissionGrantedReceiver);
-    super.onStop();
+  /** Listener to notify parents when a contact is selected. */
+  public interface OnContactSelectedListener {
+
+    /** Called when a contact is selected in {@link ContactsFragment}. */
+    void onContactSelected(ImageView photo, Uri contactUri, long contactId);
   }
 }
diff --git a/java/com/android/dialer/databasepopulator/ContactsPopulator.java b/java/com/android/dialer/databasepopulator/ContactsPopulator.java
index 79492e9..f22552d 100644
--- a/java/com/android/dialer/databasepopulator/ContactsPopulator.java
+++ b/java/com/android/dialer/databasepopulator/ContactsPopulator.java
@@ -148,6 +148,19 @@
   }
 
   @WorkerThread
+  public static void populateSpeedDialTestContacts(@NonNull Context context) {
+    Assert.isWorkerThread();
+    ArrayList<ContentProviderOperation> operations = new ArrayList<>();
+    addContact(SIMPLE_CONTACTS[0], operations);
+    addContact(SIMPLE_CONTACTS[5], operations);
+    try {
+      context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations);
+    } catch (RemoteException | OperationApplicationException e) {
+      Assert.fail("error adding contacts: " + e);
+    }
+  }
+
+  @WorkerThread
   public static void populateContacts(@NonNull Context context) {
     populateContacts(context, false);
   }
diff --git a/java/com/android/dialer/speeddial/AddFavoriteActivity.java b/java/com/android/dialer/speeddial/AddFavoriteActivity.java
new file mode 100644
index 0000000..eea6e84
--- /dev/null
+++ b/java/com/android/dialer/speeddial/AddFavoriteActivity.java
@@ -0,0 +1,109 @@
+/*
+ * 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.speeddial;
+
+import android.content.ContentValues;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.SearchView;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.ImageView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.concurrent.DialerExecutorComponent;
+import com.android.dialer.contactsfragment.ContactsFragment;
+import com.android.dialer.contactsfragment.ContactsFragment.OnContactSelectedListener;
+
+/**
+ * Activity for selecting a single contact and adding it to favorites.
+ *
+ * <p>Contacts are displayed using {@link ContactsFragment}. Contacts are searchable via search bar
+ * in the toolbar. When a contact is selected, it's uri is passed back in the result data.
+ */
+public class AddFavoriteActivity extends AppCompatActivity implements OnContactSelectedListener {
+
+  private ContactsFragment contactsFragment;
+
+  @Override
+  protected void onCreate(@Nullable Bundle bundle) {
+    super.onCreate(bundle);
+    setContentView(R.layout.add_favorite_activity);
+    contactsFragment = ContactsFragment.newAddFavoritesInstance();
+    getFragmentManager()
+        .beginTransaction()
+        .add(R.id.add_favorite_container, contactsFragment, null)
+        .commit();
+  }
+
+  @Override
+  public boolean onCreateOptionsMenu(Menu menu) {
+    getMenuInflater().inflate(R.menu.add_favorite_menu, menu);
+
+    MenuItem searchItem = menu.findItem(R.id.action_search);
+    SearchView searchView = (SearchView) searchItem.getActionView();
+    searchView.setOnQueryTextListener(
+        new SearchView.OnQueryTextListener() {
+          @Override
+          public boolean onQueryTextSubmit(String query) {
+            if (!searchView.isIconified()) {
+              searchView.setIconified(true);
+            }
+            searchItem.collapseActionView();
+            return false;
+          }
+
+          @Override
+          public boolean onQueryTextChange(String s) {
+            contactsFragment.updateQuery(s);
+            return false;
+          }
+        });
+    return true;
+  }
+
+  @Override
+  public void onContactSelected(ImageView photo, Uri contactUri, long contactId) {
+    DialerExecutorComponent.get(this)
+        .dialerExecutorFactory()
+        .createUiTaskBuilder(
+            getFragmentManager(), "mark_contact_favorite", this::markContactStarred)
+        .onSuccess(output -> finish())
+        .onFailure(this::onContactStarredFailed)
+        .build()
+        .executeParallel(contactId);
+  }
+
+  @WorkerThread
+  private int markContactStarred(long contactId) {
+    // TODO(calderwoodra): For now, we will just mark contacts as starred. This means that contacts
+    // will only be able to exist once in favorites until we implement multiple contact avenues.
+    ContentValues contentValues = new ContentValues();
+    contentValues.put(Contacts.STARRED, 1);
+
+    String where = Contacts._ID + " = ?";
+    String[] selectionArgs = new String[] {Long.toString(contactId)};
+    return getContentResolver().update(Contacts.CONTENT_URI, contentValues, where, selectionArgs);
+  }
+
+  private void onContactStarredFailed(Throwable throwable) {
+    throw Assert.createAssertionFailException(throwable.getMessage());
+  }
+}
diff --git a/java/com/android/dialer/speeddial/AndroidManifest.xml b/java/com/android/dialer/speeddial/AndroidManifest.xml
index f4f0d82..99ab12c 100644
--- a/java/com/android/dialer/speeddial/AndroidManifest.xml
+++ b/java/com/android/dialer/speeddial/AndroidManifest.xml
@@ -13,4 +13,15 @@
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License
  -->
-<manifest  package="com.android.dialer.speeddial"/>
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.dialer.speeddial">
+
+  <application android:theme="@style/Theme.AppCompat">
+    <activity
+        android:name="com.android.dialer.speeddial.AddFavoriteActivity"
+        android:label="@string/add_favorite_activity_title"
+        android:exported="false"
+        android:theme="@style/DialtactsTheme"/>
+  </application>
+</manifest>
\ No newline at end of file
diff --git a/java/com/android/dialer/speeddial/SpeedDialCursor.java b/java/com/android/dialer/speeddial/SpeedDialCursor.java
index 552fab1..1208dae 100644
--- a/java/com/android/dialer/speeddial/SpeedDialCursor.java
+++ b/java/com/android/dialer/speeddial/SpeedDialCursor.java
@@ -64,10 +64,6 @@
 
     strequentCursor.moveToPosition(-1);
     while (strequentCursor.moveToNext()) {
-      if (strequentCursor.getInt(StrequentContactsCursorLoader.PHONE_IS_SUPER_PRIMARY) != 0) {
-        continue;
-      }
-
       if (strequentCursor.getPosition() != 0) {
         long contactId = strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID);
         int position = strequentCursor.getPosition();
diff --git a/java/com/android/dialer/speeddial/SpeedDialFragment.java b/java/com/android/dialer/speeddial/SpeedDialFragment.java
index 65e542c..08861da 100644
--- a/java/com/android/dialer/speeddial/SpeedDialFragment.java
+++ b/java/com/android/dialer/speeddial/SpeedDialFragment.java
@@ -18,10 +18,10 @@
 
 import android.app.Fragment;
 import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Intent;
 import android.content.Loader;
 import android.database.Cursor;
 import android.os.Bundle;
-import android.provider.ContactsContract.Contacts;
 import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
@@ -35,7 +35,16 @@
 import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener;
 import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener;
 
-/** Favorites fragment. Contents TBD. TODO(calderwoodra) */
+/**
+ * Fragment for displaying:
+ *
+ * <ul>
+ *   <li>Favorite/Starred contacts
+ *   <li>Suggested contacts
+ * </ul>
+ *
+ * <p>Suggested contacts built from {@link android.provider.ContactsContract#STREQUENT_PHONE_ONLY}.
+ */
 public class SpeedDialFragment extends Fragment {
 
   private static final int STREQUENT_CONTACTS_LOADER_ID = 1;
@@ -73,16 +82,16 @@
   }
 
   @Override
-  public void onPause() {
-    super.onPause();
-    loaderCallback.unregisterContentObserver();
+  public void onResume() {
+    super.onResume();
+    getLoaderManager().restartLoader(STREQUENT_CONTACTS_LOADER_ID, null, loaderCallback);
   }
 
-  private static class SpeedDialFragmentHeaderListener implements SpeedDialHeaderListener {
+  private class SpeedDialFragmentHeaderListener implements SpeedDialHeaderListener {
 
     @Override
     public void onAddFavoriteClicked() {
-      // TODO(calderwoodra): implement add favorite screen
+      startActivity(new Intent(getContext(), AddFavoriteActivity.class));
     }
   }
 
@@ -123,8 +132,6 @@
    */
   private class SpeedDialFragmentLoaderCallback implements LoaderCallbacks<Cursor> {
 
-    private StrequentContactsCursorLoader cursorLoader;
-
     @Override
     public Loader<Cursor> onCreateLoader(int id, Bundle args) {
       if (id == STREQUENT_CONTACTS_LOADER_ID) {
@@ -135,24 +142,9 @@
 
     @Override
     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
-      cursorLoader = (StrequentContactsCursorLoader) loader;
-      // Since the original cursor we queried against was modified and closed, we need to register a
-      // new content observer in order to get updates on changes to our contacts.
-      getContext()
-          .getContentResolver()
-          .registerContentObserver(
-              Contacts.CONTENT_STREQUENT_URI,
-              true /* notifyForDescendants*/,
-              cursorLoader.getContentObserver());
       adapter.setCursor((SpeedDialCursor) data);
     }
 
-    public void unregisterContentObserver() {
-      getContext()
-          .getContentResolver()
-          .unregisterContentObserver(cursorLoader.getContentObserver());
-    }
-
     @Override
     public void onLoaderReset(Loader<Cursor> loader) {
       adapter.setCursor(null);
diff --git a/java/com/android/dialer/speeddial/res/layout/add_favorite_activity.xml b/java/com/android/dialer/speeddial/res/layout/add_favorite_activity.xml
new file mode 100644
index 0000000..4df2f29
--- /dev/null
+++ b/java/com/android/dialer/speeddial/res/layout/add_favorite_activity.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/add_favorite_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/java/com/android/dialer/speeddial/res/menu/add_favorite_menu.xml b/java/com/android/dialer/speeddial/res/menu/add_favorite_menu.xml
new file mode 100644
index 0000000..b11c7f5
--- /dev/null
+++ b/java/com/android/dialer/speeddial/res/menu/add_favorite_menu.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+  <item
+      android:id="@+id/action_search"
+      android:title="@android:string/search_go"
+      app:showAsAction="always"
+      app:actionViewClass="android.support.v7.widget.SearchView"/>
+</menu>
\ No newline at end of file
diff --git a/java/com/android/dialer/speeddial/res/values/strings.xml b/java/com/android/dialer/speeddial/res/values/strings.xml
index f814ed6..d64d035 100644
--- a/java/com/android/dialer/speeddial/res/values/strings.xml
+++ b/java/com/android/dialer/speeddial/res/values/strings.xml
@@ -15,12 +15,15 @@
   ~ limitations under the License
   -->
 <resources>
-  <!-- header for a list of contacts that are the users favorites. -->
+  <!-- header for a list of contacts that are the users favorites. [CHAR LIMIT=30] -->
   <string name="favorites_header">Favorites</string>
 
-  <!-- header for a list of contacts that are suggestions for the user to place calls to -->
+  <!-- header for a list of contacts that are suggestions for the user to place calls to. [CHAR LIMIT=30] -->
   <string name="suggestions_header">Suggestions</string>
 
-  <!-- text for a button that prompts the user to add a contact to their favorites -->
+  <!-- text for a button that prompts the user to add a contact to their favorites. [CHAR LIMIT=12] -->
   <string name="speed_dial_add_button_text">Add</string>
+
+  <!-- Title for screen prompting the user to select a contact to mark as a favorite. [CHAR LIMIT=NONE] -->
+  <string name="add_favorite_activity_title">Add Favorite</string>
 </resources>
\ No newline at end of file