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