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 
SmartDialCursorLoader(Context context)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      */
configureQuery(String query)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
loadInBackground()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             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 = mCursor;
121         mCursor = cursor;
122 
123         if (mObserver == null) {
124             mObserver = new ForceLoadContentObserver();
125             mContext.getContentResolver().registerContentObserver(
126                     DialerDatabaseHelper.SMART_DIAL_UPDATED_URI, true, mObserver);
127         }
128 
129         if (isStarted()) {
130             /** If the Loader is in a started state, deliver the results to the client. */
131             super.deliverResult(cursor);
132         }
133 
134         /** Invalidate the old data as we don't need it any more. */
135         if (oldCursor != null && oldCursor != cursor) {
136             releaseResources(oldCursor);
137         }
138     }
139 
140     @Override
onStartLoading()141     protected void onStartLoading() {
142         if (mCursor != null) {
143             /** Deliver any previously loaded data immediately. */
144             deliverResult(mCursor);
145         }
146         if (mCursor == null) {
147             /** Force loads every time as our results change with queries. */
148             forceLoad();
149         }
150     }
151 
152     @Override
onStopLoading()153     protected void onStopLoading() {
154         /** The Loader is in a stopped state, so we should attempt to cancel the current load. */
155         cancelLoad();
156     }
157 
158     @Override
onReset()159     protected void onReset() {
160         /** Ensure the loader has been stopped. */
161         onStopLoading();
162 
163         if (mObserver != null) {
164             mContext.getContentResolver().unregisterContentObserver(mObserver);
165             mObserver = null;
166         }
167 
168         /** Release all previously saved query results. */
169         if (mCursor != null) {
170             releaseResources(mCursor);
171             mCursor = null;
172         }
173     }
174 
175     @Override
onCanceled(Cursor cursor)176     public void onCanceled(Cursor cursor) {
177         super.onCanceled(cursor);
178 
179         if (mObserver != null) {
180             mContext.getContentResolver().unregisterContentObserver(mObserver);
181             mObserver = null;
182         }
183 
184         /** The load has been canceled, so we should release the resources associated with 'data'.*/
185         releaseResources(cursor);
186     }
187 
releaseResources(Cursor cursor)188     private void releaseResources(Cursor cursor) {
189         if (cursor != null) {
190             cursor.close();
191         }
192     }
193 }
194