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