blob: 644aa22224a277af62f1a0272032e28e5c2c7b51 [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
Santos Cordon7d4ddf62013-07-10 11:58:08 -070053 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();
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700159 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
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700178 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}