Improve & reorganize logic related to directories/remote contacts in the search fragment.

Test: DirectoriesCursorLoaderTest, RemoteContactsCursorLoaderTest
PiperOrigin-RevId: 182578207
Change-Id: I03c81bd8581c8abbef1bbca1a960f3380d588d22
diff --git a/java/com/android/dialer/searchfragment/directories/AndroidManifest.xml b/java/com/android/dialer/searchfragment/directories/AndroidManifest.xml
new file mode 100644
index 0000000..a7294cd
--- /dev/null
+++ b/java/com/android/dialer/searchfragment/directories/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~      http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest package="com.android.dialer.searchfragment.directories"/>
\ No newline at end of file
diff --git a/java/com/android/dialer/searchfragment/directories/DirectoriesCursorLoader.java b/java/com/android/dialer/searchfragment/directories/DirectoriesCursorLoader.java
new file mode 100644
index 0000000..edf5f24
--- /dev/null
+++ b/java/com/android/dialer/searchfragment/directories/DirectoriesCursorLoader.java
@@ -0,0 +1,90 @@
+/*
+
+* 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.directories;
+
+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.support.annotation.Nullable;
+import com.google.auto.value.AutoValue;
+import java.util.ArrayList;
+import java.util.List;
+
+/** {@link CursorLoader} to load the list of all directories (local and remote). */
+public final class DirectoriesCursorLoader extends CursorLoader {
+
+  public static final String[] PROJECTION = {
+    ContactsContract.Directory._ID,
+    ContactsContract.Directory.DISPLAY_NAME,
+    ContactsContract.Directory.PHOTO_SUPPORT,
+  };
+
+  // Indices of columns in PROJECTION
+  private static final int ID = 0;
+  private static final int DISPLAY_NAME = 1;
+  private static final int PHOTO_SUPPORT = 2;
+
+  public DirectoriesCursorLoader(Context context) {
+    super(context, getContentUri(), PROJECTION, null, null, ContactsContract.Directory._ID);
+  }
+
+  /**
+   * Creates a complete list of directories from the data set loaded by this loader.
+   *
+   * @param cursor A cursor pointing to the data set loaded by this loader. The caller must ensure
+   *     the cursor is not null.
+   * @return A list of directories.
+   */
+  public static List<Directory> toDirectories(Cursor cursor) {
+    List<Directory> directories = new ArrayList<>();
+    cursor.moveToPosition(-1);
+    while (cursor.moveToNext()) {
+      directories.add(
+          Directory.create(
+              cursor.getInt(ID),
+              cursor.getString(DISPLAY_NAME),
+              /* supportsPhotos = */ cursor.getInt(PHOTO_SUPPORT) != 0));
+    }
+    return directories;
+  }
+
+  private static Uri getContentUri() {
+    return VERSION.SDK_INT >= VERSION_CODES.N
+        ? ContactsContract.Directory.ENTERPRISE_CONTENT_URI
+        : ContactsContract.Directory.CONTENT_URI;
+  }
+
+  /** POJO representing the results returned from {@link DirectoriesCursorLoader}. */
+  @AutoValue
+  public abstract static class Directory {
+    public static Directory create(long id, @Nullable String displayName, boolean supportsPhotos) {
+      return new AutoValue_DirectoriesCursorLoader_Directory(id, displayName, supportsPhotos);
+    }
+
+    public abstract long getId();
+
+    /** Returns a user facing display name of the directory. Null if none exists. */
+    public abstract @Nullable String getDisplayName();
+
+    public abstract boolean supportsPhotos();
+  }
+}
diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
index 30949d3..c62d40e 100644
--- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
+++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
@@ -63,11 +63,11 @@
 import com.android.dialer.searchfragment.common.RowClickListener;
 import com.android.dialer.searchfragment.common.SearchCursor;
 import com.android.dialer.searchfragment.cp2.SearchContactsCursorLoader;
+import com.android.dialer.searchfragment.directories.DirectoriesCursorLoader;
+import com.android.dialer.searchfragment.directories.DirectoriesCursorLoader.Directory;
 import com.android.dialer.searchfragment.list.SearchActionViewHolder.Action;
 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.storage.StorageComponent;
 import com.android.dialer.util.CallUtil;
 import com.android.dialer.util.DialerUtils;
