Added remote directories to the new search fragment.

When Dialer users search for contacts, if they have an enterprise account on
their device, they can also search for enterprise/remote contacts. This change
adds the directory queries/results to the new search fragment.

screenshot: http://screen/S9mpsvnwtCv
Bug: 37209462
Test: javatests/.../searchfragment/remote
PiperOrigin-RevId: 164681686
Change-Id: I88bc5bceb4c745d8f6f7d9651929d49100283756
diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java b/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java
index 2bd9cdd..1e8224d 100644
--- a/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java
+++ b/java/com/android/dialer/searchfragment/cp2/SearchContactViewHolder.java
@@ -110,6 +110,7 @@
     }
   }
 
+  // Show the contact photo next to only the first number if a contact has multiple numbers
   private boolean shouldShowPhoto(SearchCursor cursor) {
     int currentPosition = cursor.getPosition();
     String currentLookupKey = cursor.getString(Projections.PHONE_LOOKUP_KEY);
diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
index 1a48951..2c02815 100644
--- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
+++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
@@ -40,11 +40,16 @@
 import com.android.dialer.searchfragment.common.SearchCursor;
 import com.android.dialer.searchfragment.cp2.SearchContactsCursorLoader;
 import com.android.dialer.searchfragment.nearbyplaces.NearbyPlacesCursorLoader;
+import com.android.dialer.searchfragment.remote.RemoteContactsCursorLoader;
+import com.android.dialer.searchfragment.remote.RemoteDirectoriesCursorLoader;
+import com.android.dialer.searchfragment.remote.RemoteDirectoriesCursorLoader.Directory;
 import com.android.dialer.util.PermissionsUtil;
 import com.android.dialer.util.ViewUtil;
 import com.android.dialer.widget.EmptyContentView;
 import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 
 /** Fragment used for searching contacts. */
 public final class NewSearchFragment extends Fragment
@@ -57,15 +62,21 @@
   @VisibleForTesting public static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1;
 
   private static final int CONTACTS_LOADER_ID = 0;
-  private static final int NEARBY_PLACES_ID = 1;
+  private static final int NEARBY_PLACES_LOADER_ID = 1;
+  private static final int REMOTE_DIRECTORIES_LOADER_ID = 2;
+  private static final int REMOTE_CONTACTS_LOADER_ID = 3;
 
   private EmptyContentView emptyContentView;
   private RecyclerView recyclerView;
   private SearchAdapter adapter;
   private String query;
+  private boolean remoteDirectoriesDisabledForTesting;
 
+  private final List<Directory> directories = new ArrayList<>();
   private final Runnable loadNearbyPlacesRunnable =
-      () -> getLoaderManager().restartLoader(NEARBY_PLACES_ID, null, this);
+      () -> getLoaderManager().restartLoader(NEARBY_PLACES_LOADER_ID, null, this);
+  private final Runnable loadRemoteContactsRunnable =
+      () -> getLoaderManager().restartLoader(REMOTE_CONTACTS_LOADER_ID, null, this);
 
   private Runnable updatePositionRunnable;
 
@@ -99,6 +110,7 @@
   private void initLoaders() {
     getLoaderManager().initLoader(CONTACTS_LOADER_ID, null, this);
     loadNearbyPlacesCursor();
+    loadRemoteDirectoriesCursor();
   }
 
   @Override
@@ -106,8 +118,12 @@
     // TODO(calderwoodra) add enterprise loader
     if (id == CONTACTS_LOADER_ID) {
       return new SearchContactsCursorLoader(getContext());
-    } else if (id == NEARBY_PLACES_ID) {
+    } else if (id == NEARBY_PLACES_LOADER_ID) {
       return new NearbyPlacesCursorLoader(getContext(), query);
+    } else if (id == REMOTE_DIRECTORIES_LOADER_ID) {
+      return new RemoteDirectoriesCursorLoader(getContext());
+    } else if (id == REMOTE_CONTACTS_LOADER_ID) {
+      return new RemoteContactsCursorLoader(getContext(), query, directories);
     } else {
       throw new IllegalStateException("Invalid loader id: " + id);
     }
@@ -115,14 +131,29 @@
 
   @Override
   public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
-    if (!(cursor instanceof SearchCursor)) {
+    if (cursor != null
+        && !(loader instanceof RemoteDirectoriesCursorLoader)
+        && !(cursor instanceof SearchCursor)) {
       throw Assert.createIllegalStateFailException("Cursors must implement SearchCursor");
     }
 
     if (loader instanceof SearchContactsCursorLoader) {
       adapter.setContactsCursor((SearchCursor) cursor);
+
     } else if (loader instanceof NearbyPlacesCursorLoader) {
       adapter.setNearbyPlacesCursor((SearchCursor) cursor);
+
+    } else if (loader instanceof RemoteContactsCursorLoader) {
+      adapter.setRemoteContactsCursor((SearchCursor) cursor);
+
+    } else if (loader instanceof RemoteDirectoriesCursorLoader) {
+      directories.clear();
+      cursor.moveToPosition(-1);
+      while (cursor.moveToNext()) {
+        directories.add(RemoteDirectoriesCursorLoader.readDirectory(cursor));
+      }
+      loadRemoteContactsCursors();
+
     } else {
       throw new IllegalStateException("Invalid loader: " + loader);
     }
@@ -139,6 +170,7 @@
     if (adapter != null) {
       adapter.setQuery(query);
       loadNearbyPlacesCursor();
+      loadRemoteContactsCursors();
     }
   }
 
@@ -159,6 +191,7 @@
   public void onDestroy() {
     super.onDestroy();
     ThreadUtil.getUiThreadHandler().removeCallbacks(loadNearbyPlacesRunnable);
+    ThreadUtil.getUiThreadHandler().removeCallbacks(loadRemoteContactsRunnable);
   }
 
   private void loadNearbyPlacesCursor() {
@@ -198,4 +231,29 @@
           this, deniedPermissions, READ_CONTACTS_PERMISSION_REQUEST_CODE);
     }
   }
