Home | History | Annotate | Download | only in dialpad
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.dialer.dialpad;
     18 
     19 import static com.android.dialer.dialpad.SmartDialController.LOG_TAG;
     20 
     21 import android.content.Context;
     22 import android.content.SharedPreferences;
     23 import android.database.Cursor;
     24 import android.net.Uri;
     25 import android.preference.PreferenceManager;
     26 import android.provider.ContactsContract;
     27 import android.provider.ContactsContract.CommonDataKinds.Phone;
     28 import android.provider.ContactsContract.Contacts;
     29 import android.provider.ContactsContract.Data;
     30 import android.provider.ContactsContract.Directory;
     31 import android.telephony.TelephonyManager;
     32 import android.text.TextUtils;
     33 import android.util.Log;
     34 
     35 import com.android.contacts.common.util.StopWatch;
     36 
     37 import com.google.common.annotations.VisibleForTesting;
     38 import com.google.common.base.Preconditions;
     39 
     40 import java.util.Comparator;
     41 import java.util.HashSet;
     42 import java.util.Set;
     43 import java.util.concurrent.atomic.AtomicInteger;
     44 
     45 /**
     46  * Cache object used to cache Smart Dial contacts that handles various states of the cache at the
     47  * point in time when getContacts() is called
     48  * 1) Cache is currently empty and there is no caching thread running - getContacts() starts a
     49  * caching thread and returns the cache when completed
     50  * 2) The cache is currently empty, but a caching thread has been started - getContacts() waits
     51  * till the existing caching thread is completed before immediately returning the cache
     52  * 3) The cache has already been populated, and there is no caching thread running - getContacts()
     53  * returns the existing cache immediately
     54  * 4) The cache has already been populated, but there is another caching thread running (due to
     55  * a forced cache refresh due to content updates - getContacts() returns the existing cache
     56  * immediately
     57  */
     58 public class SmartDialCache {
     59 
     60     public static class ContactNumber {
     61         public final String displayName;
     62         public final String lookupKey;
     63         public final long id;
     64         public final int affinity;
     65         public final String phoneNumber;
     66 
     67         public ContactNumber(long id, String displayName, String phoneNumber, String lookupKey,
     68                 int affinity) {
     69             this.displayName = displayName;
     70             this.lookupKey = lookupKey;
     71             this.id = id;
     72             this.affinity = affinity;
     73             this.phoneNumber = phoneNumber;
     74         }
     75     }
     76 
     77     public static interface PhoneQuery {
     78 
     79        Uri URI = Phone.CONTENT_URI.buildUpon().
     80                appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
     81                String.valueOf(Directory.DEFAULT)).
     82                appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true").
     83                build();
     84 
     85        final String[] PROJECTION_PRIMARY = new String[] {
     86             Phone._ID,                          // 0
     87             Phone.TYPE,                         // 1
     88             Phone.LABEL,                        // 2
     89             Phone.NUMBER,                       // 3
     90             Phone.CONTACT_ID,                   // 4
     91             Phone.LOOKUP_KEY,                   // 5
     92             Phone.DISPLAY_NAME_PRIMARY,         // 6
     93         };
     94 
     95         final String[] PROJECTION_ALTERNATIVE = new String[] {
     96             Phone._ID,                          // 0
     97             Phone.TYPE,                         // 1
     98             Phone.LABEL,                        // 2
     99             Phone.NUMBER,                       // 3
    100             Phone.CONTACT_ID,                   // 4
    101             Phone.LOOKUP_KEY,                   // 5
    102             Phone.DISPLAY_NAME_ALTERNATIVE,     // 6
    103         };
    104 
    105         public static final int PHONE_ID           = 0;
    106         public static final int PHONE_TYPE         = 1;
    107         public static final int PHONE_LABEL        = 2;
    108         public static final int PHONE_NUMBER       = 3;
    109         public static final int PHONE_CONTACT_ID   = 4;
    110         public static final int PHONE_LOOKUP_KEY   = 5;
    111         public static final int PHONE_DISPLAY_NAME = 6;
    112 
    113         // Current contacts - those contacted within the last 3 days (in milliseconds)
    114         final static long LAST_TIME_USED_CURRENT_MS = 3L * 24 * 60 * 60 * 1000;
    115 
    116         // Recent contacts - those contacted within the last 30 days (in milliseconds)
    117         final static long LAST_TIME_USED_RECENT_MS = 30L * 24 * 60 * 60 * 1000;
    118 
    119         final static String TIME_SINCE_LAST_USED_MS =
    120                 "(? - " + Data.LAST_TIME_USED + ")";
    121 
    122         final static String SORT_BY_DATA_USAGE =
    123                 "(CASE WHEN " + TIME_SINCE_LAST_USED_MS + " < " + LAST_TIME_USED_CURRENT_MS +
    124                 " THEN 0 " +
    125                 " WHEN " + TIME_SINCE_LAST_USED_MS + " < " + LAST_TIME_USED_RECENT_MS +
    126                 " THEN 1 " +
    127                 " ELSE 2 END), " +
    128                 Data.TIMES_USED + " DESC";
    129 
    130         // This sort order is similar to that used by the ContactsProvider when returning a list
    131         // of frequently called contacts.
    132         public static final String SORT_ORDER =
    133                 Contacts.STARRED + " DESC, "
    134                 + Data.IS_SUPER_PRIMARY + " DESC, "
    135                 + SORT_BY_DATA_USAGE + ", "
    136                 + Contacts.IN_VISIBLE_GROUP + " DESC, "
    137                 + Contacts.DISPLAY_NAME + ", "
    138                 + Data.CONTACT_ID + ", "
    139                 + Data.IS_PRIMARY + " DESC";
    140     }
    141 
    142     // Static set used to determine which countries use NANP numbers
    143     public static Set<String> sNanpCountries = null;
    144 
    145     private SmartDialTrie mContactsCache;
    146     private static AtomicInteger mCacheStatus;
    147     private final int mNameDisplayOrder;
    148     private final Context mContext;
    149     private final static Object mLock = new Object();
    150 
    151     /** The country code of the user's sim card obtained by calling getSimCountryIso*/
    152     private static final String PREF_USER_SIM_COUNTRY_CODE =
    153             "DialtactsActivity_user_sim_country_code";
    154     private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null;
    155 
    156     private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT;
    157     private static boolean sUserInNanpRegion = false;
    158 
    159     public static final int CACHE_NEEDS_RECACHE = 1;
    160     public static final int CACHE_IN_PROGRESS = 2;
    161     public static final int CACHE_COMPLETED = 3;
    162 
    163     private static final boolean DEBUG = false;
    164 
    165     private SmartDialCache(Context context, int nameDisplayOrder) {
    166         mNameDisplayOrder = nameDisplayOrder;
    167         Preconditions.checkNotNull(context, "Context must not be null");
    168         mContext = context.getApplicationContext();
    169         mCacheStatus = new AtomicInteger(CACHE_NEEDS_RECACHE);
    170 
    171         final TelephonyManager manager = (TelephonyManager) context.getSystemService(
    172                 Context.TELEPHONY_SERVICE);
    173         if (manager != null) {
    174             sUserSimCountryCode = manager.getSimCountryIso();
    175         }
    176 
    177         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
    178 
    179         if (sUserSimCountryCode != null) {
    180             // Update shared preferences with the latest country obtained from getSimCountryIso
    181             prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply();
    182         } else {
    183             // Couldn't get the country from getSimCountryIso. Maybe we are in airplane mode.
    184             // Try to load the settings, if any from SharedPreferences.
    185             sUserSimCountryCode = prefs.getString(PREF_USER_SIM_COUNTRY_CODE,
    186                     PREF_USER_SIM_COUNTRY_CODE_DEFAULT);
    187         }
    188 
    189         sUserInNanpRegion = isCountryNanp(sUserSimCountryCode);
    190 
    191     }
    192 
    193     private static SmartDialCache instance;
    194 
    195     /**
    196      * Returns an instance of SmartDialCache.
    197      *
    198      * @param context A context that provides a valid ContentResolver.
    199      * @param nameDisplayOrder One of the two name display order integer constants (1 or 2) as saved
    200      *        in settings under the key
    201      *        {@link android.provider.ContactsContract.Preferences#DISPLAY_ORDER}.
    202      * @return An instance of SmartDialCache
    203      */
    204     public static synchronized SmartDialCache getInstance(Context context, int nameDisplayOrder) {
    205         if (instance == null) {
    206             instance = new SmartDialCache(context, nameDisplayOrder);
    207         }
    208         return instance;
    209     }
    210 
    211     /**
    212      * Performs a database query, iterates through the returned cursor and saves the retrieved
    213      * contacts to a local cache.
    214      */
    215     private void cacheContacts(Context context) {
    216         mCacheStatus.set(CACHE_IN_PROGRESS);
    217         synchronized(mLock) {
    218             if (DEBUG) {
    219                 Log.d(LOG_TAG, "Starting caching thread");
    220             }
    221             final StopWatch stopWatch = DEBUG ? StopWatch.start("SmartDial Cache") : null;
    222             final String millis = String.valueOf(System.currentTimeMillis());
    223             final Cursor c = context.getContentResolver().query(PhoneQuery.URI,
    224                     (mNameDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY)
    225                         ? PhoneQuery.PROJECTION_PRIMARY : PhoneQuery.PROJECTION_ALTERNATIVE,
    226                     null, new String[] {millis, millis},
    227                     PhoneQuery.SORT_ORDER);
    228             if (DEBUG) {
    229                 stopWatch.lap("SmartDial query complete");
    230             }
    231             if (c == null) {
    232                 Log.w(LOG_TAG, "SmartDial query received null for cursor");
    233                 if (DEBUG) {
    234                     stopWatch.stopAndLog("SmartDial query received null for cursor", 0);
    235                 }
    236                 mCacheStatus.getAndSet(CACHE_NEEDS_RECACHE);
    237                 return;
    238             }
    239             final SmartDialTrie cache = new SmartDialTrie(
    240                     SmartDialNameMatcher.LATIN_LETTERS_TO_DIGITS, sUserInNanpRegion);
    241             try {
    242                 c.moveToPosition(-1);
    243                 int affinityCount = 0;
    244                 while (c.moveToNext()) {
    245                     final String displayName = c.getString(PhoneQuery.PHONE_DISPLAY_NAME);
    246                     final String phoneNumber = c.getString(PhoneQuery.PHONE_NUMBER);
    247                     final long id = c.getLong(PhoneQuery.PHONE_CONTACT_ID);
    248                     final String lookupKey = c.getString(PhoneQuery.PHONE_LOOKUP_KEY);
    249                     cache.put(new ContactNumber(id, displayName, phoneNumber, lookupKey,
    250                             affinityCount));
    251                     affinityCount++;
    252                 }
    253             } finally {
    254                 c.close();
    255                 mContactsCache = cache;
    256                 if (DEBUG) {
    257                     stopWatch.stopAndLog("SmartDial caching completed", 0);
    258                 }
    259             }
    260         }
    261         if (DEBUG) {
    262             Log.d(LOG_TAG, "Caching thread completed");
    263         }
    264         mCacheStatus.getAndSet(CACHE_COMPLETED);
    265     }
    266 
    267     /**
    268      * Returns the list of cached contacts. This is blocking so it should not be called from the UI
    269      * thread. There are 4 possible scenarios:
    270      *
    271      * 1) Cache is currently empty and there is no caching thread running - getContacts() starts a
    272      * caching thread and returns the cache when completed
    273      * 2) The cache is currently empty, but a caching thread has been started - getContacts() waits
    274      * till the existing caching thread is completed before immediately returning the cache
    275      * 3) The cache has already been populated, and there is no caching thread running -
    276      * getContacts() returns the existing cache immediately
    277      * 4) The cache has already been populated, but there is another caching thread running (due to
    278      * a forced cache refresh due to content updates - getContacts() returns the existing cache
    279      * immediately
    280      *
    281      * @return List of already cached contacts, or an empty list if the caching failed for any
    282      * reason.
    283      */
    284     public SmartDialTrie getContacts() {
    285         // Either scenario 3 or 4 - This means just go ahead and return the existing cache
    286         // immediately even if there is a caching thread currently running. We are guaranteed to
    287         // have the newest value of mContactsCache at this point because it is volatile.
    288         if (mContactsCache != null) {
    289             return mContactsCache;
    290         }
    291         // At this point we are forced to wait for cacheContacts to complete in another thread(if
    292         // one currently exists) because of mLock.
    293         synchronized(mLock) {
    294             // If mContactsCache is still null at this point, either there was never any caching
    295             // process running, or it failed (Scenario 1). If so, just go ahead and try to cache
    296             // the contacts again.
    297             if (mContactsCache == null) {
    298                 cacheContacts(mContext);
    299                 return (mContactsCache == null) ? new SmartDialTrie() : mContactsCache;
    300             } else {
    301                 // After waiting for the lock on mLock to be released, mContactsCache is now
    302                 // non-null due to the completion of the caching thread (Scenario 2). Go ahead
    303                 // and return the existing cache.
    304                 return mContactsCache;
    305             }
    306         }
    307     }
    308 
    309     /**
    310      * Cache contacts only if there is a need to (forced cache refresh or no attempt to cache yet).
    311      * This method is called in 2 places: whenever the DialpadFragment comes into view, and in
    312      * onResume.
    313      *
    314      * @param forceRecache If true, force a cache refresh.
    315      */
    316 
    317     public void cacheIfNeeded(boolean forceRecache) {
    318         if (DEBUG) {
    319             Log.d("SmartDial", "cacheIfNeeded called with " + String.valueOf(forceRecache));
    320         }
    321         if (mCacheStatus.get() == CACHE_IN_PROGRESS) {
    322             return;
    323         }
    324         if (forceRecache || mCacheStatus.get() == CACHE_NEEDS_RECACHE) {
    325             // Because this method can be possibly be called multiple times in rapid succession,
    326             // set the cache status even before starting a caching thread to avoid unnecessarily
    327             // spawning extra threads.
    328             mCacheStatus.set(CACHE_IN_PROGRESS);
    329             startCachingThread();
    330         }
    331     }
    332 
    333     private void startCachingThread() {
    334         new Thread(new Runnable() {
    335             @Override
    336             public void run() {
    337                 cacheContacts(mContext);
    338             }
    339         }).start();
    340     }
    341 
    342     public static class ContactAffinityComparator implements Comparator<ContactNumber> {
    343         @Override
    344         public int compare(ContactNumber lhs, ContactNumber rhs) {
    345             // Smaller affinity is better because they are numbered in ascending order in
    346             // the order the contacts were returned from the ContactsProvider (sorted by
    347             // frequency of use and time last used
    348             return Integer.compare(lhs.affinity, rhs.affinity);
    349         }
    350 
    351     }
    352 
    353     public boolean getUserInNanpRegion() {
    354         return sUserInNanpRegion;
    355     }
    356 
    357     /**
    358      * Indicates whether the given country uses NANP numbers
    359      *
    360      * @param country ISO 3166 country code (case doesn't matter)
    361      * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise
    362      */
    363     @VisibleForTesting
    364     static boolean isCountryNanp(String country) {
    365         if (TextUtils.isEmpty(country)) {
    366             return false;
    367         }
    368         if (sNanpCountries == null) {
    369             sNanpCountries = initNanpCountries();
    370         }
    371         return sNanpCountries.contains(country.toUpperCase());
    372     }
    373 
    374     private static Set<String> initNanpCountries() {
    375         final HashSet<String> result = new HashSet<String>();
    376         result.add("US"); // United States
    377         result.add("CA"); // Canada
    378         result.add("AS"); // American Samoa
    379         result.add("AI"); // Anguilla
    380         result.add("AG"); // Antigua and Barbuda
    381         result.add("BS"); // Bahamas
    382         result.add("BB"); // Barbados
    383         result.add("BM"); // Bermuda
    384         result.add("VG"); // British Virgin Islands
    385         result.add("KY"); // Cayman Islands
    386         result.add("DM"); // Dominica
    387         result.add("DO"); // Dominican Republic
    388         result.add("GD"); // Grenada
    389         result.add("GU"); // Guam
    390         result.add("JM"); // Jamaica
    391         result.add("PR"); // Puerto Rico
    392         result.add("MS"); // Montserrat
    393         result.add("MP"); // Northern Mariana Islands
    394         result.add("KN"); // Saint Kitts and Nevis
    395         result.add("LC"); // Saint Lucia
    396         result.add("VC"); // Saint Vincent and the Grenadines
    397         result.add("TT"); // Trinidad and Tobago
    398         result.add("TC"); // Turks and Caicos Islands
    399         result.add("VI"); // U.S. Virgin Islands
    400         return result;
    401     }
    402 }
    403