• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.bluetooth.mapclient;
18 
19 import android.bluetooth.BluetoothDevice;
20 import android.bluetooth.BluetoothMapClient;
21 import android.content.ContentResolver;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.database.ContentObserver;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.provider.Telephony;
28 import android.provider.Telephony.Mms;
29 import android.provider.Telephony.MmsSms;
30 import android.provider.Telephony.Sms;
31 import android.provider.Telephony.Threads;
32 import android.telephony.PhoneNumberUtils;
33 import android.telephony.SubscriptionInfo;
34 import android.telephony.SubscriptionManager;
35 import android.telephony.TelephonyManager;
36 import android.util.ArraySet;
37 import android.util.Log;
38 
39 import com.android.bluetooth.Utils;
40 import com.android.bluetooth.map.BluetoothMapbMessageMime;
41 import com.android.bluetooth.map.BluetoothMapbMessageMime.MimePart;
42 import com.android.vcard.VCardConstants;
43 import com.android.vcard.VCardEntry;
44 import com.android.vcard.VCardProperty;
45 
46 import com.google.android.mms.pdu.PduHeaders;
47 
48 import java.util.Arrays;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Set;
52 
53 class MapClientContent {
54 
55     private static final String INBOX_PATH = "telecom/msg/inbox";
56     private static final String TAG = "MapClientContent";
57     private static final int DEFAULT_CHARSET = 106;
58     private static final int ORIGINATOR_ADDRESS_TYPE = 137;
59     private static final int RECIPIENT_ADDRESS_TYPE = 151;
60 
61     final BluetoothDevice mDevice;
62     private final Context mContext;
63     private final Callbacks mCallbacks;
64     private final ContentResolver mResolver;
65     ContentObserver mContentObserver;
66     String mPhoneNumber = null;
67     private int mSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
68     private SubscriptionManager mSubscriptionManager;
69     private TelephonyManager mTelephonyManager;
70     private HashMap<String, Uri> mHandleToUriMap = new HashMap<>();
71     private HashMap<Uri, MessageStatus> mUriToHandleMap = new HashMap<>();
72 
73     /**
74      * Callbacks
75      * API to notify about statusChanges as observed from the content provider
76      */
77     interface Callbacks {
onMessageStatusChanged(String handle, int status)78         void onMessageStatusChanged(String handle, int status);
79     }
80 
81     /**
82      * MapClientContent manages all interactions between Bluetooth and the messaging provider.
83      *
84      * Changes to the database are mirrored between the remote and local providers, specifically new
85      * messages, changes to read status, and removal of messages.
86      *
87      * Object is invalid after cleanUp() is called.
88      *
89      * context: the context that all content provider interactions are conducted
90      * MceStateMachine:  the interface to send outbound updates such as when a message is read
91      * locally
92      * device: the associated Bluetooth device used for associating messages with a subscription
93      */
MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device)94     MapClientContent(Context context, Callbacks callbacks,
95             BluetoothDevice device) {
96         mContext = context;
97         mDevice = device;
98         mCallbacks = callbacks;
99         mResolver = mContext.getContentResolver();
100 
101         mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
102         mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
103         mSubscriptionManager
104                 .addSubscriptionInfoRecord(mDevice.getAddress(), Utils.getName(mDevice), 0,
105                         SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
106         SubscriptionInfo info = mSubscriptionManager
107                 .getActiveSubscriptionInfoForIcc(mDevice.getAddress());
108         if (info != null) {
109             mSubscriptionId = info.getSubscriptionId();
110         }
111 
112         mContentObserver = new ContentObserver(null) {
113             @Override
114             public boolean deliverSelfNotifications() {
115                 return false;
116             }
117 
118             @Override
119             public void onChange(boolean selfChange) {
120                 logV("onChange(self=" + selfChange + ")");
121                 findChangeInDatabase();
122             }
123 
124             @Override
125             public void onChange(boolean selfChange, Uri uri) {
126                 logV("onChange(self=" + selfChange + ", uri=" + uri.toString() + ")");
127                 findChangeInDatabase();
128             }
129         };
130 
131         clearMessages(mContext, mSubscriptionId);
132         mResolver.registerContentObserver(Sms.CONTENT_URI, true, mContentObserver);
133         mResolver.registerContentObserver(Mms.CONTENT_URI, true, mContentObserver);
134         mResolver.registerContentObserver(MmsSms.CONTENT_URI, true, mContentObserver);
135     }
136 
clearAllContent(Context context)137     static void clearAllContent(Context context) {
138         SubscriptionManager subscriptionManager =
139                 context.getSystemService(SubscriptionManager.class);
140         List<SubscriptionInfo> subscriptions = subscriptionManager.getActiveSubscriptionInfoList();
141         if (subscriptions == null) {
142             Log.w(TAG, "Active subscription list is missing");
143             return;
144         }
145         for (SubscriptionInfo info : subscriptions) {
146             if (info.getSubscriptionType() == SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM) {
147                 clearMessages(context, info.getSubscriptionId());
148                 try {
149                     subscriptionManager.removeSubscriptionInfoRecord(info.getIccId(),
150                             SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
151                 } catch (Exception e) {
152                     Log.w(TAG, "cleanUp failed: " + e.toString());
153                 }
154             }
155         }
156     }
157 
logI(String message)158     private static void logI(String message) {
159         Log.i(TAG, message);
160     }
161 
logD(String message)162     private static void logD(String message) {
163         if (MapClientService.DBG) {
164             Log.d(TAG, message);
165         }
166     }
167 
logV(String message)168     private static void logV(String message) {
169         if (MapClientService.VDBG) {
170             Log.v(TAG, message);
171         }
172     }
173 
174     /**
175      * This number is necessary for thread_id to work properly. thread_id is needed for
176      * (group) MMS messages to be displayed/stitched correctly.
177      */
setRemoteDeviceOwnNumber(String phoneNumber)178     void setRemoteDeviceOwnNumber(String phoneNumber) {
179         mPhoneNumber = phoneNumber;
180         logV("Remote device " + mDevice.getAddress() + " phone number set to: " + mPhoneNumber);
181     }
182 
183     /**
184      * storeMessage
185      *
186      * Store a message in database with the associated handle and timestamp.
187      * The handle is used to associate the local message with the remote message.
188      */
storeMessage(Bmessage message, String handle, Long timestamp, boolean seen)189     void storeMessage(Bmessage message, String handle, Long timestamp, boolean seen) {
190         logI("storeMessage(device=" + Utils.getLoggableAddress(mDevice) + ", time=" + timestamp
191                 + ", handle=" + handle + ", type=" + message.getType()
192                 + ", folder=" + message.getFolder());
193 
194         switch (message.getType()) {
195             case MMS:
196                 storeMms(message, handle, timestamp, seen);
197                 return;
198             case SMS_CDMA:
199             case SMS_GSM:
200                 storeSms(message, handle, timestamp, seen);
201                 return;
202             default:
203                 logD("Request to store unsupported message type: " + message.getType());
204         }
205     }
206 
storeSms(Bmessage message, String handle, Long timestamp, boolean seen)207     private void storeSms(Bmessage message, String handle, Long timestamp, boolean seen) {
208         logD("storeSms");
209         logV(message.toString());
210         VCardEntry originator = message.getOriginator();
211         String recipients;
212         if (INBOX_PATH.equals(message.getFolder())) {
213             recipients = getOriginatorNumber(message);
214         } else {
215             recipients = getFirstRecipientNumber(message);
216             if (recipients == null) {
217                 logD("invalid recipients");
218                 return;
219             }
220         }
221         logV("Received SMS from Number " + recipients);
222         String messageContent;
223 
224         Uri contentUri = INBOX_PATH.equalsIgnoreCase(message.getFolder()) ? Sms.Inbox.CONTENT_URI
225                 : Sms.Sent.CONTENT_URI;
226         ContentValues values = new ContentValues();
227         long threadId = getThreadId(message);
228         int readStatus = message.getStatus() == Bmessage.Status.READ ? 1 : 0;
229 
230         values.put(Sms.THREAD_ID, threadId);
231         values.put(Sms.ADDRESS, recipients);
232         values.put(Sms.BODY, message.getBodyContent());
233         values.put(Sms.SUBSCRIPTION_ID, mSubscriptionId);
234         values.put(Sms.DATE, timestamp);
235         values.put(Sms.READ, readStatus);
236         values.put(Sms.SEEN, seen);
237 
238         Uri results = mResolver.insert(contentUri, values);
239         mHandleToUriMap.put(handle, results);
240         mUriToHandleMap.put(results, new MessageStatus(handle, readStatus));
241         logD("Map InsertedThread" + results);
242     }
243 
244     /**
245      * deleteMessage
246      * remove a message from the local provider based on a remote change
247      */
deleteMessage(String handle)248     void deleteMessage(String handle) {
249         logD("deleting handle" + handle);
250         Uri messageToChange = mHandleToUriMap.get(handle);
251         if (messageToChange != null) {
252             mResolver.delete(messageToChange, null);
253         }
254     }
255 
256 
257     /**
258      * markRead
259      * mark a message read in the local provider based on a remote change
260      */
markRead(String handle)261     void markRead(String handle) {
262         logD("marking read " + handle);
263         Uri messageToChange = mHandleToUriMap.get(handle);
264         if (messageToChange != null) {
265             ContentValues values = new ContentValues();
266             values.put(Sms.READ, 1);
267             mResolver.update(messageToChange, values, null);
268         }
269     }
270 
271     /**
272      * findChangeInDatabase
273      * compare the current state of the local content provider to the expected state and propagate
274      * changes to the remote.
275      */
findChangeInDatabase()276     private void findChangeInDatabase() {
277         HashMap<Uri, MessageStatus> originalUriToHandleMap;
278         HashMap<Uri, MessageStatus> duplicateUriToHandleMap;
279 
280         originalUriToHandleMap = mUriToHandleMap;
281         duplicateUriToHandleMap = new HashMap<>(originalUriToHandleMap);
282         for (Uri uri : new Uri[]{Mms.CONTENT_URI, Sms.CONTENT_URI}) {
283             Cursor cursor = mResolver.query(uri, null, null, null, null);
284             while (cursor.moveToNext()) {
285                 Uri index = Uri
286                         .withAppendedPath(uri, cursor.getString(cursor.getColumnIndex("_id")));
287                 int readStatus = cursor.getInt(cursor.getColumnIndex(Sms.READ));
288                 MessageStatus currentMessage = duplicateUriToHandleMap.remove(index);
289                 if (currentMessage != null && currentMessage.mRead != readStatus) {
290                     logV(currentMessage.mHandle);
291                     currentMessage.mRead = readStatus;
292                     mCallbacks.onMessageStatusChanged(currentMessage.mHandle,
293                             BluetoothMapClient.READ);
294                 }
295             }
296         }
297         for (HashMap.Entry record : duplicateUriToHandleMap.entrySet()) {
298             logV("Deleted " + ((MessageStatus) record.getValue()).mHandle);
299             originalUriToHandleMap.remove(record.getKey());
300             mCallbacks.onMessageStatusChanged(((MessageStatus) record.getValue()).mHandle,
301                     BluetoothMapClient.DELETED);
302         }
303     }
304 
storeMms(Bmessage message, String handle, Long timestamp, boolean seen)305     private void storeMms(Bmessage message, String handle, Long timestamp, boolean seen) {
306         logD("storeMms");
307         logV(message.toString());
308         try {
309             ContentValues values = new ContentValues();
310             long threadId = getThreadId(message);
311             BluetoothMapbMessageMime mmsBmessage = new BluetoothMapbMessageMime();
312             mmsBmessage.parseMsgPart(message.getBodyContent());
313             int read = message.getStatus() == Bmessage.Status.READ ? 1 : 0;
314             Uri contentUri;
315             int messageBox;
316             if (INBOX_PATH.equalsIgnoreCase(message.getFolder())) {
317                 contentUri = Mms.Inbox.CONTENT_URI;
318                 messageBox = Mms.MESSAGE_BOX_INBOX;
319             } else {
320                 contentUri = Mms.Sent.CONTENT_URI;
321                 messageBox = Mms.MESSAGE_BOX_SENT;
322             }
323             logD("Parsed");
324             values.put(Mms.SUBSCRIPTION_ID, mSubscriptionId);
325             values.put(Mms.THREAD_ID, threadId);
326             values.put(Mms.DATE, timestamp / 1000L);
327             values.put(Mms.TEXT_ONLY, true);
328             values.put(Mms.MESSAGE_BOX, messageBox);
329             values.put(Mms.READ, read);
330             values.put(Mms.SEEN, seen);
331             values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
332             values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION);
333             values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL);
334             values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO);
335             values.put(Mms.TRANSACTION_ID, "T" + Long.toHexString(System.currentTimeMillis()));
336             values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO);
337             values.put(Mms.LOCKED, 0);
338             values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related");
339             values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
340             values.put(Mms.MESSAGE_SIZE, mmsBmessage.getSize());
341 
342             Uri results = mResolver.insert(contentUri, values);
343             mHandleToUriMap.put(handle, results);
344             mUriToHandleMap.put(results, new MessageStatus(handle, read));
345 
346             logD("Map InsertedThread" + results);
347 
348             for (MimePart part : mmsBmessage.getMimeParts()) {
349                 storeMmsPart(part, results);
350             }
351 
352             storeAddressPart(message, results);
353 
354             String messageContent = mmsBmessage.getMessageAsText();
355 
356             values.put(Mms.Part.CONTENT_TYPE, "plain/text");
357             values.put(Mms.SUBSCRIPTION_ID, mSubscriptionId);
358         } catch (Exception e) {
359             Log.e(TAG, e.toString());
360             throw e;
361         }
362     }
363 
storeMmsPart(MimePart messagePart, Uri messageUri)364     private Uri storeMmsPart(MimePart messagePart, Uri messageUri) {
365         ContentValues values = new ContentValues();
366         values.put(Mms.Part.CONTENT_TYPE, "text/plain");
367         values.put(Mms.Part.CHARSET, DEFAULT_CHARSET);
368         values.put(Mms.Part.FILENAME, "text_1.txt");
369         values.put(Mms.Part.NAME, "text_1.txt");
370         values.put(Mms.Part.CONTENT_ID, messagePart.mContentId);
371         values.put(Mms.Part.CONTENT_LOCATION, messagePart.mContentLocation);
372         values.put(Mms.Part.TEXT, messagePart.getDataAsString());
373 
374         Uri contentUri = Uri.parse(messageUri.toString() + "/part");
375         Uri results = mResolver.insert(contentUri, values);
376         logD("Inserted" + results);
377         return results;
378     }
379 
storeAddressPart(Bmessage message, Uri messageUri)380     private void storeAddressPart(Bmessage message, Uri messageUri) {
381         ContentValues values = new ContentValues();
382         Uri contentUri = Uri.parse(messageUri.toString() + "/addr");
383         String originator = getOriginatorNumber(message);
384         values.put(Mms.Addr.CHARSET, DEFAULT_CHARSET);
385 
386         values.put(Mms.Addr.ADDRESS, originator);
387         values.put(Mms.Addr.TYPE, ORIGINATOR_ADDRESS_TYPE);
388         mResolver.insert(contentUri, values);
389 
390         Set<String> messageContacts = new ArraySet<>();
391         getRecipientsFromMessage(message, messageContacts);
392         for (String recipient : messageContacts) {
393             values.put(Mms.Addr.ADDRESS, recipient);
394             values.put(Mms.Addr.TYPE, RECIPIENT_ADDRESS_TYPE);
395             mResolver.insert(contentUri, values);
396         }
397     }
398 
insertIntoMmsTable(String subject)399     private Uri insertIntoMmsTable(String subject) {
400         ContentValues mmsValues = new ContentValues();
401         mmsValues.put(Mms.TEXT_ONLY, 1);
402         mmsValues.put(Mms.MESSAGE_TYPE, 128);
403         mmsValues.put(Mms.SUBJECT, subject);
404         return mResolver.insert(Mms.CONTENT_URI, mmsValues);
405     }
406 
407     /**
408      * cleanUp
409      * clear the subscription info and content on shutdown
410      */
cleanUp()411     void cleanUp() {
412         logD("cleanUp(device=" + Utils.getLoggableAddress(mDevice)
413                 + "subscriptionId=" + mSubscriptionId);
414         mResolver.unregisterContentObserver(mContentObserver);
415         clearMessages(mContext, mSubscriptionId);
416         try {
417             mSubscriptionManager.removeSubscriptionInfoRecord(mDevice.getAddress(),
418                     SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
419             mSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
420         } catch (Exception e) {
421             Log.w(TAG, "cleanUp failed: " + e.toString());
422         }
423     }
424 
425     /**
426      * clearMessages
427      * clean up the content provider on startup
428      */
clearMessages(Context context, int subscriptionId)429     private static void clearMessages(Context context, int subscriptionId) {
430         logD("clearMessages(subscriptionId=" + subscriptionId);
431 
432         ContentResolver resolver = context.getContentResolver();
433         String threads = new String();
434 
435         Uri uri = Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
436         Cursor threadCursor = resolver.query(uri, null, null, null, null);
437         while (threadCursor.moveToNext()) {
438             threads += threadCursor.getInt(threadCursor.getColumnIndex(Threads._ID)) + ", ";
439         }
440 
441         resolver.delete(Sms.CONTENT_URI, Sms.SUBSCRIPTION_ID + " =? ",
442                 new String[]{Integer.toString(subscriptionId)});
443         resolver.delete(Mms.CONTENT_URI, Mms.SUBSCRIPTION_ID + " =? ",
444                 new String[]{Integer.toString(subscriptionId)});
445         if (threads.length() > 2) {
446             threads = threads.substring(0, threads.length() - 2);
447             resolver.delete(Threads.CONTENT_URI, Threads._ID + " IN (" + threads + ")", null);
448         }
449     }
450 
451     /**
452      * getThreadId
453      * utilize the originator and recipients to obtain the thread id
454      */
getThreadId(Bmessage message)455     private long getThreadId(Bmessage message) {
456 
457         Set<String> messageContacts = new ArraySet<>();
458         String originator = PhoneNumberUtils.extractNetworkPortion(getOriginatorNumber(message));
459         if (originator != null) {
460             messageContacts.add(originator);
461         }
462         getRecipientsFromMessage(message, messageContacts);
463         // If there is only one contact don't remove it.
464         if (messageContacts.isEmpty()) {
465             return Telephony.Threads.COMMON_THREAD;
466         } else if (messageContacts.size() > 1) {
467             if (mPhoneNumber == null) {
468                 Log.w(TAG, "getThreadId called, mPhoneNumber never found.");
469             }
470             messageContacts.removeIf(number -> (PhoneNumberUtils.areSamePhoneNumber(number,
471                     mPhoneNumber, mTelephonyManager.getNetworkCountryIso())));
472         }
473 
474         logV("Contacts = " + messageContacts.toString());
475         return Telephony.Threads.getOrCreateThreadId(mContext, messageContacts);
476     }
477 
getRecipientsFromMessage(Bmessage message, Set<String> messageContacts)478     private void getRecipientsFromMessage(Bmessage message, Set<String> messageContacts) {
479         List<VCardEntry> recipients = message.getRecipients();
480         for (VCardEntry recipient : recipients) {
481             List<VCardEntry.PhoneData> phoneData = recipient.getPhoneList();
482             if (phoneData != null && !phoneData.isEmpty()) {
483                 messageContacts
484                         .add(PhoneNumberUtils.extractNetworkPortion(phoneData.get(0).getNumber()));
485             }
486         }
487     }
488 
getOriginatorNumber(Bmessage message)489     private String getOriginatorNumber(Bmessage message) {
490         VCardEntry originator = message.getOriginator();
491         if (originator == null) {
492             return null;
493         }
494 
495         List<VCardEntry.PhoneData> phoneData = originator.getPhoneList();
496         if (phoneData == null || phoneData.isEmpty()) {
497             return null;
498         }
499 
500         return PhoneNumberUtils.extractNetworkPortion(phoneData.get(0).getNumber());
501     }
502 
getFirstRecipientNumber(Bmessage message)503     private String getFirstRecipientNumber(Bmessage message) {
504         List<VCardEntry> recipients = message.getRecipients();
505         if (recipients == null || recipients.isEmpty()) {
506             return null;
507         }
508 
509         List<VCardEntry.PhoneData> phoneData = recipients.get(0).getPhoneList();
510         if (phoneData == null || phoneData.isEmpty()) {
511             return null;
512         }
513 
514         return phoneData.get(0).getNumber();
515     }
516 
517     /**
518      * addThreadContactToEntries
519      * utilizing the thread id fill in the appropriate fields of bmsg with the intended recipients
520      */
addThreadContactsToEntries(Bmessage bmsg, String thread)521     boolean addThreadContactsToEntries(Bmessage bmsg, String thread) {
522         String threadId = Uri.parse(thread).getLastPathSegment();
523 
524         logD("MATCHING THREAD" + threadId);
525         logD(MmsSms.CONTENT_CONVERSATIONS_URI + threadId + "/recipients");
526 
527         Cursor cursor = mResolver
528                 .query(Uri.withAppendedPath(MmsSms.CONTENT_CONVERSATIONS_URI,
529                         threadId + "/recipients"),
530                         null, null,
531                         null, null);
532 
533         if (cursor.moveToNext()) {
534             logD("Columns" + Arrays.toString(cursor.getColumnNames()));
535             logV("CONTACT LIST: " + cursor.getString(cursor.getColumnIndex("recipient_ids")));
536             addRecipientsToEntries(bmsg,
537                     cursor.getString(cursor.getColumnIndex("recipient_ids")).split(" "));
538             return true;
539         } else {
540             Log.w(TAG, "Thread Not Found");
541             return false;
542         }
543     }
544 
545 
addRecipientsToEntries(Bmessage bmsg, String[] recipients)546     private void addRecipientsToEntries(Bmessage bmsg, String[] recipients) {
547         logV("CONTACT LIST: " + Arrays.toString(recipients));
548         for (String recipient : recipients) {
549             Cursor cursor = mResolver
550                     .query(Uri.parse("content://mms-sms/canonical-address/" + recipient), null,
551                             null, null,
552                             null);
553             while (cursor.moveToNext()) {
554                 String number = cursor.getString(cursor.getColumnIndex(Mms.Addr.ADDRESS));
555                 logV("CONTACT number: " + number);
556                 VCardEntry destEntry = new VCardEntry();
557                 VCardProperty destEntryPhone = new VCardProperty();
558                 destEntryPhone.setName(VCardConstants.PROPERTY_TEL);
559                 destEntryPhone.addValues(number);
560                 destEntry.addProperty(destEntryPhone);
561                 bmsg.addRecipient(destEntry);
562             }
563         }
564     }
565 
566     /**
567      * Get the total number of messages we've stored under this device's subscription ID, for a
568      * given message source, provided by the "uri" parameter.
569      */
getStoredMessagesCount(Uri uri)570     private int getStoredMessagesCount(Uri uri) {
571         if (mSubscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
572             logV("getStoredMessagesCount(uri=" + uri + "): Failed, no subscription ID");
573             return 0;
574         }
575 
576         Cursor cursor = null;
577         if (Sms.CONTENT_URI.equals(uri) || Sms.Inbox.CONTENT_URI.equals(uri)
578                 || Sms.Sent.CONTENT_URI.equals(uri)) {
579             cursor = mResolver.query(uri, new String[] {"count(*)"}, Sms.SUBSCRIPTION_ID + " =? ",
580                     new String[]{Integer.toString(mSubscriptionId)}, null);
581         } else if (Mms.CONTENT_URI.equals(uri) || Mms.Inbox.CONTENT_URI.equals(uri)
582                 || Mms.Sent.CONTENT_URI.equals(uri)) {
583             cursor = mResolver.query(uri, new String[] {"count(*)"}, Mms.SUBSCRIPTION_ID + " =? ",
584                     new String[]{Integer.toString(mSubscriptionId)}, null);
585         } else if (Threads.CONTENT_URI.equals(uri)) {
586             uri = Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
587             cursor = mResolver.query(uri, new String[] {"count(*)"}, null, null, null);
588         }
589 
590         if (cursor == null) {
591             return 0;
592         }
593 
594         cursor.moveToFirst();
595         int count = cursor.getInt(0);
596         cursor.close();
597 
598         return count;
599     }
600 
dump(StringBuilder sb)601     public void dump(StringBuilder sb) {
602         sb.append("    Device Message DB:");
603         sb.append("\n      Subscription ID: " + mSubscriptionId);
604         if (mSubscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
605             sb.append("\n      SMS Messages (Inbox/Sent/Total): "
606                     + getStoredMessagesCount(Sms.Inbox.CONTENT_URI)
607                     + " / " + getStoredMessagesCount(Sms.Sent.CONTENT_URI)
608                     + " / " + getStoredMessagesCount(Sms.CONTENT_URI));
609 
610             sb.append("\n      MMS Messages (Inbox/Sent/Total): "
611                     + getStoredMessagesCount(Mms.Inbox.CONTENT_URI)
612                     + " / " + getStoredMessagesCount(Mms.Sent.CONTENT_URI)
613                     + " / " + getStoredMessagesCount(Mms.CONTENT_URI));
614 
615             sb.append("\n      Threads: " + getStoredMessagesCount(Threads.CONTENT_URI));
616         }
617         sb.append("\n");
618     }
619 
620     /**
621      * MessageStatus
622      *
623      * Helper class to store associations between remote and local provider based on message handle
624      * and read status
625      */
626     class MessageStatus {
627 
628         String mHandle;
629         int mRead;
630 
MessageStatus(String handle, int read)631         MessageStatus(String handle, int read) {
632             mHandle = handle;
633             mRead = read;
634         }
635 
636         @Override
equals(Object other)637         public boolean equals(Object other) {
638             return ((other instanceof MessageStatus) && ((MessageStatus) other).mHandle
639                     .equals(mHandle));
640         }
641     }
642 }
643