+
+  private void loadRemoteDirectoriesCursor() {
+    if (!remoteDirectoriesDisabledForTesting) {
+      getLoaderManager().initLoader(REMOTE_DIRECTORIES_LOADER_ID, null, this);
+    }
+  }
+
+  private void loadRemoteContactsCursors() {
+    if (remoteDirectoriesDisabledForTesting) {
+      return;
+    }
+
+    // Cancel existing load if one exists.
+    ThreadUtil.getUiThreadHandler().removeCallbacks(loadRemoteContactsRunnable);
+    ThreadUtil.getUiThreadHandler()
+        .postDelayed(loadRemoteContactsRunnable, NETWORK_SEARCH_DELAY_MILLIS);
+  }
+
+  // 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
+  @VisibleForTesting
+  public void setRemoteDirectoriesDisabled(boolean disabled) {
+    remoteDirectoriesDisabledForTesting = disabled;
+  }
 }
diff --git a/java/com/android/dialer/searchfragment/list/SearchAdapter.java b/java/com/android/dialer/searchfragment/list/SearchAdapter.java
index c8588fc..81e8e38 100644
--- a/java/com/android/dialer/searchfragment/list/SearchAdapter.java
+++ b/java/com/android/dialer/searchfragment/list/SearchAdapter.java
@@ -26,6 +26,7 @@
 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;
 
 /** RecyclerView adapter for {@link NewSearchFragment}. */
 class SearchAdapter extends RecyclerView.Adapter<ViewHolder> {
@@ -54,7 +55,9 @@
       case RowType.NEARBY_PLACES_HEADER:
         return new HeaderViewHolder(
             LayoutInflater.from(context).inflate(R.layout.header_layout, root, false));
-      case RowType.DIRECTORY_ROW: // TODO(calderwoodra): add directory rows to search
+      case RowType.DIRECTORY_ROW:
+        return new RemoteContactViewHolder(
+            LayoutInflater.from(context).inflate(R.layout.search_contact_row, root, false));
       case RowType.INVALID:
       default:
         throw Assert.createIllegalStateFailException("Invalid RowType: " + rowType);
@@ -72,8 +75,12 @@
       ((SearchContactViewHolder) holder).bind(searchCursorManager.getCursor(position), query);
     } else if (holder instanceof NearbyPlaceViewHolder) {
       ((NearbyPlaceViewHolder) holder).bind(searchCursorManager.getCursor(position), query);
+    } else if (holder instanceof RemoteContactViewHolder) {
+      ((RemoteContactViewHolder) holder).bind(searchCursorManager.getCursor(position), query);
     } else if (holder instanceof HeaderViewHolder) {
-      ((HeaderViewHolder) holder).setHeader(searchCursorManager.getHeaderText(position));
+      String header =
+          searchCursorManager.getCursor(position).getString(SearchCursor.HEADER_TEXT_POSITION);
+      ((HeaderViewHolder) holder).setHeader(header);
     } else {
       throw Assert.createIllegalStateFailException("Invalid ViewHolder: " + holder);
     }
