Home | History | Annotate | Download | only in data
      1 package com.android.mms.data;
      2 
      3 import java.util.ArrayList;
      4 import java.util.HashMap;
      5 import java.util.List;
      6 import java.util.Map;
      7 
      8 import javax.annotation.concurrent.GuardedBy;
      9 import javax.annotation.concurrent.ThreadSafe;
     10 
     11 import android.content.Context;
     12 import android.content.ContentResolver;
     13 import android.content.ContentValues;
     14 import android.content.ContentUris;
     15 import android.database.Cursor;
     16 import android.net.Uri;
     17 import android.text.TextUtils;
     18 import android.util.Log;
     19 import android.provider.Telephony;
     20 
     21 import com.android.mms.LogTag;
     22 import android.database.sqlite.SqliteWrapper;
     23 
     24 @ThreadSafe
     25 public class RecipientIdCache {
     26     private static final boolean LOCAL_DEBUG = false;
     27     private static final String TAG = "Mms/cache";
     28 
     29     private static Uri sAllCanonical =
     30             Uri.parse("content://mms-sms/canonical-addresses");
     31 
     32     private static Uri sSingleCanonicalAddressUri =
     33             Uri.parse("content://mms-sms/canonical-address");
     34 
     35     private static RecipientIdCache sInstance;
     36     static RecipientIdCache getInstance() { return sInstance; }
     37 
     38     @GuardedBy("this")
     39     private final Map<Long, String> mCache;
     40 
     41     private final Context mContext;
     42 
     43     public static class Entry {
     44         public long id;
     45         public String number;
     46 
     47         public Entry(long id, String number) {
     48             this.id = id;
     49             this.number = number;
     50         }
     51     };
     52 
     53     static void init(Context context) {
     54         sInstance = new RecipientIdCache(context);
     55         new Thread(new Runnable() {
     56             public void run() {
     57                 fill();
     58             }
     59         }).start();
     60     }
     61 
     62     RecipientIdCache(Context context) {
     63         mCache = new HashMap<Long, String>();
     64         mContext = context;
     65     }
     66 
     67     public static void fill() {
     68         if (LogTag.VERBOSE || Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
     69             LogTag.debug("[RecipientIdCache] fill: begin");
     70         }
     71 
     72         Context context = sInstance.mContext;
     73         Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
     74                 sAllCanonical, null, null, null, null);
     75         if (c == null) {
     76             Log.w(TAG, "null Cursor in fill()");
     77             return;
     78         }
     79 
     80         try {
     81             synchronized (sInstance) {
     82                 // Technically we don't have to clear this because the stupid
     83                 // canonical_addresses table is never GC'ed.
     84                 sInstance.mCache.clear();
     85                 while (c.moveToNext()) {
     86                     // TODO: don't hardcode the column indices
     87                     long id = c.getLong(0);
     88                     String number = c.getString(1);
     89                     sInstance.mCache.put(id, number);
     90                 }
     91             }
     92         } finally {
     93             c.close();
     94         }
     95 
     96         if (LogTag.VERBOSE || Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
     97             LogTag.debug("[RecipientIdCache] fill: finished");
     98             dump();
     99         }
    100     }
    101 
    102     public static List<Entry> getAddresses(String spaceSepIds) {
    103         synchronized (sInstance) {
    104             List<Entry> numbers = new ArrayList<Entry>();
    105             String[] ids = spaceSepIds.split(" ");
    106             for (String id : ids) {
    107                 long longId;
    108 
    109                 try {
    110                     longId = Long.parseLong(id);
    111                 } catch (NumberFormatException ex) {
    112                     // skip this id
    113                     continue;
    114                 }
    115 
    116                 String number = sInstance.mCache.get(longId);
    117 
    118                 if (number == null) {
    119                     Log.w(TAG, "RecipientId " + longId + " not in cache!");
    120                     if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
    121                         dump();
    122                     }
    123 
    124                     fill();
    125                     number = sInstance.mCache.get(longId);
    126                 }
    127 
    128                 if (TextUtils.isEmpty(number)) {
    129                     Log.w(TAG, "RecipientId " + longId + " has empty number!");
    130                 } else {
    131                     numbers.add(new Entry(longId, number));
    132                 }
    133             }
    134             return numbers;
    135         }
    136     }
    137 
    138     public static void updateNumbers(long threadId, ContactList contacts) {
    139         long recipientId = 0;
    140 
    141         for (Contact contact : contacts) {
    142             if (contact.isNumberModified()) {
    143                 contact.setIsNumberModified(false);
    144             } else {
    145                 // if the contact's number wasn't modified, don't bother.
    146                 continue;
    147             }
    148 
    149             recipientId = contact.getRecipientId();
    150             if (recipientId == 0) {
    151                 continue;
    152             }
    153 
    154             String number1 = contact.getNumber();
    155             boolean needsDbUpdate = false;
    156             synchronized (sInstance) {
    157                 String number2 = sInstance.mCache.get(recipientId);
    158 
    159                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    160                     Log.d(TAG, "[RecipientIdCache] updateNumbers: contact=" + contact +
    161                             ", wasModified=true, recipientId=" + recipientId);
    162                     Log.d(TAG, "   contact.getNumber=" + number1 +
    163                             ", sInstance.mCache.get(recipientId)=" + number2);
    164                 }
    165 
    166                 // if the numbers don't match, let's update the RecipientIdCache's number
    167                 // with the new number in the contact.
    168                 if (!number1.equalsIgnoreCase(number2)) {
    169                     sInstance.mCache.put(recipientId, number1);
    170                     needsDbUpdate = true;
    171                 }
    172             }
    173             if (needsDbUpdate) {
    174                 // Do this without the lock held.
    175                 sInstance.updateCanonicalAddressInDb(recipientId, number1);
    176             }
    177         }
    178     }
    179 
    180     private void updateCanonicalAddressInDb(long id, String number) {
    181         if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    182             Log.d(TAG, "[RecipientIdCache] updateCanonicalAddressInDb: id=" + id +
    183                     ", number=" + number);
    184         }
    185 
    186         final ContentValues values = new ContentValues();
    187         values.put(Telephony.CanonicalAddressesColumns.ADDRESS, number);
    188 
    189         final StringBuilder buf = new StringBuilder(Telephony.CanonicalAddressesColumns._ID);
    190         buf.append('=').append(id);
    191 
    192         final Uri uri = ContentUris.withAppendedId(sSingleCanonicalAddressUri, id);
    193         final ContentResolver cr = mContext.getContentResolver();
    194 
    195         // We're running on the UI thread so just fire & forget, hope for the best.
    196         // (We were ignoring the return value anyway...)
    197         new Thread("updateCanonicalAddressInDb") {
    198             public void run() {
    199                 cr.update(uri, values, buf.toString(), null);
    200             }
    201         }.start();
    202     }
    203 
    204     public static void dump() {
    205         // Only dump user private data if we're in special debug mode
    206         synchronized (sInstance) {
    207             Log.d(TAG, "*** Recipient ID cache dump ***");
    208             for (Long id : sInstance.mCache.keySet()) {
    209                 Log.d(TAG, id + ": " + sInstance.mCache.get(id));
    210             }
    211         }
    212     }
    213 
    214     public static void canonicalTableDump() {
    215         Log.d(TAG, "**** Dump of canoncial_addresses table ****");
    216         Context context = sInstance.mContext;
    217         Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
    218                 sAllCanonical, null, null, null, null);
    219         if (c == null) {
    220             Log.w(TAG, "null Cursor in content://mms-sms/canonical-addresses");
    221         }
    222         try {
    223             while (c.moveToNext()) {
    224                 // TODO: don't hardcode the column indices
    225                 long id = c.getLong(0);
    226                 String number = c.getString(1);
    227                 Log.d(TAG, "id: " + id + " number: " + number);
    228             }
    229         } finally {
    230             c.close();
    231         }
    232     }
    233 
    234     /**
    235      * getSingleNumberFromCanonicalAddresses looks up the recipientId in the canonical_addresses
    236      * table and returns the associated number or email address.
    237      * @param context needed for the ContentResolver
    238      * @param recipientId of the contact to look up
    239      * @return phone number or email address of the recipientId
    240      */
    241     public static String getSingleAddressFromCanonicalAddressInDb(final Context context,
    242             final String recipientId) {
    243         Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
    244                 ContentUris.withAppendedId(sSingleCanonicalAddressUri, Long.parseLong(recipientId)),
    245                 null, null, null, null);
    246         if (c == null) {
    247             LogTag.warn(TAG, "null Cursor looking up recipient: " + recipientId);
    248             return null;
    249         }
    250         try {
    251             if (c.moveToFirst()) {
    252                 String number = c.getString(0);
    253                 return number;
    254             }
    255         } finally {
    256             c.close();
    257         }
    258         return null;
    259     }
    260 
    261     // used for unit tests
    262     public static void insertCanonicalAddressInDb(final Context context, String number) {
    263         if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    264             Log.d(TAG, "[RecipientIdCache] insertCanonicalAddressInDb: number=" + number);
    265         }
    266 
    267         final ContentValues values = new ContentValues();
    268         values.put(Telephony.CanonicalAddressesColumns.ADDRESS, number);
    269 
    270         final ContentResolver cr = context.getContentResolver();
    271 
    272         // We're running on the UI thread so just fire & forget, hope for the best.
    273         // (We were ignoring the return value anyway...)
    274         new Thread("updateCanonicalAddressInDb") {
    275             public void run() {
    276                 cr.insert(sAllCanonical, values);
    277             }
    278         }.start();
    279     }
    280 
    281 }
    282