Migrate cp2 search to use SmartDialerCursorLoader and Phone.CONTENT_FILTER_URI.

Bug: 70336190,70348007
Test: existing
PiperOrigin-RevId: 178325355
Change-Id: Ic43beb7a10c5127083ed33e69603b25b2831754f
diff --git a/java/com/android/dialer/searchfragment/common/Projections.java b/java/com/android/dialer/searchfragment/common/Projections.java
index cebe5c9..e0c74ed 100644
--- a/java/com/android/dialer/searchfragment/common/Projections.java
+++ b/java/com/android/dialer/searchfragment/common/Projections.java
@@ -16,8 +16,6 @@
 
 package com.android.dialer.searchfragment.common;
 
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.Data;
 
@@ -34,33 +32,32 @@
   public static final int LOOKUP_KEY = 7;
   public static final int CARRIER_PRESENCE = 8;
   public static final int CONTACT_ID = 9;
-  public static final int MIME_TYPE = 10;
 
   @SuppressWarnings("unused")
-  public static final int SORT_KEY = 11;
+  public static final int SORT_KEY = 10;
 
-  public static final int SORT_ALTERNATIVE = 12;
-
+  public static final int SORT_ALTERNATIVE = 11;
+  public static final int MIME_TYPE = 12;
   public static final int COMPANY_NAME = 13;
   public static final int NICKNAME = 14;
 
   public static final String[] CP2_PROJECTION =
       new String[] {
-        Data._ID, // 0
+        Phone._ID, // 0
         Phone.TYPE, // 1
         Phone.LABEL, // 2
         Phone.NUMBER, // 3
-        Data.DISPLAY_NAME_PRIMARY, // 4
-        Data.PHOTO_ID, // 5
-        Data.PHOTO_THUMBNAIL_URI, // 6
-        Data.LOOKUP_KEY, // 7
-        Data.CARRIER_PRESENCE, // 8
-        Data.CONTACT_ID, // 9
-        Data.MIMETYPE, // 10
-        Data.SORT_KEY_PRIMARY, // 11
-        Data.SORT_KEY_ALTERNATIVE, // 12
-        Organization.COMPANY, // 13
-        Nickname.NAME // 14
+        Phone.DISPLAY_NAME_PRIMARY, // 4
+        Phone.PHOTO_ID, // 5
+        Phone.PHOTO_THUMBNAIL_URI, // 6
+        Phone.LOOKUP_KEY, // 7
+        Phone.CARRIER_PRESENCE, // 8
+        Phone.CONTACT_ID, // 9
+        Phone.SORT_KEY_PRIMARY, // 10
+        Phone.SORT_KEY_ALTERNATIVE, // 11
+        // Data.MIMETYPE, // 12
+        // Organization.COMPANY, // 13
+        // Nickname.NAME // 14
       };
 
   // Uses alternative display names (i.e. "Bob Dylan" becomes "Dylan, Bob").
@@ -76,11 +73,11 @@
         Data.LOOKUP_KEY, // 7
         Data.CARRIER_PRESENCE, // 8
         Data.CONTACT_ID, // 9
-        Data.MIMETYPE, // 10
         Data.SORT_KEY_PRIMARY, // 11
         Data.SORT_KEY_ALTERNATIVE, // 12
-        Organization.COMPANY, // 13
-        Nickname.NAME // 14
+        // Data.MIMETYPE, // 12
+        // Organization.COMPANY, // 13
+        // Nickname.NAME // 14
       };
 
   public static final String[] DATA_PROJECTION =
diff --git a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java
index 7624bc7..23e3f9d 100644
--- a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java
+++ b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java
@@ -19,70 +19,180 @@
 import android.content.Context;
 import android.content.CursorLoader;
 import android.database.Cursor;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
+import android.net.Uri;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.text.TextUtils;
 import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.dialer.dialpadview.SmartDialCursorLoader;
 import com.android.dialer.searchfragment.common.Projections;
