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