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