blob: dbec5a6b15e8d9ac2d7e5c58f3a2e2bc4cce8cb3 [file] [log] [blame]
/*
* 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 android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Directory;
import android.util.Log;
import com.android.contacts.common.util.StopWatch;
import com.google.common.base.Preconditions;
import java.util.Comparator;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Cache object used to cache Smart Dial contacts that handles various states of the cache at the
* point in time when getContacts() is called
* 1) Cache is currently empty and there is no caching thread running - getContacts() starts a
* caching thread and returns the cache when completed
* 2) The cache is currently empty, but a caching thread has been started - getContacts() waits
* till the existing caching thread is completed before immediately returning the cache
* 3) The cache has already been populated, and there is no caching thread running - getContacts()
* returns the existing cache immediately
* 4) The cache has already been populated, but there is another caching thread running (due to
* a forced cache refresh due to content updates - getContacts() returns the existing cache
* immediately
*/
public class SmartDialCache {
public static class ContactNumber {
public final String displayName;
public final String lookupKey;
public final long id;
public final int affinity;
public final String phoneNumber;
public ContactNumber(long id, String displayName, String phoneNumber, String lookupKey,
int affinity) {
this.displayName = displayName;
this.lookupKey = lookupKey;
this.id = id;
this.affinity = affinity;
this.phoneNumber = phoneNumber;
}
}
public static interface PhoneQuery {
Uri URI = Phone.CONTENT_URI.buildUpon().
appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
String.valueOf(Directory.DEFAULT)).
appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true").
build();
final String[] PROJECTION_PRIMARY = new String[] {
Phone._ID, // 0
Phone.TYPE, // 1
Phone.LABEL, // 2
Phone.NUMBER, // 3
Phone.CONTACT_ID, // 4
Phone.LOOKUP_KEY, // 5
Phone.DISPLAY_NAME_PRIMARY, // 6
};
final String[] PROJECTION_ALTERNATIVE = new String[] {
Phone._ID, // 0
Phone.TYPE, // 1
Phone.LABEL, // 2
Phone.NUMBER, // 3
Phone.CONTACT_ID, // 4
Phone.LOOKUP_KEY, // 5
Phone.DISPLAY_NAME_ALTERNATIVE, // 6
};
public static final int PHONE_ID = 0;
public static final int PHONE_TYPE = 1;
public static final int PHONE_LABEL = 2;
public static final int PHONE_NUMBER = 3;
public static final int PHONE_CONTACT_ID = 4;
public static final int PHONE_LOOKUP_KEY = 5;
public static final int PHONE_DISPLAY_NAME = 6;
public static final String SORT_ORDER = Contacts.LAST_TIME_CONTACTED + " DESC";
}
private SmartDialTrie mContactsCache;
private static AtomicInteger mCacheStatus;
private final int mNameDisplayOrder;
private final Context mContext;
private final static Object mLock = new Object();
public static final int CACHE_NEEDS_RECACHE = 1;
public static final int CACHE_IN_PROGRESS = 2;
public static final int CACHE_COMPLETED = 3;
private static final boolean DEBUG = false;
private SmartDialCache(Context context, int nameDisplayOrder) {
mNameDisplayOrder = nameDisplayOrder;
Preconditions.checkNotNull(context, "Context must not be null");
mContext = context.getApplicationContext();
mCacheStatus = new AtomicInteger(CACHE_NEEDS_RECACHE);
}
private static SmartDialCache instance;
/**
* Returns an instance of SmartDialCache.
*
* @param context A context that provides a valid ContentResolver.
* @param nameDisplayOrder One of the two name display order integer constants (1 or 2) as saved
* in settings under the key
* {@link android.provider.ContactsContract.Preferences#DISPLAY_ORDER}.
* @return An instance of SmartDialCache
*/
public static synchronized SmartDialCache getInstance(Context context, int nameDisplayOrder) {
if (instance == null) {
instance = new SmartDialCache(context, nameDisplayOrder);
}
return instance;
}
/**
* Performs a database query, iterates through the returned cursor and saves the retrieved
* contacts to a local cache.
*/
private void cacheContacts(Context context) {
mCacheStatus.set(CACHE_IN_PROGRESS);
synchronized(mLock) {
if (DEBUG) {
Log.d(LOG_TAG, "Starting caching thread");
}
final StopWatch stopWatch = DEBUG ? StopWatch.start("SmartDial Cache") : null;
final Cursor c = context.getContentResolver().query(PhoneQuery.URI,
(mNameDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY)
? PhoneQuery.PROJECTION_PRIMARY : PhoneQuery.PROJECTION_ALTERNATIVE,
null, null, PhoneQuery.SORT_ORDER);
if (DEBUG) {
stopWatch.lap("SmartDial query complete");
}
if (c == null) {
Log.w(LOG_TAG, "SmartDial query received null for cursor");
if (DEBUG) {
stopWatch.stopAndLog("SmartDial query received null for cursor", 0);
}
mCacheStatus.getAndSet(CACHE_NEEDS_RECACHE);
return;
}
final SmartDialTrie cache = new SmartDialTrie(
SmartDialNameMatcher.LATIN_LETTERS_TO_DIGITS);
try {
c.moveToPosition(-1);
int affinityCount = 0;
while (c.moveToNext()) {
final String displayName = c.getString(PhoneQuery.PHONE_DISPLAY_NAME);
final String phoneNumber = c.getString(PhoneQuery.PHONE_NUMBER);
final long id = c.getLong(PhoneQuery.PHONE_CONTACT_ID);
final String lookupKey = c.getString(PhoneQuery.PHONE_LOOKUP_KEY);
cache.put(new ContactNumber(id, displayName, phoneNumber, lookupKey,
affinityCount));
affinityCount++;
}
} finally {
c.close();
mContactsCache = cache;
if (DEBUG) {
stopWatch.stopAndLog("SmartDial caching completed", 0);
}
}
}
if (DEBUG) {
Log.d(LOG_TAG, "Caching thread completed");
}
mCacheStatus.getAndSet(CACHE_COMPLETED);
}
/**
* Returns the list of cached contacts. This is blocking so it should not be called from the UI
* thread. There are 4 possible scenarios:
*
* 1) Cache is currently empty and there is no caching thread running - getContacts() starts a
* caching thread and returns the cache when completed
* 2) The cache is currently empty, but a caching thread has been started - getContacts() waits
* till the existing caching thread is completed before immediately returning the cache
* 3) The cache has already been populated, and there is no caching thread running -
* getContacts() returns the existing cache immediately
* 4) The cache has already been populated, but there is another caching thread running (due to
* a forced cache refresh due to content updates - getContacts() returns the existing cache
* immediately
*
* @return List of already cached contacts, or an empty list if the caching failed for any
* reason.
*/
public SmartDialTrie getContacts() {
// Either scenario 3 or 4 - This means just go ahead and return the existing cache
// immediately even if there is a caching thread currently running. We are guaranteed to
// have the newest value of mContactsCache at this point because it is volatile.
if (mContactsCache != null) {
return mContactsCache;
}
// At this point we are forced to wait for cacheContacts to complete in another thread(if
// one currently exists) because of mLock.
synchronized(mLock) {
// If mContactsCache is still null at this point, either there was never any caching
// process running, or it failed (Scenario 1). If so, just go ahead and try to cache
// the contacts again.
if (mContactsCache == null) {
cacheContacts(mContext);
return (mContactsCache == null) ? new SmartDialTrie(
SmartDialNameMatcher.LATIN_LETTERS_TO_DIGITS) : mContactsCache;
} else {
// After waiting for the lock on mLock to be released, mContactsCache is now
// non-null due to the completion of the caching thread (Scenario 2). Go ahead
// and return the existing cache.
return mContactsCache;
}
}
}
/**
* Cache contacts only if there is a need to (forced cache refresh or no attempt to cache yet).
* This method is called in 2 places: whenever the DialpadFragment comes into view, and when the
* ContentObserver observes a change in contacts.
*
* @param forceRecache If true, force a cache refresh.
*/
public void cacheIfNeeded(boolean forceRecache) {
if (DEBUG) {
Log.d("SmartDial", "cacheIfNeeded called with " + String.valueOf(forceRecache));
}
if (mCacheStatus.get() == CACHE_IN_PROGRESS) {
return;
}
if (forceRecache || mCacheStatus.get() == CACHE_NEEDS_RECACHE) {
// Because this method can be possibly be called multiple times in rapid succession,
// set the cache status even before starting a caching thread to avoid unnecessarily
// spawning extra threads.
mCacheStatus.set(CACHE_IN_PROGRESS);
startCachingThread();
}
}
private void startCachingThread() {
new Thread(new Runnable() {
@Override
public void run() {
cacheContacts(mContext);
}
}).start();
}
public static class ContactAffinityComparator implements Comparator<ContactNumber> {
@Override
public int compare(ContactNumber lhs, ContactNumber rhs) {
// Smaller affinity is better because they are numbered in ascending order in
// the order the contacts were returned from the ContactsProvider (sorted by
// frequency of use and time last used
return Integer.compare(lhs.affinity, rhs.affinity);
}
}
public static class SmartDialContentObserver extends ContentObserver {
private final SmartDialCache mCache;
// throttle updates in case onChange is called too often due to syncing, etc.
private final long mThresholdBetweenUpdates = 5000;
private long mLastCalled = 0;
private long mLastUpdated = 0;
public SmartDialContentObserver(Handler handler, SmartDialCache cache) {
super(handler);
mCache = cache;
}
@Override
public void onChange(boolean selfChange) {
mLastCalled = System.currentTimeMillis();
if (DEBUG) {
Log.d(LOG_TAG, "Contacts change observed");
}
if (mLastCalled - mLastUpdated > mThresholdBetweenUpdates) {
mLastUpdated = mLastCalled;
if (DEBUG) {
Log.d(LOG_TAG, "More than 5 seconds since last cache, forcing recache");
}
mCache.cacheIfNeeded(true);
}
super.onChange(selfChange);
}
}
}