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