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