1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.dialer.searchfragment.cp2; 18 19 import android.content.Context; 20 import android.content.CursorLoader; 21 import android.database.Cursor; 22 import android.database.MatrixCursor; 23 import android.database.MergeCursor; 24 import android.net.Uri; 25 import android.provider.ContactsContract.CommonDataKinds.Phone; 26 import android.provider.ContactsContract.Directory; 27 import android.support.annotation.NonNull; 28 import android.support.annotation.Nullable; 29 import android.text.TextUtils; 30 import com.android.dialer.common.LogUtil; 31 import com.android.dialer.contacts.ContactsComponent; 32 import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences.DisplayOrder; 33 import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences.SortOrder; 34 import com.android.dialer.searchfragment.common.Projections; 35 import com.android.dialer.searchfragment.common.SearchCursor; 36 import com.android.dialer.smartdial.SmartDialCursorLoader; 37 import com.android.dialer.util.PermissionsUtil; 38 39 /** Cursor Loader for CP2 contacts. */ 40 public final class SearchContactsCursorLoader extends CursorLoader { 41 42 private final String query; 43 private final boolean isRegularSearch; 44 45 /** @param query Contacts cursor will be filtered based on this query. */ SearchContactsCursorLoader( Context context, @Nullable String query, boolean isRegularSearch)46 public SearchContactsCursorLoader( 47 Context context, @Nullable String query, boolean isRegularSearch) { 48 super( 49 context, 50 buildUri(query), 51 getProjection(context), 52 getWhere(context), 53 null, 54 getSortKey(context) + " ASC"); 55 this.query = TextUtils.isEmpty(query) ? "" : query; 56 this.isRegularSearch = isRegularSearch; 57 } 58 getProjection(Context context)59 private static String[] getProjection(Context context) { 60 boolean displayOrderPrimary = 61 (ContactsComponent.get(context).contactDisplayPreferences().getDisplayOrder() 62 == DisplayOrder.PRIMARY); 63 return displayOrderPrimary 64 ? Projections.CP2_PROJECTION 65 : Projections.CP2_PROJECTION_ALTERNATIVE; 66 } 67 getWhere(Context context)68 private static String getWhere(Context context) { 69 String where = getProjection(context)[Projections.DISPLAY_NAME] + " IS NOT NULL"; 70 where += " AND " + Phone.NUMBER + " IS NOT NULL"; 71 return where; 72 } 73 getSortKey(Context context)74 private static String getSortKey(Context context) { 75 boolean sortOrderPrimary = 76 (ContactsComponent.get(context).contactDisplayPreferences().getSortOrder() 77 == SortOrder.BY_PRIMARY); 78 return sortOrderPrimary ? Phone.SORT_KEY_PRIMARY : Phone.SORT_KEY_ALTERNATIVE; 79 } 80 buildUri(String query)81 private static Uri buildUri(String query) { 82 return Phone.CONTENT_FILTER_URI.buildUpon().appendPath(query).build(); 83 } 84 85 @Override loadInBackground()86 public Cursor loadInBackground() { 87 if (!PermissionsUtil.hasContactsReadPermissions(getContext())) { 88 LogUtil.i("SearchContactsCursorLoader.loadInBackground", "Contacts permission denied."); 89 return null; 90 } 91 return isRegularSearch ? regularSearchLoadInBackground() : dialpadSearchLoadInBackground(); 92 } 93 regularSearchLoadInBackground()94 private Cursor regularSearchLoadInBackground() { 95 return RegularSearchCursor.newInstance(getContext(), super.loadInBackground()); 96 } 97 dialpadSearchLoadInBackground()98 private Cursor dialpadSearchLoadInBackground() { 99 SmartDialCursorLoader loader = new SmartDialCursorLoader(getContext()); 100 loader.configureQuery(query); 101 Cursor cursor = loader.loadInBackground(); 102 return SmartDialCursor.newInstance(getContext(), cursor); 103 } 104 105 static class SmartDialCursor extends MergeCursor implements SearchCursor { 106 newInstance(Context context, Cursor smartDialCursor)107 static SmartDialCursor newInstance(Context context, Cursor smartDialCursor) { 108 if (smartDialCursor == null || smartDialCursor.getCount() == 0) { 109 LogUtil.i("SmartDialCursor.newInstance", "Cursor was null or empty"); 110 return new SmartDialCursor(new Cursor[] {new MatrixCursor(Projections.CP2_PROJECTION)}); 111 } 112 113 MatrixCursor headerCursor = new MatrixCursor(HEADER_PROJECTION); 114 headerCursor.addRow(new String[] {context.getString(R.string.all_contacts)}); 115 return new SmartDialCursor( 116 new Cursor[] {headerCursor, convertSmartDialCursorToSearchCursor(smartDialCursor)}); 117 } 118 SmartDialCursor(Cursor[] cursors)119 private SmartDialCursor(Cursor[] cursors) { 120 super(cursors); 121 } 122 123 @Override isHeader()124 public boolean isHeader() { 125 return isFirst(); 126 } 127 128 @Override updateQuery(@ullable String query)129 public boolean updateQuery(@Nullable String query) { 130 return false; 131 } 132 133 @Override getDirectoryId()134 public long getDirectoryId() { 135 return Directory.DEFAULT; 136 } 137 convertSmartDialCursorToSearchCursor(Cursor smartDialCursor)138 private static MatrixCursor convertSmartDialCursorToSearchCursor(Cursor smartDialCursor) { 139 MatrixCursor cursor = new MatrixCursor(Projections.CP2_PROJECTION); 140 if (!smartDialCursor.moveToFirst()) { 141 return cursor; 142 } 143 144 do { 145 Object[] newRow = new Object[Projections.CP2_PROJECTION.length]; 146 for (int i = 0; i < Projections.CP2_PROJECTION.length; i++) { 147 String column = Projections.CP2_PROJECTION[i]; 148 int index = smartDialCursor.getColumnIndex(column); 149 if (index != -1) { 150 switch (smartDialCursor.getType(index)) { 151 case FIELD_TYPE_INTEGER: 152 newRow[i] = smartDialCursor.getInt(index); 153 break; 154 case FIELD_TYPE_STRING: 155 newRow[i] = smartDialCursor.getString(index); 156 break; 157 case FIELD_TYPE_FLOAT: 158 newRow[i] = smartDialCursor.getFloat(index); 159 break; 160 case FIELD_TYPE_BLOB: 161 newRow[i] = smartDialCursor.getBlob(index); 162 break; 163 case FIELD_TYPE_NULL: 164 default: 165 // No-op 166 break; 167 } 168 } 169 } 170 cursor.addRow(newRow); 171 } while (smartDialCursor.moveToNext()); 172 return cursor; 173 } 174 } 175 176 static class RegularSearchCursor extends MergeCursor implements SearchCursor { 177 newInstance(Context context, Cursor regularSearchCursor)178 static RegularSearchCursor newInstance(Context context, Cursor regularSearchCursor) { 179 if (regularSearchCursor == null || regularSearchCursor.getCount() == 0) { 180 LogUtil.i("RegularSearchCursor.newInstance", "Cursor was null or empty"); 181 return new RegularSearchCursor(new Cursor[] {new MatrixCursor(Projections.CP2_PROJECTION)}); 182 } 183 184 MatrixCursor headerCursor = new MatrixCursor(HEADER_PROJECTION); 185 headerCursor.addRow(new String[] {context.getString(R.string.all_contacts)}); 186 return new RegularSearchCursor(new Cursor[] {headerCursor, regularSearchCursor}); 187 } 188 RegularSearchCursor(Cursor[] cursors)189 public RegularSearchCursor(Cursor[] cursors) { 190 super(cursors); 191 } 192 193 @Override isHeader()194 public boolean isHeader() { 195 return isFirst(); 196 } 197 198 @Override updateQuery(@onNull String query)199 public boolean updateQuery(@NonNull String query) { 200 return false; // no-op 201 } 202 203 @Override getDirectoryId()204 public long getDirectoryId() { 205 return 0; // no-op 206 } 207 } 208 } 209