• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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     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;
CacheEntry(String customRingtone, boolean shouldSendToVoicemail)77         public CacheEntry(String customRingtone, boolean shouldSendToVoicemail) {
78             this.customRingtone = customRingtone;
79             this.sendToVoicemail = shouldSendToVoicemail;
80         }
81 
82         @Override
toString()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          */
acquireWakeLockAndExecute()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
doInBackground(Void... params)106         protected Void doInBackground(Void... params) {
107             if (DBG) log("Start refreshing cache.");
108             refreshCacheEntry();
109             return null;
110         }
111 
112         @Override
onPostExecute(Void result)113         protected void onPostExecute(Void result) {
114             if (VDBG) log("CacheAsyncTask#onPostExecute()");
115             super.onPostExecute(result);
116             releaseWakeLock();
117         }
118 
119         @Override
onCancelled(Void result)120         protected void onCancelled(Void result) {
121             if (VDBG) log("CacheAsyncTask#onCanceled()");
122             super.onCancelled(result);
123             releaseWakeLock();
124         }
125 
releaseWakeLock()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 
init(Context context)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 
CallerInfoCache(Context context)162     private CallerInfoCache(Context context) {
163         mContext = context;
164         mNumberToEntry = new HashMap<String, CacheEntry>();
165     }
166 
startAsyncCache()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 
refreshCacheEntry()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 
putNewEntryWhenAppropriate(HashMap<String, CacheEntry> newNumberToEntry, String numberOrSipAddress, String customRingtone, boolean sendToVoicemail)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      */
getCacheEntry(String number)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 
log(String msg)310     private static void log(String msg) {
311         Log.d(LOG_TAG, msg);
312     }
313 }
314