@@ -101,7 +108,14 @@
   }
 
   public void setNearbyPlacesCursor(SearchCursor nearbyPlacesCursor) {
-    searchCursorManager.setNearbyPlacesCursor(nearbyPlacesCursor);
-    notifyDataSetChanged();
+    if (searchCursorManager.setNearbyPlacesCursor(nearbyPlacesCursor)) {
+      notifyDataSetChanged();
+    }
+  }
+
+  public void setRemoteContactsCursor(SearchCursor remoteContactsCursor) {
+    if (searchCursorManager.setCorpDirectoryCursor(remoteContactsCursor)) {
+      notifyDataSetChanged();
+    }
   }
 }
diff --git a/java/com/android/dialer/searchfragment/list/SearchCursorManager.java b/java/com/android/dialer/searchfragment/list/SearchCursorManager.java
index 68f770a..b385aa3 100644
--- a/java/com/android/dialer/searchfragment/list/SearchCursorManager.java
+++ b/java/com/android/dialer/searchfragment/list/SearchCursorManager.java
@@ -57,6 +57,7 @@
   })
   @interface RowType {
     int INVALID = 0;
+    // TODO(calderwoodra) add suggestions header and list
     /** Header to mark the start of contact rows. */
     int CONTACT_HEADER = 1;
     /** A row containing contact information for contacts stored locally on device. */
@@ -75,9 +76,10 @@
   private SearchCursor nearbyPlacesCursor = null;
   private SearchCursor corpDirectoryCursor = null;
 
-  void setContactsCursor(SearchCursor cursor) {
+  /** Returns true if the cursor changed. */
+  boolean setContactsCursor(SearchCursor cursor) {
     if (cursor == contactsCursor) {
-      return;
+      return false;
     }
 
     if (contactsCursor != null && !contactsCursor.isClosed()) {
@@ -89,11 +91,13 @@
     } else {
       contactsCursor = null;
     }
+    return true;
   }
 
-  void setNearbyPlacesCursor(SearchCursor cursor) {
+  /** Returns true if the cursor changed. */
+  boolean setNearbyPlacesCursor(SearchCursor cursor) {
     if (cursor == nearbyPlacesCursor) {
-      return;
+      return false;
     }
 
     if (nearbyPlacesCursor != null && !nearbyPlacesCursor.isClosed()) {
@@ -105,11 +109,13 @@
     } else {
       nearbyPlacesCursor = null;
     }
+    return true;
   }
 
-  void setCorpDirectoryCursor(SearchCursor cursor) {
+  /** Returns true if a cursor changed. */
+  boolean setCorpDirectoryCursor(SearchCursor cursor) {
     if (cursor == corpDirectoryCursor) {
-      return;
+      return false;
     }
 
     if (corpDirectoryCursor != null && !corpDirectoryCursor.isClosed()) {
@@ -121,6 +127,7 @@
     } else {
       corpDirectoryCursor = null;
     }
+    return true;
   }
 
   boolean setQuery(String query) {
@@ -216,10 +223,6 @@
     throw Assert.createIllegalStateFailException("No valid cursor.");
   }
 
-  String getHeaderText(int position) {
-    return getCursor(position).getString(SearchCursor.HEADER_TEXT_POSITION);
-  }
-
   /** removes all cursors. */
   void clear() {
     if (contactsCursor != null) {
diff --git a/java/com/android/dialer/searchfragment/list/res/layout/header_layout.xml b/java/com/android/dialer/searchfragment/list/res/layout/header_layout.xml
index 36af42e..eef0dee 100644
--- a/java/com/android/dialer/searchfragment/list/res/layout/header_layout.xml
+++ b/java/com/android/dialer/searchfragment/list/res/layout/header_layout.xml
@@ -18,5 +18,6 @@
     android:id="@+id/header"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
+    android:layout_marginTop="8dp"
     android:paddingStart="16dp"
     style="@style/SecondaryText"/>
diff --git a/java/com/android/dialer/searchfragment/remote/AndroidManifest.xml b/java/com/android/dialer/searchfragment/remote/AndroidManifest.xml
new file mode 100644
index 0000000..e52f531
--- /dev/null
+++ b/java/com/android/dialer/searchfragment/remote/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ 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
+ -->
+<manifest  package="com.android.dialer.searchfragment.remote"/>
\ 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 18a8718..5fb12d3 100644
--- a/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java
+++ b/java/com/android/dialer/searchfragment/remote/RemoteContactViewHolder.java
@@ -34,6 +34,7 @@
 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.SearchCursor;
 import com.android.dialer.telecom.TelecomUtil;
 
 /** ViewHolder for a nearby place row. */
@@ -60,7 +61,7 @@
    * Binds the ViewHolder with a cursor from {@link RemoteContactsCursorLoader} with the data found
    * at the cursors current position.
    */
-  public void bind(Cursor cursor, String query) {
+  public void bind(SearchCursor cursor, String query) {
     number = cursor.getString(Projections.PHONE_NUMBER);
     String name = cursor.getString(Projections.PHONE_DISPLAY_NAME);
     String label = getLabel(context.getResources(), cursor);
@@ -91,17 +92,19 @@
     }
   }
 
-  private boolean shouldShowPhoto(Cursor cursor) {
+  // Show the contact photo next to only the first number if a contact has multiple numbers
+  private boolean shouldShowPhoto(SearchCursor cursor) {
     int currentPosition = cursor.getPosition();
-    if (currentPosition == 0) {
-      return true;
-    }
-
     String currentLookupKey = cursor.getString(Projections.PHONE_LOOKUP_KEY);
     cursor.moveToPosition(currentPosition - 1);
-    String previousLookupKey = cursor.getString(Projections.PHONE_LOOKUP_KEY);
+
+    if (!cursor.isHeader() && !cursor.isBeforeFirst()) {
+      String previousLookupKey = cursor.getString(Projections.PHONE_LOOKUP_KEY);
+      cursor.moveToPosition(currentPosition);
+      return !currentLookupKey.equals(previousLookupKey);
+    }
     cursor.moveToPosition(currentPosition);
-    return !currentLookupKey.equals(previousLookupKey);
+    return true;
   }
 
   // TODO(calderwoodra): unify this into a utility method with CallLogAdapter#getNumberType
diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java
new file mode 100644
index 0000000..d7c4f38
--- /dev/null
+++ b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java
@@ -0,0 +1,105 @@
+/*
+ * 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.remote;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import com.android.dialer.common.Assert;
+import com.android.dialer.searchfragment.common.SearchCursor;
+import com.android.dialer.searchfragment.remote.RemoteDirectoriesCursorLoader.Directory;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@link MergeCursor} used for combining remote directory cursors into one cursor.
+ *
+ * <p>Usually a device with multiple Google accounts will have multiple remote directories returned
+ * by {@link RemoteDirectoriesCursorLoader}, each represented as a {@link Directory}.
+ *
+ * <p>This cursor merges them together with a header at the start of each cursor/list using {@link
+ * Directory#getDisplayName()} as the header text.
+ */
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+public final class RemoteContactsCursor extends MergeCursor implements SearchCursor {
+
+  /**
+   * Returns a single cursor with headers inserted between each non-empty cursor. If all cursors are
+   * empty, null or closed, this method returns null.
+   */
+  @Nullable
+  @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+  public static RemoteContactsCursor newInstance(
+      Context context, Cursor[] cursors, List<Directory> directories) {
+    Assert.checkArgument(
+        cursors.length == directories.size(), "Directories and cursors must be the same size.");
+    Cursor[] cursorsWithHeaders = insertHeaders(context, cursors, directories);
+    if (cursorsWithHeaders.length > 0) {
+      return new RemoteContactsCursor(cursorsWithHeaders);
+    }
+    return null;
+  }
+
+  private RemoteContactsCursor(Cursor[] cursors) {
+    super(cursors);
+  }
+
+  private static Cursor[] insertHeaders(
+      Context context, Cursor[] cursors, List<Directory> directories) {
+    List<Cursor> cursorList = new ArrayList<>();
+    for (int i = 0; i < cursors.length; i++) {
+      Cursor cursor = cursors[i];
+
+      if (cursor == null || cursor.isClosed()) {
+        continue;
+      }
+
+      Directory directory = directories.get(i);
+      if (cursor.getCount() == 0) {
+        // Since the cursor isn't being merged in, we need to close it here.
+        cursor.close();
+        continue;
+      }
+
+      cursorList.add(createHeaderCursor(context, directory.getDisplayName()));
+      cursorList.add(cursor);
+    }
+    return cursorList.toArray(new Cursor[cursorList.size()]);
+  }
+
+  private static MatrixCursor createHeaderCursor(Context context, String name) {
+    MatrixCursor headerCursor = new MatrixCursor(HEADER_PROJECTION, 1);
+    headerCursor.addRow(new String[] {context.getString(R.string.directory, name)});
+    return headerCursor;
+  }
+
+  /** Returns true if the current position is a header row. */
+  @Override
+  public boolean isHeader() {
+    return !isClosed() && getColumnIndex(HEADER_PROJECTION[HEADER_TEXT_POSITION]) != -1;
+  }
+
+  @Override
+  public boolean updateQuery(@Nullable String query) {
+    // When the query changes, a new network request is made for nearby places. Meaning this cursor
+    // will be closed and another created, so return false.
+    return false;
+  }
+}
diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
index c9cd765..771b7f1 100644
--- a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
+++ b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
@@ -18,17 +18,26 @@
 
 import android.content.Context;
 import android.content.CursorLoader;
+import android.database.Cursor;
 import android.net.Uri;
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
 import com.android.dialer.searchfragment.common.Projections;
 import com.android.dialer.searchfragment.remote.RemoteDirectoriesCursorLoader.Directory;
+import java.util.List;
 
-/** Cursor loader to load extended contacts on device. */
-final class RemoteContactsCursorLoader extends CursorLoader {
+/**
+ * Cursor loader to load extended contacts on device.
+ *
+ * <p>This loader performs several database queries in serial and merges the resulting cursors
+ * together into {@link RemoteContactsCursor}. If there are no results, the loader will return a
+ * null cursor.
+ */
+public final class RemoteContactsCursorLoader extends CursorLoader {
 
   private static final Uri ENTERPRISE_CONTENT_FILTER_URI =
       Uri.withAppendedPath(Phone.CONTENT_URI, "filter_enterprise");
@@ -36,25 +45,55 @@
   private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE = "length(" + Phone.NUMBER + ") < 1000";
   private static final String MAX_RESULTS = "20";
 
-  private final Directory directory;
+  private final String query;
+  private final List<Directory> directories;
+  private final Cursor[] cursors;
 
-  RemoteContactsCursorLoader(Context context, String query, Directory directory) {
+  public RemoteContactsCursorLoader(Context context, String query, List<Directory> directories) {
     super(
         context,
-        getContentFilterUri(query, directory.getId()),
+        null,
         Projections.PHONE_PROJECTION,
         IGNORE_NUMBER_TOO_LONG_CLAUSE,
         null,
         Phone.SORT_KEY_PRIMARY);
-    this.directory = directory;
+    this.query = query;
+    this.directories = directories;
+    cursors = new Cursor[directories.size()];
+  }
+
+  @Override
+  public Cursor loadInBackground() {
+    for (int i = 0; i < directories.size(); i++) {
+      Directory directory = directories.get(i);
+      // Since the on device contacts could be queried as remote directories and we already query
+      // them in SearchContactsCursorLoader, avoid querying them again.
+      // TODO(calderwoodra): It's a happy coincidence that on device contacts don't have directory
+      // names set, leaving this todo to investigate a better way to isolate them from other remote
+      // directories.
+      if (TextUtils.isEmpty(directory.getDisplayName())) {
+        cursors[i] = null;
+        continue;
+      }
+      cursors[i] =
+          getContext()
+              .getContentResolver()
+              .query(
+                  getContentFilterUri(query, directory.getId()),
+                  getProjection(),
+                  getSelection(),
+                  getSelectionArgs(),
+                  getSortOrder());
+    }
+    return RemoteContactsCursor.newInstance(getContext(), cursors, directories);
   }
 
   @VisibleForTesting
   static Uri getContentFilterUri(String query, int directoryId) {
-    Uri baseUri = Phone.CONTENT_FILTER_URI;
-    if (VERSION.SDK_INT >= VERSION_CODES.N) {
-      baseUri = ENTERPRISE_CONTENT_FILTER_URI;
-    }
+    Uri baseUri =
+        VERSION.SDK_INT >= VERSION_CODES.N
+            ? ENTERPRISE_CONTENT_FILTER_URI
+            : Phone.CONTENT_FILTER_URI;
 
     return baseUri
         .buildUpon()
@@ -64,8 +103,4 @@
         .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, MAX_RESULTS)
         .build();
   }
-
-  public Directory getDirectory() {
-    return directory;
-  }
 }
diff --git a/java/com/android/dialer/searchfragment/remote/RemoteDirectoriesCursorLoader.java b/java/com/android/dialer/searchfragment/remote/RemoteDirectoriesCursorLoader.java
index 630c73c..327a62c 100644
--- a/java/com/android/dialer/searchfragment/remote/RemoteDirectoriesCursorLoader.java
+++ b/java/com/android/dialer/searchfragment/remote/RemoteDirectoriesCursorLoader.java
@@ -44,12 +44,12 @@
     ContactsContract.Directory.PHOTO_SUPPORT,
   };
 
-  RemoteDirectoriesCursorLoader(Context context) {
+  public RemoteDirectoriesCursorLoader(Context context) {
     super(context, getContentUri(), PROJECTION, null, null, ContactsContract.Directory._ID);
   }
 
   /** @return current cursor row represented as a {@link Directory}. */
-  static Directory readDirectory(Cursor cursor) {
+  public static Directory readDirectory(Cursor cursor) {
     return Directory.create(
         cursor.getInt(ID), cursor.getString(DISPLAY_NAME), cursor.getInt(PHOTO_SUPPORT) != 0);
   }
@@ -63,7 +63,7 @@
   /** POJO representing the results returned from {@link RemoteDirectoriesCursorLoader}. */
   @AutoValue
   public abstract static class Directory {
-    static Directory create(int id, @Nullable String displayName, boolean supportsPhotos) {
+    public static Directory create(int id, @Nullable String displayName, boolean supportsPhotos) {
       return new AutoValue_RemoteDirectoriesCursorLoader_Directory(id, displayName, supportsPhotos);
     }
 
diff --git a/java/com/android/dialer/searchfragment/remote/res/values/strings.xml b/java/com/android/dialer/searchfragment/remote/res/values/strings.xml
new file mode 100644
index 0000000..beabba1
--- /dev/null
+++ b/java/com/android/dialer/searchfragment/remote/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?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
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <!-- Label for a list of contacts stored in a seperate directory [CHAR LIMIT=30]-->
+  <string name="directory">Directory <xliff:g example="google.com" id="email">%1$s</xliff:g></string>
+</resources>
\ No newline at end of file