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