/* * 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.Data; 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; // Current contacts - those contacted within the last 3 days (in milliseconds) final static long LAST_TIME_USED_CURRENT_MS = 3 * 24 * 60 * 60 * 1000; // Recent contacts - those contacted within the last 30 days (in milliseconds) final static long LAST_TIME_USED_RECENT_MS = 30 * 24 * 60 * 60 * 1000; final static String TIME_SINCE_LAST_USED_MS = "(? - " + Data.LAST_TIME_USED + ")"; final static String SORT_BY_DATA_USAGE = "(CASE WHEN " + TIME_SINCE_LAST_USED_MS + " < " + LAST_TIME_USED_CURRENT_MS + " THEN 0 " + " WHEN " + TIME_SINCE_LAST_USED_MS + " < " + LAST_TIME_USED_RECENT_MS + " THEN 1 " + " ELSE 2 END), " + Data.TIMES_USED + " DESC"; // This sort order is similar to that used by the ContactsProvider when returning a list // of frequently called contacts. public static final String SORT_ORDER = Contacts.STARRED + " DESC, " + Data.IS_SUPER_PRIMARY + " DESC, " + SORT_BY_DATA_USAGE + ", " + Contacts.IN_VISIBLE_GROUP + " DESC, " + Contacts.DISPLAY_NAME + ", " + Data.CONTACT_ID + ", " + Data.IS_PRIMARY + " 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 String millis = String.valueOf(System.currentTimeMillis()); final Cursor c = context.getContentResolver().query(PhoneQuery.URI, (mNameDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) ? PhoneQuery.PROJECTION_PRIMARY : PhoneQuery.PROJECTION_ALTERNATIVE, null, new String[] {millis, millis}, 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 in * onResume. * * @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 { @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); } } }