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