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