/* * 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.content.Context; import android.database.Cursor; import android.os.AsyncTask; import android.os.PowerManager; 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 { 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 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(); } /* 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 newNumberToEntry = new HashMap(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 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 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); } }