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
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
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;
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();
159        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
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
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}
314