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.ContentResolver; 12 import android.content.ContentUris; 13 import android.content.ContentValues; 14 import android.content.Context; 15 import android.database.Cursor; 16 import android.database.sqlite.SqliteWrapper; 17 import android.net.Uri; 18 import android.provider.Telephony; 19 import android.text.TextUtils; 20 import android.util.Log; 21 22 import com.android.mms.LogTag; 23 24 @ThreadSafe 25 public class RecipientIdCache { 26 private static final boolean LOCAL_DEBUG = false; 27 private static final String TAG = LogTag.TAG; 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; getInstance()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 Entry(long id, String number)47 public Entry(long id, String number) { 48 this.id = id; 49 this.number = number; 50 } 51 }; 52 init(Context context)53 static void init(Context context) { 54 sInstance = new RecipientIdCache(context); 55 new Thread(new Runnable() { 56 public void run() { 57 fill(); 58 } 59 }, "RecipientIdCache.init").start(); 60 } 61 RecipientIdCache(Context context)62 RecipientIdCache(Context context) { 63 mCache = new HashMap<Long, String>(); 64 mContext = context; 65 } 66 fill()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 getAddresses(String spaceSepIds)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 updateNumbers(long threadId, ContactList contacts)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 updateCanonicalAddressInDb(long id, String number)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 dump()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 canonicalTableDump()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 */ getSingleAddressFromCanonicalAddressInDb(final Context context, final String recipientId)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 insertCanonicalAddressInDb(final Context context, String number)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("insertCanonicalAddressInDb") { 275 public void run() { 276 cr.insert(sAllCanonical, values); 277 } 278 }.start(); 279 } 280 281 } 282