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 com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; 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 32 /** Implements a Loader<Cursor> class to asynchronously load SmartDial search results. */ 33 public class SmartDialCursorLoader extends AsyncTaskLoader<Cursor> { 34 35 private static final String TAG = "SmartDialCursorLoader"; 36 private static final boolean DEBUG = false; 37 38 private final Context context; 39 40 private Cursor cursor; 41 42 private String query; 43 private SmartDialNameMatcher nameMatcher; 44 45 private boolean showEmptyListForNullQuery = true; 46 47 public SmartDialCursorLoader(Context context) { 48 super(context); 49 this.context = context; 50 } 51 52 /** 53 * Configures the query string to be used to find SmartDial matches. 54 * 55 * @param query The query string user typed. 56 */ 57 public void configureQuery(String query) { 58 if (DEBUG) { 59 LogUtil.v(TAG, "Configure new query to be " + query); 60 } 61 this.query = SmartDialNameMatcher.normalizeNumber(context, query); 62 63 /** Constructs a name matcher object for matching names. */ 64 nameMatcher = new SmartDialNameMatcher(this.query); 65 nameMatcher.setShouldMatchEmptyQuery(!showEmptyListForNullQuery); 66 } 67 68 /** 69 * Queries the SmartDial database and loads results in background. 70 * 71 * @return Cursor of contacts that matches the SmartDial query. 72 */ 73 @Override 74 public Cursor loadInBackground() { 75 if (DEBUG) { 76 LogUtil.v(TAG, "Load in background " + query); 77 } 78 79 if (!PermissionsUtil.hasContactsReadPermissions(context)) { 80 return new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); 81 } 82 83 /** Loads results from the database helper. */ 84 final DialerDatabaseHelper dialerDatabaseHelper = 85 Database.get(context).getDatabaseHelper(context); 86 final ArrayList<ContactNumber> allMatches = 87 dialerDatabaseHelper.getLooseMatches(query, nameMatcher); 88 89 if (DEBUG) { 90 LogUtil.v(TAG, "Loaded matches " + allMatches.size()); 91 } 92 93 /** Constructs a cursor for the returned array of results. */ 94 final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); 95 Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length]; 96 for (ContactNumber contact : allMatches) { 97 row[PhoneQuery.PHONE_ID] = contact.dataId; 98 row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber; 99 row[PhoneQuery.CONTACT_ID] = contact.id; 100 row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey; 101 row[PhoneQuery.PHOTO_ID] = contact.photoId; 102 row[PhoneQuery.DISPLAY_NAME] = contact.displayName; 103 row[PhoneQuery.CARRIER_PRESENCE] = contact.carrierPresence; 104 cursor.addRow(row); 105 } 106 return cursor; 107 } 108 109 @Override 110 public void deliverResult(Cursor cursor) { 111 if (isReset()) { 112 /** The Loader has been reset; ignore the result and invalidate the data. */ 113 releaseResources(cursor); 114 return; 115 } 116 117 /** Hold a reference to the old data so it doesn't get garbage collected. */ 118 Cursor oldCursor = this.cursor; 119 this.cursor = cursor; 120 121 if (isStarted()) { 122 /** If the Loader is in a started state, deliver the results to the client. */ 123 super.deliverResult(cursor); 124 } 125 126 /** Invalidate the old data as we don't need it any more. */ 127 if (oldCursor != null && oldCursor != cursor) { 128 releaseResources(oldCursor); 129 } 130 } 131 132 @Override 133 protected void onStartLoading() { 134 if (cursor != null) { 135 /** Deliver any previously loaded data immediately. */ 136 deliverResult(cursor); 137 } 138 if (cursor == null) { 139 /** Force loads every time as our results change with queries. */ 140 forceLoad(); 141 } 142 } 143 144 @Override 145 protected void onStopLoading() { 146 /** The Loader is in a stopped state, so we should attempt to cancel the current load. */ 147 cancelLoad(); 148 } 149 150 @Override 151 protected void onReset() { 152 /** Ensure the loader has been stopped. */ 153 onStopLoading(); 154 155 /** Release all previously saved query results. */ 156 if (cursor != null) { 157 releaseResources(cursor); 158 cursor = null; 159 } 160 } 161 162 @Override 163 public void onCanceled(Cursor cursor) { 164 super.onCanceled(cursor); 165 166 /** The load has been canceled, so we should release the resources associated with 'data'. */ 167 releaseResources(cursor); 168 } 169 170 private void releaseResources(Cursor cursor) { 171 if (cursor != null) { 172 cursor.close(); 173 } 174 } 175 176 public void setShowEmptyListForNullQuery(boolean show) { 177 showEmptyListForNullQuery = show; 178 if (nameMatcher != null) { 179 nameMatcher.setShouldMatchEmptyQuery(!show); 180 } 181 } 182 } 183