Use SmartDialCache object for caching
Extract caching methods from SmartDialLoaderTask
and use a standalone SmartDialCache object instead. This
cache object handles caching failures as well as concurrent
multiple cache requests.
Bug: 6977981
Change-Id: I6df9e273191c7ac434d094e567d7a91814f8c030
diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java
index f7a9056..2c48416 100644
--- a/src/com/android/dialer/dialpad/DialpadFragment.java
+++ b/src/com/android/dialer/dialpad/DialpadFragment.java
@@ -170,7 +170,7 @@
// Vibration (haptic feedback) for dialer key presses.
private final HapticFeedback mHaptic = new HapticFeedback();
- private boolean mNeedToCacheSmartDial = false;
+ private SmartDialCache mSmartDialCache;
/** Identifier for the "Add Call" intent extra. */
private static final String ADD_CALL_MODE_KEY = "add_call_mode";
@@ -279,7 +279,7 @@
mContactsPrefs = new ContactsPreferences(getActivity());
mCurrentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
- mNeedToCacheSmartDial = true;
+ mSmartDialCache = new SmartDialCache(getActivity(), mContactsPrefs.getDisplayOrder());
try {
mHaptic.init(getActivity(),
getResources().getBoolean(R.bool.config_enable_dialer_key_vibration));
@@ -295,13 +295,6 @@
if (state != null) {
mDigitsFilledByIntent = state.getBoolean(PREF_DIGITS_FILLED_BY_INTENT);
}
-
- // Start caching contacts to use for smart dialling only if the dialpad fragment is visible
- if (getUserVisibleHint()) {
- SmartDialLoaderTask.startCacheContactsTaskIfNeeded(
- getActivity(), mContactsPrefs.getDisplayOrder());
- mNeedToCacheSmartDial = false;
- }
}
@Override
@@ -1679,10 +1672,8 @@
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
- if (isVisibleToUser && mNeedToCacheSmartDial) {
- SmartDialLoaderTask.startCacheContactsTaskIfNeeded(
- getActivity(), mContactsPrefs.getDisplayOrder());
- mNeedToCacheSmartDial = false;
+ if (isVisibleToUser) {
+ mSmartDialCache.cacheIfNeeded();
}
}
@@ -1704,7 +1695,7 @@
if (digits.length() < 2) {
mSmartDialAdapter.clear();
} else {
- final SmartDialLoaderTask task = new SmartDialLoaderTask(this, digits);
+ final SmartDialLoaderTask task = new SmartDialLoaderTask(this, digits, mSmartDialCache);
// don't execute this in serial, otherwise we have to wait too long for results
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new String[] {});
}
diff --git a/src/com/android/dialer/dialpad/SmartDialCache.java b/src/com/android/dialer/dialpad/SmartDialCache.java
new file mode 100644
index 0000000..37746d2
--- /dev/null
+++ b/src/com/android/dialer/dialpad/SmartDialCache.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.dialpad;
+
+import static com.android.dialer.dialpad.SmartDialAdapter.LOG_TAG;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.util.Log;
+
+import com.android.contacts.common.util.StopWatch;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Cache object used to cache Smart Dial contacts that handles various states of the cache:
+ * 1) Cache has been populated
+ * 2) Cache task is currently running
+ * 3) Cache task failed
+ */
+public class SmartDialCache {
+
+ public static class Contact {
+ public final String displayName;
+ public final String lookupKey;
+ public final long id;
+
+ public Contact(long id, String displayName, String lookupKey) {
+ this.displayName = displayName;
+ this.lookupKey = lookupKey;
+ this.id = id;
+ }
+ }
+
+ /** Query used for loadByContactName */
+ private interface ContactQuery {
+ Uri URI = Contacts.CONTENT_URI.buildUpon()
+ // Visible contact only
+ //.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, "0")
+ .build();
+ String[] PROJECTION = new String[] {
+ Contacts._ID,
+ Contacts.DISPLAY_NAME,
+ Contacts.LOOKUP_KEY
+ };
+ String[] PROJECTION_ALTERNATIVE = new String[] {
+ Contacts._ID,
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ Contacts.LOOKUP_KEY
+ };
+
+ int COLUMN_ID = 0;
+ int COLUMN_DISPLAY_NAME = 1;
+ int COLUMN_LOOKUP_KEY = 2;
+
+ String SELECTION =
+ Contacts.HAS_PHONE_NUMBER + "=1";
+
+ String ORDER_BY = Contacts.LAST_TIME_CONTACTED + " DESC";
+ }
+
+ // mContactsCache and mCachingStarted need to be volatile because we check for their status
+ // in cacheIfNeeded from the UI thread, to decided whether or not to fire up a caching thread.
+ private List<Contact> mContactsCache;
+ private volatile boolean mNeedsRecache = true;
+ private final int mNameDisplayOrder;
+ private final Context mContext;
+ private final Object mLock = new Object();
+
+ private static final boolean DEBUG = true; // STOPSHIP change to false.
+
+ public SmartDialCache(Context context, int nameDisplayOrder) {
+ mNameDisplayOrder = nameDisplayOrder;
+ Preconditions.checkNotNull(context, "Context must not be null");
+ mContext = context.getApplicationContext();
+ }
+
+ /**
+ * Performs a database query, iterates through the returned cursor and saves the retrieved
+ * contacts to a local cache.
+ */
+ private void cacheContacts(Context context) {
+ synchronized(mLock) {
+ // In extremely rare edge cases, getContacts() might be called and start caching
+ // between the time mCachingThread is added to the thread pool and it starts
+ // running. If so, at this point in time mContactsCache will no longer be null
+ // since it is populated by getContacts. We thus no longer have to perform any
+ // caching.
+ if (mContactsCache != null) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Contacts already cached");
+ }
+ return;
+ }
+ final StopWatch stopWatch = DEBUG ? StopWatch.start("SmartDial Cache") : null;
+ final Cursor c = context.getContentResolver().query(ContactQuery.URI,
+ (mNameDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY)
+ ? ContactQuery.PROJECTION : ContactQuery.PROJECTION_ALTERNATIVE,
+ ContactQuery.SELECTION, null,
+ ContactQuery.ORDER_BY);
+ if (c == null) {
+ Log.w(LOG_TAG, "SmartDial query received null for cursor");
+ if (DEBUG) {
+ stopWatch.stopAndLog("Query Failure", 0);
+ }
+ return;
+ }
+ try {
+ mContactsCache = Lists.newArrayListWithCapacity(c.getCount());
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ final String displayName = c.getString(ContactQuery.COLUMN_DISPLAY_NAME);
+ final long id = c.getLong(ContactQuery.COLUMN_ID);
+ final String lookupKey = c.getString(ContactQuery.COLUMN_LOOKUP_KEY);
+ mContactsCache.add(new Contact(id, displayName, lookupKey));
+ }
+ } finally {
+ c.close();
+ if (DEBUG) {
+ stopWatch.stopAndLog("SmartDial Cache", 0);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the list of cached contacts. If the caching task has not started or been completed,
+ * the method blocks till the caching process is complete before returning the full list of
+ * cached contacts. This means that this method should not be called from the UI thread.
+ *
+ * @return List of already cached contacts, or an empty list if the caching failed for any
+ * reason.
+ */
+ public List<Contact> getContacts() {
+ synchronized(mLock) {
+ if (mContactsCache == null) {
+ cacheContacts(mContext);
+ mNeedsRecache = false;
+ return (mContactsCache == null) ? new ArrayList<Contact>() : mContactsCache;
+ } else {
+ return mContactsCache;
+ }
+ }
+ }
+
+ /**
+ * Only start a new caching task if {@link #mContactsCache} is null and there is no caching
+ * task that is currently running
+ */
+ public void cacheIfNeeded() {
+ if (mNeedsRecache) {
+ mNeedsRecache = false;
+ startCachingThread();
+ }
+ }
+
+ private void startCachingThread() {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ cacheContacts(mContext);
+ }
+ }).start();
+ }
+
+}
diff --git a/src/com/android/dialer/dialpad/SmartDialLoaderTask.java b/src/com/android/dialer/dialpad/SmartDialLoaderTask.java
index 8321837..58b29d6 100644
--- a/src/com/android/dialer/dialpad/SmartDialLoaderTask.java
+++ b/src/com/android/dialer/dialpad/SmartDialLoaderTask.java
@@ -31,35 +31,18 @@
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.StopWatch;
+import com.android.dialer.dialpad.SmartDialCache.Contact;
import java.util.ArrayList;
import java.util.List;
/**
- * AsyncTask that performs one of two functions depending on which constructor is used.
- * If {@link #SmartDialLoaderTask(Context context, int nameDisplayOrder)} is used, the task
- * caches all contacts with a phone number into the static variable {@link #sContactsCache}.
- * If {@link #SmartDialLoaderTask(SmartDialLoaderCallback callback, String query)} is used, the
- * task searches through the cache to return the top 3 contacts(ranked by confidence) that match
- * the query, then passes it back to the {@link SmartDialLoaderCallback} through a callback
- * function.
+ * This task searches through the provided cache to return the top 3 contacts(ranked by confidence)
+ * that match the query, then passes it back to the {@link SmartDialLoaderCallback} through a
+ * callback function.
*/
-// TODO: Make the cache a singleton class and refactor to fix possible concurrency issues in the
-// future
public class SmartDialLoaderTask extends AsyncTask<String, Integer, List<SmartDialEntry>> {
- private class Contact {
- final String mDisplayName;
- final String mLookupKey;
- final long mId;
-
- public Contact(long id, String displayName, String lookupKey) {
- mDisplayName = displayName;
- mLookupKey = lookupKey;
- mId = id;
- }
- }
-
public interface SmartDialLoaderCallback {
void setSmartDialAdapterEntries(List<SmartDialEntry> list);
}
@@ -68,47 +51,26 @@
private static final int MAX_ENTRIES = 3;
- private static List<Contact> sContactsCache;
-
- private final boolean mCacheOnly;
+ private final SmartDialCache mContactsCache;
private final SmartDialLoaderCallback mCallback;
- private final Context mContext;
/**
* See {@link ContactsPreferences#getDisplayOrder()}.
* {@link ContactsContract.Preferences#DISPLAY_ORDER_PRIMARY} (first name first)
* {@link ContactsContract.Preferences#DISPLAY_ORDER_ALTERNATIVE} (last name first)
*/
- private final int mNameDisplayOrder;
-
private final SmartDialNameMatcher mNameMatcher;
- // cache only constructor
- private SmartDialLoaderTask(Context context, int nameDisplayOrder) {
- this.mNameDisplayOrder = nameDisplayOrder;
- this.mContext = context;
- // we're just caching contacts so no need to initialize a SmartDialNameMatcher or callback
- this.mNameMatcher = null;
- this.mCallback = null;
- this.mCacheOnly = true;
- }
-
- public SmartDialLoaderTask(SmartDialLoaderCallback callback, String query) {
+ public SmartDialLoaderTask(SmartDialLoaderCallback callback, String query,
+ SmartDialCache cache) {
this.mCallback = callback;
- this.mContext = null;
- this.mCacheOnly = false;
- this.mNameDisplayOrder = 0;
this.mNameMatcher = new SmartDialNameMatcher(PhoneNumberUtils.normalizeNumber(query));
+ this.mContactsCache = cache;
}
@Override
protected List<SmartDialEntry> doInBackground(String... params) {
- if (mCacheOnly) {
- cacheContacts();
- return Lists.newArrayList();
- }
-
return getContactMatches();
}
@@ -119,116 +81,26 @@
}
}
- /** Query used for loadByContactName */
- private interface ContactQuery {
- Uri URI = Contacts.CONTENT_URI.buildUpon()
- // Visible contact only
- //.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, "0")
- .build();
- String[] PROJECTION = new String[] {
- Contacts._ID,
- Contacts.DISPLAY_NAME,
- Contacts.LOOKUP_KEY
- };
- String[] PROJECTION_ALTERNATIVE = new String[] {
- Contacts._ID,
- Contacts.DISPLAY_NAME_ALTERNATIVE,
- Contacts.LOOKUP_KEY
- };
-
- int COLUMN_ID = 0;
- int COLUMN_DISPLAY_NAME = 1;
- int COLUMN_LOOKUP_KEY = 2;
-
- String SELECTION =
- //Contacts.IN_VISIBLE_GROUP + "=1 and " +
- Contacts.HAS_PHONE_NUMBER + "=1";
-
- String ORDER_BY = Contacts.LAST_TIME_CONTACTED + " DESC";
- }
-
- public static void startCacheContactsTaskIfNeeded(Context context, int displayOrder) {
- if (sContactsCache != null) {
- // contacts have already been cached, just return
- return;
- }
- final SmartDialLoaderTask task =
- new SmartDialLoaderTask(context, displayOrder);
- task.execute();
- }
-
- /**
- * Caches the contacts into an in memory array list. This is called once at startup and should
- * not be cancelled.
- */
- private void cacheContacts() {
- final StopWatch stopWatch = DEBUG ? StopWatch.start("SmartDial Cache") : null;
- if (sContactsCache != null) {
- // contacts have already been cached, just return
- stopWatch.stopAndLog("SmartDial Already Cached", 0);
- return;
- }
- if (mContext == null) {
- if (DEBUG) {
- stopWatch.stopAndLog("Invalid context", 0);
- }
- return;
- }
- final Cursor c = mContext.getContentResolver().query(ContactQuery.URI,
- (mNameDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY)
- ? ContactQuery.PROJECTION : ContactQuery.PROJECTION_ALTERNATIVE,
- ContactQuery.SELECTION, null,
- ContactQuery.ORDER_BY);
- if (c == null) {
- if (DEBUG) {
- stopWatch.stopAndLog("Query Failure", 0);
- }
- return;
- }
- sContactsCache = Lists.newArrayListWithCapacity(c.getCount());
- try {
- c.moveToPosition(-1);
- while (c.moveToNext()) {
- final String displayName = c.getString(ContactQuery.COLUMN_DISPLAY_NAME);
- final long id = c.getLong(ContactQuery.COLUMN_ID);
- final String lookupKey = c.getString(ContactQuery.COLUMN_LOOKUP_KEY);
- sContactsCache.add(new Contact(id, displayName, lookupKey));
- }
- } finally {
- c.close();
- if (DEBUG) {
- stopWatch.stopAndLog("SmartDial Cache", 0);
- }
- }
- }
-
/**
* Loads all visible contacts with phone numbers and check if their display names match the
* query. Return at most {@link #MAX_ENTRIES} {@link SmartDialEntry}'s for the matching
* contacts.
*/
private ArrayList<SmartDialEntry> getContactMatches() {
- final StopWatch stopWatch = DEBUG ? StopWatch.start(LOG_TAG + " Start Match") : null;
- if (sContactsCache == null) {
- // contacts should have been cached by this point in time, but in case they
- // are not, we go ahead and cache them into memory.
- if (DEBUG) {
- Log.d(LOG_TAG, "empty cache");
- }
- cacheContacts();
- // TODO: if sContactsCache is still null at this point we should try to recache
- }
+ final List<Contact> cachedContactList = mContactsCache.getContacts();
+ // cachedContactList will never be null at this point
+
if (DEBUG) {
- Log.d(LOG_TAG, "Size of cache: " + sContactsCache.size());
+ Log.d(LOG_TAG, "Size of cache: " + cachedContactList.size());
}
+
+ final StopWatch stopWatch = DEBUG ? StopWatch.start(LOG_TAG + " Start Match") : null;
final ArrayList<SmartDialEntry> outList = Lists.newArrayList();
- if (sContactsCache == null) {
- return outList;
- }
+
int count = 0;
- for (int i = 0; i < sContactsCache.size(); i++) {
- final Contact contact = sContactsCache.get(i);
- final String displayName = contact.mDisplayName;
+ for (int i = 0; i < cachedContactList.size(); i++) {
+ final Contact contact = cachedContactList.get(i);
+ final String displayName = contact.displayName;
if (!mNameMatcher.matches(displayName)) {
continue;
@@ -236,8 +108,8 @@
// Matched; create SmartDialEntry.
@SuppressWarnings("unchecked")
final SmartDialEntry entry = new SmartDialEntry(
- contact.mDisplayName,
- Contacts.getLookupUri(contact.mId, contact.mLookupKey),
+ contact.displayName,
+ Contacts.getLookupUri(contact.id, contact.lookupKey),
(ArrayList<SmartDialMatchPosition>) mNameMatcher.getMatchPositions().clone()
);
outList.add(entry);