• 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.util.ArraySet;
36 import android.util.Log;
37 
38 import com.android.bluetooth.Utils;
39 import com.android.bluetooth.map.BluetoothMapbMessageMime;
40 import com.android.bluetooth.map.BluetoothMapbMessageMime.MimePart;
41 import com.android.vcard.VCardConstants;
42 import com.android.vcard.VCardEntry;
43 import com.android.vcard.VCardProperty;
44 
45 import com.google.android.mms.pdu.PduHeaders;
46 
47 import java.util.ArrayList;
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 HashMap<String, Uri> mHandleToUriMap = new HashMap<>();
70     private HashMap<Uri, MessageStatus> mUriToHandleMap = new HashMap<>();
71 
72     /**
73      * Callbacks
74      * API to notify about statusChanges as observed from the content provider
75      */
76     interface Callbacks {
onMessageStatusChanged(String handle, int status)77         void onMessageStatusChanged(String handle, int status);
78     }
79 
80     /**
81      * MapClientContent manages all interactions between Bluetooth and the messaging provider.
82      *
83      * Changes to the database are mirrored between the remote and local providers, specifically new
84      * messages, changes to read status, and removal of messages.
85      *
86      * context: the context that all content provider interactions are conducted
87      * MceStateMachine:  the interface to send outbound updates such as when a message is read
88      * locally
89      * device: the associated Bluetooth device used for associating messages with a subscription
90      */
MapClientContent(Context context, Callbacks callbacks, BluetoothDevice device)91     MapClientContent(Context context, Callbacks callbacks,
92             BluetoothDevice device) {
93         mContext = context;
94         mDevice = device;
95         mCallbacks = callbacks;
96         mResolver = mContext.getContentResolver();
97 
98         mSubscriptionManager = (SubscriptionManager) mContext
99                 .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
100         mSubscriptionManager
101                 .addSubscriptionInfoRecord(mDevice.getAddress(), Utils.getName(mDevice), 0,
102                         SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
103         SubscriptionInfo info = mSubscriptionManager
104                 .getActiveSubscriptionInfoForIcc(mDevice.getAddress());
105         if (info != null) {
106             mSubscriptionId = info.getSubscriptionId();
107             mSubscriptionManager.setDisplayNumber(mPhoneNumber, mSubscriptionId);
108         }
109 
110         mContentObserver = new ContentObserver(null) {
111             @Override
112             public boolean deliverSelfNotifications() {
113                 return false;
114             }
115 
116             @Override
117             public void onChange(boolean selfChange) {
118                 logV("onChange");
119                 findChangeInDatabase();
120             }
121 
122             @Override
123             public void onChange(boolean selfChange, Uri uri) {
124                 logV("onChange" + uri.toString());
125                 findChangeInDatabase();
126             }
127         };
128 
129         clearMessages(mContext, mSubscriptionId);
130         mResolver.registerContentObserver(Sms.CONTENT_URI, true, mContentObserver);
131         mResolver.registerContentObserver(Mms.CONTENT_URI, true, mContentObserver);
132         mResolver.registerContentObserver(MmsSms.CONTENT_URI, true, mContentObserver);
133     }
134 
clearAllContent(Context context)135     static void clearAllContent(Context context) {
136         SubscriptionManager subscriptionManager = (SubscriptionManager) context
137                 .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
138         List<SubscriptionInfo> subscriptions = subscriptionManager.getActiveSubscriptionInfoList();
139         for (SubscriptionInfo info : subscriptions) {
140             if (info.getSubscriptionType() == SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM) {
141                 clearMessages(context, info.getSubscriptionId());
142                 try {
143                     subscriptionManager.removeSubscriptionInfoRecord(info.getIccId(),
144                             SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
145                 } catch (Exception e) {
146                     Log.w(TAG, "cleanUp failed: " + e.toString());
147                 }
148             }
149         }
150     }
151 
logD(String message)152     private static void logD(String message) {
153         if (MapClientService.DBG) {
154             Log.d(TAG, message);
155         }
156     }
157 
logV(String message)158     private static void logV(String message) {
159         if (MapClientService.VDBG) {
160             Log.v(TAG, message);
161         }
162     }
163 
164     /**
165      * parseLocalNumber
166      *
167      * Determine the connected phone's number by extracting it from an inbound or outbound mms
168      * message.  This number is necessary such that group messages can be displayed correctly.
169      */
parseLocalNumber(Bmessage message)170     void parseLocalNumber(Bmessage message) {
171         if (mPhoneNumber != null) {
172             return;
173         }
174         if (INBOX_PATH.equals(message.getFolder())) {
175             ArrayList<VCardEntry> recipients = message.getRecipients();
176             if (recipients != null && !recipients.isEmpty()) {
177                 mPhoneNumber = PhoneNumberUtils.extractNetworkPortion(
178                         getFirstRecipientNumber(message));
179             }
180         } else {
181             mPhoneNumber = PhoneNumberUtils.extractNetworkPortion(getOriginatorNumber(message));
182         }
183 
184         logV("Found phone number: " + mPhoneNumber);
185     }
186 
187     /**
188      * storeMessage
189      *
190      * Store a message in database with the associated handle and timestamp.
191      * The handle is used to associate the local message with the remote message.
192      */
storeMessage(Bmessage message, String handle, Long timestamp)193     void storeMessage(Bmessage message, String handle, Long timestamp) {
194         switch (message.getType()) {
195             case MMS:
196                 storeMms(message, handle, timestamp);
197                 return;
198             case SMS_CDMA:
199             case SMS_GSM:
200                 storeSms(message, handle, timestamp);
201                 return;
202             default:
203                 logD("Request to store unsupported message type: " + message.getType());
204         }
205     }
206 
storeSms(Bmessage message, String handle, Long timestamp)207     private void storeSms(Bmessage message, String handle, Long timestamp) {
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 
237         Uri results = mResolver.insert(contentUri, values);
238         mHandleToUriMap.put(handle, results);
239         mUriToHandleMap.put(results, new MessageStatus(handle, readStatus));
240         logD("Map InsertedThread" + results);
241     }
242 
243     /**
244      * deleteMessage
245      * remove a message from the local provider based on a remote change
246      */
deleteMessage(String handle)247     void deleteMessage(String handle) {
248         logD("deleting handle" + handle);
249         Uri messageToChange = mHandleToUriMap.get(handle);
250         if (messageToChange != null) {
251             mResolver.delete(messageToChange, null);
252         }
253     }
254 
255 
256     /**
257      * markRead
258      * mark a message read in the local provider based on a remote change
259      */
markRead(String handle)260     void markRead(String handle) {
261         logD("marking read " + handle);
262         Uri messageToChange = mHandleToUriMap.get(handle);
263         if (messageToChange != null) {
264             ContentValues values = new ContentValues();
265             values.put(Sms.READ, 1);
266             mResolver.update(messageToChange, values, null);
267         }
268     }
269 
270     /**
271      * findChangeInDatabase
272      * compare the current state of the local content provider to the expected state and propagate
273      * changes to the remote.
274      */
findChangeInDatabase()275     private void findChangeInDatabase() {
276         HashMap<Uri, MessageStatus> originalUriToHandleMap;
277         HashMap<Uri, MessageStatus> duplicateUriToHandleMap;
278 
279         originalUriToHandleMap = mUriToHandleMap;
280         duplicateUriToHandleMap = new HashMap<>(originalUriToHandleMap);
281         for (Uri uri : new Uri[]{Mms.CONTENT_URI, Sms.CONTENT_URI}) {
282             Cursor cursor = mResolver.query(uri, null, null, null, null);
283             while (cursor.moveToNext()) {
284                 Uri index = Uri
285                         .withAppendedPath(uri, cursor.getString(cursor.getColumnIndex("_id")));
286                 int readStatus = cursor.getInt(cursor.getColumnIndex(Sms.READ));
287                 MessageStatus currentMessage = duplicateUriToHandleMap.remove(index);
288                 if (currentMessage != null && currentMessage.mRead != readStatus) {
289                     logV(currentMessage.mHandle);
290                     currentMessage.mRead = readStatus;
291                     mCallbacks.onMessageStatusChanged(currentMessage.mHandle,
292                             BluetoothMapClient.READ);
293                 }
294             }
295         }
296         for (HashMap.Entry record : duplicateUriToHandleMap.entrySet()) {
297             logV("Deleted " + ((MessageStatus) record.getValue()).mHandle);
298             originalUriToHandleMap.remove(record.getKey());
299             mCallbacks.onMessageStatusChanged(((MessageStatus) record.getValue()).mHandle,
300                     BluetoothMapClient.DELETED);
301         }
302     }
303 
storeMms(Bmessage message, String handle, Long timestamp)304     private void storeMms(Bmessage message, String handle, Long timestamp) {
305         logD("storeMms");
306         logV(message.toString());
307         try {
308             parseLocalNumber(message);
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, 0);
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         mResolver.unregisterContentObserver(mContentObserver);
413         clearMessages(mContext, mSubscriptionId);
414         try {
415             mSubscriptionManager.removeSubscriptionInfoRecord(mDevice.getAddress(),
416                     SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
417         } catch (Exception e) {
418             Log.w(TAG, "cleanUp failed: " + e.toString());
419         }
420     }
421 
422     /**
423      * clearMessages
424      * clean up the content provider on startup
425      */
clearMessages(Context context, int subscriptionId)426     private static void clearMessages(Context context, int subscriptionId) {
427         ContentResolver resolver = context.getContentResolver();
428         String threads = new String();
429 
430         Uri uri = Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build();
431         Cursor threadCursor = resolver.query(uri, null, null, null, null);
432         while (threadCursor.moveToNext()) {
433             threads += threadCursor.getInt(threadCursor.getColumnIndex(Threads._ID)) + ", ";
434         }
435 
436         resolver.delete(Sms.CONTENT_URI, Sms.SUBSCRIPTION_ID + " =? ",
437                 new String[]{Integer.toString(subscriptionId)});
438         resolver.delete(Mms.CONTENT_URI, Mms.SUBSCRIPTION_ID + " =? ",
439                 new String[]{Integer.toString(subscriptionId)});
440         if (threads.length() > 2) {
441             threads = threads.substring(0, threads.length() - 2);
442             resolver.delete(Threads.CONTENT_URI, Threads._ID + " IN (" + threads + ")", null);
443         }
444     }
445 
446     /**
447      * getThreadId
448      * utilize the originator and recipients to obtain the thread id
449      */
getThreadId(Bmessage message)450     private long getThreadId(Bmessage message) {
451 
452         Set<String> messageContacts = new ArraySet<>();
453         String originator = PhoneNumberUtils.extractNetworkPortion(getOriginatorNumber(message));
454         if (originator != null) {
455             messageContacts.add(originator);
456         }
457         getRecipientsFromMessage(message, messageContacts);
458         // If there is only one contact don't remove it.
459         if (messageContacts.isEmpty()) {
460             return Telephony.Threads.COMMON_THREAD;
461         } else if (messageContacts.size() > 1) {
462             messageContacts.removeIf(number -> (PhoneNumberUtils.compareLoosely(number,
463                     mPhoneNumber)));
464         }
465 
466         logV("Contacts = " + messageContacts.toString());
467         return Telephony.Threads.getOrCreateThreadId(mContext, messageContacts);
468     }
469 
getRecipientsFromMessage(Bmessage message, Set<String> messageContacts)470     private void getRecipientsFromMessage(Bmessage message, Set<String> messageContacts) {
471         List<VCardEntry> recipients = message.getRecipients();
472         for (VCardEntry recipient : recipients) {
473             List<VCardEntry.PhoneData> phoneData = recipient.getPhoneList();
474             if (phoneData != null && !phoneData.isEmpty()) {
475                 messageContacts
476                         .add(PhoneNumberUtils.extractNetworkPortion(phoneData.get(0).getNumber()));
477             }
478         }
479     }
480 
getOriginatorNumber(Bmessage message)481     private String getOriginatorNumber(Bmessage message) {
482         VCardEntry originator = message.getOriginator();
483         if (originator == null) {
484             return null;
485         }
486 
487         List<VCardEntry.PhoneData> phoneData = originator.getPhoneList();
488         if (phoneData == null || phoneData.isEmpty()) {
489             return null;
490         }
491 
492         return PhoneNumberUtils.extractNetworkPortion(phoneData.get(0).getNumber());
493     }
494 
getFirstRecipientNumber(Bmessage message)495     private String getFirstRecipientNumber(Bmessage message) {
496         List<VCardEntry> recipients = message.getRecipients();
497         if (recipients == null || recipients.isEmpty()) {
498             return null;
499         }
500 
501         List<VCardEntry.PhoneData> phoneData = recipients.get(0).getPhoneList();
502         if (phoneData == null || phoneData.isEmpty()) {
503             return null;
504         }
505 
506         return phoneData.get(0).getNumber();
507     }
508 
509     /**
510      * addThreadContactToEntries
511      * utilizing the thread id fill in the appropriate fields of bmsg with the intended recipients
512      */
addThreadContactsToEntries(Bmessage bmsg, String thread)513     boolean addThreadContactsToEntries(Bmessage bmsg, String thread) {
514         String threadId = Uri.parse(thread).getLastPathSegment();
515 
516         logD("MATCHING THREAD" + threadId);
517         logD(MmsSms.CONTENT_CONVERSATIONS_URI + threadId + "/recipients");
518 
519         Cursor cursor = mResolver
520                 .query(Uri.withAppendedPath(MmsSms.CONTENT_CONVERSATIONS_URI,
521                         threadId + "/recipients"),
522                         null, null,
523                         null, null);
524 
525         if (cursor.moveToNext()) {
526             logD("Columns" + Arrays.toString(cursor.getColumnNames()));
527             logV("CONTACT LIST: " + cursor.getString(cursor.getColumnIndex("recipient_ids")));
528             addRecipientsToEntries(bmsg,
529                     cursor.getString(cursor.getColumnIndex("recipient_ids")).split(" "));
530             return true;
531         } else {
532             Log.w(TAG, "Thread Not Found");
533             return false;
534         }
535     }
536 
537 
addRecipientsToEntries(Bmessage bmsg, String[] recipients)538     private void addRecipientsToEntries(Bmessage bmsg, String[] recipients) {
539         logV("CONTACT LIST: " + Arrays.toString(recipients));
540         for (String recipient : recipients) {
541             Cursor cursor = mResolver
542                     .query(Uri.parse("content://mms-sms/canonical-address/" + recipient), null,
543                             null, null,
544                             null);
545             while (cursor.moveToNext()) {
546                 String number = cursor.getString(cursor.getColumnIndex(Mms.Addr.ADDRESS));
547                 logV("CONTACT number: " + number);
548                 VCardEntry destEntry = new VCardEntry();
549                 VCardProperty destEntryPhone = new VCardProperty();
550                 destEntryPhone.setName(VCardConstants.PROPERTY_TEL);
551                 destEntryPhone.addValues(number);
552                 destEntry.addProperty(destEntryPhone);
553                 bmsg.addRecipient(destEntry);
554             }
555         }
556     }
557 
558     /**
559      * MessageStatus
560      *
561      * Helper class to store associations between remote and local provider based on message handle
562      * and read status
563      */
564     class MessageStatus {
565 
566         String mHandle;
567         int mRead;
568 
MessageStatus(String handle, int read)569         MessageStatus(String handle, int read) {
570             mHandle = handle;
571             mRead = read;
572         }
573 
574         @Override
equals(Object other)575         public boolean equals(Object other) {
576             return ((other instanceof MessageStatus) && ((MessageStatus) other).mHandle
577                     .equals(mHandle));
578         }
579     }
580 }
581