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