blob: 51e900ada5344dd1938634492b333d2dcd7fb1d5 [file] [log] [blame]
/*
* 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;
}
}