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