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