Home | History | Annotate | Download | only in phone
      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;
     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