1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.phone; 18 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.os.AsyncTask; 22 import android.os.PowerManager; 23 import android.os.SystemProperties; 24 import android.provider.ContactsContract.CommonDataKinds.Callable; 25 import android.provider.ContactsContract.CommonDataKinds.Phone; 26 import android.provider.ContactsContract.Data; 27 import android.telephony.PhoneNumberUtils; 28 import android.util.Log; 29 30 import java.util.HashMap; 31 import java.util.Map.Entry; 32 33 /** 34 * Holds "custom ringtone" and "send to voicemail" information for each contact as a fallback of 35 * contacts database. The cached information is refreshed periodically and used when database 36 * lookup (via ContentResolver) takes longer time than expected. 37 * 38 * The data inside this class shouldn't be treated as "primary"; they may not reflect the 39 * latest information stored in the original database. 40 */ 41 public class CallerInfoCache { 42 private static final String LOG_TAG = CallerInfoCache.class.getSimpleName(); 43 private static final boolean DBG = 44 (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1); 45 46 /** This must not be set to true when submitting changes. */ 47 private static final boolean VDBG = false; 48 49 public static final int MESSAGE_UPDATE_CACHE = 0; 50 51 // Assuming DATA.DATA1 corresponds to Phone.NUMBER and SipAddress.ADDRESS, we just use 52 // Data columns as much as we can. One exception: because normalized numbers won't be used in 53 // SIP cases, Phone.NORMALIZED_NUMBER is used as is instead of using Data. 54 private static final String[] PROJECTION = new String[] { 55 Data.DATA1, // 0 56 Phone.NORMALIZED_NUMBER, // 1 57 Data.CUSTOM_RINGTONE, // 2 58 Data.SEND_TO_VOICEMAIL // 3 59 }; 60 61 private static final int INDEX_NUMBER = 0; 62 private static final int INDEX_NORMALIZED_NUMBER = 1; 63 private static final int INDEX_CUSTOM_RINGTONE = 2; 64 private static final int INDEX_SEND_TO_VOICEMAIL = 3; 65 66 private static final String SELECTION = "(" 67 + "(" + Data.CUSTOM_RINGTONE + " IS NOT NULL OR " + Data.SEND_TO_VOICEMAIL + "=1)" 68 + " AND " + Data.DATA1 + " IS NOT NULL)"; 69 70 public static class CacheEntry { 71 public final String customRingtone; 72 public final boolean sendToVoicemail; CacheEntry(String customRingtone, boolean shouldSendToVoicemail)73 public CacheEntry(String customRingtone, boolean shouldSendToVoicemail) { 74 this.customRingtone = customRingtone; 75 this.sendToVoicemail = shouldSendToVoicemail; 76 } 77 78 @Override toString()79 public String toString() { 80 return "ringtone: " + customRingtone + ", " + sendToVoicemail; 81 } 82 } 83 84 private class CacheAsyncTask extends AsyncTask<Void, Void, Void> { 85 86 private PowerManager.WakeLock mWakeLock; 87 88 /** 89 * Call {@link PowerManager.WakeLock#acquire} and call {@link AsyncTask#execute(Object...)}, 90 * guaranteeing the lock is held during the asynchronous task. 91 */ acquireWakeLockAndExecute()92 public void acquireWakeLockAndExecute() { 93 // Prepare a separate partial WakeLock than what PhoneApp has so to avoid 94 // unnecessary conflict. 95 PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 96 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG); 97 mWakeLock.acquire(); 98 execute(); 99 } 100 101 @Override doInBackground(Void... params)102 protected Void doInBackground(Void... params) { 103 if (DBG) log("Start refreshing cache."); 104 refreshCacheEntry(); 105 return null; 106 } 107 108 @Override onPostExecute(Void result)109 protected void onPostExecute(Void result) { 110 if (VDBG) log("CacheAsyncTask#onPostExecute()"); 111 super.onPostExecute(result); 112 releaseWakeLock(); 113 } 114 115 @Override onCancelled(Void result)116 protected void onCancelled(Void result) { 117 if (VDBG) log("CacheAsyncTask#onCanceled()"); 118 super.onCancelled(result); 119 releaseWakeLock(); 120 } 121 releaseWakeLock()122 private void releaseWakeLock() { 123 if (mWakeLock != null && mWakeLock.isHeld()) { 124 mWakeLock.release(); 125 } 126 } 127 } 128 129 private final Context mContext; 130 131 /** 132 * The mapping from number to CacheEntry. 133 * 134 * The number will be: 135 * - last 7 digits of each "normalized phone number when it is for PSTN phone call, or 136 * - a full SIP address for SIP call 137 * 138 * When cache is being refreshed, this whole object will be replaced with a newer object, 139 * instead of updating elements inside the object. "volatile" is used to make 140 * {@link #getCacheEntry(String)} access to the newer one every time when the object is 141 * being replaced. 142 */ 143 private volatile HashMap<String, CacheEntry> mNumberToEntry; 144 145 /** 146 * Used to remember if the previous task is finished or not. Should be set to null when done. 147 */ 148 private CacheAsyncTask mCacheAsyncTask; 149 init(Context context)150 public static CallerInfoCache init(Context context) { 151 if (DBG) log("init()"); 152 CallerInfoCache cache = new CallerInfoCache(context); 153 // The first cache should be available ASAP. 154 cache.startAsyncCache(); 155 return cache; 156 } 157 CallerInfoCache(Context context)158 private CallerInfoCache(Context context) { 159 mContext = context; 160 mNumberToEntry = new HashMap<String, CacheEntry>(); 161 } 162 startAsyncCache()163 /* package */ void startAsyncCache() { 164 if (DBG) log("startAsyncCache"); 165 166 if (mCacheAsyncTask != null) { 167 Log.w(LOG_TAG, "Previous cache task is remaining."); 168 mCacheAsyncTask.cancel(true); 169 } 170 mCacheAsyncTask = new CacheAsyncTask(); 171 mCacheAsyncTask.acquireWakeLockAndExecute(); 172 } 173 refreshCacheEntry()174 private void refreshCacheEntry() { 175 if (VDBG) log("refreshCacheEntry() started"); 176 177 // There's no way to know which part of the database was updated. Also we don't want 178 // to block incoming calls asking for the cache. So this method just does full query 179 // and replaces the older cache with newer one. To refrain from blocking incoming calls, 180 // it keeps older one as much as it can, and replaces it with newer one inside a very small 181 // synchronized block. 182 183 Cursor cursor = null; 184 try { 185 cursor = mContext.getContentResolver().query(Callable.CONTENT_URI, 186 PROJECTION, SELECTION, null, null); 187 if (cursor != null) { 188 // We don't want to block real in-coming call, so prepare a completely fresh 189 // cache here again, and replace it with older one. 190 final HashMap<String, CacheEntry> newNumberToEntry = 191 new HashMap<String, CacheEntry>(cursor.getCount()); 192 193 while (cursor.moveToNext()) { 194 final String number = cursor.getString(INDEX_NUMBER); 195 String normalizedNumber = cursor.getString(INDEX_NORMALIZED_NUMBER); 196 if (normalizedNumber == null) { 197 // There's no guarantee normalized numbers are available every time and 198 // it may become null sometimes. Try formatting the original number. 199 normalizedNumber = PhoneNumberUtils.normalizeNumber(number); 200 } 201 final String customRingtone = cursor.getString(INDEX_CUSTOM_RINGTONE); 202 final boolean sendToVoicemail = cursor.getInt(INDEX_SEND_TO_VOICEMAIL) == 1; 203 204 if (PhoneNumberUtils.isUriNumber(number)) { 205 // SIP address case 206 putNewEntryWhenAppropriate( 207 newNumberToEntry, number, customRingtone, sendToVoicemail); 208 } else { 209 // PSTN number case 210 // Each normalized number may or may not have full content of the number. 211 // Contacts database may contain +15001234567 while a dialed number may be 212 // just 5001234567. Also we may have inappropriate country 213 // code in some cases (e.g. when the location of the device is inconsistent 214 // with the device's place). So to avoid confusion we just rely on the last 215 // 7 digits here. It may cause some kind of wrong behavior, which is 216 // unavoidable anyway in very rare cases.. 217 final int length = normalizedNumber.length(); 218 final String key = length > 7 219 ? normalizedNumber.substring(length - 7, length) 220 : normalizedNumber; 221 putNewEntryWhenAppropriate( 222 newNumberToEntry, key, customRingtone, sendToVoicemail); 223 } 224 } 225 226 if (VDBG) { 227 Log.d(LOG_TAG, "New cache size: " + newNumberToEntry.size()); 228 for (Entry<String, CacheEntry> entry : newNumberToEntry.entrySet()) { 229 Log.d(LOG_TAG, "Number: " + entry.getKey() + " -> " + entry.getValue()); 230 } 231 } 232 233 mNumberToEntry = newNumberToEntry; 234 235 if (DBG) { 236 log("Caching entries are done. Total: " + newNumberToEntry.size()); 237 } 238 } else { 239 // Let's just wait for the next refresh.. 240 // 241 // If the cursor became null at that exact moment, probably we don't want to 242 // drop old cache. Also the case is fairly rare in usual cases unless acore being 243 // killed, so we don't take care much of this case. 244 Log.w(LOG_TAG, "cursor is null"); 245 } 246 } finally { 247 if (cursor != null) { 248 cursor.close(); 249 } 250 } 251 252 if (VDBG) log("refreshCacheEntry() ended"); 253 } 254 putNewEntryWhenAppropriate(HashMap<String, CacheEntry> newNumberToEntry, String numberOrSipAddress, String customRingtone, boolean sendToVoicemail)255 private void putNewEntryWhenAppropriate(HashMap<String, CacheEntry> newNumberToEntry, 256 String numberOrSipAddress, String customRingtone, boolean sendToVoicemail) { 257 if (newNumberToEntry.containsKey(numberOrSipAddress)) { 258 // There may be duplicate entries here and we should prioritize 259 // "send-to-voicemail" flag in any case. 260 final CacheEntry entry = newNumberToEntry.get(numberOrSipAddress); 261 if (!entry.sendToVoicemail && sendToVoicemail) { 262 newNumberToEntry.put(numberOrSipAddress, 263 new CacheEntry(customRingtone, sendToVoicemail)); 264 } 265 } else { 266 newNumberToEntry.put(numberOrSipAddress, 267 new CacheEntry(customRingtone, sendToVoicemail)); 268 } 269 } 270 271 /** 272 * Returns CacheEntry for the given number (PSTN number or SIP address). 273 * 274 * @param number OK to be unformatted. 275 * @return CacheEntry to be used. Maybe null if there's no cache here. Note that this may 276 * return null when the cache itself is not ready. BE CAREFUL. (or might be better to throw 277 * an exception) 278 */ getCacheEntry(String number)279 public CacheEntry getCacheEntry(String number) { 280 if (mNumberToEntry == null) { 281 // Very unusual state. This implies the cache isn't ready during the request, while 282 // it should be prepared on the boot time (i.e. a way before even the first request). 283 Log.w(LOG_TAG, "Fallback cache isn't ready."); 284 return null; 285 } 286 287 CacheEntry entry; 288 if (PhoneNumberUtils.isUriNumber(number)) { 289 if (VDBG) log("Trying to lookup " + number); 290 291 entry = mNumberToEntry.get(number); 292 } else { 293 final String normalizedNumber = PhoneNumberUtils.normalizeNumber(number); 294 final int length = normalizedNumber.length(); 295 final String key = 296 (length > 7 ? normalizedNumber.substring(length - 7, length) 297 : normalizedNumber); 298 if (VDBG) log("Trying to lookup " + key); 299 300 entry = mNumberToEntry.get(key); 301 } 302 if (VDBG) log("Obtained " + entry); 303 return entry; 304 } 305 log(String msg)306 private static void log(String msg) { 307 Log.d(LOG_TAG, msg); 308 } 309 } 310