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