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