Merge "Migrate cp2 search to use SmartDialerCursorLoader and Phone.CONTENT_FILTER_URI."
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())