diff options
Diffstat (limited to 'src/com/android/phone/CallerInfoCache.java')
-rw-r--r-- | src/com/android/phone/CallerInfoCache.java | 337 |
1 files changed, 337 insertions, 0 deletions
diff --git a/src/com/android/phone/CallerInfoCache.java b/src/com/android/phone/CallerInfoCache.java new file mode 100644 index 00000000..76f79aff --- /dev/null +++ b/src/com/android/phone/CallerInfoCache.java @@ -0,0 +1,337 @@ +/* + * 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; + + /** + * Interval used with {@link AlarmManager#setInexactRepeating(int, long, long, PendingIntent)}, + * which means the actually interval may not be very accurate. + */ + private static final int CACHE_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours in millis. + + 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(); + cache.setRepeatingCacheUpdateAlarm(); + 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(); + } + + /** + * Set up periodic alarm for cache update. + */ + private void setRepeatingCacheUpdateAlarm() { + if (DBG) log("setRepeatingCacheUpdateAlarm"); + + Intent intent = new Intent(CallerInfoCacheUpdateReceiver.ACTION_UPDATE_CALLER_INFO_CACHE); + intent.setClass(mContext, CallerInfoCacheUpdateReceiver.class); + PendingIntent pendingIntent = + PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + // We don't need precise timer while this should be power efficient. + alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, + SystemClock.uptimeMillis() + CACHE_REFRESH_INTERVAL, + CACHE_REFRESH_INTERVAL, pendingIntent); + } + + 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); + } +} |