| /* |
| * 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.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<String> sNanpCountries = null; |
| |
| private SmartDialTrie mContactsCache; |
| private static AtomicInteger mCacheStatus; |
| 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) { |
| mNameDisplayOrder = nameDisplayOrder; |
| 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) { |
| 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, 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<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 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<String> initNanpCountries() { |
| final HashSet<String> result = new HashSet<String>(); |
| 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; |
| } |
| } |