blob: 644aa22224a277af62f1a0272032e28e5c2c7b51 [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.phone;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.PowerManager;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.provider.ContactsContract.CommonDataKinds.Callable;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Data;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import java.util.HashMap;
import java.util.Map.Entry;
/**
* Holds "custom ringtone" and "send to voicemail" information for each contact as a fallback of
* contacts database. The cached information is refreshed periodically and used when database
* lookup (via ContentResolver) takes longer time than expected.
*
* The data inside this class shouldn't be treated as "primary"; they may not reflect the
* latest information stored in the original database.
*/
public class CallerInfoCache {
private static final String LOG_TAG = CallerInfoCache.class.getSimpleName();
private static final boolean DBG =
(PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
/** This must not be set to true when submitting changes. */
private static final boolean VDBG = false;
public static final int MESSAGE_UPDATE_CACHE = 0;
// Assuming DATA.DATA1 corresponds to Phone.NUMBER and SipAddress.ADDRESS, we just use
// Data columns as much as we can. One exception: because normalized numbers won't be used in
// SIP cases, Phone.NORMALIZED_NUMBER is used as is instead of using Data.
private static final String[] PROJECTION = new String[] {
Data.DATA1, // 0
Phone.NORMALIZED_NUMBER, // 1
Data.CUSTOM_RINGTONE, // 2
Data.SEND_TO_VOICEMAIL // 3
};
private static final int INDEX_NUMBER = 0;
private static final int INDEX_NORMALIZED_NUMBER = 1;
private static final int INDEX_CUSTOM_RINGTONE = 2;
private static final int INDEX_SEND_TO_VOICEMAIL = 3;
private static final String SELECTION = "("
+ "(" + Data.CUSTOM_RINGTONE + " IS NOT NULL OR " + Data.SEND_TO_VOICEMAIL + "=1)"
+ " AND " + Data.DATA1 + " IS NOT NULL)";
public static class CacheEntry {
public final String customRingtone;
public final boolean sendToVoicemail;
public CacheEntry(String customRingtone, boolean shouldSendToVoicemail) {
this.customRingtone = customRingtone;
this.sendToVoicemail = shouldSendToVoicemail;
}
@Override
public String toString() {
return "ringtone: " + customRingtone + ", " + sendToVoicemail;
}
}
private class CacheAsyncTask extends AsyncTask<Void, Void, Void> {
private PowerManager.WakeLock mWakeLock;
/**
* Call {@link PowerManager.WakeLock#acquire} and call {@link AsyncTask#execute(Object...)},
* guaranteeing the lock is held during the asynchronous task.
*/
public void acquireWakeLockAndExecute() {
// Prepare a separate partial WakeLock than what PhoneApp has so to avoid
// unnecessary conflict.
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
mWakeLock.acquire();
execute();
}
@Override
protected Void doInBackground(Void... params) {
if (DBG) log("Start refreshing cache.");
refreshCacheEntry();
return null;
}
@Override
protected void onPostExecute(Void result) {
if (VDBG) log("CacheAsyncTask#onPostExecute()");
super.onPostExecute(result);
releaseWakeLock();
}
@Override
protected void onCancelled(Void result) {
if (VDBG) log("CacheAsyncTask#onCanceled()");
super.onCancelled(result);
releaseWakeLock();
}
private void releaseWakeLock() {
if (mWakeLock != null && mWakeLock.isHeld()) {
mWakeLock.release();
}
}
}
private final Context mContext;
/**
* The mapping from number to CacheEntry.
*
* The number will be:
* - last 7 digits of each "normalized phone number when it is for PSTN phone call, or
* - a full SIP address for SIP call
*
* When cache is being refreshed, this whole object will be replaced with a newer object,
* instead of updating elements inside the object. "volatile" is used to make
* {@link #getCacheEntry(String)} access to the newer one every time when the object is
* being replaced.
*/
private volatile HashMap<String, CacheEntry> mNumberToEntry;
/**
* Used to remember if the previous task is finished or not. Should be set to null when done.
*/
private CacheAsyncTask mCacheAsyncTask;
public static CallerInfoCache init(Context context) {
if (DBG) log("init()");
CallerInfoCache cache = new CallerInfoCache(context);
// The first cache should be available ASAP.
cache.startAsyncCache();
return cache;
}
private CallerInfoCache(Context context) {
mContext = context;
mNumberToEntry = new HashMap<String, CacheEntry>();
}
/* package */ void startAsyncCache() {
if (DBG) log("startAsyncCache");
if (mCacheAsyncTask != null) {
Log.w(LOG_TAG, "Previous cache task is remaining.");
mCacheAsyncTask.cancel(true);
}
mCacheAsyncTask = new CacheAsyncTask();
mCacheAsyncTask.acquireWakeLockAndExecute();
}
private void refreshCacheEntry() {
if (VDBG) log("refreshCacheEntry() started");
// There's no way to know which part of the database was updated. Also we don't want
// to block incoming calls asking for the cache. So this method just does full query
// and replaces the older cache with newer one. To refrain from blocking incoming calls,
// it keeps older one as much as it can, and replaces it with newer one inside a very small
// synchronized block.
Cursor cursor = null;
try {
cursor = mContext.getContentResolver().query(Callable.CONTENT_URI,
PROJECTION, SELECTION, null, null);
if (cursor != null) {
// We don't want to block real in-coming call, so prepare a completely fresh
// cache here again, and replace it with older one.
final HashMap<String, CacheEntry> newNumberToEntry =
new HashMap<String, CacheEntry>(cursor.getCount());
while (cursor.moveToNext()) {
final String number = cursor.getString(INDEX_NUMBER);
String normalizedNumber = cursor.getString(INDEX_NORMALIZED_NUMBER);
if (normalizedNumber == null) {
// There's no guarantee normalized numbers are available every time and
// it may become null sometimes. Try formatting the original number.
normalizedNumber = PhoneNumberUtils.normalizeNumber(number);
}
final String customRingtone = cursor.getString(INDEX_CUSTOM_RINGTONE);
final boolean sendToVoicemail = cursor.getInt(INDEX_SEND_TO_VOICEMAIL) == 1;
if (PhoneNumberUtils.isUriNumber(number)) {
// SIP address case
putNewEntryWhenAppropriate(
newNumberToEntry, number, customRingtone, sendToVoicemail);
} else {
// PSTN number case
// Each normalized number may or may not have full content of the number.
// Contacts database may contain +15001234567 while a dialed number may be
// just 5001234567. Also we may have inappropriate country
// code in some cases (e.g. when the location of the device is inconsistent
// with the device's place). So to avoid confusion we just rely on the last
// 7 digits here. It may cause some kind of wrong behavior, which is
// unavoidable anyway in very rare cases..
final int length = normalizedNumber.length();
final String key = length > 7
? normalizedNumber.substring(length - 7, length)
: normalizedNumber;
putNewEntryWhenAppropriate(
newNumberToEntry, key, customRingtone, sendToVoicemail);
}
}
if (VDBG) {
Log.d(LOG_TAG, "New cache size: " + newNumberToEntry.size());
for (Entry<String, CacheEntry> entry : newNumberToEntry.entrySet()) {
Log.d(LOG_TAG, "Number: " + entry.getKey() + " -> " + entry.getValue());
}
}
mNumberToEntry = newNumberToEntry;
if (DBG) {
log("Caching entries are done. Total: " + newNumberToEntry.size());
}
} else {
// Let's just wait for the next refresh..
//
// If the cursor became null at that exact moment, probably we don't want to
// drop old cache. Also the case is fairly rare in usual cases unless acore being
// killed, so we don't take care much of this case.
Log.w(LOG_TAG, "cursor is null");
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (VDBG) log("refreshCacheEntry() ended");
}
private void putNewEntryWhenAppropriate(HashMap<String, CacheEntry> newNumberToEntry,
String numberOrSipAddress, String customRingtone, boolean sendToVoicemail) {
if (newNumberToEntry.containsKey(numberOrSipAddress)) {
// There may be duplicate entries here and we should prioritize
// "send-to-voicemail" flag in any case.
final CacheEntry entry = newNumberToEntry.get(numberOrSipAddress);
if (!entry.sendToVoicemail && sendToVoicemail) {
newNumberToEntry.put(numberOrSipAddress,
new CacheEntry(customRingtone, sendToVoicemail));
}
} else {
newNumberToEntry.put(numberOrSipAddress,
new CacheEntry(customRingtone, sendToVoicemail));
}
}
/**
* Returns CacheEntry for the given number (PSTN number or SIP address).
*
* @param number OK to be unformatted.
* @return CacheEntry to be used. Maybe null if there's no cache here. Note that this may
* return null when the cache itself is not ready. BE CAREFUL. (or might be better to throw
* an exception)
*/
public CacheEntry getCacheEntry(String number) {
if (mNumberToEntry == null) {
// Very unusual state. This implies the cache isn't ready during the request, while
// it should be prepared on the boot time (i.e. a way before even the first request).
Log.w(LOG_TAG, "Fallback cache isn't ready.");
return null;
}
CacheEntry entry;
if (PhoneNumberUtils.isUriNumber(number)) {
if (VDBG) log("Trying to lookup " + number);
entry = mNumberToEntry.get(number);
} else {
final String normalizedNumber = PhoneNumberUtils.normalizeNumber(number);
final int length = normalizedNumber.length();
final String key =
(length > 7 ? normalizedNumber.substring(length - 7, length)
: normalizedNumber);
if (VDBG) log("Trying to lookup " + key);
entry = mNumberToEntry.get(key);
}
if (VDBG) log("Obtained " + entry);
return entry;
}
private static void log(String msg) {
Log.d(LOG_TAG, msg);
}
}