From 661834dd3b45b477165effe126a9c243750ae8d6 Mon Sep 17 00:00:00 2001 From: Christine Chen Date: Thu, 30 May 2013 16:54:09 -0700 Subject: Add SmartDial database for the Dialer app. - Creates a database helper to create a smartdial database for the Dialer app. - Queries all rows in the Contact database and copies related columns to the smart dial database. - Create another prefix database to contain all prefixes of a contact. - During keypad input, the prefix databse is queried to find contact suggestions, and suggestions are ranked by the usage data and contact status (starred, primary contact, etc.) - Created unit test for the SmartDial database insertion and prefix computing functions. Change-Id: I4d7c3b3bcc52dd6efa4d6e69d3f1687c3abaeb69 --- src/com/android/dialer/dialpad/SmartDialCache.java | 408 --------------------- 1 file changed, 408 deletions(-) delete mode 100644 src/com/android/dialer/dialpad/SmartDialCache.java (limited to 'src/com/android/dialer/dialpad/SmartDialCache.java') diff --git a/src/com/android/dialer/dialpad/SmartDialCache.java b/src/com/android/dialer/dialpad/SmartDialCache.java deleted file mode 100644 index 3d4a563af..000000000 --- a/src/com/android/dialer/dialpad/SmartDialCache.java +++ /dev/null @@ -1,408 +0,0 @@ -/* - * 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.SmartDialController.LOG_TAG; - -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; -import android.preference.PreferenceManager; -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.telephony.TelephonyManager; -import android.text.TextUtils; -import android.util.Log; - -import com.android.contacts.common.util.StopWatch; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; - -import java.util.Comparator; -import java.util.HashSet; -import java.util.Set; -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 = 3L * 24 * 60 * 60 * 1000; - - // Recent contacts - those contacted within the last 30 days (in milliseconds) - final static long LAST_TIME_USED_RECENT_MS = 30L * 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"; - } - - // Static set used to determine which countries use NANP numbers - public static Set sNanpCountries = null; - - private SmartDialTrie mContactsCache; - private static AtomicInteger mCacheStatus; - private final SmartDialMap mMap; - private final int mNameDisplayOrder; - private final Context mContext; - private final static Object mLock = new Object(); - - /** The country code of the user's sim card obtained by calling getSimCountryIso*/ - private static final String PREF_USER_SIM_COUNTRY_CODE = - "DialtactsActivity_user_sim_country_code"; - private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null; - - private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT; - private static boolean sUserInNanpRegion = false; - - 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, SmartDialMap map) { - mNameDisplayOrder = nameDisplayOrder; - mMap = map; - Preconditions.checkNotNull(context, "Context must not be null"); - mContext = context.getApplicationContext(); - mCacheStatus = new AtomicInteger(CACHE_NEEDS_RECACHE); - - final TelephonyManager manager = (TelephonyManager) context.getSystemService( - Context.TELEPHONY_SERVICE); - if (manager != null) { - sUserSimCountryCode = manager.getSimCountryIso(); - } - - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - if (sUserSimCountryCode != null) { - // Update shared preferences with the latest country obtained from getSimCountryIso - prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply(); - } else { - // Couldn't get the country from getSimCountryIso. Maybe we are in airplane mode. - // Try to load the settings, if any from SharedPreferences. - sUserSimCountryCode = prefs.getString(PREF_USER_SIM_COUNTRY_CODE, - PREF_USER_SIM_COUNTRY_CODE_DEFAULT); - } - - sUserInNanpRegion = isCountryNanp(sUserSimCountryCode); - - } - - 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, - SmartDialMap map) { - if (instance == null) { - instance = new SmartDialCache(context, nameDisplayOrder, map); - } - 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(mMap, sUserInNanpRegion); - 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() : 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); - } - - } - - public SmartDialMap getMap() { - return mMap; - } - - public boolean getUserInNanpRegion() { - return sUserInNanpRegion; - } - - /** - * Indicates whether the given country uses NANP numbers - * - * @param country ISO 3166 country code (case doesn't matter) - * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise - */ - @VisibleForTesting - static boolean isCountryNanp(String country) { - if (TextUtils.isEmpty(country)) { - return false; - } - if (sNanpCountries == null) { - sNanpCountries = initNanpCountries(); - } - return sNanpCountries.contains(country.toUpperCase()); - } - - private static Set initNanpCountries() { - final HashSet result = new HashSet(); - result.add("US"); // United States - result.add("CA"); // Canada - result.add("AS"); // American Samoa - result.add("AI"); // Anguilla - result.add("AG"); // Antigua and Barbuda - result.add("BS"); // Bahamas - result.add("BB"); // Barbados - result.add("BM"); // Bermuda - result.add("VG"); // British Virgin Islands - result.add("KY"); // Cayman Islands - result.add("DM"); // Dominica - result.add("DO"); // Dominican Republic - result.add("GD"); // Grenada - result.add("GU"); // Guam - result.add("JM"); // Jamaica - result.add("PR"); // Puerto Rico - result.add("MS"); // Montserrat - result.add("MP"); // Northern Mariana Islands - result.add("KN"); // Saint Kitts and Nevis - result.add("LC"); // Saint Lucia - result.add("VC"); // Saint Vincent and the Grenadines - result.add("TT"); // Trinidad and Tobago - result.add("TC"); // Turks and Caicos Islands - result.add("VI"); // U.S. Virgin Islands - return result; - } -} -- cgit v1.2.3