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.dialpad; 18 19 import android.content.AsyncTaskLoader; 20 import android.content.Context; 21 import android.content.Loader.ForceLoadContentObserver; 22 import android.database.Cursor; 23 import android.database.MatrixCursor; 24 import android.net.Uri; 25 import android.util.Log; 26 27 import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; 28 import com.android.contacts.common.util.PermissionsUtil; 29 import com.android.dialer.database.DialerDatabaseHelper; 30 import com.android.dialer.database.DialerDatabaseHelper.ContactNumber; 31 import com.android.dialerbind.DatabaseHelperManager; 32 33 import java.util.ArrayList; 34 35 /** 36 * Implements a Loader<Cursor> class to asynchronously load SmartDial search results. 37 */ 38 public class SmartDialCursorLoader extends AsyncTaskLoader<Cursor> { 39 40 private final String TAG = SmartDialCursorLoader.class.getSimpleName(); 41 private final boolean DEBUG = false; 42 43 private final Context mContext; 44 45 private Cursor mCursor; 46 47 private String mQuery; 48 private SmartDialNameMatcher mNameMatcher; 49 50 private ForceLoadContentObserver mObserver; 51 52 public SmartDialCursorLoader(Context context) { 53 super(context); 54 mContext = context; 55 } 56 57 /** 58 * Configures the query string to be used to find SmartDial matches. 59 * @param query The query string user typed. 60 */ 61 public void configureQuery(String query) { 62 if (DEBUG) { 63 Log.v(TAG, "Configure new query to be " + query); 64 } 65 mQuery = SmartDialNameMatcher.normalizeNumber(query, SmartDialPrefix.getMap()); 66 67 /** Constructs a name matcher object for matching names. */ 68 mNameMatcher = new SmartDialNameMatcher(mQuery, SmartDialPrefix.getMap()); 69 } 70 71 /** 72 * Queries the SmartDial database and loads results in background. 73 * @return Cursor of contacts that matches the SmartDial query. 74 */ 75 @Override 76 public Cursor loadInBackground() { 77 if (DEBUG) { 78 Log.v(TAG, "Load in background " + mQuery); 79 } 80 81 if (!PermissionsUtil.hasContactsPermissions(mContext)) { 82 return new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); 83 } 84 85 /** Loads results from the database helper. */ 86 final DialerDatabaseHelper dialerDatabaseHelper = DatabaseHelperManager.getDatabaseHelper( 87 mContext); 88 final ArrayList<ContactNumber> allMatches = dialerDatabaseHelper.getLooseMatches(mQuery, 89 mNameMatcher); 90 91 if (DEBUG) { 92 Log.v(TAG, "Loaded matches " + String.valueOf(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 cursor.addRow(row); 106 } 107 return cursor; 108 } 109 110 @Override 111 public void deliverResult(Cursor cursor) { 112 if (isReset()) { 113 /** The Loader has been reset; ignore the result and invalidate the data. */ 114 releaseResources(cursor); 115 return; 116 } 117 118 /** Hold a reference to the old data so it doesn't get garbage collected. */ 119 Cursor oldCursor = mCursor; 120 mCursor = cursor; 121 122 if (mObserver == null) { 123 mObserver = new ForceLoadContentObserver(); 124 mContext.getContentResolver().registerContentObserver( 125 DialerDatabaseHelper.SMART_DIAL_UPDATED_URI, true, mObserver); 126 } 127 128 if (isStarted()) { 129 /** If the Loader is in a started state, deliver the results to the client. */ 130 super.deliverResult(cursor); 131 } 132 133 /** Invalidate the old data as we don't need it any more. */ 134 if (oldCursor != null && oldCursor != cursor) { 135 releaseResources(oldCursor); 136 } 137 } 138 139 @Override 140 protected void onStartLoading() { 141 if (mCursor != null) { 142 /** Deliver any previously loaded data immediately. */ 143 deliverResult(mCursor); 144 } 145 if (mCursor == null) { 146 /** Force loads every time as our results change with queries. */ 147 forceLoad(); 148 } 149 } 150 151 @Override 152 protected void onStopLoading() { 153 /** The Loader is in a stopped state, so we should attempt to cancel the current load. */ 154 cancelLoad(); 155 } 156 157 @Override 158 protected void onReset() { 159 /** Ensure the loader has been stopped. */ 160 onStopLoading(); 161 162 if (mObserver != null) { 163 mContext.getContentResolver().unregisterContentObserver(mObserver); 164 mObserver = null; 165 } 166 167 /** Release all previously saved query results. */ 168 if (mCursor != null) { 169 releaseResources(mCursor); 170 mCursor = null; 171 } 172 } 173 174 @Override 175 public void onCanceled(Cursor cursor) { 176 super.onCanceled(cursor); 177 178 if (mObserver != null) { 179 mContext.getContentResolver().unregisterContentObserver(mObserver); 180 mObserver = null; 181 } 182 183 /** The load has been canceled, so we should release the resources associated with 'data'.*/ 184 releaseResources(cursor); 185 } 186 187 private void releaseResources(Cursor cursor) { 188 if (cursor != null) { 189 cursor.close(); 190 } 191 } 192 } 193