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