• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ContactNumber(long id, String displayName, String phoneNumber, String lookupKey, int affinity)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 
SmartDialCache(Context context, int nameDisplayOrder)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      */
getInstance(Context context, int nameDisplayOrder)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      */
cacheContacts(Context context)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      */
getContacts()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 
cacheIfNeeded(boolean forceRecache)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 
startCachingThread()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
compare(ContactNumber lhs, ContactNumber rhs)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 
getUserInNanpRegion()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
isCountryNanp(String country)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 
initNanpCountries()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