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