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