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                             throw new IllegalStateException("ID is null");
1416                         }
1417                         long id = c.getLong(idIndex);
1418                         int type = c.getInt(c.getColumnIndex(Sms.TYPE));
1419                         int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
1420                         int read = c.getInt(c.getColumnIndex(Sms.READ));
1421 
1422                         Msg msg = getMsgListSms().remove(id);
1423 
1424                         /* We must filter out any actions made by the MCE, hence do not send e.g.
1425                          * a message deleted and/or MessageShift for messages deleted by the MCE. */
1426 
1427                         if (msg == null) {
1428                             /* New message */
1429                             msg = new Msg(id, type, threadId, read);
1430                             msgListSms.put(id, msg);
1431                             listChanged = true;
1432                             Event evt;
1433                             if (mTransmitEvents && // extract contact details only if needed
1434                                     mMapEventReportVersion
1435                                             > BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1436                                 String date = BluetoothMapUtils.getDateTimeString(
1437                                         c.getLong(c.getColumnIndex(Sms.DATE)));
1438                                 String subject = c.getString(c.getColumnIndex(Sms.BODY));
1439                                 if (subject == null) {
1440                                     subject = "";
1441                                 }
1442                                 String name = "";
1443                                 String phone = "";
1444                                 if (type == 1) { //inbox
1445                                     phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
1446                                     if (phone != null && !phone.isEmpty()) {
1447                                         name = BluetoothMapContent.getContactNameFromPhone(phone,
1448                                                 mResolver);
1449                                         if (name == null || name.isEmpty()) {
1450                                             name = phone;
1451                                         }
1452                                     } else {
1453                                         name = phone;
1454                                     }
1455                                 } else {
1456                                     TelephonyManager tm =
1457                                             (TelephonyManager) mContext.getSystemService(
1458                                                     Context.TELEPHONY_SERVICE);
1459                                     if (tm != null) {
1460                                         phone = tm.getLine1Number();
1461                                         name = phone;
1462                                     }
1463                                 }
1464                                 String priority = "no"; // no priority for sms
1465                                 /* Incoming message from the network */
1466                                 if (mMapEventReportVersion
1467                                         == BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
1468                                     evt = new Event(EVENT_TYPE_NEW, id, getSmsFolderName(type),
1469                                             mSmsType, date, subject, name, priority);
1470                                 } else {
1471                                     evt = new Event(EVENT_TYPE_NEW, id, getSmsFolderName(type),
1472                                             mSmsType, date, subject, name, priority,
1473                                             (long) threadId, null);
1474                                 }
1475                             } else {
1476                                 /* Incoming message from the network */
1477                                 evt = new Event(EVENT_TYPE_NEW, id, getSmsFolderName(type), null,
1478                                         mSmsType);
1479                             }
1480                             sendEvent(evt);
1481                         } else {
1482                             /* Existing message */
1483                             if (type != msg.type) {
1484                                 listChanged = true;
1485                                 Log.d(TAG, "new type: " + type + " old type: " + msg.type);
1486                                 String oldFolder = getSmsFolderName(msg.type);
1487                                 String newFolder = getSmsFolderName(type);
1488                                 // Filter out the intermediate outbox steps
1489                                 if (!oldFolder.equalsIgnoreCase(newFolder)) {
1490                                     Event evt =
1491                                             new Event(EVENT_TYPE_SHIFT, id, getSmsFolderName(type),
1492                                                     oldFolder, mSmsType);
1493                                     sendEvent(evt);
1494                                 }
1495                                 msg.type = type;
1496                             } else if (threadId != msg.threadId) {
1497                                 listChanged = true;
1498                                 Log.d(TAG, "Message delete change: type: " + type + " old type: "
1499                                         + msg.type + "\n    threadId: " + threadId
1500                                         + " old threadId: " + msg.threadId);
1501                                 if (threadId == DELETED_THREAD_ID) { // Message deleted
1502                                     // TODO:
1503                                     // We shall only use the folder attribute, but can't remember
1504                                     // wether to set it to "deleted" or the name of the folder
1505                                     // from which the message have been deleted.
1506                                     // "old_folder" used only for MessageShift event
1507                                     Event evt = new Event(EVENT_TYPE_DELETE, id,
1508                                             getSmsFolderName(msg.type), null, mSmsType);
1509                                     sendEvent(evt);
1510                                     msg.threadId = threadId;
1511                                 } else { // Undelete
1512                                     Event evt = new Event(EVENT_TYPE_SHIFT, id,
1513                                             getSmsFolderName(msg.type),
1514                                             BluetoothMapContract.FOLDER_NAME_DELETED, mSmsType);
1515                                     sendEvent(evt);
1516                                     msg.threadId = threadId;
1517                                 }
1518                             }
1519                             if (read != msg.flagRead) {
1520                                 listChanged = true;
1521                                 msg.flagRead = read;
1522                                 if (mMapEventReportVersion
1523                                         > BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1524                                     Event evt = new Event(EVENT_TYPE_READ_STATUS, id,
1525                                             getSmsFolderName(msg.type), mSmsType);
1526                                     sendEvent(evt);
1527                                 }
1528                             }
1529                             msgListSms.put(id, msg);
1530                         }
1531                     } while (c.moveToNext());
1532                 }
1533             } finally {
1534                 if (c != null) {
1535                     c.close();
1536                 }
1537             }
1538             String eventType = EVENT_TYPE_DELETE;
1539             for (Msg msg : getMsgListSms().values()) {
1540                 // "old_folder" used only for MessageShift event
1541                 if (mMapEventReportVersion >= BluetoothMapUtils.MAP_EVENT_REPORT_V12) {
1542                     eventType = EVENT_TYPE_REMOVED;
1543                     if (V) Log.v(TAG," sent EVENT_TYPE_REMOVED");
1544                 }
1545                 Event evt = new Event(eventType, msg.id, getSmsFolderName(msg.type), null,
1546                         mSmsType);
1547                 sendEvent(evt);
1548                 listChanged = true;
1549             }
1550 
1551             setMsgListSms(msgListSms, listChanged);
1552         }
1553     }
1554 
handleMsgListChangesMms()1555     private void handleMsgListChangesMms() {
1556         if (V) {
1557             Log.d(TAG, "handleMsgListChangesMms");
1558         }
1559 
1560         HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();
1561         boolean listChanged = false;
1562         Cursor c;
1563         synchronized (getMsgListMms()) {
1564             if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1565                 c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT, null, null, null);
1566             } else {
1567                 c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT_EXT, null, null, null);
1568             }
1569 
1570             try {
1571                 if (c != null && c.moveToFirst()) {
1572                     do {
1573                         int idIndex = c.getColumnIndexOrThrow(Mms._ID);
1574                         if (c.isNull(idIndex)) {
1575                             throw new IllegalStateException("ID is null");
1576                         }
1577                         long id = c.getLong(idIndex);
1578                         int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
1579                         int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE));
1580                         int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
1581                         // TODO: Go through code to see if we have an issue with mismatch in types
1582                         //       for threadId. Seems to be a long in DB??
1583                         int read = c.getInt(c.getColumnIndex(Mms.READ));
1584 
1585                         Msg msg = getMsgListMms().remove(id);
1586 
1587                         /* We must filter out any actions made by the MCE, hence do not send
1588                          * e.g. a message deleted and/or MessageShift for messages deleted by the
1589                          * MCE.*/
1590 
1591                         if (msg == null) {
1592                             /* New message - only notify on retrieve conf */
1593                             listChanged = true;
1594                             if (getMmsFolderName(type).equalsIgnoreCase(
1595                                     BluetoothMapContract.FOLDER_NAME_INBOX)
1596                                     && mtype != MESSAGE_TYPE_RETRIEVE_CONF) {
1597                                 continue;
1598                             }
1599                             msg = new Msg(id, type, threadId, read);
1600                             msgListMms.put(id, msg);
1601                             Event evt;
1602                             if (mTransmitEvents && // extract contact details only if needed
1603                                     mMapEventReportVersion
1604                                             != BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1605                                 String date = BluetoothMapUtils.getDateTimeString(
1606                                         c.getLong(c.getColumnIndex(Mms.DATE)));
1607                                 String subject = c.getString(c.getColumnIndex(Mms.SUBJECT));
1608                                 if (subject == null || subject.length() == 0) {
1609                                     /* Get subject from mms text body parts - if any exists */
1610                                     subject = BluetoothMapContent.getTextPartsMms(mResolver, id);
1611                                     if (subject == null) {
1612                                         subject = "";
1613                                     }
1614                                 }
1615                                 int tmpPri = c.getInt(c.getColumnIndex(Mms.PRIORITY));
1616                                 Log.d(TAG, "TEMP handleMsgListChangesMms, "
1617                                         + "newMessage 'read' state: " + read + "priority: "
1618                                         + tmpPri);
1619 
1620                                 String address = BluetoothMapContent.getAddressMms(mResolver, id,
1621                                         BluetoothMapContent.MMS_FROM);
1622                                 if (address == null) {
1623                                     address = "";
1624                                 }
1625 
1626                                 String priority = "no";
1627                                 if (tmpPri == PduHeaders.PRIORITY_HIGH) {
1628                                     priority = "yes";
1629                                 }
1630 
1631                                 /* Incoming message from the network */
1632                                 if (mMapEventReportVersion
1633                                         == BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
1634                                     evt = new Event(EVENT_TYPE_NEW, id, getMmsFolderName(type),
1635                                             TYPE.MMS, date, subject, address, priority);
1636                                 } else {
1637                                     evt = new Event(EVENT_TYPE_NEW, id, getMmsFolderName(type),
1638                                             TYPE.MMS, date, subject, address, priority,
1639                                             (long) threadId, null);
1640                                 }
1641 
1642                             } else {
1643                                 /* Incoming message from the network */
1644                                 evt = new Event(EVENT_TYPE_NEW, id, getMmsFolderName(type), null,
1645                                         TYPE.MMS);
1646                             }
1647 
1648                             sendEvent(evt);
1649                         } else {
1650                             /* Existing message */
1651                             if (type != msg.type) {
1652                                 Log.d(TAG, "new type: " + type + " old type: " + msg.type);
1653                                 Event evt;
1654                                 listChanged = true;
1655                                 if (!msg.localInitiatedSend) {
1656                                     // Only send events about local initiated changes
1657                                     evt = new Event(EVENT_TYPE_SHIFT, id, getMmsFolderName(type),
1658                                             getMmsFolderName(msg.type), TYPE.MMS);
1659                                     sendEvent(evt);
1660                                 }
1661                                 msg.type = type;
1662 
1663                                 if (getMmsFolderName(type).equalsIgnoreCase(
1664                                         BluetoothMapContract.FOLDER_NAME_SENT)
1665                                         && msg.localInitiatedSend) {
1666                                     // Stop tracking changes for this message
1667                                     msg.localInitiatedSend = false;
1668                                     evt = new Event(EVENT_TYPE_SENDING_SUCCESS, id,
1669                                             getMmsFolderName(type), null, TYPE.MMS);
1670                                     sendEvent(evt);
1671                                 }
1672                             } else if (threadId != msg.threadId) {
1673                                 Log.d(TAG, "Message delete change: type: " + type + " old type: "
1674                                         + msg.type + "\n    threadId: " + threadId
1675                                         + " old threadId: " + msg.threadId);
1676                                 listChanged = true;
1677                                 if (threadId == DELETED_THREAD_ID) { // Message deleted
1678                                     // "old_folder" used only for MessageShift event
1679                                     Event evt = new Event(EVENT_TYPE_DELETE, id,
1680                                             getMmsFolderName(msg.type), null, TYPE.MMS);
1681                                     sendEvent(evt);
1682                                     msg.threadId = threadId;
1683                                 } else { // Undelete
1684                                     Event evt = new Event(EVENT_TYPE_SHIFT, id,
1685                                             getMmsFolderName(msg.type),
1686                                             BluetoothMapContract.FOLDER_NAME_DELETED, TYPE.MMS);
1687                                     sendEvent(evt);
1688                                     msg.threadId = threadId;
1689                                 }
1690                             }
1691                             if (read != msg.flagRead) {
1692                                 listChanged = true;
1693                                 msg.flagRead = read;
1694                                 if (mMapEventReportVersion
1695                                         > BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1696                                     Event evt = new Event(EVENT_TYPE_READ_STATUS, id,
1697                                             getMmsFolderName(msg.type), TYPE.MMS);
1698                                     sendEvent(evt);
1699                                 }
1700                             }
1701                             msgListMms.put(id, msg);
1702                         }
1703                     } while (c.moveToNext());
1704 
1705                 }
1706             } finally {
1707                 if (c != null) {
1708                     c.close();
1709                 }
1710             }
1711             for (Msg msg : getMsgListMms().values()) {
1712                 // "old_folder" used only for MessageShift event
1713                 Event evt = new Event(EVENT_TYPE_DELETE, msg.id, getMmsFolderName(msg.type), null,
1714                         TYPE.MMS);
1715                 sendEvent(evt);
1716                 listChanged = true;
1717             }
1718             setMsgListMms(msgListMms, listChanged);
1719         }
1720     }
1721 
handleMsgListChangesMsg(Uri uri)1722     private void handleMsgListChangesMsg(Uri uri) throws RemoteException {
1723         if (V) {
1724             Log.v(TAG, "handleMsgListChangesMsg uri: " + uri.toString());
1725         }
1726 
1727         // TODO: Change observer to handle accountId and message ID if present
1728 
1729         HashMap<Long, Msg> msgList = new HashMap<Long, Msg>();
1730         Cursor c;
1731         boolean listChanged = false;
1732         if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1733             c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT, null, null, null);
1734         } else if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
1735             c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT_EXT, null, null, null);
1736         } else {
1737             c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT_EXT2, null, null, null);
1738         }
1739         synchronized (getMsgListMsg()) {
1740             try {
1741                 if (c != null && c.moveToFirst()) {
1742                     do {
1743                         long id = c.getLong(
1744                                 c.getColumnIndex(BluetoothMapContract.MessageColumns._ID));
1745                         int folderId = c.getInt(
1746                                 c.getColumnIndex(BluetoothMapContract.MessageColumns.FOLDER_ID));
1747                         int readFlag = c.getInt(
1748                                 c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_READ));
1749                         Msg msg = getMsgListMsg().remove(id);
1750                         BluetoothMapFolderElement folderElement = mFolders.getFolderById(folderId);
1751                         String newFolder;
1752                         if (folderElement != null) {
1753                             newFolder = folderElement.getFullPath();
1754                         } else {
1755                             // This can happen if a new folder is created while connected
1756                             newFolder = "unknown";
1757                         }
1758                         /* We must filter out any actions made by the MCE, hence do not send e.g.
1759                          * a message deleted and/or MessageShift for messages deleted by the MCE. */
1760                         if (msg == null) {
1761                             listChanged = true;
1762                             /* New message - created with message unread */
1763                             msg = new Msg(id, folderId, 0, readFlag);
1764                             msgList.put(id, msg);
1765                             Event evt;
1766                             /* Incoming message from the network */
1767                             if (mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1768                                 String date = BluetoothMapUtils.getDateTimeString(c.getLong(
1769                                         c.getColumnIndex(
1770                                                 BluetoothMapContract.MessageColumns.DATE)));
1771                                 String subject = c.getString(c.getColumnIndex(
1772                                         BluetoothMapContract.MessageColumns.SUBJECT));
1773                                 String address = c.getString(c.getColumnIndex(
1774                                         BluetoothMapContract.MessageColumns.FROM_LIST));
1775                                 String priority = "no";
1776                                 if (c.getInt(c.getColumnIndex(
1777                                         BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY))
1778                                         == 1) {
1779                                     priority = "yes";
1780                                 }
1781                                 if (mMapEventReportVersion
1782                                         == BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
1783                                     evt = new Event(EVENT_TYPE_NEW, id, newFolder,
1784                                             mAccount.getType(), date, subject, address, priority);
1785                                 } else {
1786                                     long threadId = c.getLong(c.getColumnIndex(
1787                                             BluetoothMapContract.MessageColumns.THREAD_ID));
1788                                     String threadName = c.getString(c.getColumnIndex(
1789                                             BluetoothMapContract.MessageColumns.THREAD_NAME));
1790                                     evt = new Event(EVENT_TYPE_NEW, id, newFolder,
1791                                             mAccount.getType(), date, subject, address, priority,
1792                                             threadId, threadName);
1793                                 }
1794                             } else {
1795                                 evt = new Event(EVENT_TYPE_NEW, id, newFolder, null, TYPE.EMAIL);
1796                             }
1797                             sendEvent(evt);
1798                         } else {
1799                             /* Existing message */
1800                             if (folderId != msg.folderId && msg.folderId != -1) {
1801                                 if (D) {
1802                                     Log.d(TAG, "new folderId: " + folderId + " old folderId: "
1803                                             + msg.folderId);
1804                                 }
1805                                 BluetoothMapFolderElement oldFolderElement =
1806                                         mFolders.getFolderById(msg.folderId);
1807                                 String oldFolder;
1808                                 listChanged = true;
1809                                 if (oldFolderElement != null) {
1810                                     oldFolder = oldFolderElement.getFullPath();
1811                                 } else {
1812                                     // This can happen if a new folder is created while connected
1813                                     oldFolder = "unknown";
1814                                 }
1815                                 BluetoothMapFolderElement deletedFolder = mFolders.getFolderByName(
1816                                         BluetoothMapContract.FOLDER_NAME_DELETED);
1817                                 BluetoothMapFolderElement sentFolder = mFolders.getFolderByName(
1818                                         BluetoothMapContract.FOLDER_NAME_SENT);
1819                                 /*
1820                                  *  If the folder is now 'deleted', send a deleted-event in stead of
1821                                  *  a shift or if message is sent initiated by MAP Client, then send
1822                                  *  sending-success otherwise send folderShift
1823                                  */
1824                                 if (deletedFolder != null
1825                                         && deletedFolder.getFolderId() == folderId) {
1826                                     // "old_folder" used only for MessageShift event
1827                                     Event evt =
1828                                             new Event(EVENT_TYPE_DELETE, msg.id, oldFolder, null,
1829                                                     mAccount.getType());
1830                                     sendEvent(evt);
1831                                 } else if (sentFolder != null
1832                                         && sentFolder.getFolderId() == folderId
1833                                         && msg.localInitiatedSend) {
1834                                     if (msg.transparent) {
1835                                         mResolver.delete(
1836                                                 ContentUris.withAppendedId(mMessageUri, id), null,
1837                                                 null);
1838                                     } else {
1839                                         msg.localInitiatedSend = false;
1840                                         Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id,
1841                                                 oldFolder, null, mAccount.getType());
1842                                         sendEvent(evt);
1843                                     }
1844                                 } else {
1845                                     if (!oldFolder.equalsIgnoreCase("root")) {
1846                                         Event evt = new Event(EVENT_TYPE_SHIFT, id, newFolder,
1847                                                 oldFolder, mAccount.getType());
1848                                         sendEvent(evt);
1849                                     }
1850                                 }
1851                                 msg.folderId = folderId;
1852                             }
1853                             if (readFlag != msg.flagRead) {
1854                                 listChanged = true;
1855 
1856                                 if (mMapEventReportVersion
1857                                         > BluetoothMapUtils.MAP_EVENT_REPORT_V10) {
1858                                     Event evt = new Event(EVENT_TYPE_READ_STATUS, id, newFolder,
1859                                             mAccount.getType());
1860                                     sendEvent(evt);
1861                                     msg.flagRead = readFlag;
1862                                 }
1863                             }
1864 
1865                             msgList.put(id, msg);
1866                         }
1867                     } while (c.moveToNext());
1868                 }
1869             } finally {
1870                 if (c != null) {
1871                     c.close();
1872                 }
1873             }
1874             // For all messages no longer in the database send a delete notification
1875             for (Msg msg : getMsgListMsg().values()) {
1876                 BluetoothMapFolderElement oldFolderElement = mFolders.getFolderById(msg.folderId);
1877                 String oldFolder;
1878                 listChanged = true;
1879                 if (oldFolderElement != null) {
1880                     oldFolder = oldFolderElement.getFullPath();
1881                 } else {
1882                     oldFolder = "unknown";
1883                 }
1884                 /* Some e-mail clients delete the message after sending, and creates a
1885                  * new message in sent. We cannot track the message anymore, hence send both a
1886                  * send success and delete message.
1887                  */
1888                 if (msg.localInitiatedSend) {
1889                     msg.localInitiatedSend = false;
1890                     // If message is send with transparency don't set folder as message is deleted
1891                     if (msg.transparent) {
1892                         oldFolder = null;
1893                     }
1894                     Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, oldFolder, null,
1895                             mAccount.getType());
1896                     sendEvent(evt);
1897                 }
1898                 /* As this message deleted is only send on a real delete - don't set folder.
1899                  *  - only send delete event if message is not sent with transparency
1900                  */
1901                 if (!msg.transparent) {
1902 
1903                     // "old_folder" used only for MessageShift event
1904                     Event evt = new Event(EVENT_TYPE_DELETE, msg.id, oldFolder, null,
1905                             mAccount.getType());
1906                     sendEvent(evt);
1907                 }
1908             }
1909             setMsgListMsg(msgList, listChanged);
1910         }
1911     }
1912 
handleMsgListChanges(Uri uri)1913     private void handleMsgListChanges(Uri uri) {
1914         if (uri.getAuthority().equals(mAuthority)) {
1915             try {
1916                 if (D) {
1917                     Log.d(TAG, "handleMsgListChanges: account type = " + mAccount.getType()
1918                             .toString());
1919                 }
1920                 handleMsgListChangesMsg(uri);
1921             } catch (RemoteException e) {
1922                 mMasInstance.restartObexServerSession();
1923                 Log.w(TAG, "Problems contacting the ContentProvider in mas Instance " + mMasId
1924                         + " restaring ObexServerSession");
1925             }
1926 
1927         }
1928         // TODO: check to see if there could be problem with IM and SMS in one instance
1929         if (mEnableSmsMms) {
1930             handleMsgListChangesSms();
1931             handleMsgListChangesMms();
1932         }
1933     }
1934 
handleContactListChanges(Uri uri)1935     private void handleContactListChanges(Uri uri) {
1936         if (uri.getAuthority().equals(mAuthority)) {
1937             try {
1938                 if (V) {
1939                     Log.v(TAG, "handleContactListChanges uri: " + uri.toString());
1940                 }
1941                 Cursor c = null;
1942                 boolean listChanged = false;
1943                 try {
1944                     ConvoContactInfo cInfo = new ConvoContactInfo();
1945 
1946                     if (mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V10
1947                             && mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
1948                         c = mProviderClient.query(mContactUri,
1949                                 BluetoothMapContract.BT_CONTACT_CHATSTATE_PRESENCE_PROJECTION,
1950                                 null, null, null);
1951                         cInfo.setConvoColunms(c);
1952                     } else {
1953                         if (V) {
1954                             Log.v(TAG, "handleContactListChanges MAP version does not"
1955                                     + "support convocontact notifications");
1956                         }
1957                         return;
1958                     }
1959 
1960                     HashMap<String, BluetoothMapConvoContactElement> contactList =
1961                             new HashMap<String, BluetoothMapConvoContactElement>(
1962                                     getContactList().size());
1963 
1964                     synchronized (getContactList()) {
1965                         if (c != null && c.moveToFirst()) {
1966                             do {
1967                                 String uci = c.getString(cInfo.mContactColUci);
1968                                 long convoId = c.getLong(cInfo.mContactColConvoId);
1969                                 if (convoId == 0) {
1970                                     continue;
1971                                 }
1972 
1973                                 if (V) {
1974                                     BluetoothMapUtils.printCursor(c);
1975                                 }
1976 
1977                                 BluetoothMapConvoContactElement contact =
1978                                         getContactList().remove(uci);
1979 
1980                                 /*
1981                                  * We must filter out any actions made by the
1982                                  * MCE, hence do not send e.g. a message deleted
1983                                  * and/or MessageShift for messages deleted by
1984                                  * the MCE.
1985                                  */
1986                                 if (contact == null) {
1987                                     listChanged = true;
1988                                     /*
1989                                      * New contact - added to conversation and
1990                                      * tracked here
1991                                      */
1992                                     if (mMapEventReportVersion
1993                                             != BluetoothMapUtils.MAP_EVENT_REPORT_V10
1994                                             && mMapEventReportVersion
1995                                             != BluetoothMapUtils.MAP_EVENT_REPORT_V11) {
1996                                         Event evt;
1997                                         String name = c.getString(cInfo.mContactColName);
1998                                         String displayName = c.getString(cInfo.mContactColNickname);
1999                                         String presenceStatus =
2000                                                 c.getString(cInfo.mContactColPresenceText);
2001                                         int presenceState =
2002                                                 c.getInt(cInfo.mContactColPresenceState);
2003                                         long lastActivity = c.getLong(cInfo.mContactColLastActive);
2004                                         int chatState = c.getInt(cInfo.mContactColChatState);
2005                                         int priority = c.getInt(cInfo.mContactColPriority);
2006                                         String btUid = c.getString(cInfo.mContactColBtUid);
2007 
2008                                         // Get Conversation information for
2009                                         // event
2010 //                                        Uri convoUri = Uri
2011 //                                                .parse(mAccount.mBase_uri
2012 //                                                        + "/"
2013 //                                                        + BluetoothMapContract
2014 // .TABLE_CONVERSATION);
2015 //                                        String whereClause = "contacts._id = "
2016 //                                                + convoId;
2017 //                                        Cursor cConvo = mProviderClient
2018 //                                                .query(convoUri,
2019 //                                                       BluetoothMapContract
2020 // .BT_CONVERSATION_PROJECTION,
2021 //                                                       whereClause, null, null);
2022                                         // TODO: will move out of the loop when merged with CB's
2023                                         // changes make sure to look up col index out side loop
2024                                         String convoName = null;
2025 //                                        if (cConvo != null
2026 //                                                && cConvo.moveToFirst()) {
2027 //                                            convoName = cConvo
2028 //                                                    .getString(cConvo
2029 //                                                            .getColumnIndex
2030 // (BluetoothMapContract.ConvoContactColumns.NAME));
2031 //                                        }
2032 
2033                                         contact = new BluetoothMapConvoContactElement(uci, name,
2034                                                 displayName, presenceStatus, presenceState,
2035                                                 lastActivity, chatState, priority, btUid);
2036 
2037                                         contactList.put(uci, contact);
2038 
2039                                         evt = new Event(EVENT_TYPE_CONVERSATION, uci,
2040                                                 mAccount.getType(), name, String.valueOf(priority),
2041                                                 BluetoothMapUtils.getDateTimeString(lastActivity),
2042                                                 convoId, convoName, presenceState, presenceStatus,
2043                                                 chatState);
2044 
2045                                         sendEvent(evt);
2046                                     }
2047 
2048                                 } else {
2049                                     // Not new - compare updated content
2050 //                                    Uri convoUri = Uri
2051 //                                            .parse(mAccount.mBase_uri
2052 //                                                    + "/"
2053 //                                                    + BluetoothMapContract.TABLE_CONVERSATION);
2054                                     // TODO: Should be changed to own provider interface name
2055 //                                    String whereClause = "contacts._id = "
2056 //                                            + convoId;
2057 //                                    Cursor cConvo = mProviderClient
2058 //                                            .query(convoUri,
2059 //                                                    BluetoothMapContract
2060 // .BT_CONVERSATION_PROJECTION,
2061 //                                                    whereClause, null, null);
2062 //                                    // TODO: will move out of the loop when merged with CB's
2063 //                                    // changes make sure to look up col index out side loop
2064                                     String convoName = null;
2065 //                                    if (cConvo != null && cConvo.moveToFirst()) {
2066 //                                        convoName = cConvo
2067 //                                                .getString(cConvo
2068 //                                                        .getColumnIndex(BluetoothMapContract
2069 // .ConvoContactColumns.NAME));
2070 //                                    }
2071 
2072                                     // Check if presence is updated
2073                                     int presenceState = c.getInt(cInfo.mContactColPresenceState);
2074                                     String presenceStatus =
2075                                             c.getString(cInfo.mContactColPresenceText);
2076                                     String currentPresenceStatus = contact.getPresenceStatus();
2077                                     if (contact.getPresenceAvailability() != presenceState
2078                                             || !Objects.equals(currentPresenceStatus,
2079                                             presenceStatus)) {
2080                                         long lastOnline = c.getLong(cInfo.mContactColLastOnline);
2081                                         contact.setPresenceAvailability(presenceState);
2082                                         contact.setLastActivity(lastOnline);
2083                                         if (currentPresenceStatus != null
2084                                                 && !currentPresenceStatus.equals(presenceStatus)) {
2085                                             contact.setPresenceStatus(presenceStatus);
2086                                         }
2087                                         Event evt = new Event(EVENT_TYPE_PRESENCE, uci,
2088                                                 mAccount.getType(), contact.getName(),
2089                                                 String.valueOf(contact.getPriority()),
2090                                                 BluetoothMapUtils.getDateTimeString(lastOnline),
2091                                                 convoId, convoName, presenceState, presenceStatus,
2092                                                 0);
2093                                         sendEvent(evt);
2094                                     }
2095 
2096                                     // Check if chat state is updated
2097                                     int chatState = c.getInt(cInfo.mContactColChatState);
2098                                     if (contact.getChatState() != chatState) {
2099                                         // Get DB timestamp
2100                                         long lastActivity = c.getLong(cInfo.mContactColLastActive);
2101                                         contact.setLastActivity(lastActivity);
2102                                         contact.setChatState(chatState);
2103                                         Event evt = new Event(EVENT_TYPE_CHAT_STATE, uci,
2104                                                 mAccount.getType(), contact.getName(),
2105                                                 String.valueOf(contact.getPriority()),
2106                                                 BluetoothMapUtils.getDateTimeString(lastActivity),
2107                                                 convoId, convoName, 0, null, chatState);
2108                                         sendEvent(evt);
2109                                     }
2110                                     contactList.put(uci, contact);
2111                                 }
2112                             } while (c.moveToNext());
2113                         }
2114                         if (getContactList().size() > 0) {
2115                             // one or more contacts were deleted, hence the conversation listing
2116                             // version counter should change.
2117                             listChanged = true;
2118                         }
2119                         setContactList(contactList, listChanged);
2120                     } // end synchronized
2121                 } finally {
2122                     if (c != null) {
2123                         c.close();
2124                     }
2125                 }
2126             } catch (RemoteException e) {
2127                 mMasInstance.restartObexServerSession();
2128                 Log.w(TAG, "Problems contacting the ContentProvider in mas Instance " + mMasId
2129                         + " restaring ObexServerSession");
2130             }
2131 
2132         }
2133         // TODO: conversation contact updates if IM and SMS(MMS in one instance
2134     }
2135 
setEmailMessageStatusDelete(BluetoothMapFolderElement mCurrentFolder, String uriStr, long handle, int status)2136     private boolean setEmailMessageStatusDelete(BluetoothMapFolderElement mCurrentFolder,
2137             String uriStr, long handle, int status) {
2138         boolean res = false;
2139         Uri uri = Uri.parse(uriStr + BluetoothMapContract.TABLE_MESSAGE);
2140 
2141         int updateCount = 0;
2142         ContentValues contentValues = new ContentValues();
2143         BluetoothMapFolderElement deleteFolder =
2144                 mFolders.getFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED);
2145         contentValues.put(BluetoothMapContract.MessageColumns._ID, handle);
2146         synchronized (getMsgListMsg()) {
2147             Msg msg = getMsgListMsg().get(handle);
2148             if (status == BluetoothMapAppParams.STATUS_VALUE_YES) {
2149                 /* Set deleted folder id */
2150                 long folderId = -1;
2151                 if (deleteFolder != null) {
2152                     folderId = deleteFolder.getFolderId();
2153                 }
2154                 contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId);
2155                 updateCount = mResolver.update(uri, contentValues, null, null);
2156                 /* The race between updating the value in our cached values and the database
2157                  * is handled by the synchronized statement. */
2158                 if (updateCount > 0) {
2159                     res = true;
2160                     if (msg != null) {
2161                         msg.oldFolderId = msg.folderId;
2162                         /* Update the folder ID to avoid triggering an event for MCE
2163                          * initiated actions. */
2164                         msg.folderId = folderId;
2165                     }
2166                     if (D) {
2167                         Log.d(TAG, "Deleted MSG: " + handle + " from folderId: " + folderId);
2168                     }
2169                 } else {
2170                     Log.w(TAG, "Msg: " + handle + " - Set delete status " + status
2171                             + " failed for folderId " + folderId);
2172                 }
2173             } else if (status == BluetoothMapAppParams.STATUS_VALUE_NO) {
2174                 /* Undelete message. move to old folder if we know it,
2175                  * else move to inbox - as dictated by the spec. */
2176                 if (msg != null && deleteFolder != null
2177                         && msg.folderId == deleteFolder.getFolderId()) {
2178                     /* Only modify messages in the 'Deleted' folder */
2179                     long folderId = -1;
2180                     BluetoothMapFolderElement inboxFolder =
2181                             mCurrentFolder.getFolderByName(BluetoothMapContract.FOLDER_NAME_INBOX);
2182                     if (msg != null && msg.oldFolderId != -1) {
2183                         folderId = msg.oldFolderId;
2184                     } else {
2185                         if (inboxFolder != null) {
2186                             folderId = inboxFolder.getFolderId();
2187                         }
2188                         if (D) {
2189                             Log.d(TAG, "We did not delete the message, hence the old folder "
2190                                     + "is unknown. Moving to inbox.");
2191                         }
2192                     }
2193                     contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId);
2194                     updateCount = mResolver.update(uri, contentValues, null, null);
2195                     if (updateCount > 0) {
2196                         res = true;
2197                         /* Update the folder ID to avoid triggering an event for MCE
2198                          * initiated actions. */
2199                         /* UPDATE: Actually the BT-Spec. states that an undelete is a move of the
2200                          * message to INBOX - clearified in errata 5591.
2201                          * Therefore we update the cache to INBOX-folderId - to trigger a message
2202                          * shift event to the old-folder. */
2203                         if (inboxFolder != null) {
2204                             msg.folderId = inboxFolder.getFolderId();
2205                         } else {
2206                             msg.folderId = folderId;
2207                         }
2208                     } else {
2209                         if (D) {
2210                             Log.d(TAG, "We did not delete the message, hence the old folder "
2211                                     + "is unknown. Moving to inbox.");
2212                         }
2213                     }
2214                 }
2215             }
2216             if (V) {
2217                 BluetoothMapFolderElement folderElement;
2218                 String folderName = "unknown";
2219                 if (msg != null) {
2220                     folderElement = mCurrentFolder.getFolderById(msg.folderId);
2221                     if (folderElement != null) {
2222                         folderName = folderElement.getName();
2223                     }
2224                 }
2225                 Log.d(TAG, "setEmailMessageStatusDelete: " + handle + " from " + folderName
2226                         + " status: " + status);
2227             }
2228         }
2229         if (!res) {
2230             Log.w(TAG, "Set delete status " + status + " failed.");
2231         }
2232         return res;
2233     }
2234 
updateThreadId(Uri uri, String valueString, long threadId)2235     private void updateThreadId(Uri uri, String valueString, long threadId) {
2236         ContentValues contentValues = new ContentValues();
2237         contentValues.put(valueString, threadId);
2238         mResolver.update(uri, contentValues, null, null);
2239     }
2240 
deleteMessageMms(long handle)2241     private boolean deleteMessageMms(long handle) {
2242         boolean res = false;
2243         Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
2244         Cursor c = mResolver.query(uri, null, null, null, null);
2245         try {
2246             if (c != null && c.moveToFirst()) {
2247                 /* Move to deleted folder, or delete if already in deleted folder */
2248                 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
2249                 if (threadId != DELETED_THREAD_ID) {
2250                     /* Set deleted thread id */
2251                     synchronized (getMsgListMms()) {
2252                         Msg msg = getMsgListMms().get(handle);
2253                         if (msg != null) { // This will always be the case
2254                             msg.threadId = DELETED_THREAD_ID;
2255                         }
2256                     }
2257                     updateThreadId(uri, Mms.THREAD_ID, DELETED_THREAD_ID);
2258                 } else {
2259                     /* Delete from observer message list to avoid delete notifications */
2260                     synchronized (getMsgListMms()) {
2261                         getMsgListMms().remove(handle);
2262                     }
2263                     /* Delete message */
2264                     mResolver.delete(uri, null, null);
2265                 }
2266                 res = true;
2267             }
2268         } finally {
2269             if (c != null) {
2270                 c.close();
2271             }
2272         }
2273 
2274         return res;
2275     }
2276 
unDeleteMessageMms(long handle)2277     private boolean unDeleteMessageMms(long handle) {
2278         boolean res = false;
2279         Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
2280         Cursor c = mResolver.query(uri, null, null, null, null);
2281         try {
2282             if (c != null && c.moveToFirst()) {
2283                 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
2284                 if (threadId == DELETED_THREAD_ID) {
2285                     /* Restore thread id from address, or if no thread for address
2286                      * create new thread by insert and remove of fake message */
2287                     String address;
2288                     long id = c.getLong(c.getColumnIndex(Mms._ID));
2289                     int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
2290                     if (msgBox == Mms.MESSAGE_BOX_INBOX) {
2291                         address = BluetoothMapContent.getAddressMms(mResolver, id,
2292                                 BluetoothMapContent.MMS_FROM);
2293                     } else {
2294                         address = BluetoothMapContent.getAddressMms(mResolver, id,
2295                                 BluetoothMapContent.MMS_TO);
2296                     }
2297                     Set<String> recipients = new HashSet<String>();
2298                     recipients.addAll(Arrays.asList(address));
2299                     Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients);
2300                     synchronized (getMsgListMms()) {
2301                         Msg msg = getMsgListMms().get(handle);
2302                         if (msg != null) { // This will always be the case
2303                             msg.threadId = oldThreadId.intValue();
2304                             // Spec. states that undelete shall shift the message to Inbox.
2305                             // Hence we need to trigger a message shift from INBOX to old-folder
2306                             // after undelete.
2307                             // We do this by changing the cached folder value to being inbox - hence
2308                             // the event handler will se the update as the message have been shifted
2309                             // from INBOX to old-folder. (Errata 5591 clearifies this)
2310                             msg.type = Mms.MESSAGE_BOX_INBOX;
2311                         }
2312                     }
2313                     updateThreadId(uri, Mms.THREAD_ID, oldThreadId);
2314                 } else {
2315                     Log.d(TAG, "Message not in deleted folder: handle " + handle + " threadId "
2316                             + threadId);
2317                 }
2318                 res = true;
2319             }
2320         } finally {
2321             if (c != null) {
2322                 c.close();
2323             }
2324         }
2325         return res;
2326     }
2327 
deleteMessageSms(long handle)2328     private boolean deleteMessageSms(long handle) {
2329         boolean res = false;
2330         Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
2331         Cursor c = mResolver.query(uri, null, null, null, null);
2332         try {
2333             if (c != null && c.moveToFirst()) {
2334                 /* Move to deleted folder, or delete if already in deleted folder */
2335                 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
2336                 if (threadId != DELETED_THREAD_ID) {
2337                     synchronized (getMsgListSms()) {
2338                         Msg msg = getMsgListSms().get(handle);
2339                         if (msg != null) { // This will always be the case
2340                             msg.threadId = DELETED_THREAD_ID;
2341                         }
2342                     }
2343                     /* Set deleted thread id */
2344                     updateThreadId(uri, Sms.THREAD_ID, DELETED_THREAD_ID);
2345                 } else {
2346                     /* Delete from observer message list to avoid delete notifications */
2347                     synchronized (getMsgListSms()) {
2348                         getMsgListSms().remove(handle);
2349                     }
2350                     /* Delete message */
2351                     mResolver.delete(uri, null, null);
2352                 }
2353                 res = true;
2354             }
2355         } finally {
2356             if (c != null) {
2357                 c.close();
2358             }
2359         }
2360         return res;
2361     }
2362 
unDeleteMessageSms(long handle)2363     private boolean unDeleteMessageSms(long handle) {
2364         boolean res = false;
2365         Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
2366         Cursor c = mResolver.query(uri, null, null, null, null);
2367         try {
2368             if (c != null && c.moveToFirst()) {
2369                 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
2370                 if (threadId == DELETED_THREAD_ID) {
2371                     String address = c.getString(c.getColumnIndex(Sms.ADDRESS));
2372                     Set<String> recipients = new HashSet<String>();
2373                     recipients.addAll(Arrays.asList(address));
2374                     Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients);
2375                     synchronized (getMsgListSms()) {
2376                         Msg msg = getMsgListSms().get(handle);
2377                         if (msg != null) {
2378                             msg.threadId = oldThreadId.intValue();
2379                             /* This will always be the case
2380                              * The threadId is specified as an int, so it is safe to truncate
2381                              * TODO: Test that this will trigger a message-shift from Inbox
2382                              * to old-folder
2383                              **/
2384                             /* Spec. states that undelete shall shift the message to Inbox.
2385                              * Hence we need to trigger a message shift from INBOX to old-folder
2386                              * after undelete.
2387                              * We do this by changing the cached folder value to being inbox - hence
2388                              * the event handler will se the update as the message have been shifted
2389                              * from INBOX to old-folder. (Errata 5591 clearifies this)
2390                              * */
2391                             msg.type = Sms.MESSAGE_TYPE_INBOX;
2392                         }
2393                     }
2394                     updateThreadId(uri, Sms.THREAD_ID, oldThreadId);
2395                 } else {
2396                     Log.d(TAG, "Message not in deleted folder: handle " + handle + " threadId "
2397                             + threadId);
2398                 }
2399                 res = true;
2400             }
2401         } finally {
2402             if (c != null) {
2403                 c.close();
2404             }
2405         }
2406         return res;
2407     }
2408 
2409     /**
2410      *
2411      * @param handle
2412      * @param type
2413      * @param mCurrentFolder
2414      * @param uriStr
2415      * @param statusValue
2416      * @return true is success
2417      */
setMessageStatusDeleted(long handle, TYPE type, BluetoothMapFolderElement mCurrentFolder, String uriStr, int statusValue)2418     public boolean setMessageStatusDeleted(long handle, TYPE type,
2419             BluetoothMapFolderElement mCurrentFolder, String uriStr, int statusValue) {
2420         boolean res = false;
2421         if (D) {
2422             Log.d(TAG, "setMessageStatusDeleted: handle " + handle + " type " + type + " value "
2423                     + statusValue);
2424         }
2425 
2426         if (type == TYPE.EMAIL) {
2427             res = setEmailMessageStatusDelete(mCurrentFolder, uriStr, handle, statusValue);
2428         } else if (type == TYPE.IM) {
2429             // TODO: to do when deleting IM message
2430             if (D) {
2431                 Log.d(TAG, "setMessageStatusDeleted: IM not handled");
2432             }
2433         } else {
2434             if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) {
2435                 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
2436                     res = deleteMessageSms(handle);
2437                 } else if (type == TYPE.MMS) {
2438                     res = deleteMessageMms(handle);
2439                 }
2440             } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) {
2441                 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
2442                     res = unDeleteMessageSms(handle);
2443                 } else if (type == TYPE.MMS) {
2444                     res = unDeleteMessageMms(handle);
2445                 }
2446             }
2447         }
2448         return res;
2449     }
2450 
2451     /**
2452      *
2453      * @param handle
2454      * @param type
2455      * @param uriStr
2456      * @param statusValue
2457      * @return true at success
2458      */
setMessageStatusRead(long handle, TYPE type, String uriStr, int statusValue)2459     public boolean setMessageStatusRead(long handle, TYPE type, String uriStr, int statusValue)
2460             throws RemoteException {
2461         int count = 0;
2462 
2463         if (D) {
2464             Log.d(TAG, "setMessageStatusRead: handle " + handle + " type " + type + " value "
2465                     + statusValue);
2466         }
2467 
2468         /* Approved MAP spec errata 3445 states that read status initiated
2469          * by the MCE shall change the MSE read status. */
2470         if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
2471             Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
2472             ContentValues contentValues = new ContentValues();
2473             contentValues.put(Sms.READ, statusValue);
2474             contentValues.put(Sms.SEEN, statusValue);
2475             String values = contentValues.toString();
2476             if (D) {
2477                 Log.d(TAG, " -> SMS Uri: " + uri.toString() + " values " + values);
2478             }
2479             synchronized (getMsgListSms()) {
2480                 Msg msg = getMsgListSms().get(handle);
2481                 if (msg != null) { // This will always be the case
2482                     msg.flagRead = statusValue;
2483                 }
2484             }
2485             count = mResolver.update(uri, contentValues, null, null);
2486             if (D) {
2487                 Log.d(TAG, " -> " + count + " rows updated!");
2488             }
2489 
2490         } else if (type == TYPE.MMS) {
2491             Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
2492             if (D) {
2493                 Log.d(TAG, " -> MMS Uri: " + uri.toString());
2494             }
2495             ContentValues contentValues = new ContentValues();
2496             contentValues.put(Mms.READ, statusValue);
2497             synchronized (getMsgListMms()) {
2498                 Msg msg = getMsgListMms().get(handle);
2499                 if (msg != null) { // This will always be the case
2500                     msg.flagRead = statusValue;
2501                 }
2502             }
2503             count = mResolver.update(uri, contentValues, null, null);
2504             if (D) {
2505                 Log.d(TAG, " -> " + count + " rows updated!");
2506             }
2507         } else if (type == TYPE.EMAIL || type == TYPE.IM) {
2508             Uri uri = mMessageUri;
2509             ContentValues contentValues = new ContentValues();
2510             contentValues.put(BluetoothMapContract.MessageColumns.FLAG_READ, statusValue);
2511             contentValues.put(BluetoothMapContract.MessageColumns._ID, handle);
2512             synchronized (getMsgListMsg()) {
2513                 Msg msg = getMsgListMsg().get(handle);
2514                 if (msg != null) { // This will always be the case
2515                     msg.flagRead = statusValue;
2516                 }
2517             }
2518             count = mProviderClient.update(uri, contentValues, null, null);
2519         }
2520 
2521         return (count > 0);
2522     }
2523 
2524     private class PushMsgInfo {
2525         public long id;
2526         public int transparent;
2527         public int retry;
2528         public String phone;
2529         public Uri uri;
2530         public long timestamp;
2531         public int parts;
2532         public int partsSent;
2533         public int partsDelivered;
2534         public boolean resend;
2535         public boolean sendInProgress;
2536         public boolean failedSent; // Set to true if a single part sent fail is received.
2537         public int statusDelivered; // Set to != 0 if a single part deliver fail is received.
2538 
PushMsgInfo(long id, int transparent, int retry, String phone, Uri uri)2539         PushMsgInfo(long id, int transparent, int retry, String phone, Uri uri) {
2540             this.id = id;
2541             this.transparent = transparent;
2542             this.retry = retry;
2543             this.phone = phone;
2544             this.uri = uri;
2545             this.resend = false;
2546             this.sendInProgress = false;
2547             this.failedSent = false;
2548             this.statusDelivered = 0; /* Assume success */
2549             this.timestamp = 0;
2550         }
2551 
2552         ;
2553     }
2554 
2555     private Map<Long, PushMsgInfo> mPushMsgList =
2556             Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>());
2557 
2558     /**
2559      * Add an SMS to the given URI.
2560      *
2561      * @param resolver the content resolver to use
2562      * @param uri the URI to add the message to
2563      * @param address the address of the sender
2564      * @param body the body of the message
2565      * @param subject the pseudo-subject of the message
2566      * @param date the timestamp for the message
2567      * @return the URI for the new message
2568      */
addMessageToUri(ContentResolver resolver, Uri uri, String address, String body, String subject, Long date)2569     private static Uri addMessageToUri(ContentResolver resolver, Uri uri,
2570                                       String address, String body, String subject,
2571                                       Long date) {
2572         ContentValues values = new ContentValues(7);
2573         final int statusPending = 32;
2574         final int subId = SubscriptionManager.getDefaultSmsSubscriptionId();
2575         Log.v(TAG, "Telephony addMessageToUri sub id: " + subId);
2576 
2577         values.put(Telephony.Sms.SUBSCRIPTION_ID, subId);
2578         values.put(Telephony.Sms.ADDRESS, address);
2579         if (date != null) {
2580             values.put(Telephony.Sms.DATE, date);
2581         }
2582         values.put(Telephony.Sms.READ, 0);
2583         values.put(Telephony.Sms.SUBJECT, subject);
2584         values.put(Telephony.Sms.BODY, body);
2585         values.put(Telephony.Sms.STATUS, statusPending);
2586         return resolver.insert(uri, values);
2587     }
2588 
pushMessage(BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement, BluetoothMapAppParams ap, String emailBaseUri)2589     public long pushMessage(BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement,
2590             BluetoothMapAppParams ap, String emailBaseUri)
2591             throws IllegalArgumentException, RemoteException, IOException {
2592         if (D) {
2593             Log.d(TAG, "pushMessage");
2594         }
2595         ArrayList<BluetoothMapbMessage.VCard> recipientList = msg.getRecipients();
2596         int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ? 0
2597                 : ap.getTransparent();
2598         int retry = ap.getRetry();
2599         int charset = ap.getCharset();
2600         long handle = -1;
2601         long folderId = -1;
2602 
2603         if (recipientList == null) {
2604             if (folderElement.getName().equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT)) {
2605                 BluetoothMapbMessage.VCard empty =
2606                         new BluetoothMapbMessage.VCard("", "", null, null, 0);
2607                 recipientList = new ArrayList<BluetoothMapbMessage.VCard>();
2608                 recipientList.add(empty);
2609                 Log.w(TAG, "Added empty recipient to draft message");
2610             } else {
2611                 Log.e(TAG, "Trying to send a message with no recipients");
2612                 return -1;
2613             }
2614         }
2615 
2616         if (msg.getType().equals(TYPE.EMAIL)) {
2617             /* Write the message to the database */
2618             String msgBody = ((BluetoothMapbMessageEmail) msg).getEmailBody();
2619             if (V) {
2620                 int length = msgBody.length();
2621                 Log.v(TAG, "pushMessage: message string length = " + length);
2622                 String[] messages = msgBody.split("\r\n");
2623                 Log.v(TAG, "pushMessage: messages count=" + messages.length);
2624                 for (int i = 0; i < messages.length; i++) {
2625                     Log.v(TAG, "part " + i + ":" + messages[i]);
2626                 }
2627             }
2628             FileOutputStream os = null;
2629             ParcelFileDescriptor fdOut = null;
2630             Uri uriInsert = Uri.parse(emailBaseUri + BluetoothMapContract.TABLE_MESSAGE);
2631             if (D) {
2632                 Log.d(TAG, "pushMessage - uriInsert= " + uriInsert.toString() + ", intoFolder id="
2633                         + folderElement.getFolderId());
2634             }
2635 
2636             synchronized (getMsgListMsg()) {
2637                 // Now insert the empty message into folder
2638                 ContentValues values = new ContentValues();
2639                 folderId = folderElement.getFolderId();
2640                 values.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId);
2641                 Uri uriNew = mProviderClient.insert(uriInsert, values);
2642                 if (D) {
2643                     Log.d(TAG, "pushMessage - uriNew= " + uriNew.toString());
2644                 }
2645                 handle = Long.parseLong(uriNew.getLastPathSegment());
2646 
2647                 try {
2648                     fdOut = mProviderClient.openFile(uriNew, "w");
2649                     os = new FileOutputStream(fdOut.getFileDescriptor());
2650                     // Write Email to DB
2651                     os.write(msgBody.getBytes(), 0, msgBody.getBytes().length);
2652                 } catch (FileNotFoundException e) {
2653                     Log.w(TAG, e);
2654                     throw (new IOException("Unable to open file stream"));
2655                 } catch (NullPointerException e) {
2656                     Log.w(TAG, e);
2657                     throw (new IllegalArgumentException("Unable to parse message."));
2658                 } finally {
2659                     try {
2660                         if (os != null) {
2661                             os.close();
2662                         }
2663                     } catch (IOException e) {
2664                         Log.w(TAG, e);
2665                     }
2666                     try {
2667                         if (fdOut != null) {
2668                             fdOut.close();
2669                         }
2670                     } catch (IOException e) {
2671                         Log.w(TAG, e);
2672                     }
2673                 }
2674 
2675                 /* Extract the data for the inserted message, and store in local mirror, to
2676                  * avoid sending a NewMessage Event. */
2677                 /*TODO: We need to add the new 1.1 parameter as well:-) e.g. read*/
2678                 Msg newMsg = new Msg(handle, folderId, 1); // TODO: Create define for read-state
2679                 newMsg.transparent = transparent == 1;
2680                 if (folderId == folderElement.getFolderByName(
2681                         BluetoothMapContract.FOLDER_NAME_OUTBOX).getFolderId()) {
2682                     newMsg.localInitiatedSend = true;
2683                 }
2684                 getMsgListMsg().put(handle, newMsg);
2685             }
2686         } else if (msg.getType().equals(TYPE.MMS) && (recipientList.size() > 1)) {
2687             // Group MMS
2688             String folder = folderElement.getName();
2689             ArrayList<String> telNums = new ArrayList<String>();
2690             for (BluetoothMapbMessage.VCard recipient : recipientList) {
2691                 // Only send the message to the top level recipient
2692                 if (recipient.getEnvLevel() == 0) {
2693                     // Only send to recipient's first phone number
2694                     telNums.add(recipient.getFirstPhoneNumber());
2695                 }
2696             }
2697             // Send message if folder is outbox else just store in draft
2698             handle = sendMmsMessage(folder, telNums.toArray(new String[telNums.size()]),
2699                     (BluetoothMapbMessageMime) msg, transparent, retry);
2700         } else { // type SMS_* (single or mass text) or single MMS
2701             for (BluetoothMapbMessage.VCard recipient : recipientList) {
2702                 // Only send the message to the top level recipient
2703                 if (recipient.getEnvLevel() == 0) {
2704                     /* Only send to first address */
2705                     String phone = recipient.getFirstPhoneNumber();
2706                     String email = recipient.getFirstEmail();
2707                     String folder = folderElement.getName();
2708                     String msgBody = null;
2709 
2710                     /* If MMS contains text only and the size is less than ten SMS's
2711                      * then convert the MMS to type SMS and then proceed
2712                      */
2713                     if (msg.getType().equals(TYPE.MMS)
2714                             && (((BluetoothMapbMessageMime) msg).getTextOnly())) {
2715                         msgBody = ((BluetoothMapbMessageMime) msg).getMessageAsText();
2716                         SmsManager smsMng = SmsManager.getDefault();
2717                         ArrayList<String> parts = smsMng.divideMessage(msgBody);
2718                         int smsParts = parts.size();
2719                         if (smsParts <= CONVERT_MMS_TO_SMS_PART_COUNT) {
2720                             if (D) {
2721                                 Log.d(TAG, "pushMessage - converting MMS to SMS, sms parts="
2722                                         + smsParts);
2723                             }
2724                             msg.setType(mSmsType);
2725                         } else {
2726                             if (D) {
2727                                 Log.d(TAG, "pushMessage - MMS text only but to big to "
2728                                         + "convert to SMS");
2729                             }
2730                             msgBody = null;
2731                         }
2732 
2733                     }
2734 
2735                     if (msg.getType().equals(TYPE.MMS)) {
2736                         /* Send message if folder is outbox else just store in draft*/
2737                         handle = sendMmsMessage(folder, new String[] {phone},
2738                                 (BluetoothMapbMessageMime) msg, transparent, retry);
2739                     } else if (msg.getType().equals(TYPE.SMS_GSM) || msg.getType()
2740                             .equals(TYPE.SMS_CDMA)) {
2741                         /* Add the message to the database */
2742                         if (msgBody == null) {
2743                             msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody();
2744                         }
2745 
2746                         if (TextUtils.isEmpty(msgBody)) {
2747                             Log.d(TAG, "PushMsg: Empty msgBody ");
2748                             /* not allowed to push empty message */
2749                             throw new IllegalArgumentException("push EMPTY message: Invalid Body");
2750                         }
2751                         /* We need to lock the SMS list while updating the database,
2752                          * to avoid sending events on MCE initiated operation. */
2753                         Uri contentUri = Uri.parse(Sms.CONTENT_URI + "/" + folder);
2754                         Uri uri;
2755                         synchronized (getMsgListSms()) {
2756                             uri = addMessageToUri(mResolver, contentUri, phone, msgBody, "",
2757                                     System.currentTimeMillis());
2758 
2759                             if (V) {
2760                                 Log.v(TAG, "Sms.addMessageToUri() returned: " + uri);
2761                             }
2762                             if (uri == null) {
2763                                 if (D) {
2764                                     Log.d(TAG, "pushMessage - failure on add to uri " + contentUri);
2765                                 }
2766                                 return -1;
2767                             }
2768                             Cursor c = mResolver.query(uri, SMS_PROJECTION_SHORT, null, null, null);
2769 
2770                             /* Extract the data for the inserted message, and store in local mirror,
2771                              * to avoid sending a NewMessage Event. */
2772                             try {
2773                                 if (c != null && c.moveToFirst()) {
2774                                     long id = c.getLong(c.getColumnIndex(Sms._ID));
2775                                     int type = c.getInt(c.getColumnIndex(Sms.TYPE));
2776                                     int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
2777                                     int readFlag = c.getInt(c.getColumnIndex(Sms.READ));
2778                                     if (V) {
2779                                         Log.v(TAG, "add message with id=" + id + " type=" + type
2780                                                 + " threadId=" + threadId + " readFlag=" + readFlag
2781                                                 + "to mMsgListSms");
2782                                     }
2783                                     Msg newMsg = new Msg(id, type, threadId, readFlag);
2784                                     getMsgListSms().put(id, newMsg);
2785                                     c.close();
2786                                 } else {
2787                                     Log.w(TAG, "Message: " + uri + " no longer exist!");
2788                                     /* This can only happen, if the message is deleted
2789                                      * just as it is added */
2790                                     return -1;
2791                                 }
2792                             } finally {
2793                                 if (c != null) {
2794                                     c.close();
2795                                 }
2796                             }
2797 
2798                             handle = Long.parseLong(uri.getLastPathSegment());
2799 
2800                             /* Send message if folder is outbox */
2801                             if (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)) {
2802                                 PushMsgInfo msgInfo =
2803                                         new PushMsgInfo(handle, transparent, retry, phone, uri);
2804                                 mPushMsgList.put(handle, msgInfo);
2805                                 sendMessage(msgInfo, msgBody);
2806                                 if (V) {
2807                                     Log.v(TAG, "sendMessage returned...");
2808                                 }
2809                             } /* else just added to draft */
2810 
2811                             /* sendMessage causes the message to be deleted and reinserted,
2812                              * hence we need to lock the list while this is happening. */
2813                         }
2814                     } else {
2815                         if (D) {
2816                             Log.d(TAG, "pushMessage - failure on type ");
2817                         }
2818                         return -1;
2819                     }
2820                 }
2821             }
2822         }
2823 
2824         /* If multiple recipients return handle of last */
2825         return handle;
2826     }
2827 
sendMmsMessage(String folder, String[] toAddress, BluetoothMapbMessageMime msg, int transparent, int retry)2828     public long sendMmsMessage(String folder, String[] toAddress, BluetoothMapbMessageMime msg,
2829             int transparent, int retry) {
2830         /*
2831          *strategy:
2832          *1) parse message into parts
2833          *if folder is outbox/drafts:
2834          *2) push message to draft
2835          *if folder is outbox:
2836          *3) move message to outbox (to trigger the mms app to add msg to pending_messages list)
2837          *4) send intent to mms app in order to wake it up.
2838          *else if folder !outbox:
2839          *1) push message to folder
2840          * */
2841         if (folder != null && (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)
2842                 || folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT))) {
2843             long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, toAddress, msg);
2844             /* if invalid handle (-1) then just return the handle
2845              * - else continue sending (if folder is outbox) */
2846             if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase(
2847                     BluetoothMapContract.FOLDER_NAME_OUTBOX)) {
2848                 Uri btMmsUri = MmsFileProvider.CONTENT_URI.buildUpon()
2849                         .appendPath(Long.toString(handle))
2850                         .build();
2851                 Intent sentIntent = new Intent(ACTION_MESSAGE_SENT);
2852                 // TODO: update the mmsMsgList <- done in pushMmsToFolder() but check
2853                 sentIntent.setType("message/" + Long.toString(handle));
2854                 sentIntent.putExtra(EXTRA_MESSAGE_SENT_MSG_TYPE, TYPE.MMS.ordinal());
2855                 sentIntent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, handle); // needed for notification
2856                 sentIntent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, transparent);
2857                 sentIntent.putExtra(EXTRA_MESSAGE_SENT_RETRY, retry);
2858                 //sentIntent.setDataAndNormalize(btMmsUri);
2859                 PendingIntent pendingSendIntent =
2860                         PendingIntent.getBroadcast(mContext, 0, sentIntent, 0);
2861                 SmsManager.getDefault()
2862                         .sendMultimediaMessage(mContext, btMmsUri, null/*locationUrl*/,
2863                                 null/*configOverrides*/,
2864                                 pendingSendIntent);
2865             }
2866             return handle;
2867         } else {
2868             /* not allowed to push mms to anything but outbox/draft */
2869             throw new IllegalArgumentException(
2870                     "Cannot push message to other " + "folders than outbox/draft");
2871         }
2872     }
2873 
moveDraftToOutbox(long handle)2874     private void moveDraftToOutbox(long handle) {
2875         moveMmsToFolder(handle, mResolver, Mms.MESSAGE_BOX_OUTBOX);
2876     }
2877 
2878     /**
2879      * Move a MMS to another folder.
2880      * @param handle the CP handle of the message to move
2881      * @param resolver the ContentResolver to use
2882      * @param folder the destination folder - use Mms.MESSAGE_BOX_xxx
2883      */
moveMmsToFolder(long handle, ContentResolver resolver, int folder)2884     private static void moveMmsToFolder(long handle, ContentResolver resolver, int folder) {
2885         /*Move message by changing the msg_box value in the content provider database */
2886         if (handle != -1) {
2887             String whereClause = " _id= " + handle;
2888             Uri uri = Mms.CONTENT_URI;
2889             Cursor queryResult = resolver.query(uri, null, whereClause, null, null);
2890             try {
2891                 if (queryResult != null) {
2892                     if (queryResult.getCount() > 0) {
2893                         queryResult.moveToFirst();
2894                         ContentValues data = new ContentValues();
2895                         /* set folder to be outbox */
2896                         data.put(Mms.MESSAGE_BOX, folder);
2897                         resolver.update(uri, data, whereClause, null);
2898                         if (D) {
2899                             Log.d(TAG, "moved MMS message to " + getMmsFolderName(folder));
2900                         }
2901                     }
2902                 } else {
2903                     Log.w(TAG, "Could not move MMS message to " + getMmsFolderName(folder));
2904                 }
2905             } finally {
2906                 if (queryResult != null) {
2907                     queryResult.close();
2908                 }
2909             }
2910         }
2911     }
2912 
pushMmsToFolder(int folder, String[] toAddress, BluetoothMapbMessageMime msg)2913     private long pushMmsToFolder(int folder, String[] toAddress, BluetoothMapbMessageMime msg) {
2914         /**
2915          * strategy:
2916          * 1) parse msg into parts + header
2917          * 2) create thread id (abuse the ease of adding an SMS to get id for thread)
2918          * 3) push parts into content://mms/parts/ table
2919          * 3)
2920          */
2921 
2922         ContentValues values = new ContentValues();
2923         values.put(Mms.MESSAGE_BOX, folder);
2924         values.put(Mms.READ, 0);
2925         values.put(Mms.SEEN, 0);
2926         if (msg.getSubject() != null) {
2927             values.put(Mms.SUBJECT, msg.getSubject());
2928         } else {
2929             values.put(Mms.SUBJECT, "");
2930         }
2931 
2932         if (msg.getSubject() != null && msg.getSubject().length() > 0) {
2933             values.put(Mms.SUBJECT_CHARSET, 106);
2934         }
2935         values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related");
2936         values.put(Mms.EXPIRY, 604800);
2937         values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
2938         values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
2939         values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION);
2940         values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL);
2941         values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO);
2942         values.put(Mms.TRANSACTION_ID, "T" + Long.toHexString(System.currentTimeMillis()));
2943         values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO);
2944         values.put(Mms.LOCKED, 0);
2945         if (msg.getTextOnly()) {
2946             values.put(Mms.TEXT_ONLY, true);
2947         }
2948         values.put(Mms.MESSAGE_SIZE, msg.getSize());
2949 
2950         // Get thread id
2951         Set<String> recipients = new HashSet<String>();
2952         recipients.addAll(Arrays.asList(toAddress));
2953         values.put(Mms.THREAD_ID, Telephony.Threads.getOrCreateThreadId(mContext, recipients));
2954         Uri uri = Mms.CONTENT_URI;
2955 
2956         synchronized (getMsgListMms()) {
2957 
2958             uri = mResolver.insert(uri, values);
2959 
2960             if (uri == null) {
2961                 // unable to insert MMS
2962                 Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri);
2963                 return -1;
2964             }
2965             /* As we already have all the values we need, we could skip the query, but
2966                doing the query ensures we get any changes made by the content provider
2967                at insert. */
2968             Cursor c = mResolver.query(uri, MMS_PROJECTION_SHORT, null, null, null);
2969             try {
2970                 if (c != null && c.moveToFirst()) {
2971                     long id = c.getLong(c.getColumnIndex(Mms._ID));
2972                     int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
2973                     int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
2974                     int readStatus = c.getInt(c.getColumnIndex(Mms.READ));
2975 
2976                     /* We must filter out any actions made by the MCE. Add the new message to
2977                      * the list of known messages. */
2978 
2979                     Msg newMsg = new Msg(id, type, threadId, readStatus);
2980                     newMsg.localInitiatedSend = true;
2981                     getMsgListMms().put(id, newMsg);
2982                     c.close();
2983                 }
2984             } finally {
2985                 if (c != null) {
2986                     c.close();
2987                 }
2988             }
2989         } // Done adding changes, unlock access to mMsgListMms to allow sending MMS events again
2990 
2991         long handle = Long.parseLong(uri.getLastPathSegment());
2992         if (V) {
2993             Log.v(TAG, " NEW URI " + uri.toString());
2994         }
2995 
2996         try {
2997             if (msg.getMimeParts() == null) {
2998                 /* Perhaps this message have been deleted, and no longer have any content,
2999                  * but only headers */
3000                 Log.w(TAG, "No MMS parts present...");
3001             } else {
3002                 if (V) {
3003                     Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base.");
3004                 }
3005                 for (MimePart part : msg.getMimeParts()) {
3006                     int count = 0;
3007                     count++;
3008                     values.clear();
3009                     if (part.mContentType != null && part.mContentType.toUpperCase()
3010                             .contains("TEXT")) {
3011                         values.put(Mms.Part.CONTENT_TYPE, "text/plain");
3012                         values.put(Mms.Part.CHARSET, 106);
3013                         if (part.mPartName != null) {
3014                             values.put(Mms.Part.FILENAME, part.mPartName);
3015                             values.put(Mms.Part.NAME, part.mPartName);
3016                         } else {
3017                             values.put(Mms.Part.FILENAME, "text_" + count + ".txt");
3018                             values.put(Mms.Part.NAME, "text_" + count + ".txt");
3019                         }
3020                         // Ensure we have "ci" set
3021                         if (part.mContentId != null) {
3022                             values.put(Mms.Part.CONTENT_ID, part.mContentId);
3023                         } else {
3024                             if (part.mPartName != null) {
3025                                 values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">");
3026                             } else {
3027                                 values.put(Mms.Part.CONTENT_ID, "<text_" + count + ">");
3028                             }
3029                         }
3030                         // Ensure we have "cl" set
3031                         if (part.mContentLocation != null) {
3032                             values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
3033                         } else {
3034                             if (part.mPartName != null) {
3035                                 values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".txt");
3036                             } else {
3037                                 values.put(Mms.Part.CONTENT_LOCATION, "text_" + count + ".txt");
3038                             }
3039                         }
3040 
3041                         if (part.mContentDisposition != null) {
3042                             values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
3043                         }
3044                         values.put(Mms.Part.TEXT, part.getDataAsString());
3045                         uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part");
3046                         uri = mResolver.insert(uri, values);
3047                         if (V) {
3048                             Log.v(TAG, "Added TEXT part");
3049                         }
3050 
3051                     } else if (part.mContentType != null && part.mContentType.toUpperCase()
3052                             .contains("SMIL")) {
3053                         values.put(Mms.Part.SEQ, -1);
3054                         values.put(Mms.Part.CONTENT_TYPE, "application/smil");
3055                         if (part.mContentId != null) {
3056                             values.put(Mms.Part.CONTENT_ID, part.mContentId);
3057                         } else {
3058                             values.put(Mms.Part.CONTENT_ID, "<smil_" + count + ">");
3059                         }
3060                         if (part.mContentLocation != null) {
3061                             values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
3062                         } else {
3063                             values.put(Mms.Part.CONTENT_LOCATION, "smil_" + count + ".xml");
3064                         }
3065 
3066                         if (part.mContentDisposition != null) {
3067                             values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
3068                         }
3069                         values.put(Mms.Part.FILENAME, "smil.xml");
3070                         values.put(Mms.Part.NAME, "smil.xml");
3071                         values.put(Mms.Part.TEXT, new String(part.mData, "UTF-8"));
3072 
3073                         uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part");
3074                         uri = mResolver.insert(uri, values);
3075                         if (V) {
3076                             Log.v(TAG, "Added SMIL part");
3077                         }
3078 
3079                     } else /*VIDEO/AUDIO/IMAGE*/ {
3080                         writeMmsDataPart(handle, part, count);
3081                         if (V) {
3082                             Log.v(TAG, "Added OTHER part");
3083                         }
3084                     }
3085                     if (uri != null) {
3086                         if (V) {
3087                             Log.v(TAG, "Added part with content-type: " + part.mContentType
3088                                     + " to Uri: " + uri.toString());
3089                         }
3090                     }
3091                 }
3092             }
3093         } catch (UnsupportedEncodingException e) {
3094             Log.w(TAG, e);
3095         } catch (IOException e) {
3096             Log.w(TAG, e);
3097         }
3098 
3099         values.clear();
3100         values.put(Mms.Addr.CONTACT_ID, "null");
3101         values.put(Mms.Addr.ADDRESS, "insert-address-token");
3102         values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_FROM);
3103         values.put(Mms.Addr.CHARSET, 106);
3104 
3105         uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr");
3106         uri = mResolver.insert(uri, values);
3107         if (uri != null && V) {
3108             Log.v(TAG, " NEW URI " + uri.toString());
3109         }
3110 
3111         values.clear();
3112         values.put(Mms.Addr.CONTACT_ID, "null");
3113         values.put(Mms.Addr.ADDRESS, String.join(",", toAddress));
3114         values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_TO);
3115         values.put(Mms.Addr.CHARSET, 106);
3116 
3117         uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr");
3118         uri = mResolver.insert(uri, values);
3119         if (uri != null && V) {
3120             Log.v(TAG, " NEW URI " + uri.toString());
3121         }
3122         return handle;
3123     }
3124 
3125 
writeMmsDataPart(long handle, MimePart part, int count)3126     private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException {
3127         ContentValues values = new ContentValues();
3128         values.put(Mms.Part.MSG_ID, handle);
3129         if (part.mContentType != null) {
3130             values.put(Mms.Part.CONTENT_TYPE, part.mContentType);
3131         } else {
3132             Log.w(TAG, "MMS has no CONTENT_TYPE for part " + count);
3133         }
3134         if (part.mContentId != null) {
3135             values.put(Mms.Part.CONTENT_ID, part.mContentId);
3136         } else {
3137             if (part.mPartName != null) {
3138                 values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">");
3139             } else {
3140                 values.put(Mms.Part.CONTENT_ID, "<part_" + count + ">");
3141             }
3142         }
3143 
3144         if (part.mContentLocation != null) {
3145             values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
3146         } else {
3147             if (part.mPartName != null) {
3148                 values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".dat");
3149             } else {
3150                 values.put(Mms.Part.CONTENT_LOCATION, "part_" + count + ".dat");
3151             }
3152         }
3153         if (part.mContentDisposition != null) {
3154             values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
3155         }
3156         if (part.mPartName != null) {
3157             values.put(Mms.Part.FILENAME, part.mPartName);
3158             values.put(Mms.Part.NAME, part.mPartName);
3159         } else {
3160             /* We must set at least one part identifier */
3161             values.put(Mms.Part.FILENAME, "part_" + count + ".dat");
3162             values.put(Mms.Part.NAME, "part_" + count + ".dat");
3163         }
3164         Uri partUri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part");
3165         Uri res = mResolver.insert(partUri, values);
3166 
3167         // Add data to part
3168         OutputStream os = mResolver.openOutputStream(res);
3169         os.write(part.mData);
3170         os.close();
3171     }
3172 
3173 
sendMessage(PushMsgInfo msgInfo, String msgBody)3174     public void sendMessage(PushMsgInfo msgInfo, String msgBody) {
3175 
3176         SmsManager smsMng = SmsManager.getDefault();
3177         ArrayList<String> parts = smsMng.divideMessage(msgBody);
3178         msgInfo.parts = parts.size();
3179         // We add a time stamp to differentiate delivery reports from each other for resent messages
3180         msgInfo.timestamp = Calendar.getInstance().getTime().getTime();
3181         msgInfo.partsDelivered = 0;
3182         msgInfo.partsSent = 0;
3183 
3184         ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts);
3185         ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts);
3186 
3187         /*       We handle the SENT intent in the MAP service, as this object
3188          *       is destroyed at disconnect, hence if a disconnect occur while sending
3189          *       a message, there is no intent handler to move the message from outbox
3190          *       to the correct folder.
3191          *       The correct solution would be to create a service that will start based on
3192          *       the intent, if BT is turned off. */
3193 
3194         if (parts != null && parts.size() > 0) {
3195             for (int i = 0; i < msgInfo.parts; i++) {
3196                 Intent intentDelivery, intentSent;
3197 
3198                 intentDelivery = new Intent(ACTION_MESSAGE_DELIVERY, null);
3199                 /* Add msgId and part number to ensure the intents are different, and we
3200                  * thereby get an intent for each msg part.
3201                  * setType is needed to create different intents for each message id/ time stamp,
3202                  * as the extras are not used when comparing. */
3203                 intentDelivery.setType(
3204                         "message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i);
3205                 intentDelivery.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id);
3206                 intentDelivery.putExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, msgInfo.timestamp);
3207                 PendingIntent pendingIntentDelivery =
3208                         PendingIntent.getBroadcast(mContext, 0, intentDelivery,
3209                                 PendingIntent.FLAG_UPDATE_CURRENT);
3210 
3211                 intentSent = new Intent(ACTION_MESSAGE_SENT, null);
3212                 /* Add msgId and part number to ensure the intents are different, and we
3213                  * thereby get an intent for each msg part.
3214                  * setType is needed to create different intents for each message id/ time stamp,
3215                  * as the extras are not used when comparing. */
3216                 intentSent.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i);
3217                 intentSent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id);
3218                 intentSent.putExtra(EXTRA_MESSAGE_SENT_URI, msgInfo.uri.toString());
3219                 intentSent.putExtra(EXTRA_MESSAGE_SENT_RETRY, msgInfo.retry);
3220                 intentSent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, msgInfo.transparent);
3221 
3222                 PendingIntent pendingIntentSent =
3223                         PendingIntent.getBroadcast(mContext, 0, intentSent,
3224                                 PendingIntent.FLAG_UPDATE_CURRENT);
3225 
3226                 // We use the same pending intent for all parts, but do not set the one shot flag.
3227                 deliveryIntents.add(pendingIntentDelivery);
3228                 sentIntents.add(pendingIntentSent);
3229             }
3230 
3231             Log.d(TAG, "sendMessage to " + msgInfo.phone);
3232 
3233             if (parts.size() == 1) {
3234                 smsMng.sendTextMessageWithoutPersisting(msgInfo.phone, null, parts.get(0),
3235                         sentIntents.get(0), deliveryIntents.get(0));
3236             } else {
3237                 smsMng.sendMultipartTextMessageWithoutPersisting(msgInfo.phone, null, parts,
3238                         sentIntents, deliveryIntents);
3239             }
3240         }
3241     }
3242 
3243     private static final String[] ID_PROJECTION = new String[]{Sms._ID};
3244     private static final Uri UPDATE_STATUS_URI = Uri.withAppendedPath(Sms.CONTENT_URI, "/status");
3245 
3246     private class SmsBroadcastReceiver extends BroadcastReceiver {
register()3247         public void register() {
3248             Handler handler = new Handler(Looper.getMainLooper());
3249 
3250             IntentFilter intentFilter = new IntentFilter();
3251             intentFilter.addAction(ACTION_MESSAGE_DELIVERY);
3252             try {
3253                 intentFilter.addDataType("message/*");
3254             } catch (MalformedMimeTypeException e) {
3255                 Log.e(TAG, "Wrong mime type!!!", e);
3256             }
3257 
3258             mContext.registerReceiver(this, intentFilter, null, handler);
3259         }
3260 
unregister()3261         public void unregister() {
3262             try {
3263                 mContext.unregisterReceiver(this);
3264             } catch (IllegalArgumentException e) {
3265                 /* do nothing */
3266             }
3267         }
3268 
3269         @Override
onReceive(Context context, Intent intent)3270         public void onReceive(Context context, Intent intent) {
3271             String action = intent.getAction();
3272             long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1);
3273             PushMsgInfo msgInfo = mPushMsgList.get(handle);
3274 
3275             Log.d(TAG, "onReceive: action" + action);
3276 
3277             if (msgInfo == null) {
3278                 Log.d(TAG, "onReceive: no msgInfo found for handle " + handle);
3279                 return;
3280             }
3281 
3282             if (action.equals(ACTION_MESSAGE_SENT)) {
3283                 int result =
3284                         intent.getIntExtra(EXTRA_MESSAGE_SENT_RESULT, Activity.RESULT_CANCELED);
3285                 msgInfo.partsSent++;
3286                 if (result != Activity.RESULT_OK) {
3287                     /* If just one of the parts in the message fails, we need to send the
3288                      * entire message again
3289                      */
3290                     msgInfo.failedSent = true;
3291                 }
3292                 if (D) {
3293                     Log.d(TAG, "onReceive: msgInfo.partsSent = " + msgInfo.partsSent
3294                             + ", msgInfo.parts = " + msgInfo.parts + " result = " + result);
3295                 }
3296 
3297                 if (msgInfo.partsSent == msgInfo.parts) {
3298                     actionMessageSent(context, intent, msgInfo, handle);
3299                 }
3300             } else if (action.equals(ACTION_MESSAGE_DELIVERY)) {
3301                 long timestamp = intent.getLongExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, 0);
3302                 int status = -1;
3303                 if (msgInfo.timestamp == timestamp) {
3304                     msgInfo.partsDelivered++;
3305                 }
3306             } else {
3307                 Log.d(TAG, "onReceive: Unknown action " + action);
3308             }
3309         }
3310 
actionMessageSent( Context context, Intent intent, PushMsgInfo msgInfo, long handle)3311         private void actionMessageSent(
3312                 Context context, Intent intent, PushMsgInfo msgInfo, long handle) {
3313             /* As the MESSAGE_SENT intent is forwarded from the MAP service, we use the intent
3314              * to carry the result, as getResult() will not return the correct value.
3315              */
3316             boolean delete = false;
3317 
3318             if (D) {
3319                 Log.d(TAG, "actionMessageSent(): msgInfo.failedSent = " + msgInfo.failedSent);
3320             }
3321 
3322             msgInfo.sendInProgress = false;
3323 
3324             if (!msgInfo.failedSent) {
3325                 if (D) {
3326                     Log.d(TAG, "actionMessageSent: result OK");
3327                 }
3328                 if (msgInfo.transparent == 0) {
3329                     if (!Utils.moveMessageToFolder(context, msgInfo.uri, true)) {
3330                         Log.w(TAG, "Failed to move " + msgInfo.uri + " to SENT");
3331                     }
3332                 } else {
3333                     delete = true;
3334                 }
3335 
3336                 Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msgInfo.id,
3337                         getSmsFolderName(Sms.MESSAGE_TYPE_SENT), null, mSmsType);
3338                 sendEvent(evt);
3339 
3340             } else {
3341                 if (msgInfo.retry == 1) {
3342                     /* Notify failure, but keep message in outbox for resending */
3343                     msgInfo.resend = true;
3344                     msgInfo.partsSent = 0; // Reset counter for the retry
3345                     msgInfo.failedSent = false;
3346                     Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id,
3347                             getSmsFolderName(Sms.MESSAGE_TYPE_OUTBOX), null, mSmsType);
3348                     sendEvent(evt);
3349                 } else {
3350                     if (msgInfo.transparent == 0) {
3351                         if (!Utils.moveMessageToFolder(context, msgInfo.uri, false)) {
3352                             Log.w(TAG, "Failed to move " + msgInfo.uri + " to FAILED");
3353                         }
3354                     } else {
3355                         delete = true;
3356                     }
3357 
3358                     Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id,
3359                             getSmsFolderName(Sms.MESSAGE_TYPE_FAILED), null, mSmsType);
3360                     sendEvent(evt);
3361                 }
3362             }
3363 
3364             if (delete) {
3365                 /* Delete from Observer message list to avoid delete notifications */
3366                 synchronized (getMsgListSms()) {
3367                     getMsgListSms().remove(msgInfo.id);
3368                 }
3369 
3370                 /* Delete from DB */
3371                 Uri msgUri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
3372                 int nRows = mResolver.delete(msgUri, null, null);
3373                 if (V && nRows > 0) Log.v(TAG, "Deleted message with Uri = " + msgUri);
3374             }
3375         }
3376 
actionMessageDelivery(Context context, Intent intent, PushMsgInfo msgInfo)3377         private void actionMessageDelivery(Context context, Intent intent, PushMsgInfo msgInfo) {
3378             Uri messageUri = intent.getData();
3379             msgInfo.sendInProgress = false;
3380 
3381             Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null);
3382 
3383             try {
3384                 if (cursor.moveToFirst()) {
3385                     int messageId = cursor.getInt(0);
3386 
3387                     Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId);
3388 
3389                     if (D) {
3390                         Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status="
3391                                 + msgInfo.statusDelivered);
3392                     }
3393 
3394                     ContentValues contentValues = new ContentValues(2);
3395 
3396                     contentValues.put(Sms.STATUS, msgInfo.statusDelivered);
3397                     contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis());
3398                     mResolver.update(updateUri, contentValues, null, null);
3399                 } else {
3400                     Log.d(TAG, "Can't find message for status update: " + messageUri);
3401                 }
3402             } finally {
3403                 if (cursor != null) {
3404                     cursor.close();
3405                 }
3406             }
3407 
3408             if (msgInfo.statusDelivered == 0) {
3409                 Event evt = new Event(EVENT_TYPE_DELEVERY_SUCCESS, msgInfo.id,
3410                         getSmsFolderName(Sms.MESSAGE_TYPE_SENT), null, mSmsType);
3411                 sendEvent(evt);
3412             } else {
3413                 Event evt = new Event(EVENT_TYPE_DELIVERY_FAILURE, msgInfo.id,
3414                         getSmsFolderName(Sms.MESSAGE_TYPE_SENT), null, mSmsType);
3415                 sendEvent(evt);
3416             }
3417 
3418             mPushMsgList.remove(msgInfo.id);
3419         }
3420     }
3421 
3422     private class CeBroadcastReceiver extends BroadcastReceiver {
register()3423         public void register() {
3424             UserManager manager = UserManager.get(mContext);
3425             if (manager == null || manager.isUserUnlocked()) {
3426                 mStorageUnlocked = true;
3427                 return;
3428             }
3429 
3430             Handler handler = new Handler(Looper.getMainLooper());
3431             IntentFilter intentFilter = new IntentFilter();
3432             intentFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
3433             mContext.registerReceiver(this, intentFilter, null, handler);
3434         }
3435 
unregister()3436         public void unregister() {
3437             try {
3438                 mContext.unregisterReceiver(this);
3439             } catch (IllegalArgumentException e) {
3440                 /* do nothing */
3441             }
3442         }
3443 
3444         @Override
onReceive(Context context, Intent intent)3445         public void onReceive(Context context, Intent intent) {
3446             String action = intent.getAction();
3447             Log.d(TAG, "onReceive: action" + action);
3448 
3449             if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
3450                 try {
3451                     initMsgList();
3452                 } catch (RemoteException e) {
3453                     Log.e(TAG, "Error initializing SMS/MMS message lists.");
3454                 }
3455 
3456                 for (String folder : FOLDER_SMS_MAP.values()) {
3457                     Event evt = new Event(EVENT_TYPE_NEW, -1, folder, mSmsType);
3458                     sendEvent(evt);
3459                 }
3460                 mStorageUnlocked = true;
3461                 /* After unlock this BroadcastReceiver is never needed */
3462                 unregister();
3463             } else {
3464                 Log.d(TAG, "onReceive: Unknown action " + action);
3465             }
3466         }
3467     }
3468 
3469     /**
3470      * Handle MMS sent intents in disconnected(MNS) state, where we do not need to send any
3471      * notifications.
3472      * @param context The context to use for provider operations
3473      * @param intent The intent received
3474      * @param result The result
3475      */
actionMmsSent(Context context, Intent intent, int result, Map<Long, Msg> mmsMsgList)3476     public static void actionMmsSent(Context context, Intent intent, int result,
3477             Map<Long, Msg> mmsMsgList) {
3478         /*
3479          * if transparent:
3480          *   delete message and send notification(regardless of result)
3481          * else
3482          *   Result == Success:
3483          *     move to sent folder (will trigger notification)
3484          *   Result == Fail:
3485          *     move to outbox (send delivery fail notification)
3486          */
3487         if (D) {
3488             Log.d(TAG, "actionMmsSent()");
3489         }
3490         int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0);
3491         long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1);
3492         if (handle < 0) {
3493             Log.w(TAG, "Intent received for an invalid handle");
3494             return;
3495         }
3496         ContentResolver resolver = context.getContentResolver();
3497         if (transparent == 1) {
3498             /* The specification is a bit unclear about the transparent flag. If it is set
3499              * no copy of the message shall be kept in the send folder after the message
3500              * was send, but in the case of a send error, it is unclear what to do.
3501              * As it will not be transparent if we keep the message in any folder,
3502              * we delete the message regardless of the result.
3503              * If we however do have a MNS connection we need to send a notification. */
3504             Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
3505             /* Delete from observer message list to avoid delete notifications */
3506             if (mmsMsgList != null) {
3507                 synchronized (mmsMsgList) {
3508                     mmsMsgList.remove(handle);
3509                 }
3510             }
3511             /* Delete message */
3512             if (D) {
3513                 Log.d(TAG, "Transparent in use - delete");
3514             }
3515             resolver.delete(uri, null, null);
3516         } else if (result == Activity.RESULT_OK) {
3517             /* This will trigger a notification */
3518             moveMmsToFolder(handle, resolver, Mms.MESSAGE_BOX_SENT);
3519         } else {
3520             if (mmsMsgList != null) {
3521                 synchronized (mmsMsgList) {
3522                     Msg msg = mmsMsgList.get(handle);
3523                     if (msg != null) {
3524                         msg.type = Mms.MESSAGE_BOX_OUTBOX;
3525                     }
3526                 }
3527             }
3528             /* Hand further retries over to the MMS application */
3529             moveMmsToFolder(handle, resolver, Mms.MESSAGE_BOX_OUTBOX);
3530         }
3531     }
3532 
actionMessageSentDisconnected(Context context, Intent intent, int result)3533     public static void actionMessageSentDisconnected(Context context, Intent intent, int result) {
3534         TYPE type = TYPE.fromOrdinal(
3535                 intent.getIntExtra(EXTRA_MESSAGE_SENT_MSG_TYPE, TYPE.NONE.ordinal()));
3536         if (type == TYPE.MMS) {
3537             actionMmsSent(context, intent, result, null);
3538         } else {
3539             actionSmsSentDisconnected(context, intent, result);
3540         }
3541     }
3542 
actionSmsSentDisconnected(Context context, Intent intent, int result)3543     public static void actionSmsSentDisconnected(Context context, Intent intent, int result) {
3544         /* Check permission for message deletion. */
3545         if ((Binder.getCallingPid() != Process.myPid()) || (
3546                 context.checkCallingOrSelfPermission("android.Manifest.permission.WRITE_SMS")
3547                         != PackageManager.PERMISSION_GRANTED)) {
3548             Log.w(TAG, "actionSmsSentDisconnected: Not allowed to delete SMS/MMS messages");
3549             return;
3550         }
3551 
3552         boolean delete = false;
3553         //int retry = intent.getIntExtra(EXTRA_MESSAGE_SENT_RETRY, 0);
3554         int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0);
3555         String uriString = intent.getStringExtra(EXTRA_MESSAGE_SENT_URI);
3556         if (uriString == null) {
3557             // Nothing we can do about it, just bail out
3558             return;
3559         }
3560         Uri uri = Uri.parse(uriString);
3561 
3562         if (result == Activity.RESULT_OK) {
3563             Log.d(TAG, "actionMessageSentDisconnected: result OK");
3564             if (transparent == 0) {
3565                 if (!Utils.moveMessageToFolder(context, uri, true)) {
3566                     Log.d(TAG, "Failed to move " + uri + " to SENT");
3567                 }
3568             } else {
3569                 delete = true;
3570             }
3571         } else {
3572             /*if (retry == 1) {
3573                  The retry feature only works while connected, else we fail the send,
3574              * and move the message to failed, to let the user/app resend manually later.
3575             } else */
3576             {
3577                 if (transparent == 0) {
3578                     if (!Utils.moveMessageToFolder(context, uri, false)) {
3579                         Log.d(TAG, "Failed to move " + uri + " to FAILED");
3580                     }
3581                 } else {
3582                     delete = true;
3583                 }
3584             }
3585         }
3586 
3587         if (delete) {
3588             /* Delete from DB */
3589             ContentResolver resolver = context.getContentResolver();
3590             if (resolver != null) {
3591                 resolver.delete(uri, null, null);
3592             } else {
3593                 Log.w(TAG, "Unable to get resolver");
3594             }
3595         }
3596     }
3597 
registerPhoneServiceStateListener()3598     private void registerPhoneServiceStateListener() {
3599         TelephonyManager tm =
3600                 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
3601         tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE);
3602     }
3603 
unRegisterPhoneServiceStateListener()3604     private void unRegisterPhoneServiceStateListener() {
3605         TelephonyManager tm =
3606                 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
3607         tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE);
3608     }
3609 
resendPendingMessages()3610     private void resendPendingMessages() {
3611         /* Send pending messages in outbox */
3612         String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
3613         UserManager manager = UserManager.get(mContext);
3614         if (manager == null || !manager.isUserUnlocked()) {
3615             return;
3616         }
3617         Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null);
3618         try {
3619             if (c != null && c.moveToFirst()) {
3620                 do {
3621                     long id = c.getLong(c.getColumnIndex(Sms._ID));
3622                     String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
3623                     PushMsgInfo msgInfo = mPushMsgList.get(id);
3624                     if (msgInfo == null || !msgInfo.resend || msgInfo.sendInProgress) {
3625                         continue;
3626                     }
3627                     msgInfo.sendInProgress = true;
3628                     sendMessage(msgInfo, msgBody);
3629                 } while (c.moveToNext());
3630             }
3631         } finally {
3632             if (c != null) {
3633                 c.close();
3634             }
3635         }
3636 
3637 
3638     }
3639 
failPendingMessages()3640     private void failPendingMessages() {
3641         /* Move pending messages from outbox to failed */
3642         String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
3643         Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null);
3644         try {
3645             if (c != null && c.moveToFirst()) {
3646                 do {
3647                     long id = c.getLong(c.getColumnIndex(Sms._ID));
3648                     String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
3649                     PushMsgInfo msgInfo = mPushMsgList.get(id);
3650                     if (msgInfo == null || !msgInfo.resend) {
3651                         continue;
3652                     }
3653                     Utils.moveMessageToFolder(mContext, msgInfo.uri, false);
3654                 } while (c.moveToNext());
3655             }
3656         } finally {
3657             if (c != null) {
3658                 c.close();
3659             }
3660         }
3661 
3662     }
3663 
removeDeletedMessages()3664     private void removeDeletedMessages() {
3665         /* Remove messages from virtual "deleted" folder (thread_id -1) */
3666         mResolver.delete(Sms.CONTENT_URI, "thread_id = " + DELETED_THREAD_ID, null);
3667     }
3668 
3669     private PhoneStateListener mPhoneListener = new PhoneStateListener() {
3670         @Override
3671         public void onServiceStateChanged(ServiceState serviceState) {
3672             Log.d(TAG, "Phone service state change: " + serviceState.getState());
3673             if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
3674                 resendPendingMessages();
3675             }
3676         }
3677     };
3678 
init()3679     public void init() {
3680         if (mSmsBroadcastReceiver != null) {
3681             mSmsBroadcastReceiver.register();
3682         }
3683 
3684         if (mCeBroadcastReceiver != null) {
3685             mCeBroadcastReceiver.register();
3686         }
3687 
3688         registerPhoneServiceStateListener();
3689         mInitialized = true;
3690     }
3691 
deinit()3692     public void deinit() {
3693         mInitialized = false;
3694         unregisterObserver();
3695         if (mSmsBroadcastReceiver != null) {
3696             mSmsBroadcastReceiver.unregister();
3697         }
3698         unRegisterPhoneServiceStateListener();
3699         if (UserManager.get(mContext).isUserUnlocked()) {
3700             failPendingMessages();
3701             removeDeletedMessages();
3702         }
3703     }
3704 
handleSmsSendIntent(Context context, Intent intent)3705     public boolean handleSmsSendIntent(Context context, Intent intent) {
3706         TYPE type = TYPE.fromOrdinal(
3707                 intent.getIntExtra(EXTRA_MESSAGE_SENT_MSG_TYPE, TYPE.NONE.ordinal()));
3708         if (type == TYPE.MMS) {
3709             return handleMmsSendIntent(context, intent);
3710         } else {
3711             if (mInitialized) {
3712                 mSmsBroadcastReceiver.onReceive(context, intent);
3713                 return true;
3714             }
3715         }
3716         return false;
3717     }
3718 
handleMmsSendIntent(Context context, Intent intent)3719     public boolean handleMmsSendIntent(Context context, Intent intent) {
3720         if (D) {
3721             Log.w(TAG, "handleMmsSendIntent()");
3722         }
3723         if (!mMnsClient.isConnected()) {
3724             // No need to handle notifications, just use default handling
3725             if (D) {
3726                 Log.w(TAG, "MNS not connected - use static handling");
3727             }
3728             return false;
3729         }
3730         long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1);
3731         int result = intent.getIntExtra(EXTRA_MESSAGE_SENT_RESULT, Activity.RESULT_CANCELED);
3732         actionMmsSent(context, intent, result, getMsgListMms());
3733         if (handle < 0) {
3734             Log.w(TAG, "Intent received for an invalid handle");
3735             return true;
3736         }
3737         if (result != Activity.RESULT_OK) {
3738             if (mObserverRegistered) {
3739                 Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, handle,
3740                         getMmsFolderName(Mms.MESSAGE_BOX_OUTBOX), null, TYPE.MMS);
3741                 sendEvent(evt);
3742             }
3743         } else {
3744             int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0);
3745             if (transparent != 0) {
3746                 if (mObserverRegistered) {
3747                     Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, handle,
3748                             getMmsFolderName(Mms.MESSAGE_BOX_OUTBOX), null, TYPE.MMS);
3749                     sendEvent(evt);
3750                 }
3751             }
3752         }
3753         return true;
3754     }
3755 
3756 }
3757