+import com.android.dialer.searchfragment.common.SearchCursor;
 
 /** Cursor Loader for CP2 contacts. */
 public final class SearchContactsCursorLoader extends CursorLoader {
 
   private final String query;
+  private final boolean isRegularSearch;
 
   /** @param query Contacts cursor will be filtered based on this query. */
-  public SearchContactsCursorLoader(Context context, @Nullable String query) {
+  public SearchContactsCursorLoader(
+      Context context, @Nullable String query, boolean isRegularSearch) {
     super(
         context,
-        Data.CONTENT_URI,
-        Projections.CP2_PROJECTION,
-        whereStatement(),
+        buildUri(query),
+        getProjection(context),
+        getWhere(context),
         null,
-        Phone.SORT_KEY_PRIMARY + " ASC");
-    this.query = query;
-
-    ContactsPreferences preferences = new ContactsPreferences(getContext());
-    if (preferences.getSortOrder() == ContactsPreferences.SORT_ORDER_ALTERNATIVE) {
-      setSortOrder(Phone.SORT_KEY_ALTERNATIVE + " ASC");
-    }
-    if (preferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE) {
-      setProjection(Projections.CP2_PROJECTION_ALTERNATIVE);
-    }
+        getSortKey(context) + " ASC");
+    this.query = TextUtils.isEmpty(query) ? "" : query;
+    this.isRegularSearch = isRegularSearch;
   }
 
-  /**
-   * Note: ContactsProvider can make no guarantee that any given field is non-null, and display name
-   * has been observed to be null in the wild, though it is unclear when that might happen (possibly
-   * a third-party is inserting such data). See a bug.
-   *
-   * <p>We skip showing contacts without a display name because there is no UI treatment for showing
-   * such results. (Note that even contacts with only a number still have a display name set to the
-   * number.)
-   */
-  private static String whereStatement() {
-    return (Phone.NUMBER + " IS NOT NULL")
-        + " AND "
-        + (Data.DISPLAY_NAME_PRIMARY + " IS NOT NULL")
-        + " AND "
-        + Data.MIMETYPE
-        + " IN (\'"
-        + Phone.CONTENT_ITEM_TYPE
-        + "\', \'"
-        + Nickname.CONTENT_ITEM_TYPE
-        + "\', \'"
-        + Organization.CONTENT_ITEM_TYPE
-        + "\')";
+  private static String[] getProjection(Context context) {
+    ContactsPreferences contactsPrefs = new ContactsPreferences(context);
+    boolean displayOrderPrimary =
+        (contactsPrefs.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY);
+    return displayOrderPrimary
+        ? Projections.CP2_PROJECTION
+        : Projections.CP2_PROJECTION_ALTERNATIVE;
+  }
+
+  private static String getWhere(Context context) {
+    String where = getProjection(context)[Projections.DISPLAY_NAME] + " IS NOT NULL";
+    where += " AND " + Phone.NUMBER + " IS NOT NULL";
+    return where;
+  }
+
+  private static String getSortKey(Context context) {
+    ContactsPreferences contactsPrefs = new ContactsPreferences(context);
+    boolean sortOrderPrimary =
+        (contactsPrefs.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY);
+    return sortOrderPrimary ? Phone.SORT_KEY_PRIMARY : Phone.SORT_KEY_ALTERNATIVE;
+  }
+
+  private static Uri buildUri(String query) {
+    return Phone.CONTENT_FILTER_URI.buildUpon().appendPath(query).build();
   }
 
   @Override
   public Cursor loadInBackground() {
-    // All contacts
-    Cursor cursor = super.loadInBackground();
-    // Filtering logic
-    ContactFilterCursor contactFilterCursor = new ContactFilterCursor(cursor, query, getContext());
-    // Header logic
-    return SearchContactsCursor.newInstance(getContext(), contactFilterCursor);
+    return isRegularSearch ? regularSearchLoadInBackground() : dialpadSearchLoadInBackground();
+  }
+
+  private Cursor regularSearchLoadInBackground() {
+    return RegularSearchCursor.newInstance(getContext(), super.loadInBackground());
+  }
+
+  private Cursor dialpadSearchLoadInBackground() {
+    SmartDialCursorLoader loader = new SmartDialCursorLoader(getContext());
+    loader.configureQuery(query);
+    Cursor cursor = loader.loadInBackground();
+    return SmartDialCursor.newInstance(getContext(), cursor);
+  }
+
+  static class SmartDialCursor extends MergeCursor implements SearchCursor {
+
+    static SmartDialCursor newInstance(Context context, Cursor smartDialCursor) {
+      if (smartDialCursor.getCount() == 0) {
+        return new SmartDialCursor(new Cursor[] {new MatrixCursor(Projections.CP2_PROJECTION)});
+      }
+
+      MatrixCursor headerCursor = new MatrixCursor(HEADER_PROJECTION);
+      headerCursor.addRow(new String[] {context.getString(R.string.all_contacts)});
+      return new SmartDialCursor(
+          new Cursor[] {headerCursor, convertSmartDialCursorToSearchCursor(smartDialCursor)});
+    }
+
+    private SmartDialCursor(Cursor[] cursors) {
+      super(cursors);
+    }
+
+    @Override
+    public boolean isHeader() {
+      return isFirst();
+    }
+
+    @Override
+    public boolean updateQuery(@Nullable String query) {
+      return false;
+    }
+
+    @Override
+    public long getDirectoryId() {
+      return Directory.DEFAULT;
+    }
+
+    private static MatrixCursor convertSmartDialCursorToSearchCursor(Cursor smartDialCursor) {
+      MatrixCursor cursor = new MatrixCursor(Projections.CP2_PROJECTION);
+      if (!smartDialCursor.moveToFirst()) {
+        return cursor;
+      }
+
+      do {
+        Object[] newRow = new Object[Projections.CP2_PROJECTION.length];
+        for (int i = 0; i < Projections.CP2_PROJECTION.length; i++) {
+          String column = Projections.CP2_PROJECTION[i];
+          int index = smartDialCursor.getColumnIndex(column);
+          if (index != -1) {
+            switch (smartDialCursor.getType(index)) {
+              case FIELD_TYPE_INTEGER:
+                newRow[i] = smartDialCursor.getInt(index);
+                break;
+              case FIELD_TYPE_STRING:
+                newRow[i] = smartDialCursor.getString(index);
+                break;
+              case FIELD_TYPE_FLOAT:
+                newRow[i] = smartDialCursor.getFloat(index);
+                break;
+              case FIELD_TYPE_BLOB:
+                newRow[i] = smartDialCursor.getBlob(index);
+                break;
+              case FIELD_TYPE_NULL:
+              default:
+                // No-op
+                break;
+            }
+          }
+        }
+        cursor.addRow(newRow);
+      } while (smartDialCursor.moveToNext());
+      return cursor;
+    }
+  }
+
+  static class RegularSearchCursor extends MergeCursor implements SearchCursor {
+
+    static RegularSearchCursor newInstance(Context context, Cursor regularSearchCursor) {
+      if (regularSearchCursor.getCount() == 0) {
+        return new RegularSearchCursor(new Cursor[] {new MatrixCursor(Projections.CP2_PROJECTION)});
+      }
+
+      MatrixCursor headerCursor = new MatrixCursor(HEADER_PROJECTION);
+      headerCursor.addRow(new String[] {context.getString(R.string.all_contacts)});
+      return new RegularSearchCursor(new Cursor[] {headerCursor, regularSearchCursor});
+    }
+
+    public RegularSearchCursor(Cursor[] cursors) {
+      super(cursors);
+    }
+
+    @Override
+    public boolean isHeader() {
+      return isFirst();
+    }
+
+    @Override
+    public boolean updateQuery(@NonNull String query) {
+      return false; // no-op
+    }
+
+    @Override
+    public long getDirectoryId() {
+      return 0; // no-op
+    }
   }
 }
diff --git a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
index 8fe0918..1e63048 100644
--- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
+++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java
@@ -120,6 +120,8 @@
   private boolean remoteDirectoriesDisabledForTesting;
 
   private final List<Directory> directories = new ArrayList<>();
+  private final Runnable loaderCp2ContactsRunnable =
+      () -> getLoaderManager().restartLoader(CONTACTS_LOADER_ID, null, this);
   private final Runnable loadNearbyPlacesRunnable =
       () -> getLoaderManager().restartLoader(NEARBY_PLACES_LOADER_ID, null, this);
   private final Runnable loadRemoteContactsRunnable =
@@ -189,7 +191,7 @@
   public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
     LogUtil.i("NewSearchFragment.onCreateLoader", "loading cursor: " + id);
     if (id == CONTACTS_LOADER_ID) {
-      return new SearchContactsCursorLoader(getContext(), query);
+      return new SearchContactsCursorLoader(getContext(), query, isRegularSearch());
     } else if (id == NEARBY_PLACES_LOADER_ID) {
       // 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
@@ -263,6 +265,7 @@
       adapter.setQuery(query, rawNumber, callInitiationType);
       adapter.setSearchActions(getActions());
       adapter.setZeroSuggestVisible(isRegularSearch());
+      loadCp2ContactsCursor();
       loadNearbyPlacesCursor();
       loadRemoteContactsCursors();
     }
@@ -304,6 +307,7 @@
   @Override
   public void onDestroy() {
     super.onDestroy();
+    ThreadUtil.getUiThreadHandler().removeCallbacks(loaderCp2ContactsRunnable);
     ThreadUtil.getUiThreadHandler().removeCallbacks(loadNearbyPlacesRunnable);
     ThreadUtil.getUiThreadHandler().removeCallbacks(loadRemoteContactsRunnable);
     ThreadUtil.getUiThreadHandler().removeCallbacks(capabilitiesUpdatedRunnable);
@@ -360,6 +364,13 @@
         .postDelayed(loadRemoteContactsRunnable, NETWORK_SEARCH_DELAY_MILLIS);
   }
 
+  private void loadCp2ContactsCursor() {
+    // Cancel existing load if one exists.
+    ThreadUtil.getUiThreadHandler().removeCallbacks(loaderCp2ContactsRunnable);
+    ThreadUtil.getUiThreadHandler()
+        .postDelayed(loaderCp2ContactsRunnable, NETWORK_SEARCH_DELAY_MILLIS);
+  }
+
   // Should not be called before remote directories (not contacts) have finished loading.
   private void loadNearbyPlacesCursor() {
     if (!PermissionsUtil.hasLocationPermissions(getContext())