1package com.android.mms.data;
2
3import java.util.ArrayList;
4import java.util.HashMap;
5import java.util.List;
6import java.util.Map;
7
8import javax.annotation.concurrent.GuardedBy;
9import javax.annotation.concurrent.ThreadSafe;
10
11import android.content.ContentResolver;
12import android.content.ContentUris;
13import android.content.ContentValues;
14import android.content.Context;
15import android.database.Cursor;
16import android.database.sqlite.SqliteWrapper;
17import android.net.Uri;
18import android.provider.Telephony;
19import android.text.TextUtils;
20import android.util.Log;
21
22import com.android.mms.LogTag;
23
24@ThreadSafe
25public 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;
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        }, "RecipientIdCache.init").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("insertCanonicalAddressInDb") {
275            public void run() {
276                cr.insert(sAllCanonical, values);
277            }
278        }.start();
279    }
280
281}
282