1 /* 2 * Copyright (C) 2013 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.smartdial; 18 19 import android.content.AsyncTaskLoader; 20 import android.content.Context; 21 import android.database.Cursor; 22 import android.database.MatrixCursor; 23 import android.provider.ContactsContract.CommonDataKinds.Phone; 24 import com.android.dialer.common.LogUtil; 25 import com.android.dialer.database.Database; 26 import com.android.dialer.database.DialerDatabaseHelper; 27 import com.android.dialer.database.DialerDatabaseHelper.ContactNumber; 28 import com.android.dialer.smartdial.util.SmartDialNameMatcher; 29 import com.android.dialer.util.PermissionsUtil; 30 import java.util.ArrayList; 31 import java.util.Arrays; 32 import java.util.List; 33 34 /** Implements a Loader<Cursor> class to asynchronously load SmartDial search results. */ 35 public class SmartDialCursorLoader extends AsyncTaskLoader<Cursor> { 36 37 private static final String TAG = "SmartDialCursorLoader"; 38 private static final boolean DEBUG = false; 39 40 private final Context context; 41 42 private Cursor cursor; 43 44 private String query; 45 private SmartDialNameMatcher nameMatcher; 46 47 private boolean showEmptyListForNullQuery = true; 48 SmartDialCursorLoader(Context context)49 public SmartDialCursorLoader(Context context) { 50 super(context); 51 this.context = context; 52 } 53 54 /** 55 * Configures the query string to be used to find SmartDial matches. 56 * 57 * @param query The query string user typed. 58 */ configureQuery(String query)59 public void configureQuery(String query) { 60 if (DEBUG) { 61 LogUtil.v(TAG, "Configure new query to be " + query); 62 } 63 this.query = SmartDialNameMatcher.normalizeNumber(context, query); 64 65 /** Constructs a name matcher object for matching names. */ 66 nameMatcher = new SmartDialNameMatcher(this.query); 67 nameMatcher.setShouldMatchEmptyQuery(!showEmptyListForNullQuery); 68 } 69 70 /** 71 * Queries the SmartDial database and loads results in background. 72 * 73 * @return Cursor of contacts that matches the SmartDial query. 74 */ 75 @Override loadInBackground()76 public Cursor loadInBackground() { 77 if (DEBUG) { 78 LogUtil.v(TAG, "Load in background " + query); 79 } 80 81 if (!PermissionsUtil.hasContactsReadPermissions(context)) { 82 return new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); 83 } 84 85 /** Loads results from the database helper. */ 86 final DialerDatabaseHelper dialerDatabaseHelper = 87 Database.get(context).getDatabaseHelper(context); 88 final ArrayList<ContactNumber> allMatches = 89 dialerDatabaseHelper.getLooseMatches(query, nameMatcher); 90 91 if (DEBUG) { 92 LogUtil.v(TAG, "Loaded matches " + allMatches.size()); 93 } 94 95 /** Constructs a cursor for the returned array of results. */ 96 final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); 97 Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length]; 98 for (ContactNumber contact : allMatches) { 99 row[PhoneQuery.PHONE_ID] = contact.dataId; 100 row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber; 101 row[PhoneQuery.CONTACT_ID] = contact.id; 102 row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey; 103 row[PhoneQuery.PHOTO_ID] = contact.photoId; 104 row[PhoneQuery.DISPLAY_NAME] = contact.displayName; 105 row[PhoneQuery.CARRIER_PRESENCE] = contact.carrierPresence; 106 cursor.addRow(row); 107 } 108 return cursor; 109 } 110 111 @Override deliverResult(Cursor cursor)112 public void deliverResult(Cursor cursor) { 113 if (isReset()) { 114 /** The Loader has been reset; ignore the result and invalidate the data. */ 115 releaseResources(cursor); 116 return; 117 } 118 119 /** Hold a reference to the old data so it doesn't get garbage collected. */ 120 Cursor oldCursor = this.cursor; 121 this.cursor = cursor; 122 123 if (isStarted()) { 124 /** If the Loader is in a started state, deliver the results to the client. */ 125 super.deliverResult(cursor); 126 } 127 128 /** Invalidate the old data as we don't need it any more. */ 129 if (oldCursor != null && oldCursor != cursor) { 130 releaseResources(oldCursor); 131 } 132 } 133 134 @Override onStartLoading()135 protected void onStartLoading() { 136 if (cursor != null) { 137 /** Deliver any previously loaded data immediately. */ 138 deliverResult(cursor); 139 } 140 if (cursor == null) { 141 /** Force loads every time as our results change with queries. */ 142 forceLoad(); 143 } 144 } 145 146 @Override onStopLoading()147 protected void onStopLoading() { 148 /** The Loader is in a stopped state, so we should attempt to cancel the current load. */ 149 cancelLoad(); 150 } 151 152 @Override onReset()153 protected void onReset() { 154 /** Ensure the loader has been stopped. */ 155 onStopLoading(); 156 157 /** Release all previously saved query results. */ 158 if (cursor != null) { 159 releaseResources(cursor); 160 cursor = null; 161 } 162 } 163 164 @Override onCanceled(Cursor cursor)165 public void onCanceled(Cursor cursor) { 166 super.onCanceled(cursor); 167 168 /** The load has been canceled, so we should release the resources associated with 'data'. */ 169 releaseResources(cursor); 170 } 171 releaseResources(Cursor cursor)172 private void releaseResources(Cursor cursor) { 173 if (cursor != null) { 174 cursor.close(); 175 } 176 } 177 setShowEmptyListForNullQuery(boolean show)178 public void setShowEmptyListForNullQuery(boolean show) { 179 showEmptyListForNullQuery = show; 180 if (nameMatcher != null) { 181 nameMatcher.setShouldMatchEmptyQuery(!show); 182 } 183 } 184 185 /** Moved from contacts/common, contains all of the projections needed for Smart Dial queries. */ 186 public static class PhoneQuery { 187 188 public static final String[] PROJECTION_PRIMARY_INTERNAL = 189 new String[] { 190 Phone._ID, // 0 191 Phone.TYPE, // 1 192 Phone.LABEL, // 2 193 Phone.NUMBER, // 3 194 Phone.CONTACT_ID, // 4 195 Phone.LOOKUP_KEY, // 5 196 Phone.PHOTO_ID, // 6 197 Phone.DISPLAY_NAME_PRIMARY, // 7 198 Phone.PHOTO_THUMBNAIL_URI, // 8 199 }; 200 201 public static final String[] PROJECTION_PRIMARY; 202 public static final String[] PROJECTION_ALTERNATIVE_INTERNAL = 203 new String[] { 204 Phone._ID, // 0 205 Phone.TYPE, // 1 206 Phone.LABEL, // 2 207 Phone.NUMBER, // 3 208 Phone.CONTACT_ID, // 4 209 Phone.LOOKUP_KEY, // 5 210 Phone.PHOTO_ID, // 6 211 Phone.DISPLAY_NAME_ALTERNATIVE, // 7 212 Phone.PHOTO_THUMBNAIL_URI, // 8 213 }; 214 public static final String[] PROJECTION_ALTERNATIVE; 215 public static final int PHONE_ID = 0; 216 public static final int PHONE_TYPE = 1; 217 public static final int PHONE_LABEL = 2; 218 public static final int PHONE_NUMBER = 3; 219 public static final int CONTACT_ID = 4; 220 public static final int LOOKUP_KEY = 5; 221 public static final int PHOTO_ID = 6; 222 public static final int DISPLAY_NAME = 7; 223 public static final int PHOTO_URI = 8; 224 public static final int CARRIER_PRESENCE = 9; 225 226 static { 227 final List<String> projectionList = 228 new ArrayList<>(Arrays.asList(PROJECTION_PRIMARY_INTERNAL)); 229 projectionList.add(Phone.CARRIER_PRESENCE); // 9 230 PROJECTION_PRIMARY = projectionList.toArray(new String[projectionList.size()]); 231 } 232 233 static { 234 final List<String> projectionList = 235 new ArrayList<>(Arrays.asList(PROJECTION_ALTERNATIVE_INTERNAL)); 236 projectionList.add(Phone.CARRIER_PRESENCE); // 9 237 PROJECTION_ALTERNATIVE = projectionList.toArray(new String[projectionList.size()]); 238 } 239 } 240 } 241