blob: 76f79affdac9a8bbf788a3a60992340f61e9e6fb [file] [log] [blame]
Santos Cordon7d4ddf62013-07-10 11:58:08 -07001/*
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 /**
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}