• 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             (PhoneApp.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;
CacheEntry(String customRingtone, boolean shouldSendToVoicemail)83         public CacheEntry(String customRingtone, boolean shouldSendToVoicemail) {
84             this.customRingtone = customRingtone;
85             this.sendToVoicemail = shouldSendToVoicemail;
86         }
87 
88         @Override
toString()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          */
acquireWakeLockAndExecute()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
doInBackground(Void... params)112         protected Void doInBackground(Void... params) {
113             if (DBG) log("Start refreshing cache.");
114             refreshCacheEntry();
115             return null;
116         }
117 
118         @Override
onPostExecute(Void result)119         protected void onPostExecute(Void result) {
120             if (VDBG) log("CacheAsyncTask#onPostExecute()");
121             super.onPostExecute(result);
122             releaseWakeLock();
123         }
124 
125         @Override
onCancelled(Void result)126         protected void onCancelled(Void result) {
127             if (VDBG) log("CacheAsyncTask#onCanceled()");
128             super.onCancelled(result);
129             releaseWakeLock();
130         }
131 
releaseWakeLock()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 
init(Context context)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 
CallerInfoCache(Context context)169     private CallerInfoCache(Context context) {
170         mContext = context;
171         mNumberToEntry = new HashMap<String, CacheEntry>();
172     }
173 
startAsyncCache()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      */
setRepeatingCacheUpdateAlarm()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 
refreshCacheEntry()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 
putNewEntryWhenAppropriate(HashMap<String, CacheEntry> newNumberToEntry, String numberOrSipAddress, String customRingtone, boolean sendToVoicemail)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      */
getCacheEntry(String number)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 
log(String msg)334     private static void log(String msg) {
335         Log.d(LOG_TAG, msg);
336     }
337 }
338