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 android.content.Context; 9 import android.content.ContentResolver; 10 import android.content.ContentValues; 11 import android.content.ContentUris; 12 import android.database.Cursor; 13 import android.net.Uri; 14 import android.text.TextUtils; 15 import android.util.Log; 16 import android.provider.Telephony; 17 18 import com.android.mms.LogTag; 19 import android.database.sqlite.SqliteWrapper; 20 21 public class RecipientIdCache { 22 private static final boolean LOCAL_DEBUG = false; 23 private static final String TAG = "Mms/cache"; 24 25 private static Uri sAllCanonical = 26 Uri.parse("content://mms-sms/canonical-addresses"); 27 28 private static Uri sSingleCanonicalAddressUri = 29 Uri.parse("content://mms-sms/canonical-address"); 30 31 private static RecipientIdCache sInstance; getInstance()32 static RecipientIdCache getInstance() { return sInstance; } 33 private final Map<Long, String> mCache; 34 private final Context mContext; 35 36 public static class Entry { 37 public long id; 38 public String number; 39 Entry(long id, String number)40 public Entry(long id, String number) { 41 this.id = id; 42 this.number = number; 43 } 44 }; 45 init(Context context)46 static void init(Context context) { 47 sInstance = new RecipientIdCache(context); 48 new Thread(new Runnable() { 49 public void run() { 50 fill(); 51 } 52 }).start(); 53 } 54 RecipientIdCache(Context context)55 RecipientIdCache(Context context) { 56 mCache = new HashMap<Long, String>(); 57 mContext = context; 58 } 59 fill()60 public static void fill() { 61 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 62 LogTag.debug("[RecipientIdCache] fill: begin"); 63 } 64 65 Context context = sInstance.mContext; 66 Cursor c = SqliteWrapper.query(context, context.getContentResolver(), 67 sAllCanonical, null, null, null, null); 68 if (c == null) { 69 Log.w(TAG, "null Cursor in fill()"); 70 return; 71 } 72 73 try { 74 synchronized (sInstance) { 75 // Technically we don't have to clear this because the stupid 76 // canonical_addresses table is never GC'ed. 77 sInstance.mCache.clear(); 78 while (c.moveToNext()) { 79 // TODO: don't hardcode the column indices 80 long id = c.getLong(0); 81 String number = c.getString(1); 82 sInstance.mCache.put(id, number); 83 } 84 } 85 } finally { 86 c.close(); 87 } 88 89 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 90 LogTag.debug("[RecipientIdCache] fill: finished"); 91 dump(); 92 } 93 } 94 getAddresses(String spaceSepIds)95 public static List<Entry> getAddresses(String spaceSepIds) { 96 synchronized (sInstance) { 97 List<Entry> numbers = new ArrayList<Entry>(); 98 String[] ids = spaceSepIds.split(" "); 99 for (String id : ids) { 100 long longId; 101 102 try { 103 longId = Long.parseLong(id); 104 } catch (NumberFormatException ex) { 105 // skip this id 106 continue; 107 } 108 109 String number = sInstance.mCache.get(longId); 110 111 if (number == null) { 112 Log.w(TAG, "RecipientId " + longId + " not in cache!"); 113 if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) { 114 dump(); 115 } 116 117 fill(); 118 number = sInstance.mCache.get(longId); 119 } 120 121 if (TextUtils.isEmpty(number)) { 122 Log.w(TAG, "RecipientId " + longId + " has empty number!"); 123 } else { 124 numbers.add(new Entry(longId, number)); 125 } 126 } 127 return numbers; 128 } 129 } 130 updateNumbers(long threadId, ContactList contacts)131 public static void updateNumbers(long threadId, ContactList contacts) { 132 long recipientId = 0; 133 134 for (Contact contact : contacts) { 135 if (contact.isNumberModified()) { 136 contact.setIsNumberModified(false); 137 } else { 138 // if the contact's number wasn't modified, don't bother. 139 continue; 140 } 141 142 recipientId = contact.getRecipientId(); 143 if (recipientId == 0) { 144 continue; 145 } 146 147 String number1 = contact.getNumber(); 148 String number2 = sInstance.mCache.get(recipientId); 149 150 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 151 Log.d(TAG, "[RecipientIdCache] updateNumbers: comparing " + number1 + 152 " with " + number2); 153 } 154 155 // if the numbers don't match, let's update the RecipientIdCache's number 156 // with the new number in the contact. 157 if (!number1.equalsIgnoreCase(number2)) { 158 sInstance.mCache.put(recipientId, number1); 159 sInstance.updateCanonicalAddressInDb(recipientId, number1); 160 } 161 } 162 } 163 updateCanonicalAddressInDb(long id, String number)164 private void updateCanonicalAddressInDb(long id, String number) { 165 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 166 Log.d(TAG, "[RecipientIdCache] updateCanonicalAddressInDb: id=" + id + 167 ", number=" + number); 168 } 169 170 final ContentValues values = new ContentValues(); 171 values.put(Telephony.CanonicalAddressesColumns.ADDRESS, number); 172 173 final StringBuilder buf = new StringBuilder(Telephony.CanonicalAddressesColumns._ID); 174 buf.append('=').append(id); 175 176 final Uri uri = ContentUris.withAppendedId(sSingleCanonicalAddressUri, id); 177 final ContentResolver cr = mContext.getContentResolver(); 178 179 // We're running on the UI thread so just fire & forget, hope for the best. 180 // (We were ignoring the return value anyway...) 181 new Thread("updateCanonicalAddressInDb") { 182 public void run() { 183 cr.update(uri, values, buf.toString(), null); 184 } 185 }.start(); 186 } 187 dump()188 public static void dump() { 189 // Only dump user private data if we're in special debug mode 190 synchronized (sInstance) { 191 Log.d(TAG, "*** Recipient ID cache dump ***"); 192 for (Long id : sInstance.mCache.keySet()) { 193 Log.d(TAG, id + ": " + sInstance.mCache.get(id)); 194 } 195 } 196 } 197 } 198