1 /*
2  * Copyright (C) 2014 Samsung System LSI
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 package com.android.bluetooth.map;
16 
17 import android.app.Activity;
18 import android.app.PendingIntent;
19 import android.bluetooth.BluetoothProfile;
20 import android.bluetooth.BluetoothProtoEnums;
21 import android.content.BroadcastReceiver;
22 import android.content.ContentProviderClient;
23 import android.content.ContentResolver;
24 import android.content.ContentUris;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.IntentFilter.MalformedMimeTypeException;
30 import android.database.ContentObserver;
31 import android.database.Cursor;
32 import android.database.sqlite.SQLiteException;
33 import android.net.Uri;
34 import android.os.Binder;
35 import android.os.Handler;
36 import android.os.Looper;
37 import android.os.Message;
38 import android.os.ParcelFileDescriptor;
39 import android.os.Process;
40 import android.os.RemoteException;
41 import android.os.UserManager;
42 import android.provider.Telephony;
43 import android.provider.Telephony.Mms;
44 import android.provider.Telephony.MmsSms;
45 import android.provider.Telephony.Sms;
46 import android.telephony.PhoneStateListener;
47 import android.telephony.ServiceState;
48 import android.telephony.SmsManager;
49 import android.telephony.SubscriptionManager;
50 import android.telephony.TelephonyManager;
51 import android.text.TextUtils;
52 import android.text.format.DateUtils;
53 import android.util.Log;
54 import android.util.Xml;
55 
56 import com.android.bluetooth.BluetoothMethodProxy;
57 import com.android.bluetooth.BluetoothStatsLog;
58 import com.android.bluetooth.Utils;
59 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils;
60 import com.android.bluetooth.flags.Flags;
61 import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
62 import com.android.bluetooth.map.BluetoothMapbMessageMime.MimePart;
63 import com.android.bluetooth.mapapi.BluetoothMapContract;
64 import com.android.bluetooth.mapapi.BluetoothMapContract.MessageColumns;
65 import com.android.internal.annotations.VisibleForTesting;
66 import com.android.obex.ResponseCodes;
67 
68 import com.google.android.mms.pdu.PduHeaders;
69 
70 import org.xmlpull.v1.XmlSerializer;
71 
72 import java.io.FileNotFoundException;
73 import java.io.FileOutputStream;
74 import java.io.IOException;
75 import java.io.OutputStream;
76 import java.io.StringWriter;
77 import java.io.UnsupportedEncodingException;
78 import java.time.Duration;
79 import java.util.ArrayList;
80 import java.util.Arrays;
81 import java.util.Calendar;
82 import java.util.Collections;
83 import java.util.HashMap;
84 import java.util.HashSet;
85 import java.util.Map;
86 import java.util.Objects;
87 import java.util.Set;
88 import java.util.concurrent.TimeUnit;
89 
90 // Next tag value for ContentProfileErrorReportUtils.report(): 41
91 public class BluetoothMapContentObserver {
92     private static final String TAG = "BluetoothMapContentObserver";
93 
94     // A message older than this will be ignored when notifying a new message.
95     @VisibleForTesting
96     static final Duration NEW_MESSAGE_DURATION_FOR_NOTIFICATION = Duration.ofDays(7);
97 
98     @VisibleForTesting static final String EVENT_TYPE_NEW = "NewMessage";
99     @VisibleForTesting static final String EVENT_TYPE_DELETE = "MessageDeleted";
100     @VisibleForTesting static final String EVENT_TYPE_REMOVED = "MessageRemoved";
101     @VisibleForTesting static final String EVENT_TYPE_SHIFT = "MessageShift";
102     @VisibleForTesting static final String EVENT_TYPE_DELEVERY_SUCCESS = "DeliverySuccess";
103     @VisibleForTesting static final String EVENT_TYPE_SENDING_SUCCESS = "SendingSuccess";
104     @VisibleForTesting static final String EVENT_TYPE_SENDING_FAILURE = "SendingFailure";
105     @VisibleForTesting static final String EVENT_TYPE_DELIVERY_FAILURE = "DeliveryFailure";
106     @VisibleForTesting static final String EVENT_TYPE_READ_STATUS = "ReadStatusChanged";
107     @VisibleForTesting static final String EVENT_TYPE_CONVERSATION = "ConversationChanged";
108     @VisibleForTesting static final String EVENT_TYPE_PRESENCE = "ParticipantPresenceChanged";
109     @VisibleForTesting static final String EVENT_TYPE_CHAT_STATE = "ParticipantChatStateChanged";
110 
111     private static final long EVENT_FILTER_NEW_MESSAGE = 1L;
112     private static final long EVENT_FILTER_MESSAGE_DELETED = 1L << 1;
113     private static final long EVENT_FILTER_MESSAGE_SHIFT = 1L << 2;
114     private static final long EVENT_FILTER_SENDING_SUCCESS = 1L << 3;
115     private static final long EVENT_FILTER_SENDING_FAILED = 1L << 4;
116     private static final long EVENT_FILTER_DELIVERY_SUCCESS = 1L << 5;
117     private static final long EVENT_FILTER_DELIVERY_FAILED = 1L << 6;
118     // private static final long EVENT_FILTER_MEMORY_FULL = 1L << 7; // Unused
119     // private static final long EVENT_FILTER_MEMORY_AVAILABLE = 1L << 8; // Unused
120     private static final long EVENT_FILTER_READ_STATUS_CHANGED = 1L << 9;
121     private static final long EVENT_FILTER_CONVERSATION_CHANGED = 1L << 10;
122     private static final long EVENT_FILTER_PARTICIPANT_PRESENCE_CHANGED = 1L << 11;
123     private static final long EVENT_FILTER_PARTICIPANT_CHATSTATE_CHANGED = 1L << 12;
124     private static final long EVENT_FILTER_MESSAGE_REMOVED = 1L << 14;
125 
126     // TODO: If we are requesting a large message from the network, on a slow connection
127     //       20 seconds might not be enough... But then again 20 seconds is long for other
128     //       cases.
129     private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
130 
131     private Context mContext;
132     private ContentResolver mResolver;
133     @VisibleForTesting ContentProviderClient mProviderClient = null;
134     private BluetoothMnsObexClient mMnsClient;
135     private BluetoothMapMasInstance mMasInstance = null;
136     private int mMasId;
137     private boolean mEnableSmsMms = false;
138     @VisibleForTesting boolean mObserverRegistered = false;
139     @VisibleForTesting BluetoothMapAccountItem mAccount;
140     @VisibleForTesting String mAuthority = null;
141 
142     // Default supported feature bit mask is 0x1f
143     private int mMapSupportedFeatures = BluetoothMapUtils.MAP_FEATURE_DEFAULT_BITMASK;
144     // Default event report version is 1.0
145     @VisibleForTesting int mMapEventReportVersion = BluetoothMapUtils.MAP_EVENT_REPORT_V10;
146 
147     private BluetoothMapFolderElement mFolders = new BluetoothMapFolderElement("DUMMY", null);
148     // Will be set by the MAS when generated.
149     @VisibleForTesting Uri mMessageUri = null;
150     @VisibleForTesting Uri mContactUri = null;
151 
152     private boolean mTransmitEvents = true;
153 
154     /* To make the filter update atomic, we declare it volatile.
155      * To avoid a penalty when using it, copy the value to a local
156      * non-volatile variable when used more than once.
157      * Actually we only ever use the lower 4 bytes of this variable,
158      * hence we could manage without the volatile keyword, but as
159      * we tend to copy ways of doing things, we better do it right:-) */
160     private volatile long mEventFilter = 0xFFFFFFFFL;
161 
162     public static final int DELETED_THREAD_ID = -1;
163 
164     // X-Mms-Message-Type field types. These are from PduHeaders.java
165     public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84;
166 
167     // Text only MMS converted to SMS if sms parts less than or equal to defined count
168     private static final int CONVERT_MMS_TO_SMS_PART_COUNT = 10;
169 
170     private TYPE mSmsType;
171 
172     private static final String ACTION_MESSAGE_DELIVERY =
173             "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY";
174     /*package*/ static final String ACTION_MESSAGE_SENT =
175             "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT";
176 
177     public static final String EXTRA_MESSAGE_SENT_HANDLE = "HANDLE";
178     public static final String EXTRA_MESSAGE_SENT_RESULT = "result";
179     public static final String EXTRA_MESSAGE_SENT_MSG_TYPE = "type";
180     public static final String EXTRA_MESSAGE_SENT_URI = "uri";
181     public static final String EXTRA_MESSAGE_SENT_RETRY = "retry";
182     public static final String EXTRA_MESSAGE_SENT_TRANSPARENT = "transparent";
183     public static final String EXTRA_MESSAGE_SENT_TIMESTAMP = "timestamp";
184 
185     private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver();
186     private CeBroadcastReceiver mCeBroadcastReceiver = new CeBroadcastReceiver();
187 
188     private boolean mStorageUnlocked = false;
189     private boolean mInitialized = false;
190 
191     static final String[] SMS_PROJECTION =
192             new String[] {
193                 Sms._ID,
194                 Sms.THREAD_ID,
195                 Sms.ADDRESS,
196                 Sms.BODY,
197                 Sms.DATE,
198                 Sms.READ,
199                 Sms.TYPE,
200                 Sms.STATUS,
201                 Sms.LOCKED,
202                 Sms.ERROR_CODE
203             };
204 
205     static final String[] SMS_PROJECTION_SHORT =
206             new String[] {Sms._ID, Sms.THREAD_ID, Sms.TYPE, Sms.READ};
207 
208     static final String[] SMS_PROJECTION_SHORT_EXT =
209             new String[] {
210                 Sms._ID, Sms.THREAD_ID, Sms.ADDRESS, Sms.BODY, Sms.DATE, Sms.READ, Sms.TYPE,
211             };
212 
213     static final String[] MMS_PROJECTION_SHORT =
214             new String[] {Mms._ID, Mms.THREAD_ID, Mms.MESSAGE_TYPE, Mms.MESSAGE_BOX, Mms.READ};
215 
216     static final String[] MMS_PROJECTION_SHORT_EXT =
217             new String[] {
218                 Mms._ID,
219                 Mms.THREAD_ID,
220                 Mms.MESSAGE_TYPE,
221                 Mms.MESSAGE_BOX,
222                 Mms.READ,
223                 Mms.DATE,
224                 Mms.SUBJECT,
225                 Mms.PRIORITY
226             };
227 
228     static final String[] MSG_PROJECTION_SHORT =
229             new String[] {
230                 BluetoothMapContract.MessageColumns._ID,
231                 BluetoothMapContract.MessageColumns.FOLDER_ID,
232                 BluetoothMapContract.MessageColumns.FLAG_READ
233             };
234 
235     static final String[] MSG_PROJECTION_SHORT_EXT =
236             new String[] {
237                 BluetoothMapContract.MessageColumns._ID,
238                 BluetoothMapContract.MessageColumns.FOLDER_ID,
239                 BluetoothMapContract.MessageColumns.FLAG_READ,
240                 BluetoothMapContract.MessageColumns.DATE,
241                 BluetoothMapContract.MessageColumns.SUBJECT,
242                 BluetoothMapContract.MessageColumns.FROM_LIST,
243                 BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY
244             };
245 
246     static final String[] MSG_PROJECTION_SHORT_EXT2 =
247             new String[] {
248                 BluetoothMapContract.MessageColumns._ID,
249                 BluetoothMapContract.MessageColumns.FOLDER_ID,
250                 BluetoothMapContract.MessageColumns.FLAG_READ,
251                 BluetoothMapContract.MessageColumns.DATE,
252                 BluetoothMapContract.MessageColumns.SUBJECT,
253                 BluetoothMapContract.MessageColumns.FROM_LIST,
254                 BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY,
255                 BluetoothMapContract.MessageColumns.THREAD_ID,
256                 BluetoothMapContract.MessageColumns.THREAD_NAME
257             };
258 
BluetoothMapContentObserver( final Context context, BluetoothMnsObexClient mnsClient, BluetoothMapMasInstance masInstance, BluetoothMapAccountItem account, boolean enableSmsMms)259     public BluetoothMapContentObserver(
260             final Context context,
261             BluetoothMnsObexClient mnsClient,
262             BluetoothMapMasInstance masInstance,
263             BluetoothMapAccountItem account,
264             boolean enableSmsMms)
265             throws RemoteException {
266         mContext = context;
267         mResolver = mContext.getContentResolver();
268         mAccount = account;
269         mMasInstance = masInstance;
270         mMasId = mMasInstance.getMasId();
271         setObserverRemoteFeatureMask(mMasInstance.getRemoteFeatureMask());
272 
273         if (account != null) {
274             mAuthority = Uri.parse(account.mBase_uri).getAuthority();
275             mMessageUri = Uri.parse(account.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE);
276             if (mAccount.getType() == TYPE.IM) {
277                 mContactUri =
278                         Uri.parse(
279                                 account.mBase_uri + "/" + BluetoothMapContract.TABLE_CONVOCONTACT);
280             }
281             // TODO: We need to release this again!
282             mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority);
283             if (mProviderClient == null) {
284                 throw new RemoteException("Failed to acquire provider for " + mAuthority);
285             }
286             mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
287             mContactList = mMasInstance.getContactList();
288             if (mContactList == null) {
289                 setContactList(new HashMap<String, BluetoothMapConvoContactElement>(), false);
290                 initContactsList();
291             }
292         }
293         mEnableSmsMms = enableSmsMms;
294         mSmsType = getSmsType();
295         mMnsClient = mnsClient;
296         /* Get the cached list - if any, else create */
297         mMsgListSms = mMasInstance.getMsgListSms();
298         boolean doInit = false;
299         if (mEnableSmsMms) {
300             if (mMsgListSms == null) {
301                 setMsgListSms(new HashMap<Long, Msg>(), false);
302                 doInit = true;
303             }
304             mMsgListMms = mMasInstance.getMsgListMms();
305             if (mMsgListMms == null) {
306                 setMsgListMms(new HashMap<Long, Msg>(), false);
307                 doInit = true;
308             }
309         }
310         if (mAccount != null) {
311             mMsgListMsg = mMasInstance.getMsgListMsg();
312             if (mMsgListMsg == null) {
313                 setMsgListMsg(new HashMap<Long, Msg>(), false);
314                 doInit = true;
315             }
316         }
317         if (doInit) {
318             initMsgList();
319         }
320     }
321 
getObserverRemoteFeatureMask()322     public int getObserverRemoteFeatureMask() {
323         Log.v(
324                 TAG,
325                 "getObserverRemoteFeatureMask : "
326                         + mMapEventReportVersion
327                         + " mMapSupportedFeatures: "
328                         + mMapSupportedFeatures);
329         return mMapSupportedFeatures;
330     }
331 
setObserverRemoteFeatureMask(int remoteSupportedFeatures)332     public void setObserverRemoteFeatureMask(int remoteSupportedFeatures) {
333         mMapSupportedFeatures = remoteSupportedFeatures & BluetoothMapMasInstance.getFeatureMask();
334         if ((BluetoothMapUtils.MAP_FEATURE_EXTENDED_EVENT_REPORT_11_BIT & mMapSupportedFeatures)
335                 != 0) {
336             mMapEventReportVersion = BluetoothMapUtils.MAP_EVENT_REPORT_V11;
337         }
338         // Make sure support for all formats result in latest version returned
339         if ((BluetoothMapUtils.MAP_FEATURE_EVENT_REPORT_V12_BIT & mMapSupportedFeatures) != 0) {
340             mMapEventReportVersion = BluetoothMapUtils.MAP_EVENT_REPORT_V12;
341         } else if (((BluetoothMapUtils.MAP_FEATURE_PARTICIPANT_PRESENCE_CHANGE_BIT
342                                 | BluetoothMapUtils.MAP_FEATURE_PARTICIPANT_CHAT_STATE_CHANGE_BIT)
343                         & mMapSupportedFeatures)
344                 != 0) {
345             // Warning according to page 46/123 of MAP 1.3 spec
346             Log.w(
347                     TAG,
348                     "setObserverRemoteFeatureMask: Extended Event Reports 1.2 is not set eventhough"
349                         + " PARTICIPANT_PRESENCE_CHANGE_BIT or PARTICIPANT_CHAT_STATE_CHANGE_BIT"
350                         + " were set, mMapSupportedFeatures="
351                             + mMapSupportedFeatures);
352             ContentProfileErrorReportUtils.report(
353                     BluetoothProfile.MAP,
354                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
355                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
356                     0);
357         }
358         Log.d(
359                 TAG,
360                 "setObserverRemoteFeatureMask: mMapEventReportVersion="
361                         + mMapEventReportVersion
362                         + " mMapSupportedFeatures="
363                         + mMapSupportedFeatures);
364     }
365 
366     @VisibleForTesting
getMsgListSms()367     Map<Long, Msg> getMsgListSms() {
368         return mMsgListSms;
369     }
370 
371     @VisibleForTesting
setMsgListSms(Map<Long, Msg> msgListSms, boolean changesDetected)372     void setMsgListSms(Map<Long, Msg> msgListSms, boolean changesDetected) {
373         mMsgListSms = msgListSms;
374         if (changesDetected) {
375             mMasInstance.updateFolderVersionCounter();
376         }
377         mMasInstance.setMsgListSms(msgListSms);
378     }
379 
380     @VisibleForTesting
getMsgListMms()381     Map<Long, Msg> getMsgListMms() {
382         return mMsgListMms;
383     }
384 
385     @VisibleForTesting
setMsgListMms(Map<Long, Msg> msgListMms, boolean changesDetected)386     void setMsgListMms(Map<Long, Msg> msgListMms, boolean changesDetected) {
387         mMsgListMms = msgListMms;
388         if (changesDetected) {
389             mMasInstance.updateFolderVersionCounter();
390         }
391         mMasInstance.setMsgListMms(msgListMms);
392     }
393 
394     @VisibleForTesting
getMsgListMsg()395     Map<Long, Msg> getMsgListMsg() {
396         return mMsgListMsg;
397     }
398 
399     @VisibleForTesting
setMsgListMsg(Map<Long, Msg> msgListMsg, boolean changesDetected)400     void setMsgListMsg(Map<Long, Msg> msgListMsg, boolean changesDetected) {
401         mMsgListMsg = msgListMsg;
402         if (changesDetected) {
403             mMasInstance.updateFolderVersionCounter();
404         }
405         mMasInstance.setMsgListMsg(msgListMsg);
406     }
407 
408     @VisibleForTesting
getContactList()409     Map<String, BluetoothMapConvoContactElement> getContactList() {
410         return mContactList;
411     }
412 
413     /**
414      * Currently we only have data for IM / email contacts
415      *
416      * @param changesDetected that is not chat state changed nor presence state changed.
417      */
418     @VisibleForTesting
setContactList( Map<String, BluetoothMapConvoContactElement> contactList, boolean changesDetected)419     void setContactList(
420             Map<String, BluetoothMapConvoContactElement> contactList, boolean changesDetected) {
421         mContactList = contactList;
422         if (changesDetected) {
423             mMasInstance.updateImEmailConvoListVersionCounter();
424         }
425         mMasInstance.setContactList(contactList);
426     }
427 
sendEventNewMessage(long eventFilter)428     private static boolean sendEventNewMessage(long eventFilter) {
429         return ((eventFilter & EVENT_FILTER_NEW_MESSAGE) > 0);
430     }
431 
sendEventMessageDeleted(long eventFilter)432     private static boolean sendEventMessageDeleted(long eventFilter) {
433         return ((eventFilter & EVENT_FILTER_MESSAGE_DELETED) > 0);
434     }
435 
sendEventMessageShift(long eventFilter)436     private static boolean sendEventMessageShift(long eventFilter) {
437         return ((eventFilter & EVENT_FILTER_MESSAGE_SHIFT) > 0);
438     }
439 
sendEventSendingSuccess(long eventFilter)440     private static boolean sendEventSendingSuccess(long eventFilter) {
441         return ((eventFilter & EVENT_FILTER_SENDING_SUCCESS) > 0);
442     }
443 
sendEventSendingFailed(long eventFilter)444     private static boolean sendEventSendingFailed(long eventFilter) {
445         return ((eventFilter & EVENT_FILTER_SENDING_FAILED) > 0);
446     }
447 
sendEventDeliverySuccess(long eventFilter)448     private static boolean sendEventDeliverySuccess(long eventFilter) {
449         return ((eventFilter & EVENT_FILTER_DELIVERY_SUCCESS) > 0);
450     }
451 
sendEventDeliveryFailed(long eventFilter)452     private static boolean sendEventDeliveryFailed(long eventFilter) {
453         return ((eventFilter & EVENT_FILTER_DELIVERY_FAILED) > 0);
454     }
455 
sendEventReadStatusChanged(long eventFilter)456     private static boolean sendEventReadStatusChanged(long eventFilter) {
457         return ((eventFilter & EVENT_FILTER_READ_STATUS_CHANGED) > 0);
458     }
459 
sendEventConversationChanged(long eventFilter)460     private static boolean sendEventConversationChanged(long eventFilter) {
461         return ((eventFilter & EVENT_FILTER_CONVERSATION_CHANGED) > 0);
462     }
463 
sendEventParticipantPresenceChanged(long eventFilter)464     private static boolean sendEventParticipantPresenceChanged(long eventFilter) {
465         return ((eventFilter & EVENT_FILTER_PARTICIPANT_PRESENCE_CHANGED) > 0);
466     }
467 
sendEventParticipantChatstateChanged(long eventFilter)468     private static boolean sendEventParticipantChatstateChanged(long eventFilter) {
469         return ((eventFilter & EVENT_FILTER_PARTICIPANT_CHATSTATE_CHANGED) > 0);
470     }
471 
sendEventMessageRemoved(long eventFilter)472     private static boolean sendEventMessageRemoved(long eventFilter) {
473         return ((eventFilter & EVENT_FILTER_MESSAGE_REMOVED) > 0);
474     }
475 
getSmsType()476     private TYPE getSmsType() {
477         TYPE smsType = null;
478         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
479 
480         if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
481             smsType = TYPE.SMS_CDMA;
482         } else {
483             smsType = TYPE.SMS_GSM;
484         }
485 
486         return smsType;
487     }
488 
489     private final ContentObserver mObserver =
490             new ContentObserver(new Handler()) {
491                 @Override
492                 public void onChange(boolean selfChange) {
493                     onChange(selfChange, null);
494                 }
495 
496                 @Override
497                 public void onChange(boolean selfChange, Uri uri) {
498                     if (uri == null) {
499                         Log.w(TAG, "onChange() with URI == null - not handled.");
500                         ContentProfileErrorReportUtils.report(
501                                 BluetoothProfile.MAP,
502                                 BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
503                                 BluetoothStatsLog
504                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
505                                 1);
506                         return;
507                     }
508 
509                     if (!mStorageUnlocked) {
510                         Log.v(TAG, "Ignore events until storage is completely unlocked");
511                         return;
512                     }
513 
514                     Log.v(
515                             TAG,
516                             "onChange on thread: "
517                                     + Thread.currentThread().getId()
518                                     + " Uri: "
519                                     + uri.toString()
520                                     + " selfchange: "
521                                     + selfChange);
522 
523                     if (uri.toString().contains(BluetoothMapContract.TABLE_CONVOCONTACT)) {
524                         handleContactListChanges(uri);
525                     } else {
526                         handleMsgListChanges(uri);
527                     }
528                 }
529             };
530 
531     private static final HashMap<Integer, String> FOLDER_SMS_MAP;
532 
533     static {
534         FOLDER_SMS_MAP = new HashMap<Integer, String>();
FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_INBOX, BluetoothMapContract.FOLDER_NAME_INBOX)535         FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_INBOX, BluetoothMapContract.FOLDER_NAME_INBOX);
FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_SENT, BluetoothMapContract.FOLDER_NAME_SENT)536         FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_SENT, BluetoothMapContract.FOLDER_NAME_SENT);
FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_DRAFT, BluetoothMapContract.FOLDER_NAME_DRAFT)537         FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_DRAFT, BluetoothMapContract.FOLDER_NAME_DRAFT);
FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_OUTBOX, BluetoothMapContract.FOLDER_NAME_OUTBOX)538         FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_OUTBOX, BluetoothMapContract.FOLDER_NAME_OUTBOX);
FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_FAILED, BluetoothMapContract.FOLDER_NAME_OUTBOX)539         FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_FAILED, BluetoothMapContract.FOLDER_NAME_OUTBOX);
FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_QUEUED, BluetoothMapContract.FOLDER_NAME_OUTBOX)540         FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_QUEUED, BluetoothMapContract.FOLDER_NAME_OUTBOX);
541     }
542 
getSmsFolderName(int type)543     private static String getSmsFolderName(int type) {
544         String name = FOLDER_SMS_MAP.get(type);
545         if (name != null) {
546             return name;
547         }
548         Log.e(TAG, "New SMS mailbox types have been introduced, without an update in BT...");
549         ContentProfileErrorReportUtils.report(
550                 BluetoothProfile.MAP,
551                 BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
552                 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
553                 2);
554         return "Unknown";
555     }
556 
557     private static final HashMap<Integer, String> FOLDER_MMS_MAP;
558 
559     static {
560         FOLDER_MMS_MAP = new HashMap<Integer, String>();
FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_INBOX, BluetoothMapContract.FOLDER_NAME_INBOX)561         FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_INBOX, BluetoothMapContract.FOLDER_NAME_INBOX);
FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_SENT, BluetoothMapContract.FOLDER_NAME_SENT)562         FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_SENT, BluetoothMapContract.FOLDER_NAME_SENT);
FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_DRAFTS, BluetoothMapContract.FOLDER_NAME_DRAFT)563         FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_DRAFTS, BluetoothMapContract.FOLDER_NAME_DRAFT);
FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_OUTBOX, BluetoothMapContract.FOLDER_NAME_OUTBOX)564         FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_OUTBOX, BluetoothMapContract.FOLDER_NAME_OUTBOX);
565     }
566 
getMmsFolderName(int mailbox)567     private static String getMmsFolderName(int mailbox) {
568         String name = FOLDER_MMS_MAP.get(mailbox);
569         if (name != null) {
570             return name;
571         }
572         Log.e(TAG, "New MMS mailboxes have been introduced, without an update in BT...");
573         ContentProfileErrorReportUtils.report(
574                 BluetoothProfile.MAP,
575                 BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
576                 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
577                 3);
578         return "Unknown";
579     }
580 
581     /** Set the folder structure to be used for this instance. */
setFolderStructure(BluetoothMapFolderElement folderStructure)582     void setFolderStructure(BluetoothMapFolderElement folderStructure) {
583         this.mFolders = folderStructure;
584     }
585 
586     @VisibleForTesting
587     static class ConvoContactInfo {
588         public int mConvoColConvoId = -1;
589         public int mConvoColLastActivity = -1;
590         public int mConvoColName = -1;
591         //        public int mConvoColRead            = -1;
592         //        public int mConvoColVersionCounter  = -1;
593         public int mContactColUci = -1;
594         public int mContactColConvoId = -1;
595         public int mContactColName = -1;
596         public int mContactColNickname = -1;
597         public int mContactColBtUid = -1;
598         public int mContactColChatState = -1;
599         public int mContactColContactId = -1;
600         public int mContactColLastActive = -1;
601         public int mContactColPresenceState = -1;
602         public int mContactColPresenceText = -1;
603         public int mContactColPriority = -1;
604         public int mContactColLastOnline = -1;
605 
setConvoColunms(Cursor c)606         public void setConvoColunms(Cursor c) {
607             //            mConvoColConvoId         = c.getColumnIndex(
608             //                    BluetoothMapContract.ConversationColumns.THREAD_ID);
609             //            mConvoColLastActivity    = c.getColumnIndex(
610             //                    BluetoothMapContract.ConversationColumns.LAST_THREAD_ACTIVITY);
611             //            mConvoColName            = c.getColumnIndex(
612             //                    BluetoothMapContract.ConversationColumns.THREAD_NAME);
613             mContactColConvoId =
614                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.CONVO_ID);
615             mContactColName = c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.NAME);
616             mContactColNickname =
617                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.NICKNAME);
618             mContactColBtUid = c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.X_BT_UID);
619             mContactColChatState =
620                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.CHAT_STATE);
621             mContactColUci = c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.UCI);
622             mContactColNickname =
623                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.NICKNAME);
624             mContactColLastActive =
625                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.LAST_ACTIVE);
626             mContactColName = c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.NAME);
627             mContactColPresenceState =
628                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.PRESENCE_STATE);
629             mContactColPresenceText =
630                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.STATUS_TEXT);
631             mContactColPriority =
632                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.PRIORITY);
633             mContactColLastOnline =
634                     c.getColumnIndex(BluetoothMapContract.ConvoContactColumns.LAST_ONLINE);
635         }
636     }
637 
638     @VisibleForTesting
639     class Event {
640         public String eventType;
641         public long handle;
642         public String folder = null;
643         public String oldFolder = null;
644         public TYPE msgType;
645         /* Extended event parameters in MAP Event version 1.1 */
646         public String datetime = null; // OBEX time "YYYYMMDDTHHMMSS"
647         public String uci = null;
648         public String subject = null;
649         public String senderName = null;
650         public String priority = null;
651         /* Event parameters in MAP Event version 1.2 */
652         public String conversationName = null;
653         public long conversationID = -1;
654         public int presenceState = BluetoothMapContract.PresenceState.UNKNOWN;
655         public String presenceStatus = null;
656         public int chatState = BluetoothMapContract.ChatState.UNKNOWN;
657 
658         static final String PATH = "telecom/msg/";
659 
660         @VisibleForTesting
setFolderPath(String name, TYPE type)661         void setFolderPath(String name, TYPE type) {
662             if (name != null) {
663                 if (type == TYPE.EMAIL || type == TYPE.IM) {
664                     this.folder = name;
665                 } else {
666                     this.folder = PATH + name;
667                 }
668             } else {
669                 this.folder = null;
670             }
671         }
672 
Event(String eventType, long handle, String folder, String oldFolder, TYPE msgType)673         Event(String eventType, long handle, String folder, String oldFolder, TYPE msgType) {
674             this.eventType = eventType;
675             this.handle = handle;
676             setFolderPath(folder, msgType);
677             if (oldFolder != null) {
678                 if (msgType == TYPE.EMAIL || msgType == TYPE.IM) {
679                     this.oldFolder = oldFolder;
680                 } else {
681                     this.oldFolder = PATH + oldFolder;
682                 }
683             } else {
684                 this.oldFolder = null;
685             }
686             this.msgType = msgType;
687         }
688 
Event(String eventType, long handle, String folder, TYPE msgType)689         Event(String eventType, long handle, String folder, TYPE msgType) {
690             this.eventType = eventType;
691             this.handle = handle;
692             setFolderPath(folder, msgType);
693             this.msgType = msgType;
694         }
695 
696         /* extended event type 1.1 */
Event( String eventType, long handle, String folder, TYPE msgType, String datetime, String subject, String senderName, String priority)697         Event(
698                 String eventType,
699                 long handle,
700                 String folder,
701                 TYPE msgType,
702                 String datetime,
703                 String subject,
704                 String senderName,
705                 String priority) {
706             this.eventType = eventType;
707             this.handle = handle;
708             setFolderPath(folder, msgType);
709             this.msgType = msgType;
710             this.datetime = datetime;
711             if (subject != null) {
712                 this.subject = BluetoothMapUtils.stripInvalidChars(subject);
713             }
714             if (senderName != null) {
715                 this.senderName = BluetoothMapUtils.stripInvalidChars(senderName);
716             }
717             this.priority = priority;
718         }
719 
720         /* extended event type 1.2 message events */
Event( String eventType, long handle, String folder, TYPE msgType, String datetime, String subject, String senderName, String priority, long conversationID, String conversationName)721         Event(
722                 String eventType,
723                 long handle,
724                 String folder,
725                 TYPE msgType,
726                 String datetime,
727                 String subject,
728                 String senderName,
729                 String priority,
730                 long conversationID,
731                 String conversationName) {
732             this.eventType = eventType;
733             this.handle = handle;
734             setFolderPath(folder, msgType);
735             this.msgType = msgType;
736             this.datetime = datetime;
737             if (subject != null) {
738                 this.subject = BluetoothMapUtils.stripInvalidChars(subject);
739             }
740             if (senderName != null) {
741                 this.senderName = BluetoothMapUtils.stripInvalidChars(senderName);
742             }
743             if (conversationID != 0) {
744                 this.conversationID = conversationID;
745             }
746             if (conversationName != null) {
747                 this.conversationName = BluetoothMapUtils.stripInvalidChars(conversationName);
748             }
749             this.priority = priority;
750         }
751 
752         /* extended event type 1.2 for conversation, presence or chat state changed events */
Event( String eventType, String uci, TYPE msgType, String name, String priority, String lastActivity, long conversationID, String conversationName, int presenceState, String presenceStatus, int chatState)753         Event(
754                 String eventType,
755                 String uci,
756                 TYPE msgType,
757                 String name,
758                 String priority,
759                 String lastActivity,
760                 long conversationID,
761                 String conversationName,
762                 int presenceState,
763                 String presenceStatus,
764                 int chatState) {
765             this.eventType = eventType;
766             this.uci = uci;
767             this.msgType = msgType;
768             if (name != null) {
769                 this.senderName = BluetoothMapUtils.stripInvalidChars(name);
770             }
771             this.priority = priority;
772             this.datetime = lastActivity;
773             if (conversationID != 0) {
774                 this.conversationID = conversationID;
775             }
776             if (conversationName != null) {
777                 this.conversationName = BluetoothMapUtils.stripInvalidChars(conversationName);
778             }
779             if (presenceState != BluetoothMapContract.PresenceState.UNKNOWN) {
780                 this.presenceState = presenceState;
781             }
782             if (presenceStatus != null) {
783                 this.presenceStatus = BluetoothMapUtils.stripInvalidChars(presenceStatus);
784             }
785             if (chatState != BluetoothMapContract.ChatState.UNKNOWN) {
786                 this.chatState = chatState;
787             }
788         }
789 
encode()790         public byte[] encode() throws UnsupportedEncodingException {
791             StringWriter sw = new StringWriter();
792             XmlSerializer xmlEvtReport = Xml.newSerializer();
793 
794             try {
795                 xmlEvtReport.setOutput(sw);
796                 xmlEvtReport.startDocument("UTF-8", true);
797                 xmlEvtReport.text("\r\n");
798                 xmlEvtReport.startTag("", "MAP-event-report");
799                 if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V12) {
800                     xmlEvtReport.attribute("", "version", BluetoothMapUtils.MAP_V12_STR);
801                 } else if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
802                     xmlEvtReport.attribute("", "version", BluetoothMapUtils.MAP_V11_STR);
803                 } else {
804                     xmlEvtReport.attribute("", "version", BluetoothMapUtils.MAP_V10_STR);
805                 }
806                 xmlEvtReport.startTag("", "event");
807                 xmlEvtReport.attribute("", "type", eventType);
808                 if (eventType.equals(EVENT_TYPE_CONVERSATION)
809                         || eventType.equals(EVENT_TYPE_PRESENCE)
810                         || eventType.equals(EVENT_TYPE_CHAT_STATE)) {
811                     xmlEvtReport.attribute("", "participant_uci", uci);
812                 } else {
813                     xmlEvtReport.attribute(
814                             "", "handle", BluetoothMapUtils.getMapHandle(handle, msgType));
815                 }
816 
817                 if (folder != null) {
818                     xmlEvtReport.attribute("", "folder", folder);
819                 }
820                 if (oldFolder != null) {
821                     xmlEvtReport.attribute("", "old_folder", oldFolder);
822                 }
823                 /* Avoid possible NPE for "msgType" "null" value. "msgType"
824                  * is a implied attribute and will be set "null" for events
825                  * like "memory full" or "memory available" */
826                 if (msgType != null) {
827                     xmlEvtReport.attribute("", "msg_type", msgType.name());
828                 }
829                 /* If MAP event report version is above 1.0 send
830                  * extended event report parameters */
831                 if (datetime != null) {
832                     xmlEvtReport.attribute("", "datetime", datetime);
833                 }
834                 if (subject != null) {
835                     xmlEvtReport.attribute(
836                             "",
837                             "subject",
838                             subject.substring(0, subject.length() < 256 ? subject.length() : 256));
839                 }
840                 if (senderName != null) {
841                     xmlEvtReport.attribute(
842                             "",
843                             "sender_name",
844                             senderName.substring(
845                                     0, senderName.length() < 256 ? senderName.length() : 255));
846                 }
847                 if (priority != null) {
848                     xmlEvtReport.attribute("", "priority", priority);
849                 }
850 
851                 // }
852                 /* Include conversation information from event version 1.2 */
853                 if (mMapEventReportVersion > BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
854                     if (conversationName != null) {
855                         xmlEvtReport.attribute("", "conversation_name", conversationName);
856                     }
857                     if (conversationID != -1) {
858                         // Convert provider conversation handle to string incl type
859                         xmlEvtReport.attribute(
860                                 "",
861                                 "conversation_id",
862                                 BluetoothMapUtils.getMapConvoHandle(conversationID, msgType));
863                     }
864                     if (eventType.equals(EVENT_TYPE_PRESENCE)) {
865                         if (presenceState != 0) {
866                             // Convert provider conversation handle to string incl type
867                             xmlEvtReport.attribute(
868                                     "", "presence_availability", String.valueOf(presenceState));
869                         }
870                         if (presenceStatus != null) {
871                             // Convert provider conversation handle to string incl type
872                             xmlEvtReport.attribute(
873                                     "",
874                                     "presence_status",
875                                     presenceStatus.substring(
876                                             0,
877                                             presenceStatus.length() < 256
878                                                     ? subject.length()
879                                                     : 256));
880                         }
881                     }
882                     if (eventType.equals(EVENT_TYPE_PRESENCE)) {
883                         if (chatState != 0) {
884                             // Convert provider conversation handle to string incl type
885                             xmlEvtReport.attribute("", "chat_state", String.valueOf(chatState));
886                         }
887                     }
888                 }
889                 xmlEvtReport.endTag("", "event");
890                 xmlEvtReport.endTag("", "MAP-event-report");
891                 xmlEvtReport.endDocument();
892             } catch (IllegalArgumentException e) {
893                 ContentProfileErrorReportUtils.report(
894                         BluetoothProfile.MAP,
895                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
896                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
897                         4);
898                 Log.w(TAG, e);
899             } catch (IllegalStateException e) {
900                 ContentProfileErrorReportUtils.report(
901                         BluetoothProfile.MAP,
902                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
903                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
904                         5);
905                 Log.w(TAG, e);
906             } catch (IOException e) {
907                 ContentProfileErrorReportUtils.report(
908                         BluetoothProfile.MAP,
909                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
910                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
911                         6);
912                 Log.w(TAG, e);
913             }
914 
915             Log.v(TAG, sw.toString());
916 
917             return sw.toString().getBytes("UTF-8");
918         }
919     }
920 
921     static class Msg {
922         public long id;
923         public int type; // Used as folder for SMS/MMS
924         public int threadId; // Used for SMS/MMS at delete
925         public long folderId = -1; // Email folder ID
926         public long oldFolderId = -1; // Used for email undelete
927         public boolean localInitiatedSend = false; // Used for MMS to filter out events
928         public boolean transparent = false;
929         // Used for EMAIL to delete message sent with transparency
930         public int flagRead = -1; // Message status read/unread
931 
Msg(long id, int type, int threadId, int readFlag)932         Msg(long id, int type, int threadId, int readFlag) {
933             this.id = id;
934             this.type = type;
935             this.threadId = threadId;
936             this.flagRead = readFlag;
937         }
938 
Msg(long id, long folderId, int readFlag)939         Msg(long id, long folderId, int readFlag) {
940             this.id = id;
941             this.folderId = folderId;
942             this.flagRead = readFlag;
943         }
944 
945         /* Eclipse generated hashCode() and equals() to make
946          * hashMap lookup work independent of whether the obj
947          * is used for email or SMS/MMS and whether or not the
948          * oldFolder is set. */
949         @Override
hashCode()950         public int hashCode() {
951             final int prime = 31;
952             int result = 1;
953             result = prime * result + (int) (id ^ (id >>> 32));
954             return result;
955         }
956 
957         @Override
equals(Object obj)958         public boolean equals(Object obj) {
959             if (this == obj) {
960                 return true;
961             }
962             if (obj == null) {
963                 return false;
964             }
965             if (!(obj instanceof Msg)) {
966                 return false;
967             }
968             Msg other = (Msg) obj;
969             if (id != other.id) {
970                 return false;
971             }
972             return true;
973         }
974     }
975 
976     private Map<Long, Msg> mMsgListSms = null;
977 
978     private Map<Long, Msg> mMsgListMms = null;
979 
980     private Map<Long, Msg> mMsgListMsg = null;
981 
982     private Map<String, BluetoothMapConvoContactElement> mContactList = null;
983 
setNotificationRegistration(int notificationStatus)984     public int setNotificationRegistration(int notificationStatus) throws RemoteException {
985         // Forward the request to the MNS thread as a message - including the MAS instance ID.
986         Log.d(TAG, "setNotificationRegistration() enter");
987         if (mMnsClient == null) {
988             return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
989         }
990         Handler mns = mMnsClient.getMessageHandler();
991         if (mns != null) {
992             Message msg = mns.obtainMessage();
993             if (mMnsClient.isValidMnsRecord()) {
994                 msg.what = BluetoothMnsObexClient.MSG_MNS_NOTIFICATION_REGISTRATION;
995             } else {
996                 // Trigger SDP Search and notificaiton registration , if SDP record not found.
997                 msg.what = BluetoothMnsObexClient.MSG_MNS_SDP_SEARCH_REGISTRATION;
998                 if (mMnsClient.mMnsLstRegRqst != null
999                         && (mMnsClient.mMnsLstRegRqst.isSearchInProgress())) {
1000                     /*  1. Disallow next Notification ON Request :
1001                      *     - Respond "Service Unavailable" as SDP Search and last notification
1002                      *       registration ON request is already InProgress.
1003                      *     - Next notification ON Request will be allowed ONLY after search
1004                      *       and connect for last saved request [Replied with OK ] is processed.
1005                      */
1006                     if (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) {
1007                         return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
1008                     } else {
1009                         /*  2. Allow next Notification OFF Request:
1010                          *    - Keep the SDP search still in progress.
1011                          *    - Disconnect and Deregister the contentObserver.
1012                          */
1013                         msg.what = BluetoothMnsObexClient.MSG_MNS_NOTIFICATION_REGISTRATION;
1014                     }
1015                 }
1016             }
1017             msg.arg1 = mMasId;
1018             msg.arg2 = notificationStatus;
1019             mns.sendMessageDelayed(msg, 10); // Send message without forcing a context switch
1020             /* Some devices - e.g. PTS needs to get the unregister confirm before we actually
1021              * disconnect the MNS. */
1022             Log.d(TAG, "setNotificationRegistration() send : " + msg.what + " to MNS ");
1023             return ResponseCodes.OBEX_HTTP_OK;
1024         } else {
1025             // This should not happen except at shutdown.
1026             Log.d(TAG, "setNotificationRegistration() Unable to send registration request");
1027             return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
1028         }
1029     }
1030 
eventMaskContainsContacts(long mask)1031     boolean eventMaskContainsContacts(long mask) {
1032         return sendEventParticipantPresenceChanged(mask);
1033     }
1034 
eventMaskContainsCovo(long mask)1035     boolean eventMaskContainsCovo(long mask) {
1036         return (sendEventConversationChanged(mask) || sendEventParticipantChatstateChanged(mask));
1037     }
1038 
1039     /* Overwrite the existing notification filter. Will register/deregister observers for
1040      * the Contacts and Conversation table as needed. We keep the message observer
1041      * at all times. */
1042     /*package*/
setNotificationFilter(long newFilter)1043     synchronized void setNotificationFilter(long newFilter) {
1044         long oldFilter = mEventFilter;
1045         mEventFilter = newFilter;
1046         /* Contacts */
1047         if (!eventMaskContainsContacts(oldFilter) && eventMaskContainsContacts(newFilter)) {
1048             // TODO:
1049             // Enable the observer
1050             // Reset the contacts list
1051         }
1052         /* Conversations */
1053         if (!eventMaskContainsCovo(oldFilter) && eventMaskContainsCovo(newFilter)) {
1054             // TODO:
1055             // Enable the observer
1056             // Reset the conversations list
1057         }
1058     }
1059 
registerObserver()1060     public void registerObserver() throws RemoteException {
1061         Log.v(TAG, "registerObserver");
1062 
1063         if (mObserverRegistered) {
1064             return;
1065         }
1066 
1067         if (mAccount != null) {
1068 
1069             mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority);
1070             if (mProviderClient == null) {
1071                 throw new RemoteException("Failed to acquire provider for " + mAuthority);
1072             }
1073             mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
1074 
1075             // If there is a change in the database before we init the lists we will be sending
1076             // loads of events - hence init before register.
1077             if (mAccount.getType() == TYPE.IM) {
1078                 // Further add contact list tracking
1079                 initContactsList();
1080             }
1081         }
1082         // If there is a change in the database before we init the lists we will be sending
1083         // loads of events - hence init before register.
1084         initMsgList();
1085 
1086         /* Use MmsSms Uri since the Sms Uri is not notified on deletes */
1087         if (mEnableSmsMms) {
1088             // this is sms/mms
1089             mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver);
1090             mObserverRegistered = true;
1091         }
1092 
1093         if (mAccount != null) {
1094             /* For URI's without account ID */
1095             Uri uri =
1096                     Uri.parse(
1097                             mAccount.mBase_uri_no_account
1098                                     + "/"
1099                                     + BluetoothMapContract.TABLE_MESSAGE);
1100             Log.d(TAG, "Registering observer for: " + uri);
1101             mResolver.registerContentObserver(uri, true, mObserver);
1102 
1103             /* For URI's with account ID - is handled the same way as without ID, but is
1104              * only triggered for MAS instances with matching account ID. */
1105             uri = Uri.parse(mAccount.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE);
1106             Log.d(TAG, "Registering observer for: " + uri);
1107             mResolver.registerContentObserver(uri, true, mObserver);
1108 
1109             if (mAccount.getType() == TYPE.IM) {
1110 
1111                 uri =
1112                         Uri.parse(
1113                                 mAccount.mBase_uri_no_account
1114                                         + "/"
1115                                         + BluetoothMapContract.TABLE_CONVOCONTACT);
1116                 Log.d(TAG, "Registering observer for: " + uri);
1117                 mResolver.registerContentObserver(uri, true, mObserver);
1118 
1119                 /* For URI's with account ID - is handled the same way as without ID, but is
1120                  * only triggered for MAS instances with matching account ID. */
1121                 uri = Uri.parse(mAccount.mBase_uri + "/" + BluetoothMapContract.TABLE_CONVOCONTACT);
1122                 Log.d(TAG, "Registering observer for: " + uri);
1123                 mResolver.registerContentObserver(uri, true, mObserver);
1124             }
1125 
1126             mObserverRegistered = true;
1127         }
1128     }
1129 
unregisterObserver()1130     public void unregisterObserver() {
1131         Log.v(TAG, "unregisterObserver");
1132         mResolver.unregisterContentObserver(mObserver);
1133         mObserverRegistered = false;
1134         if (mProviderClient != null) {
1135             mProviderClient.close();
1136             mProviderClient = null;
1137         }
1138     }
1139 
1140     /**
1141      * Per design it is only possible to call the refreshXxxx functions sequentially, hence it is
1142      * safe to modify mTransmitEvents without synchronization.
1143      */
refreshFolderVersionCounter()1144     /* package */ void refreshFolderVersionCounter() {
1145         if (mObserverRegistered) {
1146             // As we have observers, we already keep the counter up-to-date.
1147             return;
1148         }
1149         /* We need to perform the same functionality, as when we receive a notification change,
1150         hence we:
1151          - disable the event transmission
1152          - triggers the code for updates
1153          - enable the event transmission */
1154         mTransmitEvents = false;
1155         try {
1156             if (mEnableSmsMms) {
1157                 handleMsgListChangesSms();
1158                 handleMsgListChangesMms();
1159             }
1160             if (mAccount != null) {
1161                 try {
1162                     handleMsgListChangesMsg(mMessageUri);
1163                 } catch (RemoteException e) {
1164                     ContentProfileErrorReportUtils.report(
1165                             BluetoothProfile.MAP,
1166                             BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
1167                             BluetoothStatsLog
1168                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
1169                             7);
1170                     Log.e(
1171                             TAG,
1172                             "Unable to update FolderVersionCounter. - Not fatal, but can cause"
1173                                     + " undesirable user experience!",
1174                             e);
1175                 }
1176             }
1177         } finally {
1178             // Ensure we always enable events again
1179             mTransmitEvents = true;
1180         }
1181     }
1182 
refreshConvoListVersionCounter()1183     /* package */ void refreshConvoListVersionCounter() {
1184         if (mObserverRegistered) {
1185             // As we have observers, we already keep the counter up-to-date.
1186             return;
1187         }
1188         /* We need to perform the same functionality, as when we receive a notification change,
1189         hence we:
1190          - disable event transmission
1191          - triggers the code for updates
1192          - enable event transmission */
1193         mTransmitEvents = false;
1194         try {
1195             if ((mAccount != null) && (mContactUri != null)) {
1196                 handleContactListChanges(mContactUri);
1197             }
1198         } finally {
1199             // Ensure we always enable events again
1200             mTransmitEvents = true;
1201         }
1202     }
1203 
1204     @VisibleForTesting
sendEvent(Event evt)1205     void sendEvent(Event evt) {
1206 
1207         if (!mTransmitEvents) {
1208             Log.v(TAG, "mTransmitEvents == false - don't send event.");
1209             return;
1210         }
1211 
1212         Log.d(
1213                 TAG,
1214                 "sendEvent: "
1215                         + evt.eventType
1216                         + " "
1217                         + evt.handle
1218                         + " "
1219                         + evt.folder
1220                         + " "
1221                         + evt.oldFolder
1222                         + " "
1223                         + evt.msgType
1224                         + " "
1225                         + evt.datetime
1226                         + " "
1227                         + evt.subject
1228                         + " "
1229                         + evt.senderName
1230                         + " "
1231                         + evt.priority);
1232 
1233         if (mMnsClient == null || !mMnsClient.isConnected()) {
1234             Log.d(TAG, "sendEvent: No MNS client registered or connected- don't send event");
1235             return;
1236         }
1237 
1238         /* Enable use of the cache for checking the filter */
1239         long eventFilter = mEventFilter;
1240 
1241         /* This should have been a switch on the string, but it is not allowed in Java 1.6 */
1242         /* WARNING: Here we do pointer compare for the string to speed up things, that is.
1243          * HENCE: always use the EVENT_TYPE_"defines" */
1244         if (Objects.equals(evt.eventType, EVENT_TYPE_NEW)) {
1245             if (!sendEventNewMessage(eventFilter)) {
1246                 Log.d(TAG, "Skip sending event of type: " + evt.eventType);
1247                 return;
1248             }
1249         } else if (Objects.equals(evt.eventType, EVENT_TYPE_DELETE)) {
1250             if (!sendEventMessageDeleted(eventFilter)) {
1251                 Log.d(TAG, "Skip sending event of type: " + evt.eventType);
1252                 return;
1253             }
1254         } else if (Objects.equals(evt.eventType, EVENT_TYPE_REMOVED)) {
1255             if (!sendEventMessageRemoved(eventFilter)) {
1256                 Log.d(TAG, "Skip sending event of type: " + evt.eventType);
1257                 return;
1258             }
1259         } else if (Objects.equals(evt.eventType, EVENT_TYPE_SHIFT)) {
1260             if (!sendEventMessageShift(eventFilter)) {
1261                 Log.d(TAG, "Skip sending event of type: " + evt.eventType);
1262                 return;
1263             }
1264         } else if (Objects.equals(evt.eventType, EVENT_TYPE_DELEVERY_SUCCESS)) {
1265             if (!sendEventDeliverySuccess(eventFilter)) {
1266                 Log.d(TAG, "Skip sending event of type: " + evt.eventType);
1267                 return;
1268             }
1269         } else if (Objects.equals(evt.eventType, EVENT_TYPE_SENDING_SUCCESS)) {
1270             if (!sendEventSendingSuccess(eventFilter)) {
1271                 Log.d(TAG, "Skip sending event of type: " + evt.eventType);
1272                 return;
1273             }
1274         } else if (Objects.equals(evt.eventType, EVENT_TYPE_SENDING_FAILURE)) {
1275             if (!sendEventSendingFailed(eventFilter)) {
1276                 Log.d(TAG, "Skip sending event of type: " + evt.eventType);
1277                 return;
1278             }
1279         } else if (Objects.equals(evt.eventType, EVENT_TYPE_DELIVERY_FAILURE)) {
1280             if (!sendEventDeliveryFailed(eventFilter)) {
1281                 Log.d(TAG, "Skip sending event of type: " + evt.eventType);
1282                 return;
1283             }
1284         } else if (Objects.equals(evt.eventType, EVENT_TYPE_READ_STATUS)) {
1285             if (!sendEventReadStatusChanged(eventFilter)) {
1286                 Log.d(TAG, "Skip sending event of type: " + evt.eventType);
1287                 return;
1288             }
1289         } else if (Objects.equals(evt.eventType, EVENT_TYPE_CONVERSATION)) {
1290             if (!sendEventConversationChanged(eventFilter)) {
1291                 Log.d(TAG, "Skip sending event of type: " + evt.eventType);
1292                 return;
1293             }
1294         } else if (Objects.equals(evt.eventType, EVENT_TYPE_PRESENCE)) {
1295             if (!sendEventParticipantPresenceChanged(eventFilter)) {
1296                 Log.d(TAG, "Skip sending event of type: " + evt.eventType);
1297                 return;
1298             }
1299         } else if (Objects.equals(evt.eventType, EVENT_TYPE_CHAT_STATE)) {
1300             if (!sendEventParticipantChatstateChanged(eventFilter)) {
1301                 Log.d(TAG, "Skip sending event of type: " + evt.eventType);
1302                 return;
1303             }
1304         }
1305 
1306         try {
1307             mMnsClient.sendEvent(evt.encode(), mMasId);
1308         } catch (UnsupportedEncodingException ex) {
1309             ContentProfileErrorReportUtils.report(
1310                     BluetoothProfile.MAP,
1311                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
1312                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
1313                     8);
1314             /* do nothing */
1315             Log.w(TAG, "Exception encoding sendEvent response", ex);
1316         }
1317     }
1318 
1319     @VisibleForTesting
initMsgList()1320     void initMsgList() throws RemoteException {
1321         Log.v(TAG, "initMsgList");
1322         UserManager manager = mContext.getSystemService(UserManager.class);
1323         if (manager == null || !manager.isUserUnlocked()) {
1324             return;
1325         }
1326 
1327         if (mEnableSmsMms) {
1328             HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();
1329 
1330             Cursor c;
1331             try {
1332                 c =
1333                         BluetoothMethodProxy.getInstance()
1334                                 .contentResolverQuery(
1335                                         mResolver,
1336                                         Sms.CONTENT_URI,
1337                                         SMS_PROJECTION_SHORT,
1338                                         null,
1339                                         null,
1340                                         null);
1341             } catch (SQLiteException e) {
1342                 ContentProfileErrorReportUtils.report(
1343                         BluetoothProfile.MAP,
1344                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
1345                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
1346                         9);
1347                 Log.e(TAG, "Failed to initialize the list of messages: " + e.toString());
1348                 return;
1349             }
1350 
1351             try {
1352                 if (c != null && c.moveToFirst()) {
1353                     do {
1354                         long id = c.getLong(c.getColumnIndex(Sms._ID));
1355                         int type = c.getInt(c.getColumnIndex(Sms.TYPE));
1356                         int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
1357                         int read = c.getInt(c.getColumnIndex(Sms.READ));
1358 
1359                         Msg msg = new Msg(id, type, threadId, read);
1360                         msgListSms.put(id, msg);
1361                     } while (c.moveToNext());
1362                 }
1363             } finally {
1364                 if (c != null) {
1365                     c.close();
1366                 }
1367             }
1368 
1369             synchronized (getMsgListSms()) {
1370                 getMsgListSms().clear();
1371                 setMsgListSms(msgListSms, true); // Set initial folder version counter
1372             }
1373 
1374             HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();
1375 
1376             c =
1377                     BluetoothMethodProxy.getInstance()
1378                             .contentResolverQuery(
1379                                     mResolver,
1380                                     Mms.CONTENT_URI,
1381                                     MMS_PROJECTION_SHORT,
1382                                     null,
1383                                     null,
1384                                     null);
1385             try {
1386                 if (c != null && c.moveToFirst()) {
1387                     do {
1388                         long id = c.getLong(c.getColumnIndex(Mms._ID));
1389                         int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
1390                         int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
1391                         int read = c.getInt(c.getColumnIndex(Mms.READ));
1392 
1393                         Msg msg = new Msg(id, type, threadId, read);
1394                         msgListMms.put(id, msg);
1395                     } while (c.moveToNext());
1396                 }
1397             } finally {
1398                 if (c != null) {
1399                     c.close();
1400                 }
1401             }
1402 
1403             synchronized (getMsgListMms()) {
1404                 getMsgListMms().clear();
1405                 setMsgListMms(msgListMms, true); // Set initial folder version counter
1406             }
1407         }
1408 
1409         if (mAccount != null) {
1410             HashMap<Long, Msg> msgList = new HashMap<Long, Msg>();
1411             Uri uri = mMessageUri;
1412             Cursor c = mProviderClient.query(uri, MSG_PROJECTION_SHORT, null, null, null);
1413 
1414             try {
1415                 if (c != null && c.moveToFirst()) {
1416                     do {
1417                         long id = c.getLong(c.getColumnIndex(MessageColumns._ID));
1418                         long folderId =
1419                                 c.getInt(
1420                                         c.getColumnIndex(
1421                                                 BluetoothMapContract.MessageColumns.FOLDER_ID));
1422                         int readFlag =
1423                                 c.getInt(
1424                                         c.getColumnIndex(
1425                                                 BluetoothMapContract.MessageColumns.FLAG_READ));
1426                         Msg msg = new Msg(id, folderId, readFlag);
1427                         msgList.put(id, msg);
1428                     } while (c.moveToNext());
1429                 }
1430             } finally {
1431                 if (c != null) {
1432                     c.close();
1433                 }
1434             }
1435 
1436             synchronized (getMsgListMsg()) {
1437                 getMsgListMsg().clear();
1438                 setMsgListMsg(msgList, true);
1439             }
1440         }
1441     }
1442 
1443     @VisibleForTesting
initContactsList()1444     void initContactsList() throws RemoteException {
1445         Log.v(TAG, "initContactsList");
1446         if (mContactUri == null) {
1447             Log.d(TAG, "initContactsList() no mContactUri - nothing to init");
1448             return;
1449         }
1450         Uri uri = mContactUri;
1451         Cursor c =
1452                 mProviderClient.query(
1453                         uri,
1454                         BluetoothMapContract.BT_CONTACT_CHATSTATE_PRESENCE_PROJECTION,
1455                         null,
1456                         null,
1457                         null);
1458         Map<String, BluetoothMapConvoContactElement> contactList =
1459                 new HashMap<String, BluetoothMapConvoContactElement>();
1460         try {
1461             if (c != null && c.moveToFirst()) {
1462                 ConvoContactInfo cInfo = new ConvoContactInfo();
1463                 cInfo.setConvoColunms(c);
1464                 do {
1465                     long convoId = c.getLong(cInfo.mContactColConvoId);
1466                     if (convoId == 0) {
1467                         continue;
1468                     }
1469                     BluetoothMapUtils.printCursor(c);
1470                     String uci = c.getString(cInfo.mContactColUci);
1471                     String name = c.getString(cInfo.mContactColName);
1472                     String displayName = c.getString(cInfo.mContactColNickname);
1473                     String presenceStatus = c.getString(cInfo.mContactColPresenceText);
1474                     int presenceState = c.getInt(cInfo.mContactColPresenceState);
1475                     long lastActivity = c.getLong(cInfo.mContactColLastActive);
1476                     int chatState = c.getInt(cInfo.mContactColChatState);
1477                     int priority = c.getInt(cInfo.mContactColPriority);
1478                     String btUid = c.getString(cInfo.mContactColBtUid);
1479                     BluetoothMapConvoContactElement contact =
1480                             new BluetoothMapConvoContactElement(
1481                                     uci,
1482                                     name,
1483                                     displayName,
1484                                     presenceStatus,
1485                                     presenceState,
1486                                     lastActivity,
1487                                     chatState,
1488                                     priority,
1489                                     btUid);
1490                     contactList.put(uci, contact);
1491                 } while (c.moveToNext());
1492             }
1493         } finally {
1494             if (c != null) {
1495                 c.close();
1496             }
1497         }
1498         synchronized (getContactList()) {
1499             getContactList().clear();
1500             setContactList(contactList, true);
1501         }
1502     }
1503 
1504     @VisibleForTesting
handleMsgListChangesSms()1505     void handleMsgListChangesSms() {
1506         Log.v(TAG, "handleMsgListChangesSms");
1507 
1508         HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();
1509         boolean listChanged = false;
1510 
1511         Cursor c;
1512         synchronized (getMsgListSms()) {
1513             if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1514                 c =
1515                         BluetoothMethodProxy.getInstance()
1516                                 .contentResolverQuery(
1517                                         mResolver,
1518                                         Sms.CONTENT_URI,
1519                                         SMS_PROJECTION_SHORT,
1520                                         null,
1521                                         null,
1522                                         null);
1523             } else {
1524                 c =
1525                         BluetoothMethodProxy.getInstance()
1526                                 .contentResolverQuery(
1527                                         mResolver,
1528                                         Sms.CONTENT_URI,
1529                                         SMS_PROJECTION_SHORT_EXT,
1530                                         null,
1531                                         null,
1532                                         null);
1533             }
1534             try {
1535                 if (c != null && c.moveToFirst()) {
1536                     do {
1537                         int idIndex = c.getColumnIndexOrThrow(Sms._ID);
1538                         if (c.isNull(idIndex)) {
1539                             Log.w(TAG, "handleMsgListChangesSms, ID is null");
1540                             ContentProfileErrorReportUtils.report(
1541                                     BluetoothProfile.MAP,
1542                                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
1543                                     BluetoothStatsLog
1544                                             .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
1545                                     10);
1546                             continue;
1547                         }
1548                         long id = c.getLong(idIndex);
1549                         int type = c.getInt(c.getColumnIndex(Sms.TYPE));
1550                         int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
1551                         int read = c.getInt(c.getColumnIndex(Sms.READ));
1552 
1553                         Msg msg = getMsgListSms().remove(id);
1554 
1555                         /* We must filter out any actions made by the MCE, hence do not send e.g.
1556                          * a message deleted and/or MessageShift for messages deleted by the MCE. */
1557 
1558                         if (msg == null) {
1559                             /* New message */
1560                             msg = new Msg(id, type, threadId, read);
1561                             msgListSms.put(id, msg);
1562                             Event evt;
1563                             if (mTransmitEvents
1564                                     && // extract contact details only if needed
1565                                     mMapEventReportVersion
1566                                             > BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1567                                 long timestamp = c.getLong(c.getColumnIndex(Sms.DATE));
1568                                 String date = BluetoothMapUtils.getDateTimeString(timestamp);
1569                                 if (Flags.mapLimitNotification()) {
1570                                     if (BluetoothMapUtils.isDateTimeOlderThanDuration(
1571                                             timestamp, NEW_MESSAGE_DURATION_FOR_NOTIFICATION)) {
1572                                         msgListSms.remove(id);
1573                                         continue;
1574                                     }
1575                                 } else {
1576                                     if (BluetoothMapUtils.isDateTimeOlderThanOneYear(timestamp)) {
1577                                         // Skip sending message events older than one year
1578                                         msgListSms.remove(id);
1579                                         continue;
1580                                     }
1581                                 }
1582                                 String subject = c.getString(c.getColumnIndex(Sms.BODY));
1583                                 if (subject == null) {
1584                                     subject = "";
1585                                 }
1586                                 String name = "";
1587                                 String phone = "";
1588                                 if (type == 1) { // inbox
1589                                     phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
1590                                     if (phone != null && !phone.isEmpty()) {
1591                                         name =
1592                                                 BluetoothMapContent.getContactNameFromPhone(
1593                                                         phone, mResolver);
1594                                         if (name == null || name.isEmpty()) {
1595                                             name = phone;
1596                                         }
1597                                     } else {
1598                                         name = phone;
1599                                     }
1600                                 } else {
1601                                     TelephonyManager tm =
1602                                             mContext.getSystemService(TelephonyManager.class);
1603                                     if (tm != null) {
1604                                         phone = tm.getLine1Number();
1605                                         name = phone;
1606                                     }
1607                                 }
1608                                 String priority = "no"; // no priority for sms
1609                                 /* Incoming message from the network */
1610                                 if (mMapEventReportVersion
1611                                         == BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
1612                                     evt =
1613                                             new Event(
1614                                                     EVENT_TYPE_NEW,
1615                                                     id,
1616                                                     getSmsFolderName(type),
1617                                                     mSmsType,
1618                                                     date,
1619                                                     subject,
1620                                                     name,
1621                                                     priority);
1622                                 } else {
1623                                     evt =
1624                                             new Event(
1625                                                     EVENT_TYPE_NEW,
1626                                                     id,
1627                                                     getSmsFolderName(type),
1628                                                     mSmsType,
1629                                                     date,
1630                                                     subject,
1631                                                     name,
1632                                                     priority,
1633                                                     (long) threadId,
1634                                                     null);
1635                                 }
1636                             } else {
1637                                 /* Incoming message from the network */
1638                                 evt =
1639                                         new Event(
1640                                                 EVENT_TYPE_NEW,
1641                                                 id,
1642                                                 getSmsFolderName(type),
1643                                                 null,
1644                                                 mSmsType);
1645                             }
1646                             listChanged = true;
1647                             sendEvent(evt);
1648                         } else {
1649                             /* Existing message */
1650                             if (type != msg.type) {
1651                                 listChanged = true;
1652                                 Log.d(TAG, "new type: " + type + " old type: " + msg.type);
1653                                 String oldFolder = getSmsFolderName(msg.type);
1654                                 String newFolder = getSmsFolderName(type);
1655                                 // Filter out the intermediate outbox steps
1656                                 if (!oldFolder.equalsIgnoreCase(newFolder)) {
1657                                     Event evt =
1658                                             new Event(
1659                                                     EVENT_TYPE_SHIFT,
1660                                                     id,
1661                                                     getSmsFolderName(type),
1662                                                     oldFolder,
1663                                                     mSmsType);
1664                                     sendEvent(evt);
1665                                 }
1666                                 msg.type = type;
1667                             } else if (threadId != msg.threadId) {
1668                                 listChanged = true;
1669                                 Log.d(
1670                                         TAG,
1671                                         "Message delete change: type: "
1672                                                 + type
1673                                                 + " old type: "
1674                                                 + msg.type
1675                                                 + "\n    threadId: "
1676                                                 + threadId
1677                                                 + " old threadId: "
1678                                                 + msg.threadId);
1679                                 if (threadId == DELETED_THREAD_ID) { // Message deleted
1680                                     // TODO:
1681                                     // We shall only use the folder attribute, but can't remember
1682                                     // wether to set it to "deleted" or the name of the folder
1683                                     // from which the message have been deleted.
1684                                     // "old_folder" used only for MessageShift event
1685                                     Event evt =
1686                                             new Event(
1687                                                     EVENT_TYPE_DELETE,
1688                                                     id,
1689                                                     getSmsFolderName(msg.type),
1690                                                     null,
1691                                                     mSmsType);
1692                                     sendEvent(evt);
1693                                     msg.threadId = threadId;
1694                                 } else { // Undelete
1695                                     Event evt =
1696                                             new Event(
1697                                                     EVENT_TYPE_SHIFT,
1698                                                     id,
1699                                                     getSmsFolderName(msg.type),
1700                                                     BluetoothMapContract.FOLDER_NAME_DELETED,
1701                                                     mSmsType);
1702                                     sendEvent(evt);
1703                                     msg.threadId = threadId;
1704                                 }
1705                             }
1706                             if (read != msg.flagRead) {
1707                                 listChanged = true;
1708                                 msg.flagRead = read;
1709                                 if (mMapEventReportVersion
1710                                         > BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1711                                     Event evt =
1712                                             new Event(
1713                                                     EVENT_TYPE_READ_STATUS,
1714                                                     id,
1715                                                     getSmsFolderName(msg.type),
1716                                                     mSmsType);
1717                                     sendEvent(evt);
1718                                 }
1719                             }
1720                             msgListSms.put(id, msg);
1721                         }
1722                     } while (c.moveToNext());
1723                 }
1724             } finally {
1725                 if (c != null) {
1726                     c.close();
1727                 }
1728             }
1729             String eventType = EVENT_TYPE_DELETE;
1730             for (Msg msg : getMsgListSms().values()) {
1731                 // "old_folder" used only for MessageShift event
1732                 if (mMapEventReportVersion >= BluetoothMapUtils.MAP_EVENT_REPORT_V12) {
1733                     eventType = EVENT_TYPE_REMOVED;
1734                     Log.v(TAG, " sent EVENT_TYPE_REMOVED");
1735                 }
1736                 Event evt =
1737                         new Event(eventType, msg.id, getSmsFolderName(msg.type), null, mSmsType);
1738                 sendEvent(evt);
1739                 listChanged = true;
1740             }
1741 
1742             setMsgListSms(msgListSms, listChanged);
1743         }
1744     }
1745 
1746     @VisibleForTesting
handleMsgListChangesMms()1747     void handleMsgListChangesMms() {
1748         Log.v(TAG, "handleMsgListChangesMms");
1749 
1750         HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();
1751         boolean listChanged = false;
1752         Cursor c;
1753         synchronized (getMsgListMms()) {
1754             if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1755                 c =
1756                         BluetoothMethodProxy.getInstance()
1757                                 .contentResolverQuery(
1758                                         mResolver,
1759                                         Mms.CONTENT_URI,
1760                                         MMS_PROJECTION_SHORT,
1761                                         null,
1762                                         null,
1763                                         null);
1764             } else {
1765                 c =
1766                         BluetoothMethodProxy.getInstance()
1767                                 .contentResolverQuery(
1768                                         mResolver,
1769                                         Mms.CONTENT_URI,
1770                                         MMS_PROJECTION_SHORT_EXT,
1771                                         null,
1772                                         null,
1773                                         null);
1774             }
1775 
1776             try {
1777                 if (c != null && c.moveToFirst()) {
1778                     do {
1779                         int idIndex = c.getColumnIndexOrThrow(Mms._ID);
1780                         if (c.isNull(idIndex)) {
1781                             Log.w(TAG, "handleMsgListChangesMms, ID is null");
1782                             ContentProfileErrorReportUtils.report(
1783                                     BluetoothProfile.MAP,
1784                                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
1785                                     BluetoothStatsLog
1786                                             .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
1787                                     11);
1788                             continue;
1789                         }
1790                         long id = c.getLong(idIndex);
1791                         int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
1792                         int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE));
1793                         int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
1794                         // TODO: Go through code to see if we have an issue with mismatch in types
1795                         //       for threadId. Seems to be a long in DB??
1796                         int read = c.getInt(c.getColumnIndex(Mms.READ));
1797 
1798                         Msg msg = getMsgListMms().remove(id);
1799 
1800                         /* We must filter out any actions made by the MCE, hence do not send
1801                          * e.g. a message deleted and/or MessageShift for messages deleted by the
1802                          * MCE.*/
1803 
1804                         if (msg == null) {
1805                             /* New message - only notify on retrieve conf */
1806                             if (getMmsFolderName(type)
1807                                             .equalsIgnoreCase(
1808                                                     BluetoothMapContract.FOLDER_NAME_INBOX)
1809                                     && mtype != MESSAGE_TYPE_RETRIEVE_CONF) {
1810                                 continue;
1811                             }
1812                             msg = new Msg(id, type, threadId, read);
1813                             msgListMms.put(id, msg);
1814                             Event evt;
1815                             if (mTransmitEvents
1816                                     && // extract contact details only if needed
1817                                     mMapEventReportVersion
1818                                             != BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1819                                 // MMS date field is in seconds
1820                                 long timestamp =
1821                                         TimeUnit.SECONDS.toMillis(
1822                                                 c.getLong(c.getColumnIndex(Mms.DATE)));
1823                                 String date = BluetoothMapUtils.getDateTimeString(timestamp);
1824                                 if (Flags.mapLimitNotification()) {
1825                                     if (BluetoothMapUtils.isDateTimeOlderThanDuration(
1826                                             timestamp, NEW_MESSAGE_DURATION_FOR_NOTIFICATION)) {
1827                                         msgListMms.remove(id);
1828                                         continue;
1829                                     }
1830                                 } else {
1831                                     if (BluetoothMapUtils.isDateTimeOlderThanOneYear(timestamp)) {
1832                                         // Skip sending new message events older than one year
1833                                         msgListMms.remove(id);
1834                                         continue;
1835                                     }
1836                                 }
1837 
1838                                 String subject = c.getString(c.getColumnIndex(Mms.SUBJECT));
1839                                 if (subject == null || subject.length() == 0) {
1840                                     /* Get subject from mms text body parts - if any exists */
1841                                     subject = BluetoothMapContent.getTextPartsMms(mResolver, id);
1842                                     if (subject == null) {
1843                                         subject = "";
1844                                     }
1845                                 }
1846                                 int tmpPri = c.getInt(c.getColumnIndex(Mms.PRIORITY));
1847                                 Log.d(
1848                                         TAG,
1849                                         "TEMP handleMsgListChangesMms, "
1850                                                 + "newMessage 'read' state: "
1851                                                 + read
1852                                                 + "priority: "
1853                                                 + tmpPri);
1854 
1855                                 String address =
1856                                         BluetoothMapContent.getAddressMms(
1857                                                 mResolver, id, BluetoothMapContent.MMS_FROM);
1858                                 if (address == null) {
1859                                     address = "";
1860                                 }
1861 
1862                                 String priority = "no";
1863                                 if (tmpPri == PduHeaders.PRIORITY_HIGH) {
1864                                     priority = "yes";
1865                                 }
1866 
1867                                 /* Incoming message from the network */
1868                                 if (mMapEventReportVersion
1869                                         == BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
1870                                     evt =
1871                                             new Event(
1872                                                     EVENT_TYPE_NEW,
1873                                                     id,
1874                                                     getMmsFolderName(type),
1875                                                     TYPE.MMS,
1876                                                     date,
1877                                                     subject,
1878                                                     address,
1879                                                     priority);
1880                                 } else {
1881                                     evt =
1882                                             new Event(
1883                                                     EVENT_TYPE_NEW,
1884                                                     id,
1885                                                     getMmsFolderName(type),
1886                                                     TYPE.MMS,
1887                                                     date,
1888                                                     subject,
1889                                                     address,
1890                                                     priority,
1891                                                     (long) threadId,
1892                                                     null);
1893                                 }
1894 
1895                             } else {
1896                                 /* Incoming message from the network */
1897                                 evt =
1898                                         new Event(
1899                                                 EVENT_TYPE_NEW,
1900                                                 id,
1901                                                 getMmsFolderName(type),
1902                                                 null,
1903                                                 TYPE.MMS);
1904                             }
1905                             listChanged = true;
1906 
1907                             sendEvent(evt);
1908                         } else {
1909                             /* Existing message */
1910                             if (type != msg.type) {
1911                                 Log.d(TAG, "new type: " + type + " old type: " + msg.type);
1912                                 Event evt;
1913                                 listChanged = true;
1914                                 if (!msg.localInitiatedSend) {
1915                                     // Only send events about local initiated changes
1916                                     evt =
1917                                             new Event(
1918                                                     EVENT_TYPE_SHIFT,
1919                                                     id,
1920                                                     getMmsFolderName(type),
1921                                                     getMmsFolderName(msg.type),
1922                                                     TYPE.MMS);
1923                                     sendEvent(evt);
1924                                 }
1925                                 msg.type = type;
1926 
1927                                 if (getMmsFolderName(type)
1928                                                 .equalsIgnoreCase(
1929                                                         BluetoothMapContract.FOLDER_NAME_SENT)
1930                                         && msg.localInitiatedSend) {
1931                                     // Stop tracking changes for this message
1932                                     msg.localInitiatedSend = false;
1933                                     evt =
1934                                             new Event(
1935                                                     EVENT_TYPE_SENDING_SUCCESS,
1936                                                     id,
1937                                                     getMmsFolderName(type),
1938                                                     null,
1939                                                     TYPE.MMS);
1940                                     sendEvent(evt);
1941                                 }
1942                             } else if (threadId != msg.threadId) {
1943                                 Log.d(
1944                                         TAG,
1945                                         "Message delete change: type: "
1946                                                 + type
1947                                                 + " old type: "
1948                                                 + msg.type
1949                                                 + "\n    threadId: "
1950                                                 + threadId
1951                                                 + " old threadId: "
1952                                                 + msg.threadId);
1953                                 listChanged = true;
1954                                 if (threadId == DELETED_THREAD_ID) { // Message deleted
1955                                     // "old_folder" used only for MessageShift event
1956                                     Event evt =
1957                                             new Event(
1958                                                     EVENT_TYPE_DELETE,
1959                                                     id,
1960                                                     getMmsFolderName(msg.type),
1961                                                     null,
1962                                                     TYPE.MMS);
1963                                     sendEvent(evt);
1964                                     msg.threadId = threadId;
1965                                 } else { // Undelete
1966                                     Event evt =
1967                                             new Event(
1968                                                     EVENT_TYPE_SHIFT,
1969                                                     id,
1970                                                     getMmsFolderName(msg.type),
1971                                                     BluetoothMapContract.FOLDER_NAME_DELETED,
1972                                                     TYPE.MMS);
1973                                     sendEvent(evt);
1974                                     msg.threadId = threadId;
1975                                 }
1976                             }
1977                             if (read != msg.flagRead) {
1978                                 listChanged = true;
1979                                 msg.flagRead = read;
1980                                 if (mMapEventReportVersion
1981                                         > BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1982                                     Event evt =
1983                                             new Event(
1984                                                     EVENT_TYPE_READ_STATUS,
1985                                                     id,
1986                                                     getMmsFolderName(msg.type),
1987                                                     TYPE.MMS);
1988                                     sendEvent(evt);
1989                                 }
1990                             }
1991                             msgListMms.put(id, msg);
1992                         }
1993                     } while (c.moveToNext());
1994                 }
1995             } finally {
1996                 if (c != null) {
1997                     c.close();
1998                 }
1999             }
2000             for (Msg msg : getMsgListMms().values()) {
2001                 // "old_folder" used only for MessageShift event
2002                 Event evt =
2003                         new Event(
2004                                 EVENT_TYPE_DELETE,
2005                                 msg.id,
2006                                 getMmsFolderName(msg.type),
2007                                 null,
2008                                 TYPE.MMS);
2009                 sendEvent(evt);
2010                 listChanged = true;
2011             }
2012             setMsgListMms(msgListMms, listChanged);
2013         }
2014     }
2015 
2016     @VisibleForTesting
handleMsgListChangesMsg(Uri uri)2017     void handleMsgListChangesMsg(Uri uri) throws RemoteException {
2018         Log.v(TAG, "handleMsgListChangesMsg uri: " + uri.toString());
2019 
2020         // TODO: Change observer to handle accountId and message ID if present
2021 
2022         HashMap<Long, Msg> msgList = new HashMap<Long, Msg>();
2023         Cursor c;
2024         boolean listChanged = false;
2025         if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
2026             c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT, null, null, null);
2027         } else if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
2028             c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT_EXT, null, null, null);
2029         } else {
2030             c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT_EXT2, null, null, null);
2031         }
2032         synchronized (getMsgListMsg()) {
2033             try {
2034                 if (c != null && c.moveToFirst()) {
2035                     do {
2036                         long id =
2037                                 c.getLong(
2038                                         c.getColumnIndex(BluetoothMapContract.MessageColumns._ID));
2039                         int folderId =
2040                                 c.getInt(
2041                                         c.getColumnIndex(
2042                                                 BluetoothMapContract.MessageColumns.FOLDER_ID));
2043                         int readFlag =
2044                                 c.getInt(
2045                                         c.getColumnIndex(
2046                                                 BluetoothMapContract.MessageColumns.FLAG_READ));
2047                         Msg msg = getMsgListMsg().remove(id);
2048                         BluetoothMapFolderElement folderElement = mFolders.getFolderById(folderId);
2049                         String newFolder;
2050                         if (folderElement != null) {
2051                             newFolder = folderElement.getFullPath();
2052                         } else {
2053                             // This can happen if a new folder is created while connected
2054                             newFolder = "unknown";
2055                         }
2056                         /* We must filter out any actions made by the MCE, hence do not send e.g.
2057                          * a message deleted and/or MessageShift for messages deleted by the MCE. */
2058                         if (msg == null) {
2059                             listChanged = true;
2060                             /* New message - created with message unread */
2061                             msg = new Msg(id, folderId, 0, readFlag);
2062                             msgList.put(id, msg);
2063                             Event evt;
2064                             /* Incoming message from the network */
2065                             if (mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
2066                                 String date =
2067                                         BluetoothMapUtils.getDateTimeString(
2068                                                 c.getLong(
2069                                                         c.getColumnIndex(
2070                                                                 BluetoothMapContract.MessageColumns
2071                                                                         .DATE)));
2072                                 String subject =
2073                                         c.getString(
2074                                                 c.getColumnIndex(
2075                                                         BluetoothMapContract.MessageColumns
2076                                                                 .SUBJECT));
2077                                 String address =
2078                                         c.getString(
2079                                                 c.getColumnIndex(
2080                                                         BluetoothMapContract.MessageColumns
2081                                                                 .FROM_LIST));
2082                                 String priority = "no";
2083                                 if (c.getInt(
2084                                                 c.getColumnIndex(
2085                                                         BluetoothMapContract.MessageColumns
2086                                                                 .FLAG_HIGH_PRIORITY))
2087                                         == 1) {
2088                                     priority = "yes";
2089                                 }
2090                                 if (mMapEventReportVersion
2091                                         == BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
2092                                     evt =
2093                                             new Event(
2094                                                     EVENT_TYPE_NEW,
2095                                                     id,
2096                                                     newFolder,
2097                                                     mAccount.getType(),
2098                                                     date,
2099                                                     subject,
2100                                                     address,
2101                                                     priority);
2102                                 } else {
2103                                     long threadId =
2104                                             c.getLong(
2105                                                     c.getColumnIndex(
2106                                                             BluetoothMapContract.MessageColumns
2107                                                                     .THREAD_ID));
2108                                     String threadName =
2109                                             c.getString(
2110                                                     c.getColumnIndex(
2111                                                             BluetoothMapContract.MessageColumns
2112                                                                     .THREAD_NAME));
2113                                     evt =
2114                                             new Event(
2115                                                     EVENT_TYPE_NEW,
2116                                                     id,
2117                                                     newFolder,
2118                                                     mAccount.getType(),
2119                                                     date,
2120                                                     subject,
2121                                                     address,
2122                                                     priority,
2123                                                     threadId,
2124                                                     threadName);
2125                                 }
2126                             } else {
2127                                 evt = new Event(EVENT_TYPE_NEW, id, newFolder, null, TYPE.EMAIL);
2128                             }
2129                             sendEvent(evt);
2130                         } else {
2131                             /* Existing message */
2132                             if (folderId != msg.folderId && msg.folderId != -1) {
2133                                 Log.d(
2134                                         TAG,
2135                                         "new folderId: "
2136                                                 + folderId
2137                                                 + " old folderId: "
2138                                                 + msg.folderId);
2139                                 BluetoothMapFolderElement oldFolderElement =
2140                                         mFolders.getFolderById(msg.folderId);
2141                                 String oldFolder;
2142                                 listChanged = true;
2143                                 if (oldFolderElement != null) {
2144                                     oldFolder = oldFolderElement.getFullPath();
2145                                 } else {
2146                                     // This can happen if a new folder is created while connected
2147                                     oldFolder = "unknown";
2148                                 }
2149                                 BluetoothMapFolderElement deletedFolder =
2150                                         mFolders.getFolderByName(
2151                                                 BluetoothMapContract.FOLDER_NAME_DELETED);
2152                                 BluetoothMapFolderElement sentFolder =
2153                                         mFolders.getFolderByName(
2154                                                 BluetoothMapContract.FOLDER_NAME_SENT);
2155                                 /*
2156                                  *  If the folder is now 'deleted', send a deleted-event in stead of
2157                                  *  a shift or if message is sent initiated by MAP Client, then send
2158                                  *  sending-success otherwise send folderShift
2159                                  */
2160                                 if (deletedFolder != null
2161                                         && deletedFolder.getFolderId() == folderId) {
2162                                     // "old_folder" used only for MessageShift event
2163                                     Event evt =
2164                                             new Event(
2165                                                     EVENT_TYPE_DELETE,
2166                                                     msg.id,
2167                                                     oldFolder,
2168                                                     null,
2169                                                     mAccount.getType());
2170                                     sendEvent(evt);
2171                                 } else if (sentFolder != null
2172                                         && sentFolder.getFolderId() == folderId
2173                                         && msg.localInitiatedSend) {
2174                                     if (msg.transparent) {
2175                                         BluetoothMethodProxy.getInstance()
2176                                                 .contentResolverDelete(
2177                                                         mResolver,
2178                                                         ContentUris.withAppendedId(mMessageUri, id),
2179                                                         null,
2180                                                         null);
2181                                     } else {
2182                                         msg.localInitiatedSend = false;
2183                                         Event evt =
2184                                                 new Event(
2185                                                         EVENT_TYPE_SENDING_SUCCESS,
2186                                                         msg.id,
2187                                                         oldFolder,
2188                                                         null,
2189                                                         mAccount.getType());
2190                                         sendEvent(evt);
2191                                     }
2192                                 } else {
2193                                     if (!oldFolder.equalsIgnoreCase("root")) {
2194                                         Event evt =
2195                                                 new Event(
2196                                                         EVENT_TYPE_SHIFT,
2197                                                         id,
2198                                                         newFolder,
2199                                                         oldFolder,
2200                                                         mAccount.getType());
2201                                         sendEvent(evt);
2202                                     }
2203                                 }
2204                                 msg.folderId = folderId;
2205                             }
2206                             if (readFlag != msg.flagRead) {
2207                                 listChanged = true;
2208 
2209                                 if (mMapEventReportVersion
2210                                         > BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
2211                                     Event evt =
2212                                             new Event(
2213                                                     EVENT_TYPE_READ_STATUS,
2214                                                     id,
2215                                                     newFolder,
2216                                                     mAccount.getType());
2217                                     sendEvent(evt);
2218                                     msg.flagRead = readFlag;
2219                                 }
2220                             }
2221 
2222                             msgList.put(id, msg);
2223                         }
2224                     } while (c.moveToNext());
2225                 }
2226             } finally {
2227                 if (c != null) {
2228                     c.close();
2229                 }
2230             }
2231             // For all messages no longer in the database send a delete notification
2232             for (Msg msg : getMsgListMsg().values()) {
2233                 BluetoothMapFolderElement oldFolderElement = mFolders.getFolderById(msg.folderId);
2234                 String oldFolder;
2235                 listChanged = true;
2236                 if (oldFolderElement != null) {
2237                     oldFolder = oldFolderElement.getFullPath();
2238                 } else {
2239                     oldFolder = "unknown";
2240                 }
2241                 /* Some e-mail clients delete the message after sending, and creates a
2242                  * new message in sent. We cannot track the message anymore, hence send both a
2243                  * send success and delete message.
2244                  */
2245                 if (msg.localInitiatedSend) {
2246                     msg.localInitiatedSend = false;
2247                     // If message is send with transparency don't set folder as message is deleted
2248                     if (msg.transparent) {
2249                         oldFolder = null;
2250                     }
2251                     Event evt =
2252                             new Event(
2253                                     EVENT_TYPE_SENDING_SUCCESS,
2254                                     msg.id,
2255                                     oldFolder,
2256                                     null,
2257                                     mAccount.getType());
2258                     sendEvent(evt);
2259                 }
2260                 /* As this message deleted is only send on a real delete - don't set folder.
2261                  *  - only send delete event if message is not sent with transparency
2262                  */
2263                 if (!msg.transparent) {
2264 
2265                     // "old_folder" used only for MessageShift event
2266                     Event evt =
2267                             new Event(
2268                                     EVENT_TYPE_DELETE, msg.id, oldFolder, null, mAccount.getType());
2269                     sendEvent(evt);
2270                 }
2271             }
2272             setMsgListMsg(msgList, listChanged);
2273         }
2274     }
2275 
handleMsgListChanges(Uri uri)2276     private void handleMsgListChanges(Uri uri) {
2277         if (uri.getAuthority().equals(mAuthority)) {
2278             try {
2279                 Log.d(TAG, "handleMsgListChanges: account type = " + mAccount.getType().toString());
2280                 handleMsgListChangesMsg(uri);
2281             } catch (RemoteException e) {
2282                 ContentProfileErrorReportUtils.report(
2283                         BluetoothProfile.MAP,
2284                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
2285                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
2286                         12);
2287                 mMasInstance.restartObexServerSession();
2288                 Log.w(
2289                         TAG,
2290                         "Problems contacting the ContentProvider in mas Instance "
2291                                 + mMasId
2292                                 + " restaring ObexServerSession");
2293             }
2294         }
2295         // TODO: check to see if there could be problem with IM and SMS in one instance
2296         if (mEnableSmsMms) {
2297             handleMsgListChangesSms();
2298             handleMsgListChangesMms();
2299         }
2300     }
2301 
2302     @VisibleForTesting
handleContactListChanges(Uri uri)2303     void handleContactListChanges(Uri uri) {
2304         if (uri.getAuthority().equals(mAuthority)) {
2305             try {
2306                 Log.v(TAG, "handleContactListChanges uri: " + uri.toString());
2307                 Cursor c = null;
2308                 boolean listChanged = false;
2309                 try {
2310                     ConvoContactInfo cInfo = new ConvoContactInfo();
2311 
2312                     if (mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V10
2313                             && mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
2314                         c =
2315                                 mProviderClient.query(
2316                                         mContactUri,
2317                                         BluetoothMapContract
2318                                                 .BT_CONTACT_CHATSTATE_PRESENCE_PROJECTION,
2319                                         null,
2320                                         null,
2321                                         null);
2322                         cInfo.setConvoColunms(c);
2323                     } else {
2324                         Log.v(
2325                                 TAG,
2326                                 "handleContactListChanges MAP version does not"
2327                                         + "support convocontact notifications");
2328                         return;
2329                     }
2330 
2331                     HashMap<String, BluetoothMapConvoContactElement> contactList =
2332                             new HashMap<String, BluetoothMapConvoContactElement>(
2333                                     getContactList().size());
2334 
2335                     synchronized (getContactList()) {
2336                         if (c != null && c.moveToFirst()) {
2337                             do {
2338                                 String uci = c.getString(cInfo.mContactColUci);
2339                                 long convoId = c.getLong(cInfo.mContactColConvoId);
2340                                 if (convoId == 0) {
2341                                     continue;
2342                                 }
2343 
2344                                 BluetoothMapUtils.printCursor(c);
2345 
2346                                 BluetoothMapConvoContactElement contact =
2347                                         getContactList().remove(uci);
2348 
2349                                 /*
2350                                  * We must filter out any actions made by the
2351                                  * MCE, hence do not send e.g. a message deleted
2352                                  * and/or MessageShift for messages deleted by
2353                                  * the MCE.
2354                                  */
2355                                 if (contact == null) {
2356                                     listChanged = true;
2357                                     /*
2358                                      * New contact - added to conversation and
2359                                      * tracked here
2360                                      */
2361                                     if (mMapEventReportVersion
2362                                                     != BluetoothMapUtils.MAP_EVENT_REPORT_V10
2363                                             && mMapEventReportVersion
2364                                                     != BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
2365                                         Event evt;
2366                                         String name = c.getString(cInfo.mContactColName);
2367                                         String displayName = c.getString(cInfo.mContactColNickname);
2368                                         String presenceStatus =
2369                                                 c.getString(cInfo.mContactColPresenceText);
2370                                         int presenceState =
2371                                                 c.getInt(cInfo.mContactColPresenceState);
2372                                         long lastActivity = c.getLong(cInfo.mContactColLastActive);
2373                                         int chatState = c.getInt(cInfo.mContactColChatState);
2374                                         int priority = c.getInt(cInfo.mContactColPriority);
2375                                         String btUid = c.getString(cInfo.mContactColBtUid);
2376 
2377                                         // Get Conversation information for
2378                                         // event
2379                                         //                                        Uri convoUri = Uri
2380                                         //
2381                                         // .parse(mAccount.mBase_uri
2382                                         //                                                        +
2383                                         // "/"
2384                                         //                                                        +
2385                                         // BluetoothMapContract
2386                                         // .TABLE_CONVERSATION);
2387                                         //                                        String whereClause
2388                                         // = "contacts._id = "
2389                                         //                                                + convoId;
2390                                         //                                        Cursor cConvo =
2391                                         // mProviderClient
2392                                         //
2393                                         // .query(convoUri,
2394                                         //
2395                                         // BluetoothMapContract
2396                                         // .BT_CONVERSATION_PROJECTION,
2397                                         //
2398                                         // whereClause, null, null);
2399                                         // TODO: will move out of the loop when merged with CB's
2400                                         // changes make sure to look up col index out side loop
2401                                         String convoName = null;
2402                                         //                                        if (cConvo != null
2403                                         //                                                &&
2404                                         // cConvo.moveToFirst()) {
2405                                         //                                            convoName =
2406                                         // cConvo
2407                                         //
2408                                         // .getString(cConvo
2409                                         //
2410                                         //  .getColumnIndex
2411                                         // (BluetoothMapContract.ConvoContactColumns.NAME));
2412                                         //                                        }
2413 
2414                                         contact =
2415                                                 new BluetoothMapConvoContactElement(
2416                                                         uci,
2417                                                         name,
2418                                                         displayName,
2419                                                         presenceStatus,
2420                                                         presenceState,
2421                                                         lastActivity,
2422                                                         chatState,
2423                                                         priority,
2424                                                         btUid);
2425 
2426                                         contactList.put(uci, contact);
2427 
2428                                         evt =
2429                                                 new Event(
2430                                                         EVENT_TYPE_CONVERSATION,
2431                                                         uci,
2432                                                         mAccount.getType(),
2433                                                         name,
2434                                                         String.valueOf(priority),
2435                                                         BluetoothMapUtils.getDateTimeString(
2436                                                                 lastActivity),
2437                                                         convoId,
2438                                                         convoName,
2439                                                         presenceState,
2440                                                         presenceStatus,
2441                                                         chatState);
2442 
2443                                         sendEvent(evt);
2444                                     }
2445 
2446                                 } else {
2447                                     // Not new - compare updated content
2448                                     //                                    Uri convoUri = Uri
2449                                     //
2450                                     // .parse(mAccount.mBase_uri
2451                                     //                                                    + "/"
2452                                     //                                                    +
2453                                     // BluetoothMapContract.TABLE_CONVERSATION);
2454                                     // TODO: Should be changed to own provider interface name
2455                                     //                                    String whereClause =
2456                                     // "contacts._id = "
2457                                     //                                            + convoId;
2458                                     //                                    Cursor cConvo =
2459                                     // mProviderClient
2460                                     //                                            .query(convoUri,
2461                                     //
2462                                     // BluetoothMapContract
2463                                     // .BT_CONVERSATION_PROJECTION,
2464                                     //
2465                                     // whereClause, null, null);
2466                                     //                                    // TODO: will move out of
2467                                     // the loop when merged with CB's
2468                                     //                                    // changes make sure to
2469                                     // look up col index out side loop
2470                                     String convoName = null;
2471                                     //                                    if (cConvo != null &&
2472                                     // cConvo.moveToFirst()) {
2473                                     //                                        convoName = cConvo
2474                                     //
2475                                     // .getString(cConvo
2476                                     //
2477                                     // .getColumnIndex(BluetoothMapContract
2478                                     // .ConvoContactColumns.NAME));
2479                                     //                                    }
2480 
2481                                     // Check if presence is updated
2482                                     int presenceState = c.getInt(cInfo.mContactColPresenceState);
2483                                     String presenceStatus =
2484                                             c.getString(cInfo.mContactColPresenceText);
2485                                     String currentPresenceStatus = contact.getPresenceStatus();
2486                                     if (contact.getPresenceAvailability() != presenceState
2487                                             || !Objects.equals(
2488                                                     currentPresenceStatus, presenceStatus)) {
2489                                         long lastOnline = c.getLong(cInfo.mContactColLastOnline);
2490                                         contact.setPresenceAvailability(presenceState);
2491                                         contact.setLastActivity(lastOnline);
2492                                         if (currentPresenceStatus != null
2493                                                 && !currentPresenceStatus.equals(presenceStatus)) {
2494                                             contact.setPresenceStatus(presenceStatus);
2495                                         }
2496                                         Event evt =
2497                                                 new Event(
2498                                                         EVENT_TYPE_PRESENCE,
2499                                                         uci,
2500                                                         mAccount.getType(),
2501                                                         contact.getName(),
2502                                                         String.valueOf(contact.getPriority()),
2503                                                         BluetoothMapUtils.getDateTimeString(
2504                                                                 lastOnline),
2505                                                         convoId,
2506                                                         convoName,
2507                                                         presenceState,
2508                                                         presenceStatus,
2509                                                         0);
2510                                         sendEvent(evt);
2511                                     }
2512 
2513                                     // Check if chat state is updated
2514                                     int chatState = c.getInt(cInfo.mContactColChatState);
2515                                     if (contact.getChatState() != chatState) {
2516                                         // Get DB timestamp
2517                                         long lastActivity = c.getLong(cInfo.mContactColLastActive);
2518                                         contact.setLastActivity(lastActivity);
2519                                         contact.setChatState(chatState);
2520                                         Event evt =
2521                                                 new Event(
2522                                                         EVENT_TYPE_CHAT_STATE,
2523                                                         uci,
2524                                                         mAccount.getType(),
2525                                                         contact.getName(),
2526                                                         String.valueOf(contact.getPriority()),
2527                                                         BluetoothMapUtils.getDateTimeString(
2528                                                                 lastActivity),
2529                                                         convoId,
2530                                                         convoName,
2531                                                         0,
2532                                                         null,
2533                                                         chatState);
2534                                         sendEvent(evt);
2535                                     }
2536                                     contactList.put(uci, contact);
2537                                 }
2538                             } while (c.moveToNext());
2539                         }
2540                         if (getContactList().size() > 0) {
2541                             // one or more contacts were deleted, hence the conversation listing
2542                             // version counter should change.
2543                             listChanged = true;
2544                         }
2545                         setContactList(contactList, listChanged);
2546                     } // end synchronized
2547                 } finally {
2548                     if (c != null) {
2549                         c.close();
2550                     }
2551                 }
2552             } catch (RemoteException e) {
2553                 ContentProfileErrorReportUtils.report(
2554                         BluetoothProfile.MAP,
2555                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
2556                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
2557                         13);
2558                 mMasInstance.restartObexServerSession();
2559                 Log.w(
2560                         TAG,
2561                         "Problems contacting the ContentProvider in mas Instance "
2562                                 + mMasId
2563                                 + " restaring ObexServerSession");
2564             }
2565         }
2566         // TODO: conversation contact updates if IM and SMS(MMS in one instance
2567     }
2568 
2569     @VisibleForTesting
setEmailMessageStatusDelete( BluetoothMapFolderElement mCurrentFolder, String uriStr, long handle, int status)2570     boolean setEmailMessageStatusDelete(
2571             BluetoothMapFolderElement mCurrentFolder, String uriStr, long handle, int status) {
2572         boolean res = false;
2573         Uri uri = Uri.parse(uriStr + BluetoothMapContract.TABLE_MESSAGE);
2574 
2575         int updateCount = 0;
2576         ContentValues contentValues = new ContentValues();
2577         BluetoothMapFolderElement deleteFolder =
2578                 mFolders.getFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED);
2579         contentValues.put(BluetoothMapContract.MessageColumns._ID, handle);
2580         synchronized (getMsgListMsg()) {
2581             Msg msg = getMsgListMsg().get(handle);
2582             if (status == BluetoothMapAppParams.STATUS_VALUE_YES) {
2583                 /* Set deleted folder id */
2584                 long folderId = -1;
2585                 if (deleteFolder != null) {
2586                     folderId = deleteFolder.getFolderId();
2587                 }
2588                 contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId);
2589                 updateCount =
2590                         BluetoothMethodProxy.getInstance()
2591                                 .contentResolverUpdate(mResolver, uri, contentValues, null, null);
2592                 /* The race between updating the value in our cached values and the database
2593                  * is handled by the synchronized statement. */
2594                 if (updateCount > 0) {
2595                     res = true;
2596                     if (msg != null) {
2597                         msg.oldFolderId = msg.folderId;
2598                         /* Update the folder ID to avoid triggering an event for MCE
2599                          * initiated actions. */
2600                         msg.folderId = folderId;
2601                     }
2602                     Log.d(TAG, "Deleted MSG: " + handle + " from folderId: " + folderId);
2603                 } else {
2604                     Log.w(
2605                             TAG,
2606                             "Msg: "
2607                                     + handle
2608                                     + " - Set delete status "
2609                                     + status
2610                                     + " failed for folderId "
2611                                     + folderId);
2612                     ContentProfileErrorReportUtils.report(
2613                             BluetoothProfile.MAP,
2614                             BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
2615                             BluetoothStatsLog
2616                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
2617                             14);
2618                 }
2619             } else if (status == BluetoothMapAppParams.STATUS_VALUE_NO) {
2620                 /* Undelete message. move to old folder if we know it,
2621                  * else move to inbox - as dictated by the spec. */
2622                 if (msg != null
2623                         && deleteFolder != null
2624                         && msg.folderId == deleteFolder.getFolderId()) {
2625                     /* Only modify messages in the 'Deleted' folder */
2626                     long folderId = -1;
2627                     BluetoothMapFolderElement inboxFolder =
2628                             mCurrentFolder.getFolderByName(BluetoothMapContract.FOLDER_NAME_INBOX);
2629                     if (msg != null && msg.oldFolderId != -1) {
2630                         folderId = msg.oldFolderId;
2631                     } else {
2632                         if (inboxFolder != null) {
2633                             folderId = inboxFolder.getFolderId();
2634                         }
2635                         Log.d(
2636                                 TAG,
2637                                 "We did not delete the message, hence the old folder "
2638                                         + "is unknown. Moving to inbox.");
2639                     }
2640                     contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId);
2641                     updateCount =
2642                             BluetoothMethodProxy.getInstance()
2643                                     .contentResolverUpdate(
2644                                             mResolver, uri, contentValues, null, null);
2645                     if (updateCount > 0) {
2646                         res = true;
2647                         /* Update the folder ID to avoid triggering an event for MCE
2648                          * initiated actions. */
2649                         /* UPDATE: Actually the BT-Spec. states that an undelete is a move of the
2650                          * message to INBOX - clearified in errata 5591.
2651                          * Therefore we update the cache to INBOX-folderId - to trigger a message
2652                          * shift event to the old-folder. */
2653                         if (inboxFolder != null) {
2654                             msg.folderId = inboxFolder.getFolderId();
2655                         } else {
2656                             msg.folderId = folderId;
2657                         }
2658                     } else {
2659                         Log.d(
2660                                 TAG,
2661                                 "We did not delete the message, hence the old folder "
2662                                         + "is unknown. Moving to inbox.");
2663                     }
2664                 }
2665             }
2666 
2667             Log.v(
2668                     TAG,
2669                     "setEmailMessageStatusDelete: "
2670                             + handle
2671                             + " from "
2672                             + mCurrentFolder.getFolderById(msg.folderId)
2673                             + " status: "
2674                             + status);
2675         }
2676         if (!res) {
2677             Log.w(TAG, "Set delete status " + status + " failed.");
2678             ContentProfileErrorReportUtils.report(
2679                     BluetoothProfile.MAP,
2680                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
2681                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
2682                     15);
2683         }
2684         return res;
2685     }
2686 
updateThreadId(Uri uri, String valueString, long threadId)2687     private void updateThreadId(Uri uri, String valueString, long threadId) {
2688         ContentValues contentValues = new ContentValues();
2689         contentValues.put(valueString, threadId);
2690         BluetoothMethodProxy.getInstance()
2691                 .contentResolverUpdate(mResolver, uri, contentValues, null, null);
2692     }
2693 
2694     @VisibleForTesting
deleteMessageMms(long handle)2695     boolean deleteMessageMms(long handle) {
2696         boolean res = false;
2697         Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
2698         Cursor c =
2699                 BluetoothMethodProxy.getInstance()
2700                         .contentResolverQuery(mResolver, uri, null, null, null, null);
2701         try {
2702             if (c != null && c.moveToFirst()) {
2703                 /* Move to deleted folder, or delete if already in deleted folder */
2704                 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
2705                 if (threadId != DELETED_THREAD_ID) {
2706                     /* Set deleted thread id */
2707                     synchronized (getMsgListMms()) {
2708                         Msg msg = getMsgListMms().get(handle);
2709                         if (msg != null) { // This will always be the case
2710                             msg.threadId = DELETED_THREAD_ID;
2711                         }
2712                     }
2713                     updateThreadId(uri, Mms.THREAD_ID, DELETED_THREAD_ID);
2714                 } else {
2715                     /* Delete from observer message list to avoid delete notifications */
2716                     synchronized (getMsgListMms()) {
2717                         getMsgListMms().remove(handle);
2718                     }
2719                     /* Delete message */
2720                     BluetoothMethodProxy.getInstance()
2721                             .contentResolverDelete(mResolver, uri, null, null);
2722                 }
2723                 res = true;
2724             }
2725         } finally {
2726             if (c != null) {
2727                 c.close();
2728             }
2729         }
2730 
2731         return res;
2732     }
2733 
2734     @VisibleForTesting
unDeleteMessageMms(long handle)2735     boolean unDeleteMessageMms(long handle) {
2736         boolean res = false;
2737         Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
2738         Cursor c =
2739                 BluetoothMethodProxy.getInstance()
2740                         .contentResolverQuery(mResolver, uri, null, null, null, null);
2741         try {
2742             if (c != null && c.moveToFirst()) {
2743                 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
2744                 if (threadId == DELETED_THREAD_ID) {
2745                     /* Restore thread id from address, or if no thread for address
2746                      * create new thread by insert and remove of fake message */
2747                     String address;
2748                     long id = c.getLong(c.getColumnIndex(Mms._ID));
2749                     int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
2750                     if (msgBox == Mms.MESSAGE_BOX_INBOX) {
2751                         address =
2752                                 BluetoothMapContent.getAddressMms(
2753                                         mResolver, id, BluetoothMapContent.MMS_FROM);
2754                     } else {
2755                         address =
2756                                 BluetoothMapContent.getAddressMms(
2757                                         mResolver, id, BluetoothMapContent.MMS_TO);
2758                     }
2759                     Set<String> recipients = new HashSet<String>();
2760                     recipients.addAll(Arrays.asList(address));
2761                     Long oldThreadId =
2762                             BluetoothMethodProxy.getInstance()
2763                                     .telephonyGetOrCreateThreadId(mContext, recipients);
2764                     synchronized (getMsgListMms()) {
2765                         Msg msg = getMsgListMms().get(handle);
2766                         if (msg != null) { // This will always be the case
2767                             msg.threadId = oldThreadId.intValue();
2768                             // Spec. states that undelete shall shift the message to Inbox.
2769                             // Hence we need to trigger a message shift from INBOX to old-folder
2770                             // after undelete.
2771                             // We do this by changing the cached folder value to being inbox - hence
2772                             // the event handler will se the update as the message have been shifted
2773                             // from INBOX to old-folder. (Errata 5591 clearifies this)
2774                             msg.type = Mms.MESSAGE_BOX_INBOX;
2775                         }
2776                     }
2777                     updateThreadId(uri, Mms.THREAD_ID, oldThreadId);
2778                 } else {
2779                     Log.d(
2780                             TAG,
2781                             "Message not in deleted folder: handle "
2782                                     + handle
2783                                     + " threadId "
2784                                     + threadId);
2785                 }
2786                 res = true;
2787             }
2788         } finally {
2789             if (c != null) {
2790                 c.close();
2791             }
2792         }
2793         return res;
2794     }
2795 
2796     @VisibleForTesting
deleteMessageSms(long handle)2797     boolean deleteMessageSms(long handle) {
2798         boolean res = false;
2799         Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
2800         Cursor c =
2801                 BluetoothMethodProxy.getInstance()
2802                         .contentResolverQuery(mResolver, uri, null, null, null, null);
2803         try {
2804             if (c != null && c.moveToFirst()) {
2805                 /* Move to deleted folder, or delete if already in deleted folder */
2806                 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
2807                 if (threadId != DELETED_THREAD_ID) {
2808                     synchronized (getMsgListSms()) {
2809                         Msg msg = getMsgListSms().get(handle);
2810                         if (msg != null) { // This will always be the case
2811                             msg.threadId = DELETED_THREAD_ID;
2812                         }
2813                     }
2814                     /* Set deleted thread id */
2815                     updateThreadId(uri, Sms.THREAD_ID, DELETED_THREAD_ID);
2816                 } else {
2817                     /* Delete from observer message list to avoid delete notifications */
2818                     synchronized (getMsgListSms()) {
2819                         getMsgListSms().remove(handle);
2820                     }
2821                     /* Delete message */
2822                     BluetoothMethodProxy.getInstance()
2823                             .contentResolverDelete(mResolver, uri, null, null);
2824                 }
2825                 res = true;
2826             }
2827         } finally {
2828             if (c != null) {
2829                 c.close();
2830             }
2831         }
2832         return res;
2833     }
2834 
2835     @VisibleForTesting
unDeleteMessageSms(long handle)2836     boolean unDeleteMessageSms(long handle) {
2837         boolean res = false;
2838         Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
2839         Cursor c =
2840                 BluetoothMethodProxy.getInstance()
2841                         .contentResolverQuery(mResolver, uri, null, null, null, null);
2842         try {
2843             if (c != null && c.moveToFirst()) {
2844                 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
2845                 if (threadId == DELETED_THREAD_ID) {
2846                     String address = c.getString(c.getColumnIndex(Sms.ADDRESS));
2847                     Set<String> recipients = new HashSet<String>();
2848                     recipients.addAll(Arrays.asList(address));
2849                     Long oldThreadId =
2850                             BluetoothMethodProxy.getInstance()
2851                                     .telephonyGetOrCreateThreadId(mContext, recipients);
2852                     synchronized (getMsgListSms()) {
2853                         Msg msg = getMsgListSms().get(handle);
2854                         if (msg != null) {
2855                             msg.threadId = oldThreadId.intValue();
2856                             /* This will always be the case
2857                              * The threadId is specified as an int, so it is safe to truncate
2858                              * TODO: Test that this will trigger a message-shift from Inbox
2859                              * to old-folder
2860                              **/
2861                             /* Spec. states that undelete shall shift the message to Inbox.
2862                              * Hence we need to trigger a message shift from INBOX to old-folder
2863                              * after undelete.
2864                              * We do this by changing the cached folder value to being inbox - hence
2865                              * the event handler will se the update as the message have been shifted
2866                              * from INBOX to old-folder. (Errata 5591 clearifies this)
2867                              * */
2868                             msg.type = Sms.MESSAGE_TYPE_INBOX;
2869                         }
2870                     }
2871                     updateThreadId(uri, Sms.THREAD_ID, oldThreadId);
2872                 } else {
2873                     Log.d(
2874                             TAG,
2875                             "Message not in deleted folder: handle "
2876                                     + handle
2877                                     + " threadId "
2878                                     + threadId);
2879                 }
2880                 res = true;
2881             }
2882         } finally {
2883             if (c != null) {
2884                 c.close();
2885             }
2886         }
2887         return res;
2888     }
2889 
2890     /**
2891      * @return true is success
2892      */
setMessageStatusDeleted( long handle, TYPE type, BluetoothMapFolderElement mCurrentFolder, String uriStr, int statusValue)2893     boolean setMessageStatusDeleted(
2894             long handle,
2895             TYPE type,
2896             BluetoothMapFolderElement mCurrentFolder,
2897             String uriStr,
2898             int statusValue) {
2899         boolean res = false;
2900         Log.d(
2901                 TAG,
2902                 "setMessageStatusDeleted: handle "
2903                         + handle
2904                         + " type "
2905                         + type
2906                         + " value "
2907                         + statusValue);
2908 
2909         if (type == TYPE.EMAIL) {
2910             res = setEmailMessageStatusDelete(mCurrentFolder, uriStr, handle, statusValue);
2911         } else if (type == TYPE.IM) {
2912             // TODO: to do when deleting IM message
2913             Log.d(TAG, "setMessageStatusDeleted: IM not handled");
2914         } else {
2915             if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) {
2916                 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
2917                     res = deleteMessageSms(handle);
2918                 } else if (type == TYPE.MMS) {
2919                     res = deleteMessageMms(handle);
2920                 }
2921             } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) {
2922                 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
2923                     res = unDeleteMessageSms(handle);
2924                 } else if (type == TYPE.MMS) {
2925                     res = unDeleteMessageMms(handle);
2926                 }
2927             }
2928         }
2929         return res;
2930     }
2931 
2932     /**
2933      * @return true at success
2934      */
setMessageStatusRead(long handle, TYPE type, String uriStr, int statusValue)2935     boolean setMessageStatusRead(long handle, TYPE type, String uriStr, int statusValue)
2936             throws RemoteException {
2937         int count = 0;
2938 
2939         Log.d(
2940                 TAG,
2941                 "setMessageStatusRead: handle "
2942                         + handle
2943                         + " type "
2944                         + type
2945                         + " value "
2946                         + statusValue);
2947 
2948         /* Approved MAP spec errata 3445 states that read status initiated
2949          * by the MCE shall change the MSE read status. */
2950         if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
2951             Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
2952             ContentValues contentValues = new ContentValues();
2953             contentValues.put(Sms.READ, statusValue);
2954             contentValues.put(Sms.SEEN, statusValue);
2955             String values = contentValues.toString();
2956             Log.d(TAG, " -> SMS Uri: " + uri.toString() + " values " + values);
2957             synchronized (getMsgListSms()) {
2958                 Msg msg = getMsgListSms().get(handle);
2959                 if (msg != null) { // This will always be the case
2960                     msg.flagRead = statusValue;
2961                 }
2962             }
2963             count =
2964                     BluetoothMethodProxy.getInstance()
2965                             .contentResolverUpdate(mResolver, uri, contentValues, null, null);
2966             Log.d(TAG, " -> " + count + " rows updated!");
2967 
2968         } else if (type == TYPE.MMS) {
2969             Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
2970             Log.d(TAG, " -> MMS Uri: " + uri.toString());
2971             ContentValues contentValues = new ContentValues();
2972             contentValues.put(Mms.READ, statusValue);
2973             synchronized (getMsgListMms()) {
2974                 Msg msg = getMsgListMms().get(handle);
2975                 if (msg != null) { // This will always be the case
2976                     msg.flagRead = statusValue;
2977                 }
2978             }
2979             count =
2980                     BluetoothMethodProxy.getInstance()
2981                             .contentResolverUpdate(mResolver, uri, contentValues, null, null);
2982             Log.d(TAG, " -> " + count + " rows updated!");
2983         } else if (type == TYPE.EMAIL || type == TYPE.IM) {
2984             Uri uri = mMessageUri;
2985             ContentValues contentValues = new ContentValues();
2986             contentValues.put(BluetoothMapContract.MessageColumns.FLAG_READ, statusValue);
2987             contentValues.put(BluetoothMapContract.MessageColumns._ID, handle);
2988             synchronized (getMsgListMsg()) {
2989                 Msg msg = getMsgListMsg().get(handle);
2990                 if (msg != null) { // This will always be the case
2991                     msg.flagRead = statusValue;
2992                 }
2993             }
2994             count = mProviderClient.update(uri, contentValues, null, null);
2995         }
2996 
2997         return (count > 0);
2998     }
2999 
3000     @VisibleForTesting
3001     static class PushMsgInfo {
3002         public long id;
3003         public int transparent;
3004         public int retry;
3005         public String phone;
3006         public Uri uri;
3007         public long timestamp;
3008         public int parts;
3009         public int partsSent;
3010         public int partsDelivered;
3011         public boolean resend;
3012         public boolean sendInProgress;
3013         public boolean failedSent; // Set to true if a single part sent fail is received.
3014         public int statusDelivered; // Set to != 0 if a single part deliver fail is received.
3015 
PushMsgInfo(long id, int transparent, int retry, String phone, Uri uri)3016         PushMsgInfo(long id, int transparent, int retry, String phone, Uri uri) {
3017             this.id = id;
3018             this.transparent = transparent;
3019             this.retry = retry;
3020             this.phone = phone;
3021             this.uri = uri;
3022             this.resend = false;
3023             this.sendInProgress = false;
3024             this.failedSent = false;
3025             this.statusDelivered = 0; /* Assume success */
3026             this.timestamp = 0;
3027         }
3028         ;
3029     }
3030 
3031     private Map<Long, PushMsgInfo> mPushMsgList =
3032             Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>());
3033 
3034     /**
3035      * Add an SMS to the given URI.
3036      *
3037      * @param resolver the content resolver to use
3038      * @param uri the URI to add the message to
3039      * @param address the address of the sender
3040      * @param body the body of the message
3041      * @param subject the pseudo-subject of the message
3042      * @param date the timestamp for the message
3043      * @return the URI for the new message
3044      */
addMessageToUri( ContentResolver resolver, Uri uri, String address, String body, String subject, Long date)3045     private static Uri addMessageToUri(
3046             ContentResolver resolver,
3047             Uri uri,
3048             String address,
3049             String body,
3050             String subject,
3051             Long date) {
3052         ContentValues values = new ContentValues(7);
3053         final int statusPending = 32;
3054         final int subId = SubscriptionManager.getDefaultSmsSubscriptionId();
3055         Log.v(TAG, "Telephony addMessageToUri sub id: " + subId);
3056 
3057         values.put(Telephony.Sms.SUBSCRIPTION_ID, subId);
3058         values.put(Telephony.Sms.ADDRESS, address);
3059         if (date != null) {
3060             values.put(Telephony.Sms.DATE, date);
3061         }
3062         values.put(Telephony.Sms.READ, 0);
3063         values.put(Telephony.Sms.SUBJECT, subject);
3064         values.put(Telephony.Sms.BODY, body);
3065         values.put(Telephony.Sms.STATUS, statusPending);
3066         return resolver.insert(uri, values);
3067     }
3068 
pushMessage( BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement, BluetoothMapAppParams ap, String emailBaseUri)3069     public long pushMessage(
3070             BluetoothMapbMessage msg,
3071             BluetoothMapFolderElement folderElement,
3072             BluetoothMapAppParams ap,
3073             String emailBaseUri)
3074             throws IllegalArgumentException, RemoteException, IOException {
3075         Log.d(TAG, "pushMessage");
3076         ArrayList<BluetoothMapbMessage.VCard> recipientList = msg.getRecipients();
3077         int transparent =
3078                 (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER)
3079                         ? 0
3080                         : ap.getTransparent();
3081         int retry = ap.getRetry();
3082         long handle = -1;
3083         long folderId = -1;
3084 
3085         if (recipientList == null) {
3086             if (folderElement.getName().equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT)) {
3087                 BluetoothMapbMessage.VCard empty =
3088                         new BluetoothMapbMessage.VCard("", "", null, null, 0);
3089                 recipientList = new ArrayList<BluetoothMapbMessage.VCard>();
3090                 recipientList.add(empty);
3091                 Log.w(TAG, "Added empty recipient to draft message");
3092                 ContentProfileErrorReportUtils.report(
3093                         BluetoothProfile.MAP,
3094                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3095                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
3096                         16);
3097             } else {
3098                 Log.e(TAG, "Trying to send a message with no recipients");
3099                 ContentProfileErrorReportUtils.report(
3100                         BluetoothProfile.MAP,
3101                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3102                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
3103                         17);
3104                 return -1;
3105             }
3106         }
3107 
3108         if (msg.getType().equals(TYPE.EMAIL)) {
3109             /* Write the message to the database */
3110             String msgBody = ((BluetoothMapbMessageEmail) msg).getEmailBody();
3111 
3112             Log.v(TAG, "pushMessage: message string length = " + msgBody.length());
3113 
3114             String[] messages = msgBody.split("\r\n");
3115             Log.v(TAG, "pushMessage: messages count=" + messages.length);
3116             for (int i = 0; i < messages.length; i++) {
3117                 Log.v(TAG, "part " + i + ": " + messages[i]);
3118             }
3119 
3120             FileOutputStream os = null;
3121             ParcelFileDescriptor fdOut = null;
3122             Uri uriInsert = Uri.parse(emailBaseUri + BluetoothMapContract.TABLE_MESSAGE);
3123             Log.d(
3124                     TAG,
3125                     "pushMessage - uriInsert= "
3126                             + uriInsert.toString()
3127                             + ", intoFolder id="
3128                             + folderElement.getFolderId());
3129 
3130             synchronized (getMsgListMsg()) {
3131                 // Now insert the empty message into folder
3132                 ContentValues values = new ContentValues();
3133                 folderId = folderElement.getFolderId();
3134                 values.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId);
3135                 Uri uriNew = mProviderClient.insert(uriInsert, values);
3136                 Log.d(TAG, "pushMessage - uriNew= " + uriNew.toString());
3137                 handle = Long.parseLong(uriNew.getLastPathSegment());
3138 
3139                 try {
3140                     fdOut = mProviderClient.openFile(uriNew, "w");
3141                     os = new FileOutputStream(fdOut.getFileDescriptor());
3142                     // Write Email to DB
3143                     os.write(msgBody.getBytes(), 0, msgBody.getBytes().length);
3144                 } catch (FileNotFoundException e) {
3145                     ContentProfileErrorReportUtils.report(
3146                             BluetoothProfile.MAP,
3147                             BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3148                             BluetoothStatsLog
3149                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3150                             18);
3151                     Log.w(TAG, e);
3152                     throw (new IOException("Unable to open file stream"));
3153                 } catch (NullPointerException e) {
3154                     ContentProfileErrorReportUtils.report(
3155                             BluetoothProfile.MAP,
3156                             BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3157                             BluetoothStatsLog
3158                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3159                             19);
3160                     Log.w(TAG, e);
3161                     throw (new IllegalArgumentException("Unable to parse message."));
3162                 } finally {
3163                     try {
3164                         if (os != null) {
3165                             os.close();
3166                         }
3167                     } catch (IOException e) {
3168                         ContentProfileErrorReportUtils.report(
3169                                 BluetoothProfile.MAP,
3170                                 BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3171                                 BluetoothStatsLog
3172                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3173                                 20);
3174                         Log.w(TAG, e);
3175                     }
3176                     try {
3177                         if (fdOut != null) {
3178                             fdOut.close();
3179                         }
3180                     } catch (IOException e) {
3181                         ContentProfileErrorReportUtils.report(
3182                                 BluetoothProfile.MAP,
3183                                 BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3184                                 BluetoothStatsLog
3185                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3186                                 21);
3187                         Log.w(TAG, e);
3188                     }
3189                 }
3190 
3191                 /* Extract the data for the inserted message, and store in local mirror, to
3192                  * avoid sending a NewMessage Event. */
3193                 /*TODO: We need to add the new 1.1 parameter as well:-) e.g. read*/
3194                 Msg newMsg = new Msg(handle, folderId, 1); // TODO: Create define for read-state
3195                 newMsg.transparent = transparent == 1;
3196                 if (folderId
3197                         == folderElement
3198                                 .getFolderByName(BluetoothMapContract.FOLDER_NAME_OUTBOX)
3199                                 .getFolderId()) {
3200                     newMsg.localInitiatedSend = true;
3201                 }
3202                 getMsgListMsg().put(handle, newMsg);
3203             }
3204         } else if (msg.getType().equals(TYPE.MMS) && (recipientList.size() > 1)) {
3205             // Group MMS
3206             String folder = folderElement.getName();
3207             ArrayList<String> telNums = new ArrayList<String>();
3208             for (BluetoothMapbMessage.VCard recipient : recipientList) {
3209                 // Only send the message to the top level recipient
3210                 if (recipient.getEnvLevel() == 0) {
3211                     // Only send to recipient's first phone number
3212                     telNums.add(recipient.getFirstPhoneNumber());
3213                 }
3214             }
3215             // Send message if folder is outbox else just store in draft
3216             handle =
3217                     sendMmsMessage(
3218                             folder,
3219                             telNums.toArray(new String[telNums.size()]),
3220                             (BluetoothMapbMessageMime) msg,
3221                             transparent,
3222                             retry);
3223         } else { // type SMS_* (single or mass text) or single MMS
3224             for (BluetoothMapbMessage.VCard recipient : recipientList) {
3225                 // Only send the message to the top level recipient
3226                 if (recipient.getEnvLevel() == 0) {
3227                     /* Only send to first address */
3228                     String phone = recipient.getFirstPhoneNumber();
3229                     String folder = folderElement.getName();
3230                     String msgBody = null;
3231 
3232                     /* If MMS contains text only and the size is less than ten SMS's
3233                      * then convert the MMS to type SMS and then proceed
3234                      */
3235                     if (msg.getType().equals(TYPE.MMS)
3236                             && (((BluetoothMapbMessageMime) msg).getTextOnly())) {
3237                         msgBody = ((BluetoothMapbMessageMime) msg).getMessageAsText();
3238                         SmsManager smsMng = SmsManager.getDefault();
3239                         ArrayList<String> parts = smsMng.divideMessage(msgBody);
3240                         int smsParts = parts.size();
3241                         if (smsParts <= CONVERT_MMS_TO_SMS_PART_COUNT) {
3242                             Log.d(
3243                                     TAG,
3244                                     "pushMessage - converting MMS to SMS, sms parts=" + smsParts);
3245                             msg.setType(mSmsType);
3246                         } else {
3247                             Log.d(
3248                                     TAG,
3249                                     "pushMessage - MMS text only but to big to "
3250                                             + "convert to SMS");
3251                             msgBody = null;
3252                         }
3253                     }
3254 
3255                     if (msg.getType().equals(TYPE.MMS)) {
3256                         /* Send message if folder is outbox else just store in draft*/
3257                         handle =
3258                                 sendMmsMessage(
3259                                         folder,
3260                                         new String[] {phone},
3261                                         (BluetoothMapbMessageMime) msg,
3262                                         transparent,
3263                                         retry);
3264                     } else if (msg.getType().equals(TYPE.SMS_GSM)
3265                             || msg.getType().equals(TYPE.SMS_CDMA)) {
3266                         /* Add the message to the database */
3267                         if (msgBody == null) {
3268                             msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody();
3269                         }
3270 
3271                         if (TextUtils.isEmpty(msgBody)) {
3272                             Log.d(TAG, "PushMsg: Empty msgBody ");
3273                             /* not allowed to push empty message */
3274                             throw new IllegalArgumentException("push EMPTY message: Invalid Body");
3275                         }
3276                         /* We need to lock the SMS list while updating the database,
3277                          * to avoid sending events on MCE initiated operation. */
3278                         Uri contentUri = Uri.parse(Sms.CONTENT_URI + "/" + folder);
3279                         Uri uri;
3280                         synchronized (getMsgListSms()) {
3281                             uri =
3282                                     addMessageToUri(
3283                                             mResolver,
3284                                             contentUri,
3285                                             phone,
3286                                             msgBody,
3287                                             "",
3288                                             System.currentTimeMillis());
3289 
3290                             Log.v(TAG, "Sms.addMessageToUri() returned: " + uri);
3291                             if (uri == null) {
3292                                 Log.d(TAG, "pushMessage - failure on add to uri " + contentUri);
3293                                 return -1;
3294                             }
3295                             Cursor c = mResolver.query(uri, SMS_PROJECTION_SHORT, null, null, null);
3296 
3297                             /* Extract the data for the inserted message, and store in local mirror,
3298                              * to avoid sending a NewMessage Event. */
3299                             try {
3300                                 if (c != null && c.moveToFirst()) {
3301                                     long id = c.getLong(c.getColumnIndex(Sms._ID));
3302                                     int type = c.getInt(c.getColumnIndex(Sms.TYPE));
3303                                     int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
3304                                     int readFlag = c.getInt(c.getColumnIndex(Sms.READ));
3305                                     Log.v(
3306                                             TAG,
3307                                             "add message with id="
3308                                                     + id
3309                                                     + " type="
3310                                                     + type
3311                                                     + " threadId="
3312                                                     + threadId
3313                                                     + " readFlag="
3314                                                     + readFlag
3315                                                     + "to mMsgListSms");
3316                                     Msg newMsg = new Msg(id, type, threadId, readFlag);
3317                                     getMsgListSms().put(id, newMsg);
3318                                     c.close();
3319                                 } else {
3320                                     Log.w(TAG, "Message: " + uri + " no longer exist!");
3321                                     ContentProfileErrorReportUtils.report(
3322                                             BluetoothProfile.MAP,
3323                                             BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3324                                             BluetoothStatsLog
3325                                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
3326                                             22);
3327                                     /* This can only happen, if the message is deleted
3328                                      * just as it is added */
3329                                     return -1;
3330                                 }
3331                             } finally {
3332                                 if (c != null) {
3333                                     c.close();
3334                                 }
3335                             }
3336 
3337                             handle = Long.parseLong(uri.getLastPathSegment());
3338 
3339                             /* Send message if folder is outbox */
3340                             if (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)) {
3341                                 PushMsgInfo msgInfo =
3342                                         new PushMsgInfo(handle, transparent, retry, phone, uri);
3343                                 mPushMsgList.put(handle, msgInfo);
3344                                 sendMessage(msgInfo, msgBody);
3345                                 Log.v(TAG, "sendMessage returned...");
3346                             } /* else just added to draft */
3347 
3348                             /* sendMessage causes the message to be deleted and reinserted,
3349                              * hence we need to lock the list while this is happening. */
3350                         }
3351                     } else {
3352                         Log.d(TAG, "pushMessage - failure on type ");
3353                         return -1;
3354                     }
3355                 }
3356             }
3357         }
3358 
3359         /* If multiple recipients return handle of last */
3360         return handle;
3361     }
3362 
sendMmsMessage( String folder, String[] toAddress, BluetoothMapbMessageMime msg, int transparent, int retry)3363     public long sendMmsMessage(
3364             String folder,
3365             String[] toAddress,
3366             BluetoothMapbMessageMime msg,
3367             int transparent,
3368             int retry) {
3369         /*
3370          *strategy:
3371          *1) parse message into parts
3372          *if folder is outbox/drafts:
3373          *2) push message to draft
3374          *if folder is outbox:
3375          *3) move message to outbox (to trigger the mms app to add msg to pending_messages list)
3376          *4) send intent to mms app in order to wake it up.
3377          *else if folder !outbox:
3378          *1) push message to folder
3379          * */
3380         if (folder != null
3381                 && (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)
3382                         || folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT))) {
3383             long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, toAddress, msg);
3384             /* if invalid handle (-1) then just return the handle
3385              * - else continue sending (if folder is outbox) */
3386             if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle
3387                     && folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)) {
3388                 Uri btMmsUri =
3389                         MmsFileProvider.CONTENT_URI
3390                                 .buildUpon()
3391                                 .appendPath(Long.toString(handle))
3392                                 .build();
3393                 Intent sentIntent = new Intent(ACTION_MESSAGE_SENT);
3394                 // TODO: update the mmsMsgList <- done in pushMmsToFolder() but check
3395                 sentIntent.setType("message/" + Long.toString(handle));
3396                 sentIntent.putExtra(EXTRA_MESSAGE_SENT_MSG_TYPE, TYPE.MMS.ordinal());
3397                 sentIntent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, handle); // needed for notification
3398                 sentIntent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, transparent);
3399                 sentIntent.putExtra(EXTRA_MESSAGE_SENT_RETRY, retry);
3400                 // sentIntent.setDataAndNormalize(btMmsUri);
3401                 PendingIntent pendingSendIntent =
3402                         PendingIntent.getBroadcast(
3403                                 mContext, 0, sentIntent, PendingIntent.FLAG_IMMUTABLE);
3404                 SmsManager.getDefault()
3405                         .sendMultimediaMessage(
3406                                 mContext,
3407                                 btMmsUri,
3408                                 null /*locationUrl*/,
3409                                 null /*configOverrides*/,
3410                                 pendingSendIntent);
3411             }
3412             return handle;
3413         } else {
3414             /* not allowed to push mms to anything but outbox/draft */
3415             throw new IllegalArgumentException(
3416                     "Cannot push message to other " + "folders than outbox/draft");
3417         }
3418     }
3419 
3420     /**
3421      * Move a MMS to another folder.
3422      *
3423      * @param handle the CP handle of the message to move
3424      * @param resolver the ContentResolver to use
3425      * @param folder the destination folder - use Mms.MESSAGE_BOX_xxx
3426      */
moveMmsToFolder(long handle, ContentResolver resolver, int folder)3427     private static void moveMmsToFolder(long handle, ContentResolver resolver, int folder) {
3428         /*Move message by changing the msg_box value in the content provider database */
3429         if (handle != -1) {
3430             String whereClause = " _id= " + handle;
3431             Uri uri = Mms.CONTENT_URI;
3432             Cursor queryResult =
3433                     BluetoothMethodProxy.getInstance()
3434                             .contentResolverQuery(resolver, uri, null, whereClause, null, null);
3435             try {
3436                 if (queryResult != null) {
3437                     if (queryResult.getCount() > 0) {
3438                         queryResult.moveToFirst();
3439                         ContentValues data = new ContentValues();
3440                         /* set folder to be outbox */
3441                         data.put(Mms.MESSAGE_BOX, folder);
3442                         BluetoothMethodProxy.getInstance()
3443                                 .contentResolverUpdate(resolver, uri, data, whereClause, null);
3444                         Log.d(TAG, "moved MMS message to " + getMmsFolderName(folder));
3445                     }
3446                 } else {
3447                     Log.w(TAG, "Could not move MMS message to " + getMmsFolderName(folder));
3448                     ContentProfileErrorReportUtils.report(
3449                             BluetoothProfile.MAP,
3450                             BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3451                             BluetoothStatsLog
3452                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
3453                             23);
3454                 }
3455             } finally {
3456                 if (queryResult != null) {
3457                     queryResult.close();
3458                 }
3459             }
3460         }
3461     }
3462 
pushMmsToFolder(int folder, String[] toAddress, BluetoothMapbMessageMime msg)3463     private long pushMmsToFolder(int folder, String[] toAddress, BluetoothMapbMessageMime msg) {
3464         /**
3465          * strategy: 1) parse msg into parts + header 2) create thread id (abuse the ease of adding
3466          * an SMS to get id for thread) 3) push parts into content://mms/parts/ table 3)
3467          */
3468         ContentValues values = new ContentValues();
3469         values.put(Mms.MESSAGE_BOX, folder);
3470         values.put(Mms.READ, 0);
3471         values.put(Mms.SEEN, 0);
3472         if (msg.getSubject() != null) {
3473             values.put(Mms.SUBJECT, msg.getSubject());
3474         } else {
3475             values.put(Mms.SUBJECT, "");
3476         }
3477 
3478         if (msg.getSubject() != null && msg.getSubject().length() > 0) {
3479             values.put(Mms.SUBJECT_CHARSET, 106);
3480         }
3481         values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related");
3482         values.put(Mms.EXPIRY, 604800);
3483         values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
3484         values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
3485         values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION);
3486         values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL);
3487         values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO);
3488         values.put(Mms.TRANSACTION_ID, "T" + Long.toHexString(System.currentTimeMillis()));
3489         values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO);
3490         values.put(Mms.LOCKED, 0);
3491         if (msg.getTextOnly()) {
3492             values.put(Mms.TEXT_ONLY, true);
3493         }
3494         values.put(Mms.MESSAGE_SIZE, msg.getSize());
3495 
3496         // Get thread id
3497         Set<String> recipients = new HashSet<String>();
3498         recipients.addAll(Arrays.asList(toAddress));
3499         values.put(Mms.THREAD_ID, Telephony.Threads.getOrCreateThreadId(mContext, recipients));
3500         Uri uri = Mms.CONTENT_URI;
3501 
3502         synchronized (getMsgListMms()) {
3503             uri = mResolver.insert(uri, values);
3504 
3505             if (uri == null) {
3506                 // unable to insert MMS
3507                 Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri);
3508                 ContentProfileErrorReportUtils.report(
3509                         BluetoothProfile.MAP,
3510                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3511                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
3512                         24);
3513                 return -1;
3514             }
3515             /* As we already have all the values we need, we could skip the query, but
3516             doing the query ensures we get any changes made by the content provider
3517             at insert. */
3518             Cursor c = mResolver.query(uri, MMS_PROJECTION_SHORT, null, null, null);
3519             try {
3520                 if (c != null && c.moveToFirst()) {
3521                     long id = c.getLong(c.getColumnIndex(Mms._ID));
3522                     int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
3523                     int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
3524                     int readStatus = c.getInt(c.getColumnIndex(Mms.READ));
3525 
3526                     /* We must filter out any actions made by the MCE. Add the new message to
3527                      * the list of known messages. */
3528 
3529                     Msg newMsg = new Msg(id, type, threadId, readStatus);
3530                     newMsg.localInitiatedSend = true;
3531                     getMsgListMms().put(id, newMsg);
3532                     c.close();
3533                 }
3534             } finally {
3535                 if (c != null) {
3536                     c.close();
3537                 }
3538             }
3539         } // Done adding changes, unlock access to mMsgListMms to allow sending MMS events again
3540 
3541         long handle = Long.parseLong(uri.getLastPathSegment());
3542         Log.v(TAG, " NEW URI " + uri.toString());
3543 
3544         try {
3545             if (msg.getMimeParts() == null) {
3546                 /* Perhaps this message have been deleted, and no longer have any content,
3547                  * but only headers */
3548                 Log.w(TAG, "No MMS parts present...");
3549                 ContentProfileErrorReportUtils.report(
3550                         BluetoothProfile.MAP,
3551                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3552                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
3553                         25);
3554             } else {
3555                 Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base.");
3556                 for (MimePart part : msg.getMimeParts()) {
3557                     int count = 0;
3558                     count++;
3559                     values.clear();
3560                     if (part.mContentType != null
3561                             && part.mContentType.toUpperCase().contains("TEXT")) {
3562                         values.put(Mms.Part.CONTENT_TYPE, "text/plain");
3563                         values.put(Mms.Part.CHARSET, 106);
3564                         if (part.mPartName != null) {
3565                             values.put(Mms.Part.FILENAME, part.mPartName);
3566                             values.put(Mms.Part.NAME, part.mPartName);
3567                         } else {
3568                             values.put(Mms.Part.FILENAME, "text_" + count + ".txt");
3569                             values.put(Mms.Part.NAME, "text_" + count + ".txt");
3570                         }
3571                         // Ensure we have "ci" set
3572                         if (part.mContentId != null) {
3573                             values.put(Mms.Part.CONTENT_ID, part.mContentId);
3574                         } else {
3575                             if (part.mPartName != null) {
3576                                 values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">");
3577                             } else {
3578                                 values.put(Mms.Part.CONTENT_ID, "<text_" + count + ">");
3579                             }
3580                         }
3581                         // Ensure we have "cl" set
3582                         if (part.mContentLocation != null) {
3583                             values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
3584                         } else {
3585                             if (part.mPartName != null) {
3586                                 values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".txt");
3587                             } else {
3588                                 values.put(Mms.Part.CONTENT_LOCATION, "text_" + count + ".txt");
3589                             }
3590                         }
3591 
3592                         if (part.mContentDisposition != null) {
3593                             values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
3594                         }
3595                         values.put(Mms.Part.TEXT, part.getDataAsString());
3596                         uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part");
3597                         uri = mResolver.insert(uri, values);
3598                         Log.v(TAG, "Added TEXT part");
3599 
3600                     } else if (part.mContentType != null
3601                             && part.mContentType.toUpperCase().contains("SMIL")) {
3602                         values.put(Mms.Part.SEQ, -1);
3603                         values.put(Mms.Part.CONTENT_TYPE, "application/smil");
3604                         if (part.mContentId != null) {
3605                             values.put(Mms.Part.CONTENT_ID, part.mContentId);
3606                         } else {
3607                             values.put(Mms.Part.CONTENT_ID, "<smil_" + count + ">");
3608                         }
3609                         if (part.mContentLocation != null) {
3610                             values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
3611                         } else {
3612                             values.put(Mms.Part.CONTENT_LOCATION, "smil_" + count + ".xml");
3613                         }
3614 
3615                         if (part.mContentDisposition != null) {
3616                             values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
3617                         }
3618                         values.put(Mms.Part.FILENAME, "smil.xml");
3619                         values.put(Mms.Part.NAME, "smil.xml");
3620                         values.put(Mms.Part.TEXT, new String(part.mData, "UTF-8"));
3621 
3622                         uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part");
3623                         uri = mResolver.insert(uri, values);
3624                         Log.v(TAG, "Added SMIL part");
3625 
3626                     } else /*VIDEO/AUDIO/IMAGE*/ {
3627                         writeMmsDataPart(handle, part, count);
3628                         Log.v(TAG, "Added OTHER part");
3629                     }
3630                     if (uri != null) {
3631                         Log.v(
3632                                 TAG,
3633                                 "Added part with content-type: "
3634                                         + part.mContentType
3635                                         + " to Uri: "
3636                                         + uri.toString());
3637                     }
3638                 }
3639             }
3640         } catch (UnsupportedEncodingException e) {
3641             ContentProfileErrorReportUtils.report(
3642                     BluetoothProfile.MAP,
3643                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3644                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3645                     26);
3646             Log.w(TAG, e);
3647         } catch (IOException e) {
3648             ContentProfileErrorReportUtils.report(
3649                     BluetoothProfile.MAP,
3650                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3651                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3652                     27);
3653             Log.w(TAG, e);
3654         }
3655 
3656         values.clear();
3657         values.put(Mms.Addr.CONTACT_ID, "null");
3658         values.put(Mms.Addr.ADDRESS, "insert-address-token");
3659         values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_FROM);
3660         values.put(Mms.Addr.CHARSET, 106);
3661 
3662         uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr");
3663         uri = mResolver.insert(uri, values);
3664         if (uri != null) {
3665             Log.v(TAG, " NEW URI " + uri.toString());
3666         }
3667 
3668         values.clear();
3669         values.put(Mms.Addr.CONTACT_ID, "null");
3670         values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_TO);
3671         values.put(Mms.Addr.CHARSET, 106);
3672         for (String address : toAddress) {
3673             values.put(Mms.Addr.ADDRESS, address);
3674             uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr");
3675             uri = mResolver.insert(uri, values);
3676             if (uri != null) {
3677                 Log.v(TAG, " NEW URI " + uri.toString());
3678             }
3679         }
3680         return handle;
3681     }
3682 
writeMmsDataPart(long handle, MimePart part, int count)3683     private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException {
3684         ContentValues values = new ContentValues();
3685         values.put(Mms.Part.MSG_ID, handle);
3686         if (part.mContentType != null) {
3687             values.put(Mms.Part.CONTENT_TYPE, part.mContentType);
3688         } else {
3689             Log.w(TAG, "MMS has no CONTENT_TYPE for part " + count);
3690             ContentProfileErrorReportUtils.report(
3691                     BluetoothProfile.MAP,
3692                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3693                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
3694                     28);
3695         }
3696         if (part.mContentId != null) {
3697             values.put(Mms.Part.CONTENT_ID, part.mContentId);
3698         } else {
3699             if (part.mPartName != null) {
3700                 values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">");
3701             } else {
3702                 values.put(Mms.Part.CONTENT_ID, "<part_" + count + ">");
3703             }
3704         }
3705 
3706         if (part.mContentLocation != null) {
3707             values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
3708         } else {
3709             if (part.mPartName != null) {
3710                 values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".dat");
3711             } else {
3712                 values.put(Mms.Part.CONTENT_LOCATION, "part_" + count + ".dat");
3713             }
3714         }
3715         if (part.mContentDisposition != null) {
3716             values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
3717         }
3718         if (part.mPartName != null) {
3719             values.put(Mms.Part.FILENAME, part.mPartName);
3720             values.put(Mms.Part.NAME, part.mPartName);
3721         } else {
3722             /* We must set at least one part identifier */
3723             values.put(Mms.Part.FILENAME, "part_" + count + ".dat");
3724             values.put(Mms.Part.NAME, "part_" + count + ".dat");
3725         }
3726         Uri partUri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part");
3727         Uri res = mResolver.insert(partUri, values);
3728 
3729         // Add data to part
3730         OutputStream os = mResolver.openOutputStream(res);
3731         os.write(part.mData);
3732         os.close();
3733     }
3734 
sendMessage(PushMsgInfo msgInfo, String msgBody)3735     public void sendMessage(PushMsgInfo msgInfo, String msgBody) {
3736         SmsManager smsMng = SmsManager.getDefault();
3737         ArrayList<String> parts = smsMng.divideMessage(msgBody);
3738         msgInfo.parts = parts.size();
3739         // We add a time stamp to differentiate delivery reports from each other for resent messages
3740         msgInfo.timestamp = Calendar.getInstance().getTimeInMillis();
3741         msgInfo.partsDelivered = 0;
3742         msgInfo.partsSent = 0;
3743 
3744         ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts);
3745         ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts);
3746 
3747         /*       We handle the SENT intent in the MAP service, as this object
3748          *       is destroyed at disconnect, hence if a disconnect occur while sending
3749          *       a message, there is no intent handler to move the message from outbox
3750          *       to the correct folder.
3751          *       The correct solution would be to create a service that will start based on
3752          *       the intent, if BT is turned off. */
3753 
3754         if (parts != null && parts.size() > 0) {
3755             for (int i = 0; i < msgInfo.parts; i++) {
3756                 Intent intentDelivery, intentSent;
3757 
3758                 intentDelivery = new Intent(ACTION_MESSAGE_DELIVERY, null);
3759                 /* Add msgId and part number to ensure the intents are different, and we
3760                  * thereby get an intent for each msg part.
3761                  * setType is needed to create different intents for each message id/ time stamp,
3762                  * as the extras are not used when comparing. */
3763                 intentDelivery.setType(
3764                         "message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i);
3765                 intentDelivery.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id);
3766                 intentDelivery.putExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, msgInfo.timestamp);
3767                 PendingIntent pendingIntentDelivery =
3768                         PendingIntent.getBroadcast(
3769                                 mContext,
3770                                 0,
3771                                 intentDelivery,
3772                                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
3773 
3774                 intentSent = new Intent(ACTION_MESSAGE_SENT, null);
3775                 /* Add msgId and part number to ensure the intents are different, and we
3776                  * thereby get an intent for each msg part.
3777                  * setType is needed to create different intents for each message id/ time stamp,
3778                  * as the extras are not used when comparing. */
3779                 intentSent.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i);
3780                 intentSent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id);
3781                 intentSent.putExtra(EXTRA_MESSAGE_SENT_URI, msgInfo.uri.toString());
3782                 intentSent.putExtra(EXTRA_MESSAGE_SENT_RETRY, msgInfo.retry);
3783                 intentSent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, msgInfo.transparent);
3784 
3785                 PendingIntent pendingIntentSent =
3786                         PendingIntent.getBroadcast(
3787                                 mContext,
3788                                 0,
3789                                 intentSent,
3790                                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
3791 
3792                 // We use the same pending intent for all parts, but do not set the one shot flag.
3793                 deliveryIntents.add(pendingIntentDelivery);
3794                 sentIntents.add(pendingIntentSent);
3795             }
3796 
3797             Log.d(TAG, "sendMessage to " + msgInfo.phone);
3798 
3799             if (parts.size() == 1) {
3800                 smsMng.sendTextMessageWithoutPersisting(
3801                         msgInfo.phone,
3802                         null,
3803                         parts.get(0),
3804                         sentIntents.get(0),
3805                         deliveryIntents.get(0));
3806             } else {
3807                 smsMng.sendMultipartTextMessageWithoutPersisting(
3808                         msgInfo.phone, null, parts, sentIntents, deliveryIntents);
3809             }
3810         }
3811     }
3812 
3813     private class SmsBroadcastReceiver extends BroadcastReceiver {
register()3814         public void register() {
3815             Handler handler = new Handler(Looper.getMainLooper());
3816 
3817             IntentFilter intentFilter = new IntentFilter();
3818             intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
3819             intentFilter.addAction(ACTION_MESSAGE_DELIVERY);
3820             try {
3821                 intentFilter.addDataType("message/*");
3822             } catch (MalformedMimeTypeException e) {
3823                 ContentProfileErrorReportUtils.report(
3824                         BluetoothProfile.MAP,
3825                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3826                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3827                         29);
3828                 Log.e(TAG, "Wrong mime type!!!", e);
3829             }
3830 
3831             mContext.registerReceiver(this, intentFilter, null, handler);
3832         }
3833 
unregister()3834         public void unregister() {
3835             try {
3836                 mContext.unregisterReceiver(this);
3837             } catch (IllegalArgumentException e) {
3838                 ContentProfileErrorReportUtils.report(
3839                         BluetoothProfile.MAP,
3840                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3841                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
3842                         30);
3843                 /* do nothing */
3844             }
3845         }
3846 
3847         @Override
onReceive(Context context, Intent intent)3848         public void onReceive(Context context, Intent intent) {
3849             String action = intent.getAction();
3850             long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1);
3851             PushMsgInfo msgInfo = mPushMsgList.get(handle);
3852 
3853             Log.d(TAG, "onReceive: action" + action);
3854 
3855             if (msgInfo == null) {
3856                 Log.d(TAG, "onReceive: no msgInfo found for handle " + handle);
3857                 return;
3858             }
3859 
3860             if (action.equals(ACTION_MESSAGE_SENT)) {
3861                 int result =
3862                         intent.getIntExtra(EXTRA_MESSAGE_SENT_RESULT, Activity.RESULT_CANCELED);
3863                 msgInfo.partsSent++;
3864                 if (result != Activity.RESULT_OK) {
3865                     /* If just one of the parts in the message fails, we need to send the
3866                      * entire message again
3867                      */
3868                     msgInfo.failedSent = true;
3869                 }
3870                 Log.d(
3871                         TAG,
3872                         "onReceive: msgInfo.partsSent = "
3873                                 + msgInfo.partsSent
3874                                 + ", msgInfo.parts = "
3875                                 + msgInfo.parts
3876                                 + " result = "
3877                                 + result);
3878 
3879                 if (msgInfo.partsSent == msgInfo.parts) {
3880                     actionMessageSent(context, msgInfo, handle);
3881                 }
3882             } else if (action.equals(ACTION_MESSAGE_DELIVERY)) {
3883                 long timestamp = intent.getLongExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, 0);
3884                 if (msgInfo.timestamp == timestamp) {
3885                     msgInfo.partsDelivered++;
3886                 }
3887             } else {
3888                 Log.d(TAG, "onReceive: Unknown action " + action);
3889             }
3890         }
3891 
actionMessageSent(Context context, PushMsgInfo msgInfo, long handle)3892         private void actionMessageSent(Context context, PushMsgInfo msgInfo, long handle) {
3893             /* As the MESSAGE_SENT intent is forwarded from the MAP service, we use the intent
3894              * to carry the result, as getResult() will not return the correct value.
3895              */
3896             boolean delete = false;
3897 
3898             Log.d(TAG, "actionMessageSent(): msgInfo.failedSent = " + msgInfo.failedSent);
3899 
3900             msgInfo.sendInProgress = false;
3901 
3902             if (!msgInfo.failedSent) {
3903                 Log.d(TAG, "actionMessageSent: result OK");
3904                 if (msgInfo.transparent == 0) {
3905                     if (!Utils.moveMessageToFolder(context, msgInfo.uri, true)) {
3906                         Log.w(TAG, "Failed to move " + msgInfo.uri + " to SENT");
3907                         ContentProfileErrorReportUtils.report(
3908                                 BluetoothProfile.MAP,
3909                                 BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3910                                 BluetoothStatsLog
3911                                         .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
3912                                 31);
3913                     }
3914                 } else {
3915                     delete = true;
3916                 }
3917 
3918                 Event evt =
3919                         new Event(
3920                                 EVENT_TYPE_SENDING_SUCCESS,
3921                                 msgInfo.id,
3922                                 getSmsFolderName(Sms.MESSAGE_TYPE_SENT),
3923                                 null,
3924                                 mSmsType);
3925                 sendEvent(evt);
3926 
3927             } else {
3928                 if (msgInfo.retry == 1) {
3929                     /* Notify failure, but keep message in outbox for resending */
3930                     msgInfo.resend = true;
3931                     msgInfo.partsSent = 0; // Reset counter for the retry
3932                     msgInfo.failedSent = false;
3933                     Event evt =
3934                             new Event(
3935                                     EVENT_TYPE_SENDING_FAILURE,
3936                                     msgInfo.id,
3937                                     getSmsFolderName(Sms.MESSAGE_TYPE_OUTBOX),
3938                                     null,
3939                                     mSmsType);
3940                     sendEvent(evt);
3941                 } else {
3942                     if (msgInfo.transparent == 0) {
3943                         if (!Utils.moveMessageToFolder(context, msgInfo.uri, false)) {
3944                             Log.w(TAG, "Failed to move " + msgInfo.uri + " to FAILED");
3945                             ContentProfileErrorReportUtils.report(
3946                                     BluetoothProfile.MAP,
3947                                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
3948                                     BluetoothStatsLog
3949                                             .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
3950                                     32);
3951                         }
3952                     } else {
3953                         delete = true;
3954                     }
3955 
3956                     Event evt =
3957                             new Event(
3958                                     EVENT_TYPE_SENDING_FAILURE,
3959                                     msgInfo.id,
3960                                     getSmsFolderName(Sms.MESSAGE_TYPE_FAILED),
3961                                     null,
3962                                     mSmsType);
3963                     sendEvent(evt);
3964                 }
3965             }
3966 
3967             if (delete) {
3968                 /* Delete from Observer message list to avoid delete notifications */
3969                 synchronized (getMsgListSms()) {
3970                     getMsgListSms().remove(msgInfo.id);
3971                 }
3972 
3973                 /* Delete from DB */
3974                 Uri msgUri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
3975                 int nRows = mResolver.delete(msgUri, null, null);
3976                 if (nRows > 0) {
3977                     Log.v(TAG, "Deleted message with Uri = " + msgUri);
3978                 }
3979             }
3980         }
3981     }
3982 
3983     private class CeBroadcastReceiver extends BroadcastReceiver {
register()3984         public void register() {
3985             UserManager manager = mContext.getSystemService(UserManager.class);
3986             if (manager == null || manager.isUserUnlocked()) {
3987                 mStorageUnlocked = true;
3988                 return;
3989             }
3990 
3991             Handler handler = new Handler(Looper.getMainLooper());
3992             IntentFilter intentFilter = new IntentFilter();
3993             intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
3994             intentFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
3995             mContext.registerReceiver(this, intentFilter, null, handler);
3996         }
3997 
unregister()3998         public void unregister() {
3999             try {
4000                 mContext.unregisterReceiver(this);
4001             } catch (IllegalArgumentException e) {
4002                 ContentProfileErrorReportUtils.report(
4003                         BluetoothProfile.MAP,
4004                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
4005                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
4006                         33);
4007                 /* do nothing */
4008             }
4009         }
4010 
4011         @Override
onReceive(Context context, Intent intent)4012         public void onReceive(Context context, Intent intent) {
4013             String action = intent.getAction();
4014             Log.d(TAG, "onReceive: action" + action);
4015 
4016             if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
4017                 try {
4018                     initMsgList();
4019                 } catch (RemoteException e) {
4020                     ContentProfileErrorReportUtils.report(
4021                             BluetoothProfile.MAP,
4022                             BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
4023                             BluetoothStatsLog
4024                                     .BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
4025                             34);
4026                     Log.e(TAG, "Error initializing SMS/MMS message lists.");
4027                 }
4028 
4029                 for (String folder : FOLDER_SMS_MAP.values()) {
4030                     Event evt = new Event(EVENT_TYPE_NEW, -1, folder, mSmsType);
4031                     sendEvent(evt);
4032                 }
4033                 mStorageUnlocked = true;
4034                 /* After unlock this BroadcastReceiver is never needed */
4035                 unregister();
4036             } else {
4037                 Log.d(TAG, "onReceive: Unknown action " + action);
4038             }
4039         }
4040     }
4041 
4042     /**
4043      * Handle MMS sent intents in disconnected(MNS) state, where we do not need to send any
4044      * notifications.
4045      *
4046      * @param context The context to use for provider operations
4047      * @param intent The intent received
4048      * @param result The result
4049      */
actionMmsSent( Context context, Intent intent, int result, Map<Long, Msg> mmsMsgList)4050     public static void actionMmsSent(
4051             Context context, Intent intent, int result, Map<Long, Msg> mmsMsgList) {
4052         /*
4053          * if transparent:
4054          *   delete message and send notification(regardless of result)
4055          * else
4056          *   Result == Success:
4057          *     move to sent folder (will trigger notification)
4058          *   Result == Fail:
4059          *     move to outbox (send delivery fail notification)
4060          */
4061         Log.d(TAG, "actionMmsSent()");
4062         int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0);
4063         long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1);
4064         if (handle < 0) {
4065             Log.w(TAG, "Intent received for an invalid handle");
4066             ContentProfileErrorReportUtils.report(
4067                     BluetoothProfile.MAP,
4068                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
4069                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
4070                     35);
4071             return;
4072         }
4073         ContentResolver resolver = context.getContentResolver();
4074         if (transparent == 1) {
4075             /* The specification is a bit unclear about the transparent flag. If it is set
4076              * no copy of the message shall be kept in the send folder after the message
4077              * was send, but in the case of a send error, it is unclear what to do.
4078              * As it will not be transparent if we keep the message in any folder,
4079              * we delete the message regardless of the result.
4080              * If we however do have a MNS connection we need to send a notification. */
4081             Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
4082             /* Delete from observer message list to avoid delete notifications */
4083             if (mmsMsgList != null) {
4084                 synchronized (mmsMsgList) {
4085                     mmsMsgList.remove(handle);
4086                 }
4087             }
4088             /* Delete message */
4089             Log.d(TAG, "Transparent in use - delete");
4090             BluetoothMethodProxy.getInstance().contentResolverDelete(resolver, uri, null, null);
4091         } else if (result == Activity.RESULT_OK) {
4092             /* This will trigger a notification */
4093             moveMmsToFolder(handle, resolver, Mms.MESSAGE_BOX_SENT);
4094         } else {
4095             if (mmsMsgList != null) {
4096                 synchronized (mmsMsgList) {
4097                     Msg msg = mmsMsgList.get(handle);
4098                     if (msg != null) {
4099                         msg.type = Mms.MESSAGE_BOX_OUTBOX;
4100                     }
4101                 }
4102             }
4103             /* Hand further retries over to the MMS application */
4104             moveMmsToFolder(handle, resolver, Mms.MESSAGE_BOX_OUTBOX);
4105         }
4106     }
4107 
actionMessageSentDisconnected(Context context, Intent intent, int result)4108     public static void actionMessageSentDisconnected(Context context, Intent intent, int result) {
4109         TYPE type =
4110                 TYPE.fromOrdinal(
4111                         intent.getIntExtra(EXTRA_MESSAGE_SENT_MSG_TYPE, TYPE.NONE.ordinal()));
4112         if (type == TYPE.MMS) {
4113             actionMmsSent(context, intent, result, null);
4114         } else {
4115             actionSmsSentDisconnected(context, intent, result);
4116         }
4117     }
4118 
actionSmsSentDisconnected(Context context, Intent intent, int result)4119     public static void actionSmsSentDisconnected(Context context, Intent intent, int result) {
4120         /* Check permission for message deletion. */
4121         if ((Binder.getCallingPid() != Process.myPid())
4122                 || !Utils.checkCallerHasWriteSmsPermission(context)) {
4123             Log.w(TAG, "actionSmsSentDisconnected: Not allowed to delete SMS/MMS messages");
4124             ContentProfileErrorReportUtils.report(
4125                     BluetoothProfile.MAP,
4126                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
4127                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
4128                     36);
4129             return;
4130         }
4131 
4132         boolean delete = false;
4133         // int retry = intent.getIntExtra(EXTRA_MESSAGE_SENT_RETRY, 0);
4134         int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0);
4135         String uriString = intent.getStringExtra(EXTRA_MESSAGE_SENT_URI);
4136         if (uriString == null) {
4137             // Nothing we can do about it, just bail out
4138             return;
4139         }
4140         Uri uri = Uri.parse(uriString);
4141 
4142         if (result == Activity.RESULT_OK) {
4143             Log.d(TAG, "actionMessageSentDisconnected: result OK");
4144             if (transparent == 0) {
4145                 if (!Utils.moveMessageToFolder(context, uri, true)) {
4146                     Log.d(TAG, "Failed to move " + uri + " to SENT");
4147                 }
4148             } else {
4149                 delete = true;
4150             }
4151         } else {
4152             /*if (retry == 1) {
4153                  The retry feature only works while connected, else we fail the send,
4154              * and move the message to failed, to let the user/app resend manually later.
4155             } else */
4156             {
4157                 if (transparent == 0) {
4158                     if (!Utils.moveMessageToFolder(context, uri, false)) {
4159                         Log.d(TAG, "Failed to move " + uri + " to FAILED");
4160                     }
4161                 } else {
4162                     delete = true;
4163                 }
4164             }
4165         }
4166 
4167         if (delete) {
4168             /* Delete from DB */
4169             ContentResolver resolver = context.getContentResolver();
4170             if (resolver != null) {
4171                 BluetoothMethodProxy.getInstance().contentResolverDelete(resolver, uri, null, null);
4172             } else {
4173                 Log.w(TAG, "Unable to get resolver");
4174                 ContentProfileErrorReportUtils.report(
4175                         BluetoothProfile.MAP,
4176                         BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
4177                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
4178                         37);
4179             }
4180         }
4181     }
4182 
registerPhoneServiceStateListener()4183     private void registerPhoneServiceStateListener() {
4184         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
4185         tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE);
4186     }
4187 
unRegisterPhoneServiceStateListener()4188     private void unRegisterPhoneServiceStateListener() {
4189         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
4190         tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE);
4191     }
4192 
resendPendingMessages()4193     private void resendPendingMessages() {
4194         /* Send pending messages in outbox */
4195         String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
4196         UserManager manager = mContext.getSystemService(UserManager.class);
4197         if (manager == null || !manager.isUserUnlocked()) {
4198             return;
4199         }
4200         Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null);
4201         try {
4202             if (c != null && c.moveToFirst()) {
4203                 do {
4204                     long id = c.getLong(c.getColumnIndex(Sms._ID));
4205                     String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
4206                     PushMsgInfo msgInfo = mPushMsgList.get(id);
4207                     if (msgInfo == null || !msgInfo.resend || msgInfo.sendInProgress) {
4208                         continue;
4209                     }
4210                     msgInfo.sendInProgress = true;
4211                     sendMessage(msgInfo, msgBody);
4212                 } while (c.moveToNext());
4213             }
4214         } finally {
4215             if (c != null) {
4216                 c.close();
4217             }
4218         }
4219     }
4220 
failPendingMessages()4221     private void failPendingMessages() {
4222         /* Move pending messages from outbox to failed */
4223         String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
4224         Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null);
4225         try {
4226             if (c != null && c.moveToFirst()) {
4227                 do {
4228                     long id = c.getLong(c.getColumnIndex(Sms._ID));
4229                     PushMsgInfo msgInfo = mPushMsgList.get(id);
4230                     if (msgInfo == null || !msgInfo.resend) {
4231                         continue;
4232                     }
4233                     Utils.moveMessageToFolder(mContext, msgInfo.uri, false);
4234                 } while (c.moveToNext());
4235             }
4236         } finally {
4237             if (c != null) {
4238                 c.close();
4239             }
4240         }
4241     }
4242 
removeDeletedMessages()4243     private void removeDeletedMessages() {
4244         try {
4245             /* Remove messages from virtual "deleted" folder (thread_id -1) */
4246             mResolver.delete(Sms.CONTENT_URI, "thread_id = " + DELETED_THREAD_ID, null);
4247         } catch (SQLiteException e) {
4248             ContentProfileErrorReportUtils.report(
4249                     BluetoothProfile.MAP,
4250                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
4251                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
4252                     38);
4253             // TODO: Include this unexpected exception in Bluetooth metrics
4254             Log.w("SQLite exception while removing deleted messages.", e);
4255         }
4256     }
4257 
4258     private PhoneStateListener mPhoneListener =
4259             new PhoneStateListener() {
4260                 @Override
4261                 public void onServiceStateChanged(ServiceState serviceState) {
4262                     Log.d(TAG, "Phone service state change: " + serviceState.getState());
4263                     if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
4264                         resendPendingMessages();
4265                     }
4266                 }
4267             };
4268 
init()4269     public void init() {
4270         if (mSmsBroadcastReceiver != null) {
4271             mSmsBroadcastReceiver.register();
4272         }
4273 
4274         if (mCeBroadcastReceiver != null) {
4275             mCeBroadcastReceiver.register();
4276         }
4277 
4278         registerPhoneServiceStateListener();
4279         mInitialized = true;
4280     }
4281 
deinit()4282     public void deinit() {
4283         mInitialized = false;
4284         unregisterObserver();
4285         if (mSmsBroadcastReceiver != null) {
4286             mSmsBroadcastReceiver.unregister();
4287         }
4288         unRegisterPhoneServiceStateListener();
4289         if (mContext.getSystemService(UserManager.class).isUserUnlocked()) {
4290             failPendingMessages();
4291             removeDeletedMessages();
4292         }
4293     }
4294 
handleSmsSendIntent(Context context, Intent intent)4295     public boolean handleSmsSendIntent(Context context, Intent intent) {
4296         TYPE type =
4297                 TYPE.fromOrdinal(
4298                         intent.getIntExtra(EXTRA_MESSAGE_SENT_MSG_TYPE, TYPE.NONE.ordinal()));
4299         if (type == TYPE.MMS) {
4300             return handleMmsSendIntent(context, intent);
4301         } else {
4302             if (mInitialized) {
4303                 mSmsBroadcastReceiver.onReceive(context, intent);
4304                 return true;
4305             }
4306         }
4307         return false;
4308     }
4309 
handleMmsSendIntent(Context context, Intent intent)4310     public boolean handleMmsSendIntent(Context context, Intent intent) {
4311         Log.d(TAG, "handleMmsSendIntent()");
4312         if (!mMnsClient.isConnected()) {
4313             // No need to handle notifications, just use default handling
4314             Log.w(TAG, "MNS not connected - use static handling");
4315             ContentProfileErrorReportUtils.report(
4316                     BluetoothProfile.MAP,
4317                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
4318                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
4319                     39);
4320             return false;
4321         }
4322         long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1);
4323         int result = intent.getIntExtra(EXTRA_MESSAGE_SENT_RESULT, Activity.RESULT_CANCELED);
4324         actionMmsSent(context, intent, result, getMsgListMms());
4325         if (handle < 0) {
4326             Log.w(TAG, "Intent received for an invalid handle");
4327             ContentProfileErrorReportUtils.report(
4328                     BluetoothProfile.MAP,
4329                     BluetoothProtoEnums.BLUETOOTH_MAP_CONTENT_OBSERVER,
4330                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_WARN,
4331                     40);
4332             return true;
4333         }
4334         if (result != Activity.RESULT_OK) {
4335             if (mObserverRegistered) {
4336                 Event evt =
4337                         new Event(
4338                                 EVENT_TYPE_SENDING_FAILURE,
4339                                 handle,
4340                                 getMmsFolderName(Mms.MESSAGE_BOX_OUTBOX),
4341                                 null,
4342                                 TYPE.MMS);
4343                 sendEvent(evt);
4344             }
4345         } else {
4346             int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0);
4347             if (transparent != 0) {
4348                 if (mObserverRegistered) {
4349                     Event evt =
4350                             new Event(
4351                                     EVENT_TYPE_SENDING_SUCCESS,
4352                                     handle,
4353                                     getMmsFolderName(Mms.MESSAGE_BOX_OUTBOX),
4354                                     null,
4355                                     TYPE.MMS);
4356                     sendEvent(evt);
4357                 }
4358             }
4359         }
4360         return true;
4361     }
4362 }
4363