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