blob: 2707c7cc85394425cdc33d5cd8771b8147876638 [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
Santos Cordon7d4ddf62013-07-10 11:58:08 -070019import android.content.Context;
Santos Cordon7d4ddf62013-07-10 11:58:08 -070020import android.database.Cursor;
21import android.os.AsyncTask;
22import android.os.PowerManager;
Santos Cordon7d4ddf62013-07-10 11:58:08 -070023import android.os.SystemProperties;
24import android.provider.ContactsContract.CommonDataKinds.Callable;
25import android.provider.ContactsContract.CommonDataKinds.Phone;
26import android.provider.ContactsContract.Data;
27import android.telephony.PhoneNumberUtils;
28import android.util.Log;
29
30import java.util.HashMap;
31import 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 */
41public 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
Santos Cordon7d4ddf62013-07-10 11:58:08 -070049 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;
73 public CacheEntry(String customRingtone, boolean shouldSendToVoicemail) {
74 this.customRingtone = customRingtone;
75 this.sendToVoicemail = shouldSendToVoicemail;
76 }
77
78 @Override
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 */
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
102 protected Void doInBackground(Void... params) {
103 if (DBG) log("Start refreshing cache.");
104 refreshCacheEntry();
105 return null;
106 }
107
108 @Override
109 protected void onPostExecute(Void result) {
110 if (VDBG) log("CacheAsyncTask#onPostExecute()");
111 super.onPostExecute(result);
112 releaseWakeLock();
113 }
114
115 @Override
116 protected void onCancelled(Void result) {
117 if (VDBG) log("CacheAsyncTask#onCanceled()");
118 super.onCancelled(result);
119 releaseWakeLock();
120 }
121
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
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();
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700155 return cache;
156 }
157
158 private CallerInfoCache(Context context) {
159 mContext = context;
160 mNumberToEntry = new HashMap<String, CacheEntry>();
161 }
162
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
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700174 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
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 */
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
306 private static void log(String msg) {
307 Log.d(LOG_TAG, msg);
308 }
309}