|  | /* | 
|  | * 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<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); | 
|  | } | 
|  | } |