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