From 04a275cc28c484481bbaed74bfa9a0cba2e0a002 Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Thu, 7 Dec 2017 18:15:58 -0800 Subject: Migrate cp2 search to use SmartDialerCursorLoader and Phone.CONTENT_FILTER_URI. Bug: 70336190,70348007 Test: existing PiperOrigin-RevId: 178325355 Change-Id: Ic43beb7a10c5127083ed33e69603b25b2831754f --- .../dialer/searchfragment/common/Projections.java | 39 ++-- .../cp2/SearchContactsCursorLoader.java | 198 ++++++++++++++++----- .../searchfragment/list/NewSearchFragment.java | 13 +- 3 files changed, 184 insertions(+), 66 deletions(-) diff --git a/java/com/android/dialer/searchfragment/common/Projections.java b/java/com/android/dialer/searchfragment/common/Projections.java index cebe5c9a9..e0c74ed66 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 class Projections { 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_ALTERNATIVE = 12; + public static final int SORT_KEY = 10; + 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 @@ public class Projections { 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 7624bc712..23e3f9d88 100644 --- a/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java +++ b/java/com/android/dialer/searchfragment/cp2/SearchContactsCursorLoader.java @@ -19,70 +19,180 @@ package com.android.dialer.searchfragment.cp2; 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; + getSortKey(context) + " ASC"); + this.query = TextUtils.isEmpty(query) ? "" : query; + this.isRegularSearch = isRegularSearch; + } - 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); - } + 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; } - /** - * 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. - * - *

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 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 8fe0918c3..1e630488d 100644 --- a/java/com/android/dialer/searchfragment/list/NewSearchFragment.java +++ b/java/com/android/dialer/searchfragment/list/NewSearchFragment.java @@ -120,6 +120,8 @@ public final class NewSearchFragment extends Fragment private boolean remoteDirectoriesDisabledForTesting; private final List 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 final class NewSearchFragment extends Fragment public Loader 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 @@ public final class NewSearchFragment extends Fragment adapter.setQuery(query, rawNumber, callInitiationType); adapter.setSearchActions(getActions()); adapter.setZeroSuggestVisible(isRegularSearch()); + loadCp2ContactsCursor(); loadNearbyPlacesCursor(); loadRemoteContactsCursors(); } @@ -304,6 +307,7 @@ public final class NewSearchFragment extends Fragment @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 @@ public final class NewSearchFragment extends Fragment .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()) -- cgit v1.2.3