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