@@ -196,13 +196,13 @@
       // Directories represent contact data sources on the device, but since nearby places aren't
       // stored on the device, they don't have a directory ID. We pass the list of all existing IDs
       // so that we can find one that doesn't collide.
-      List<Integer> directoryIds = new ArrayList<>();
+      List<Long> directoryIds = new ArrayList<>();
       for (Directory directory : directories) {
         directoryIds.add(directory.getId());
       }
       return new NearbyPlacesCursorLoader(getContext(), query, directoryIds);
     } else if (id == REMOTE_DIRECTORIES_LOADER_ID) {
-      return new RemoteDirectoriesCursorLoader(getContext());
+      return new DirectoriesCursorLoader(getContext());
     } else if (id == REMOTE_CONTACTS_LOADER_ID) {
       return new RemoteContactsCursorLoader(getContext(), query, directories);
     } else {
@@ -214,7 +214,7 @@
   public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
     LogUtil.i("NewSearchFragment.onLoadFinished", "Loader finished: " + loader);
     if (cursor != null
-        && !(loader instanceof RemoteDirectoriesCursorLoader)
+        && !(loader instanceof DirectoriesCursorLoader)
         && !(cursor instanceof SearchCursor)) {
       throw Assert.createIllegalStateFailException("Cursors must implement SearchCursor");
     }
@@ -228,12 +228,9 @@
     } else if (loader instanceof RemoteContactsCursorLoader) {
       adapter.setRemoteContactsCursor((SearchCursor) cursor);
 
-    } else if (loader instanceof RemoteDirectoriesCursorLoader) {
+    } else if (loader instanceof DirectoriesCursorLoader) {
       directories.clear();
-      cursor.moveToPosition(-1);
-      while (cursor.moveToNext()) {
-        directories.add(RemoteDirectoriesCursorLoader.readDirectory(cursor));
-      }
+      directories.addAll(DirectoriesCursorLoader.toDirectories(cursor));
       loadNearbyPlacesCursor();
       loadRemoteContactsCursors();
 
diff --git a/java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlacesCursorLoader.java b/java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlacesCursorLoader.java
index 0d52c10..9ba6d56 100644
--- a/java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlacesCursorLoader.java
+++ b/java/com/android/dialer/searchfragment/nearbyplaces/NearbyPlacesCursorLoader.java
@@ -39,8 +39,7 @@
    *     order to find a directory ID for the nearby places cursor that doesn't collide with
    *     existing directories.
    */
-  public NearbyPlacesCursorLoader(
-      Context context, String query, @NonNull List<Integer> directoryIds) {
+  public NearbyPlacesCursorLoader(Context context, String query, @NonNull List<Long> directoryIds) {
     super(context, getContentUri(context, query), Projections.DATA_PROJECTION, null, null, null);
     this.directoryId = getDirectoryId(directoryIds);
   }
@@ -63,7 +62,7 @@
         .build();
   }
 
-  private static long getDirectoryId(List<Integer> directoryIds) {
+  private static long getDirectoryId(List<Long> directoryIds) {
     if (directoryIds.isEmpty()) {
       return INVALID_DIRECTORY_ID;
     }
diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java
index e9e83c1..9510443 100644
--- a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java
+++ b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursor.java
@@ -24,7 +24,8 @@
 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 com.android.dialer.searchfragment.directories.DirectoriesCursorLoader;
+import com.android.dialer.searchfragment.directories.DirectoriesCursorLoader.Directory;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -33,7 +34,7 @@
  * {@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}.
+ * by {@link DirectoriesCursorLoader}, 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.
@@ -98,7 +99,7 @@
     return cursorList.toArray(new Cursor[cursorList.size()]);
   }
 
-  private static MatrixCursor createHeaderCursor(Context context, String name, int id) {
+  private static MatrixCursor createHeaderCursor(Context context, String name, long id) {
     MatrixCursor headerCursor = new MatrixCursor(PROJECTION, 1);
     headerCursor.addRow(new Object[] {context.getString(R.string.directory, name), id});
     return headerCursor;
diff --git a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
index 5f92c49..9feeb7e 100644
--- a/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
+++ b/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
@@ -27,9 +27,8 @@
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.support.annotation.NonNull;
 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 com.android.dialer.searchfragment.directories.DirectoriesCursorLoader.Directory;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -70,15 +69,13 @@
   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())) {
+
+      // Filter out local directories
+      if (!isRemoteDirectory(directory.getId())) {
         cursors[i] = null;
         continue;
       }
+
       Cursor cursor =
           getContext()
               .getContentResolver()
@@ -96,6 +93,17 @@
     return RemoteContactsCursor.newInstance(getContext(), cursors, directories);
   }
 
+  private static boolean isRemoteDirectory(long directoryId) {
+    return VERSION.SDK_INT >= VERSION_CODES.N
+        ? ContactsContract.Directory.isRemoteDirectoryId(directoryId)
+        : (directoryId != ContactsContract.Directory.DEFAULT
+            && directoryId != ContactsContract.Directory.LOCAL_INVISIBLE
+            // Directory.ENTERPRISE_DEFAULT is the default work profile directory for locally stored
+            // contacts
+            && directoryId != ContactsContract.Directory.ENTERPRISE_DEFAULT
+            && directoryId != ContactsContract.Directory.ENTERPRISE_LOCAL_INVISIBLE);
+  }
+
   private MatrixCursor createMatrixCursorFilteringNullNumbers(Cursor cursor) {
     if (cursor == null) {
       return null;
@@ -140,7 +148,7 @@
   }
 
   @VisibleForTesting
-  static Uri getContentFilterUri(String query, int directoryId) {
+  static Uri getContentFilterUri(String query, long directoryId) {
     Uri baseUri =
         VERSION.SDK_INT >= VERSION_CODES.N
             ? ENTERPRISE_CONTENT_FILTER_URI
diff --git a/java/com/android/dialer/searchfragment/remote/RemoteDirectoriesCursorLoader.java b/java/com/android/dialer/searchfragment/remote/RemoteDirectoriesCursorLoader.java
deleted file mode 100644
index de71025..0000000
--- a/java/com/android/dialer/searchfragment/remote/RemoteDirectoriesCursorLoader.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
-
-* 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.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.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import com.google.auto.value.AutoValue;
-
-/** CursorLoader to load the list of remote directories on the device. */
-public final class RemoteDirectoriesCursorLoader extends CursorLoader {
-
-  /** Positions of columns in {@code PROJECTIONS}. */
-  private static final int ID = 0;
-
-  private static final int DISPLAY_NAME = 1;
-  private static final int PHOTO_SUPPORT = 2;
-
-  @VisibleForTesting
-  static final String[] PROJECTION = {
-    ContactsContract.Directory._ID,
-    ContactsContract.Directory.DISPLAY_NAME,
-    ContactsContract.Directory.PHOTO_SUPPORT,
-  };
-
-  public RemoteDirectoriesCursorLoader(Context context) {
-    super(context, getContentUri(), PROJECTION, null, null, ContactsContract.Directory._ID);
-  }
-
-  /** @return current cursor row represented as a {@link Directory}. */
-  public static Directory readDirectory(Cursor cursor) {
-    return Directory.create(
-        cursor.getInt(ID), cursor.getString(DISPLAY_NAME), cursor.getInt(PHOTO_SUPPORT) != 0);
-  }
-
-  private static Uri getContentUri() {
-    return VERSION.SDK_INT >= VERSION_CODES.N
-        ? ContactsContract.Directory.ENTERPRISE_CONTENT_URI
-        : ContactsContract.Directory.CONTENT_URI;
-  }
-
-  /** POJO representing the results returned from {@link RemoteDirectoriesCursorLoader}. */
-  @AutoValue
-  public abstract static class Directory {
-    public static Directory create(int id, @Nullable String displayName, boolean supportsPhotos) {
-      return new AutoValue_RemoteDirectoriesCursorLoader_Directory(id, displayName, supportsPhotos);
-    }
-
-    public abstract int getId();
-
-    /** Returns a user facing display name of the directory. Null if none exists. */
-    abstract @Nullable String getDisplayName();
-
-    abstract boolean supportsPhotos();
-  }
-}
diff --git a/packages.mk b/packages.mk
index d4b225a..0edf0b5 100644
--- a/packages.mk
+++ b/packages.mk
@@ -49,6 +49,7 @@
 	com.android.dialer.preferredsim.impl \
 	com.android.dialer.searchfragment.common \
 	com.android.dialer.searchfragment.cp2 \
+	com.android.dialer.searchfragment.directories \
 	com.android.dialer.searchfragment.list \
 	com.android.dialer.searchfragment.nearbyplaces \
 	com.android.dialer.searchfragment.remote \