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