/*
* Copyright (C) 2014 Samsung System LSI
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.bluetooth.map;

import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.obex.ResponseCodes;

import org.xmlpull.v1.XmlSerializer;

import android.Manifest;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentFilter.MalformedMimeTypeException;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Binder;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.provider.BaseColumns;
import com.android.bluetooth.mapapi.BluetoothMapContract;
import com.android.bluetooth.mapapi.BluetoothMapContract.MessageColumns;
import android.provider.Telephony;
import android.provider.Telephony.Mms;
import android.provider.Telephony.MmsSms;
import android.provider.Telephony.Sms;
import android.provider.Telephony.Sms.Inbox;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.telephony.TelephonyManager;
import android.text.format.DateUtils;
import android.util.Log;
import android.util.Xml;
import android.os.Looper;

import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
import com.android.bluetooth.map.BluetoothMapbMessageMms.MimePart;
import com.google.android.mms.pdu.PduHeaders;

public class BluetoothMapContentObserver {
    private static final String TAG = "BluetoothMapContentObserver";

    private static final boolean D = BluetoothMapService.DEBUG;
    private static final boolean V = BluetoothMapService.VERBOSE;

    private static final String EVENT_TYPE_DELETE = "MessageDeleted";
    private static final String EVENT_TYPE_SHIFT  = "MessageShift";
    private static final String EVENT_TYPE_NEW    = "NewMessage";
    private static final String EVENT_TYPE_DELEVERY_SUCCESS = "DeliverySuccess";
    private static final String EVENT_TYPE_SENDING_SUCCESS  = "SendingSuccess";
    private static final String EVENT_TYPE_SENDING_FAILURE  = "SendingFailure";
    private static final String EVENT_TYPE_DELIVERY_FAILURE = "DeliveryFailure";


    private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;

    private Context mContext;
    private ContentResolver mResolver;
    private ContentProviderClient mProviderClient = null;
    private BluetoothMnsObexClient mMnsClient;
    private BluetoothMapMasInstance mMasInstance = null;
    private int mMasId;
    private boolean mEnableSmsMms = false;
    private boolean mObserverRegistered = false;
    private BluetoothMapEmailSettingsItem mAccount;
    private String mAuthority = null;

    private BluetoothMapFolderElement mFolders =
            new BluetoothMapFolderElement("DUMMY", null); // Will be set by the MAS when generated.
    private Uri mMessageUri = null;

    public static final int DELETED_THREAD_ID = -1;

    // X-Mms-Message-Type field types. These are from PduHeaders.java
    public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84;

    // Text only MMS converted to SMS if sms parts less than or equal to defined count
    private static final int CONVERT_MMS_TO_SMS_PART_COUNT = 10;

    private TYPE mSmsType;

    private static void close(Closeable c) {
        try {
            if (c != null) c.close();
        } catch (IOException e) {
        }
    }

    static final String[] SMS_PROJECTION = new String[] {
        Sms._ID,
        Sms.THREAD_ID,
        Sms.ADDRESS,
        Sms.BODY,
        Sms.DATE,
        Sms.READ,
        Sms.TYPE,
        Sms.STATUS,
        Sms.LOCKED,
        Sms.ERROR_CODE
    };

    static final String[] SMS_PROJECTION_SHORT = new String[] {
        Sms._ID,
        Sms.THREAD_ID,
        Sms.TYPE
    };

    static final String[] MMS_PROJECTION_SHORT = new String[] {
        Mms._ID,
        Mms.THREAD_ID,
        Mms.MESSAGE_TYPE,
        Mms.MESSAGE_BOX
    };

    static final String[] EMAIL_PROJECTION_SHORT = new String[] {
        BluetoothMapContract.MessageColumns._ID,
        BluetoothMapContract.MessageColumns.FOLDER_ID,
        BluetoothMapContract.MessageColumns.FLAG_READ
    };


    public BluetoothMapContentObserver(final Context context,
                                       BluetoothMnsObexClient mnsClient,
                                       BluetoothMapMasInstance masInstance,
                                       BluetoothMapEmailSettingsItem account,
                                       boolean enableSmsMms) throws RemoteException {
        mContext = context;
        mResolver = mContext.getContentResolver();
        mAccount = account;
        mMasInstance = masInstance;
        mMasId = mMasInstance.getMasId();
        if(account != null) {
            mAuthority = Uri.parse(account.mBase_uri).getAuthority();
            mMessageUri = Uri.parse(account.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE);
            mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority);
            if (mProviderClient == null) {
                throw new RemoteException("Failed to acquire provider for " + mAuthority);
            }
            mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
        }

        mEnableSmsMms = enableSmsMms;
        mSmsType = getSmsType();
        mMnsClient = mnsClient;
    }

    /**
     * Set the folder structure to be used for this instance.
     * @param folderStructure
     */
    public void setFolderStructure(BluetoothMapFolderElement folderStructure) {
        this.mFolders = folderStructure;
    }

    private TYPE getSmsType() {
        TYPE smsType = null;
        TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);

        if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) {
            smsType = TYPE.SMS_GSM;
        } else if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
            smsType = TYPE.SMS_CDMA;
        }

        return smsType;
    }

    private final ContentObserver mObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
        @Override
        public void onChange(boolean selfChange) {
            onChange(selfChange, null);
        }

        @Override
        public void onChange(boolean selfChange, Uri uri) {
            if (V) Log.d(TAG, "onChange on thread: " + Thread.currentThread().getId()
                + " Uri: " + uri.toString() + " selfchange: " + selfChange);

            handleMsgListChanges(uri);
        }
    };

    private static final String folderSms[] = {
        "",
        BluetoothMapContract.FOLDER_NAME_INBOX,
        BluetoothMapContract.FOLDER_NAME_SENT,
        BluetoothMapContract.FOLDER_NAME_DRAFT,
        BluetoothMapContract.FOLDER_NAME_OUTBOX,
        BluetoothMapContract.FOLDER_NAME_OUTBOX,
        BluetoothMapContract.FOLDER_NAME_OUTBOX,
        BluetoothMapContract.FOLDER_NAME_INBOX,
        BluetoothMapContract.FOLDER_NAME_INBOX,
    };

    private static final String folderMms[] = {
        "",
        BluetoothMapContract.FOLDER_NAME_INBOX,
        BluetoothMapContract.FOLDER_NAME_SENT,
        BluetoothMapContract.FOLDER_NAME_DRAFT,
        BluetoothMapContract.FOLDER_NAME_OUTBOX,
    };

    private class Event {
        String eventType;
        long handle;
        String folder;
        String oldFolder;
        TYPE msgType;

        final static String PATH = "telecom/msg/";

        public Event(String eventType, long handle, String folder,
            String oldFolder, TYPE msgType) {

            this.eventType = eventType;
            this.handle = handle;
            if (folder != null) {
                if(msgType == TYPE.EMAIL) {
                    this.folder = folder;
                } else {
                    this.folder = PATH + folder;
                }
            } else {
                this.folder = null;
            }
            if (oldFolder != null) {
                if(msgType == TYPE.EMAIL) {
                    this.oldFolder = oldFolder;
                } else {
                    this.oldFolder = PATH + oldFolder;
                }
            } else {
                this.oldFolder = null;
            }
            this.msgType = msgType;
        }

        public byte[] encode() throws UnsupportedEncodingException {
            StringWriter sw = new StringWriter();
            XmlSerializer xmlEvtReport = Xml.newSerializer();
            try {
                xmlEvtReport.setOutput(sw);
                xmlEvtReport.startDocument(null, null);
                xmlEvtReport.text("\r\n");
                xmlEvtReport.startTag("", "MAP-event-report");
                xmlEvtReport.attribute("", "version", "1.0");

                xmlEvtReport.startTag("", "event");
                xmlEvtReport.attribute("", "type", eventType);
                xmlEvtReport.attribute("", "handle", BluetoothMapUtils.getMapHandle(handle, msgType));
                if (folder != null) {
                    xmlEvtReport.attribute("", "folder", folder);
                }
                if (oldFolder != null) {
                    xmlEvtReport.attribute("", "old_folder", oldFolder);
                }
                xmlEvtReport.attribute("", "msg_type", msgType.name());
                xmlEvtReport.endTag("", "event");

                xmlEvtReport.endTag("", "MAP-event-report");
                xmlEvtReport.endDocument();
            } catch (IllegalArgumentException e) {
                if(D) Log.w(TAG,e);
            } catch (IllegalStateException e) {
                if(D) Log.w(TAG,e);
            } catch (IOException e) {
                if(D) Log.w(TAG,e);
            }

            if (V) Log.d(TAG, sw.toString());

            return sw.toString().getBytes("UTF-8");
        }
    }

    private class Msg {
        long id;
        int type;               // Used as folder for SMS/MMS
        int threadId;           // Used for SMS/MMS at delete
        long folderId = -1;     // Email folder ID
        long oldFolderId = -1;  // Used for email undelete
        boolean localInitiatedSend = false; // Used for MMS to filter out events
        boolean transparent = false; // Used for EMAIL to delete message sent with transparency

        public Msg(long id, int type, int threadId) {
            this.id = id;
            this.type = type;
            this.threadId = threadId;
        }
        public Msg(long id, long folderId) {
            this.id = id;
            this.folderId = folderId;
        }

        /* Eclipse generated hashCode() and equals() to make
         * hashMap lookup work independent of whether the obj
         * is used for email or SMS/MMS and whether or not the
         * oldFolder is set. */
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + (int) (id ^ (id >>> 32));
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            Msg other = (Msg) obj;
            if (id != other.id)
                return false;
            return true;
        }
    }

    private Map<Long, Msg> mMsgListSms = new HashMap<Long, Msg>();

    private Map<Long, Msg> mMsgListMms = new HashMap<Long, Msg>();

    private Map<Long, Msg> mMsgListEmail = new HashMap<Long, Msg>();

    public int setNotificationRegistration(int notificationStatus) throws RemoteException {
        // Forward the request to the MNS thread as a message - including the MAS instance ID.
        if(D) Log.d(TAG,"setNotificationRegistration() enter");
        Handler mns = mMnsClient.getMessageHandler();
        if(mns != null) {
            Message msg = mns.obtainMessage();
            msg.what = BluetoothMnsObexClient.MSG_MNS_NOTIFICATION_REGISTRATION;
            msg.arg1 = mMasId;
            msg.arg2 = notificationStatus;
            mns.sendMessageDelayed(msg, 10); // Send message without forcing a context switch
            /* Some devices - e.g. PTS needs to get the unregister confirm before we actually
             * disconnect the MNS. */
            if(D) Log.d(TAG,"setNotificationRegistration() MSG_MNS_NOTIFICATION_REGISTRATION send to MNS");
        } else {
            // This should not happen except at shutdown.
            if(D) Log.d(TAG,"setNotificationRegistration() Unable to send registration request");
            return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
        }
        if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) {
            registerObserver();
        } else {
            unregisterObserver();
        }
        return ResponseCodes.OBEX_HTTP_OK;
    }

    public void registerObserver() throws RemoteException{
        if (V) Log.d(TAG, "registerObserver");

        if (mObserverRegistered)
            return;

        /* Use MmsSms Uri since the Sms Uri is not notified on deletes */
        if(mEnableSmsMms){
            //this is sms/mms
            mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver);
            mObserverRegistered = true;
        }
        if(mAccount != null) {

            mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority);
            if (mProviderClient == null) {
                throw new RemoteException("Failed to acquire provider for " + mAuthority);
            }
            mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);

            /* For URI's without account ID */
            Uri uri = Uri.parse(mAccount.mBase_uri_no_account + "/" + BluetoothMapContract.TABLE_MESSAGE);
            if(D) Log.d(TAG, "Registering observer for: " + uri);
            mResolver.registerContentObserver(uri, true, mObserver);

            /* For URI's with account ID - is handled the same way as without ID, but is
             * only triggered for MAS instances with matching account ID. */
            uri = Uri.parse(mAccount.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE);
            if(D) Log.d(TAG, "Registering observer for: " + uri);
            mResolver.registerContentObserver(uri, true, mObserver);
            mObserverRegistered = true;
        }
        initMsgList();
    }

    public void unregisterObserver() {
        if (V) Log.d(TAG, "unregisterObserver");
        mResolver.unregisterContentObserver(mObserver);
        mObserverRegistered = false;
        if(mProviderClient != null){
            mProviderClient.release();
            mProviderClient = null;
        }
    }

    private void sendEvent(Event evt) {
        Log.d(TAG, "sendEvent: " + evt.eventType + " " + evt.handle + " "
        + evt.folder + " " + evt.oldFolder + " " + evt.msgType.name());

        if (mMnsClient == null || mMnsClient.isConnected() == false) {
            Log.d(TAG, "sendEvent: No MNS client registered or connected- don't send event");
            return;
        }

        try {
            mMnsClient.sendEvent(evt.encode(), mMasId);
        } catch (UnsupportedEncodingException ex) {
            /* do nothing */
        }
    }

    private void initMsgList() throws RemoteException {
        if (V) Log.d(TAG, "initMsgList");

        if(mEnableSmsMms) {

            HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();

            Cursor c = mResolver.query(Sms.CONTENT_URI,
                SMS_PROJECTION_SHORT, null, null, null);

            try {
                while (c != null && c.moveToNext()) {
                    long id = c.getLong(c.getColumnIndex(Sms._ID));
                    int type = c.getInt(c.getColumnIndex(Sms.TYPE));
                    int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));

                    Msg msg = new Msg(id, type, threadId);
                    msgListSms.put(id, msg);
                }
            } finally {
                close(c);
            }

            synchronized(mMsgListSms) {
                mMsgListSms.clear();
                mMsgListSms = msgListSms;
            }

            HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();

            c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT, null, null, null);

            try {
                while (c != null && c.moveToNext()) {
                    long id = c.getLong(c.getColumnIndex(Mms._ID));
                    int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
                    int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));

                    Msg msg = new Msg(id, type, threadId);
                    msgListMms.put(id, msg);
                }
            } finally {
                close(c);
            }

            synchronized(mMsgListMms) {
                mMsgListMms.clear();
                mMsgListMms = msgListMms;
            }
        }

        if(mAccount != null) {
            HashMap<Long, Msg> msgListEmail = new HashMap<Long, Msg>();
            Uri uri = mMessageUri;
            Cursor c = mProviderClient.query(uri, EMAIL_PROJECTION_SHORT, null, null, null);

            try {
                while (c != null && c.moveToNext()) {
                    long id = c.getLong(c.getColumnIndex(MessageColumns._ID));
                    long folderId = c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.FOLDER_ID));

                    Msg msg = new Msg(id, folderId);
                    msgListEmail.put(id, msg);
                }
            } finally {
                close(c);
            }

            synchronized(mMsgListEmail) {
                mMsgListEmail.clear();
                mMsgListEmail = msgListEmail;
            }
        }
    }

    private void handleMsgListChangesSms() {
        if (V) Log.d(TAG, "handleMsgListChangesSms");

        HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();

        Cursor c = mResolver.query(Sms.CONTENT_URI,
            SMS_PROJECTION_SHORT, null, null, null);

        synchronized(mMsgListSms) {
            try {
                while (c != null && c.moveToNext()) {
                    long id = c.getLong(c.getColumnIndex(Sms._ID));
                    int type = c.getInt(c.getColumnIndex(Sms.TYPE));
                    int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));

                    Msg msg = mMsgListSms.remove(id);

                    /* We must filter out any actions made by the MCE, hence do not send e.g. a message
                     * deleted and/or MessageShift for messages deleted by the MCE. */

                    if (msg == null) {
                        /* New message */
                        msg = new Msg(id, type, threadId);
                        msgListSms.put(id, msg);

                        /* Incoming message from the network */
                        Event evt = new Event(EVENT_TYPE_NEW, id, folderSms[type],
                            null, mSmsType);
                        sendEvent(evt);
                    } else {
                        /* Existing message */
                        if (type != msg.type) {
                            Log.d(TAG, "new type: " + type + " old type: " + msg.type);
                            String oldFolder = folderSms[msg.type];
                            String newFolder = folderSms[type];
                            // Filter out the intermediate outbox steps
                            if(!oldFolder.equals(newFolder)) {
                                Event evt = new Event(EVENT_TYPE_SHIFT, id, folderSms[type],
                                    oldFolder, mSmsType);
                                sendEvent(evt);
                            }
                            msg.type = type;
                        } else if(threadId != msg.threadId) {
                            Log.d(TAG, "Message delete change: type: " + type + " old type: " + msg.type
                                    + "\n    threadId: " + threadId + " old threadId: " + msg.threadId);
                            if(threadId == DELETED_THREAD_ID) { // Message deleted
                                Event evt = new Event(EVENT_TYPE_DELETE, id, BluetoothMapContract.FOLDER_NAME_DELETED,
                                    folderSms[msg.type], mSmsType);
                                sendEvent(evt);
                                msg.threadId = threadId;
                            } else { // Undelete
                                Event evt = new Event(EVENT_TYPE_SHIFT, id, folderSms[msg.type],
                                    BluetoothMapContract.FOLDER_NAME_DELETED, mSmsType);
                                sendEvent(evt);
                                msg.threadId = threadId;
                            }
                        }
                        msgListSms.put(id, msg);
                    }
                }
            } finally {
                close(c);
            }

            for (Msg msg : mMsgListSms.values()) {
                Event evt = new Event(EVENT_TYPE_DELETE, msg.id,
                                        BluetoothMapContract.FOLDER_NAME_DELETED,
                                        folderSms[msg.type], mSmsType);
                sendEvent(evt);
            }

            mMsgListSms = msgListSms;
        }
    }

    private void handleMsgListChangesMms() {
        if (V) Log.d(TAG, "handleMsgListChangesMms");

        HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();

        Cursor c = mResolver.query(Mms.CONTENT_URI,
            MMS_PROJECTION_SHORT, null, null, null);

        synchronized(mMsgListMms) {
            try {
                while (c != null && c.moveToNext()) {
                    long id = c.getLong(c.getColumnIndex(Mms._ID));
                    int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
                    int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE));
                    int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));

                    Msg msg = mMsgListMms.remove(id);

                    /* We must filter out any actions made by the MCE, hence do not send e.g. a message
                     * deleted and/or MessageShift for messages deleted by the MCE. */

                    if (msg == null) {
                        /* New message - only notify on retrieve conf */
                        if (folderMms[type].equals(BluetoothMapContract.FOLDER_NAME_INBOX) &&
                            mtype != MESSAGE_TYPE_RETRIEVE_CONF) {
                                continue;
                        }

                        msg = new Msg(id, type, threadId);
                        msgListMms.put(id, msg);

                        /* Incoming message from the network */
                        Event evt = new Event(EVENT_TYPE_NEW, id, folderMms[type],
                                null, TYPE.MMS);
                        sendEvent(evt);
                    } else {
                        /* Existing message */
                        if (type != msg.type) {
                            Log.d(TAG, "new type: " + type + " old type: " + msg.type);
                            Event evt;
                            if(msg.localInitiatedSend == false) {
                                // Only send events about local initiated changes
                                evt = new Event(EVENT_TYPE_SHIFT, id, folderMms[type],
                                        folderMms[msg.type], TYPE.MMS);
                                sendEvent(evt);
                            }
                            msg.type = type;

                            if (folderMms[type].equals(BluetoothMapContract.FOLDER_NAME_SENT)
                                    && msg.localInitiatedSend == true) {
                                msg.localInitiatedSend = false; // Stop tracking changes for this message
                                evt = new Event(EVENT_TYPE_SENDING_SUCCESS, id,
                                    folderSms[type], null, TYPE.MMS);
                                sendEvent(evt);
                            }
                        } else if(threadId != msg.threadId) {
                            Log.d(TAG, "Message delete change: type: " + type + " old type: " + msg.type
                                    + "\n    threadId: " + threadId + " old threadId: " + msg.threadId);
                            if(threadId == DELETED_THREAD_ID) { // Message deleted
                                Event evt = new Event(EVENT_TYPE_DELETE, id, BluetoothMapContract.FOLDER_NAME_DELETED,
                                    folderMms[msg.type], TYPE.MMS);
                                sendEvent(evt);
                                msg.threadId = threadId;
                            } else { // Undelete
                                Event evt = new Event(EVENT_TYPE_SHIFT, id, folderMms[msg.type],
                                    BluetoothMapContract.FOLDER_NAME_DELETED, TYPE.MMS);
                                sendEvent(evt);
                                msg.threadId = threadId;
                            }
                        }
                        msgListMms.put(id, msg);
                    }
                }
            } finally {
                close(c);
            }

            for (Msg msg : mMsgListMms.values()) {
                Event evt = new Event(EVENT_TYPE_DELETE, msg.id,
                                        BluetoothMapContract.FOLDER_NAME_DELETED,
                                        folderMms[msg.type], TYPE.MMS);
                sendEvent(evt);
            }
            mMsgListMms = msgListMms;
        }
    }

    private void handleMsgListChangesEmail(Uri uri)  throws RemoteException{
        if (V) Log.v(TAG, "handleMsgListChangesEmail uri: " + uri.toString());

        // TODO: Change observer to handle accountId and message ID if present

        HashMap<Long, Msg> msgListEmail = new HashMap<Long, Msg>();

        Cursor c = mProviderClient.query(mMessageUri, EMAIL_PROJECTION_SHORT, null, null, null);

        synchronized(mMsgListEmail) {
            try {
                while (c != null && c.moveToNext()) {
                    long id = c.getLong(c.getColumnIndex(BluetoothMapContract.MessageColumns._ID));
                    int folderId = c.getInt(c.getColumnIndex(
                            BluetoothMapContract.MessageColumns.FOLDER_ID));
                    Msg msg = mMsgListEmail.remove(id);
                    BluetoothMapFolderElement folderElement = mFolders.getEmailFolderById(folderId);
                    String newFolder;
                    if(folderElement != null) {
                        newFolder = folderElement.getFullPath();
                    } else {
                        newFolder = "unknown"; // This can happen if a new folder is created while connected
                    }

                    /* We must filter out any actions made by the MCE, hence do not send e.g. a message
                     * deleted and/or MessageShift for messages deleted by the MCE. */

                    if (msg == null) {
                        /* New message */
                        msg = new Msg(id, folderId);
                        msgListEmail.put(id, msg);
                        Event evt = new Event(EVENT_TYPE_NEW, id, newFolder,
                            null, TYPE.EMAIL);
                        sendEvent(evt);
                    } else {
                        /* Existing message */
                        if (folderId != msg.folderId) {
                            if (D) Log.d(TAG, "new folderId: " + folderId + " old folderId: " + msg.folderId);
                            BluetoothMapFolderElement oldFolderElement = mFolders.getEmailFolderById(msg.folderId);
                            String oldFolder;
                            if(oldFolderElement != null) {
                                oldFolder = oldFolderElement.getFullPath();
                            } else {
                                // This can happen if a new folder is created while connected
                                oldFolder = "unknown";
                            }
                            BluetoothMapFolderElement deletedFolder =
                                    mFolders.getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED);
                            BluetoothMapFolderElement sentFolder =
                                    mFolders.getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_SENT);
                            /*
                             *  If the folder is now 'deleted', send a deleted-event in stead of a shift
                             *  or if message is sent initiated by MAP Client, then send sending-success
                             *  otherwise send folderShift
                             */
                            if(deletedFolder != null && deletedFolder.getEmailFolderId() == folderId) {
                                Event evt = new Event(EVENT_TYPE_DELETE, msg.id, newFolder,
                                        oldFolder, TYPE.EMAIL);
                                sendEvent(evt);
                            } else if(sentFolder != null
                                      && sentFolder.getEmailFolderId() == folderId
                                      && msg.localInitiatedSend == true) {
                                if(msg.transparent) {
                                    mResolver.delete(ContentUris.withAppendedId(mMessageUri, id), null, null);
                                } else {
                                    msg.localInitiatedSend = false;
                                    Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id,
                                                          oldFolder, null, TYPE.EMAIL);
                                    sendEvent(evt);
                                }
                            } else {
                                Event evt = new Event(EVENT_TYPE_SHIFT, id, newFolder,
                                                      oldFolder, TYPE.EMAIL);
                                sendEvent(evt);
                            }
                            msg.folderId = folderId;
                        }
                        msgListEmail.put(id, msg);
                    }
                }
            } finally {
                close(c);
            }

            // For all messages no longer in the database send a delete notification
            for (Msg msg : mMsgListEmail.values()) {
                BluetoothMapFolderElement oldFolderElement = mFolders.getEmailFolderById(msg.folderId);
                String oldFolder;
                if(oldFolderElement != null) {
                    oldFolder = oldFolderElement.getFullPath();
                } else {
                    oldFolder = "unknown";
                }
                /* Some e-mail clients delete the message after sending, and creates a new message in sent.
                 * We cannot track the message anymore, hence send both a send success and delete message.
                 */
                if(msg.localInitiatedSend == true) {
                    msg.localInitiatedSend = false;
                    // If message is send with transparency don't set folder as message is deleted
                    if (msg.transparent)
                        oldFolder = null;
                    Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, oldFolder, null, TYPE.EMAIL);
                    sendEvent(evt);
                }
                /* As this message deleted is only send on a real delete - don't set folder.
                 *  - only send delete event if message is not sent with transparency
                 */
                if (!msg.transparent) {

                    Event evt = new Event(EVENT_TYPE_DELETE, msg.id, null, oldFolder, TYPE.EMAIL);
                    sendEvent(evt);
                }
            }
            mMsgListEmail = msgListEmail;
        }
    }

    private void handleMsgListChanges(Uri uri) {
        if(uri.getAuthority().equals(mAuthority)) {
            try {
                handleMsgListChangesEmail(uri);
            }catch(RemoteException e){
                mMasInstance.restartObexServerSession();
                Log.w(TAG, "Problems contacting the ContentProvider in mas Instance "+mMasId+" restaring ObexServerSession");
            }

        } else {
            handleMsgListChangesSms();
            handleMsgListChangesMms();
        }
    }

    private boolean setEmailMessageStatusDelete(BluetoothMapFolderElement mCurrentFolder,
            String uriStr, long handle, int status) {
        boolean res = false;
        Uri uri = Uri.parse(uriStr + BluetoothMapContract.TABLE_MESSAGE);

        int updateCount = 0;
        ContentValues contentValues = new ContentValues();
        BluetoothMapFolderElement deleteFolder = mFolders.
                getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED);
        contentValues.put(BluetoothMapContract.MessageColumns._ID, handle);
        synchronized(mMsgListEmail) {
            Msg msg = mMsgListEmail.get(handle);
            if (status == BluetoothMapAppParams.STATUS_VALUE_YES) {
                /* Set deleted folder id */
                long folderId = -1;
                if(deleteFolder != null) {
                    folderId = deleteFolder.getEmailFolderId();
                }
                contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID,folderId);
                updateCount = mResolver.update(uri, contentValues, null, null);
                /* The race between updating the value in our cached values and the database
                 * is handled by the synchronized statement. */
                if(updateCount > 0) {
                    res = true;
                    if (msg != null) {
                        msg.oldFolderId = msg.folderId;
                        // Update the folder ID to avoid triggering an event for MCE initiated actions.
                        msg.folderId = folderId;
                    }
                    if(D) Log.d(TAG, "Deleted MSG: " + handle + " from folderId: " + folderId);
                } else {
                    Log.w(TAG, "Msg: " + handle + " - Set delete status " + status
                            + " failed for folderId " + folderId);
                }
            } else if (status == BluetoothMapAppParams.STATUS_VALUE_NO) {
                /* Undelete message. move to old folder if we know it,
                 * else move to inbox - as dictated by the spec. */
                if(msg != null && deleteFolder != null &&
                        msg.folderId == deleteFolder.getEmailFolderId()) {
                    /* Only modify messages in the 'Deleted' folder */
                    long folderId = -1;
                    if (msg != null && msg.oldFolderId != -1) {
                        folderId = msg.oldFolderId;
                    } else {
                        BluetoothMapFolderElement inboxFolder = mCurrentFolder.
                                getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_INBOX);
                        if(inboxFolder != null) {
                            folderId = inboxFolder.getEmailFolderId();
                        }
                        if(D)Log.d(TAG,"We did not delete the message, hence the old folder is unknown. Moving to inbox.");
                    }
                    contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId);
                    updateCount = mResolver.update(uri, contentValues, null, null);
                    if(updateCount > 0) {
                        res = true;
                        // Update the folder ID to avoid triggering an event for MCE initiated actions.
                        msg.folderId = folderId;
                    } else {
                        if(D)Log.d(TAG,"We did not delete the message, hence the old folder is unknown. Moving to inbox.");
                    }
                }
            }
            if(V) {
                BluetoothMapFolderElement folderElement;
                String folderName = "unknown";
                if (msg != null) {
                    folderElement = mCurrentFolder.getEmailFolderById(msg.folderId);
                    if(folderElement != null) {
                        folderName = folderElement.getName();
                    }
                }
                Log.d(TAG,"setEmailMessageStatusDelete: " + handle + " from " + folderName
                        + " status: " + status);
            }
        }
        if(res == false) {
            Log.w(TAG, "Set delete status " + status + " failed.");
        }
        return res;
    }

    private void updateThreadId(Uri uri, String valueString, long threadId) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(valueString, threadId);
        mResolver.update(uri, contentValues, null, null);
    }

    private boolean deleteMessageMms(long handle) {
        boolean res = false;
        Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
        Cursor c = mResolver.query(uri, null, null, null, null);

        try {
            if (c != null && c.moveToFirst()) {
                /* Move to deleted folder, or delete if already in deleted folder */
                int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
                if (threadId != DELETED_THREAD_ID) {
                    /* Set deleted thread id */
                    synchronized(mMsgListMms) {
                        Msg msg = mMsgListMms.get(handle);
                        if(msg != null) { // This will always be the case
                            msg.threadId = DELETED_THREAD_ID;
                        }
                    }
                    updateThreadId(uri, Mms.THREAD_ID, DELETED_THREAD_ID);
                } else {
                    /* Delete from observer message list to avoid delete notifications */
                    synchronized(mMsgListMms) {
                        mMsgListMms.remove(handle);
                    }
                    /* Delete message */
                    mResolver.delete(uri, null, null);
                }
                res = true;
            }
        } finally {
            close(c);
        }

        return res;
    }

    private boolean unDeleteMessageMms(long handle) {
        boolean res = false;
        Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
        Cursor c = mResolver.query(uri, null, null, null, null);

        try {
            if (c != null && c.moveToFirst()) {
                int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
                if (threadId == DELETED_THREAD_ID) {
                    /* Restore thread id from address, or if no thread for address
                    * create new thread by insert and remove of fake message */
                    String address;
                    long id = c.getLong(c.getColumnIndex(Mms._ID));
                    int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
                    if (msgBox == Mms.MESSAGE_BOX_INBOX) {
                        address = BluetoothMapContent.getAddressMms(mResolver, id,
                            BluetoothMapContent.MMS_FROM);
                    } else {
                        address = BluetoothMapContent.getAddressMms(mResolver, id,
                            BluetoothMapContent.MMS_TO);
                    }
                    Set<String> recipients = new HashSet<String>();
                    recipients.addAll(Arrays.asList(address));
                    Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients);
                    synchronized(mMsgListMms) {
                        Msg msg = mMsgListMms.get(handle);
                        if(msg != null) { // This will always be the case
                            msg.threadId = oldThreadId.intValue();
                        }
                    }
                    updateThreadId(uri, Mms.THREAD_ID, oldThreadId);
                } else {
                    Log.d(TAG, "Message not in deleted folder: handle " + handle
                        + " threadId " + threadId);
                }
                res = true;
            }
        } finally {
            close(c);
        }

        return res;
    }

    private boolean deleteMessageSms(long handle) {
        boolean res = false;
        Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
        Cursor c = mResolver.query(uri, null, null, null, null);

        try {
            if (c != null && c.moveToFirst()) {
                /* Move to deleted folder, or delete if already in deleted folder */
                int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
                if (threadId != DELETED_THREAD_ID) {
                    synchronized(mMsgListSms) {
                        Msg msg = mMsgListSms.get(handle);
                        if(msg != null) { // This will always be the case
                            msg.threadId = DELETED_THREAD_ID;
                        }
                    }
                    /* Set deleted thread id */
                    updateThreadId(uri, Sms.THREAD_ID, DELETED_THREAD_ID);
                } else {
                    /* Delete from observer message list to avoid delete notifications */
                    synchronized(mMsgListSms) {
                        mMsgListSms.remove(handle);
                    }
                    /* Delete message */
                    mResolver.delete(uri, null, null);
                }
                res = true;
            }
        } finally {
            close(c);
        }

        return res;
    }

    private boolean unDeleteMessageSms(long handle) {
        boolean res = false;
        Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
        Cursor c = mResolver.query(uri, null, null, null, null);

        try {
            if (c != null && c.moveToFirst()) {
                int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
                if (threadId == DELETED_THREAD_ID) {
                    String address = c.getString(c.getColumnIndex(Sms.ADDRESS));
                    Set<String> recipients = new HashSet<String>();
                    recipients.addAll(Arrays.asList(address));
                    Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients);
                    synchronized(mMsgListSms) {
                        Msg msg = mMsgListSms.get(handle);
                        if(msg != null) { // This will always be the case
                            msg.threadId = oldThreadId.intValue(); // The threadId is specified as an int, so it is safe to truncate
                        }
                    }
                    updateThreadId(uri, Sms.THREAD_ID, oldThreadId);
                } else {
                    Log.d(TAG, "Message not in deleted folder: handle " + handle
                        + " threadId " + threadId);
                }
                res = true;
            }
        } finally {
            close(c);
        }

        return res;
    }

    public boolean setMessageStatusDeleted(long handle, TYPE type,
            BluetoothMapFolderElement mCurrentFolder, String uriStr, int statusValue) {
        boolean res = false;
        if (D) Log.d(TAG, "setMessageStatusDeleted: handle " + handle
            + " type " + type + " value " + statusValue);

        if (type == TYPE.EMAIL) {
            res = setEmailMessageStatusDelete(mCurrentFolder, uriStr, handle, statusValue);
        } else {
            if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) {
                if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
                    res = deleteMessageSms(handle);
                } else if (type == TYPE.MMS) {
                    res = deleteMessageMms(handle);
                }
            } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) {
                if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
                    res = unDeleteMessageSms(handle);
                } else if (type == TYPE.MMS) {
                    res = unDeleteMessageMms(handle);
                }
            }
        }

        return res;
    }

    /**
     *
     * @param handle
     * @param type
     * @param uriStr
     * @param statusValue
     * @return true at success
     */
    public boolean setMessageStatusRead(long handle, TYPE type, String uriStr, int statusValue) throws RemoteException{
        int count = 0;

        if (D) Log.d(TAG, "setMessageStatusRead: handle " + handle
            + " type " + type + " value " + statusValue);

        /* Approved MAP spec errata 3445 states that read status initiated */
        /* by the MCE shall change the MSE read status. */

        if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
            Uri uri = Sms.Inbox.CONTENT_URI;//ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
            ContentValues contentValues = new ContentValues();
            contentValues.put(Sms.READ, statusValue);
            contentValues.put(Sms.SEEN, statusValue);
            String where = Sms._ID+"="+handle;
            String values = contentValues.toString();
            if (D) Log.d(TAG, " -> SMS Uri: " + uri.toString() + " Where " + where + " values " + values);
            count = mResolver.update(uri, contentValues, where, null);
            if (D) Log.d(TAG, " -> "+count +" rows updated!");

        } else if (type == TYPE.MMS) {
            Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
            if (D) Log.d(TAG, " -> MMS Uri: " + uri.toString());
            ContentValues contentValues = new ContentValues();
            contentValues.put(Mms.READ, statusValue);
            count = mResolver.update(uri, contentValues, null, null);
            if (D) Log.d(TAG, " -> "+count +" rows updated!");

        } if (type == TYPE.EMAIL) {
            Uri uri = mMessageUri;
            ContentValues contentValues = new ContentValues();
            contentValues.put(BluetoothMapContract.MessageColumns.FLAG_READ, statusValue);
            contentValues.put(BluetoothMapContract.MessageColumns._ID, handle);
            count = mProviderClient.update(uri, contentValues, null, null);
        }

        return (count > 0);
    }

    private class PushMsgInfo {
        long id;
        int transparent;
        int retry;
        String phone;
        Uri uri;
        long timestamp;
        int parts;
        int partsSent;
        int partsDelivered;
        boolean resend;
        boolean sendInProgress;
        boolean failedSent; // Set to true if a single part sent fail is received.
        int statusDelivered; // Set to != 0 if a single part deliver fail is received.

        public PushMsgInfo(long id, int transparent,
            int retry, String phone, Uri uri) {
            this.id = id;
            this.transparent = transparent;
            this.retry = retry;
            this.phone = phone;
            this.uri = uri;
            this.resend = false;
            this.sendInProgress = false;
            this.failedSent = false;
            this.statusDelivered = 0; /* Assume success */
            this.timestamp = 0;
        };
    }

    private Map<Long, PushMsgInfo> mPushMsgList =
        Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>());

    public long pushMessage(BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement,
            BluetoothMapAppParams ap, String emailBaseUri)
                    throws IllegalArgumentException, RemoteException, IOException {
        if (D) Log.d(TAG, "pushMessage");
        ArrayList<BluetoothMapbMessage.vCard> recipientList = msg.getRecipients();
        int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ?
                0 : ap.getTransparent();
        int retry = ap.getRetry();
        int charset = ap.getCharset();
        long handle = -1;
        long folderId = -1;

        if (recipientList == null) {
            if (D) Log.d(TAG, "empty recipient list");
            return -1;
        }

        if ( msg.getType().equals(TYPE.EMAIL) ) {
            /* Write the message to the database */
            String msgBody = ((BluetoothMapbMessageEmail) msg).getEmailBody();
            if (V) {
                int length = msgBody.length();
                Log.v(TAG, "pushMessage: message string length = " + length);
                String messages[] = msgBody.split("\r\n");
                Log.v(TAG, "pushMessage: messages count=" + messages.length);
                for(int i = 0; i < messages.length; i++) {
                    Log.v(TAG, "part " + i + ":" + messages[i]);
                }
            }
            FileOutputStream os = null;
            ParcelFileDescriptor fdOut = null;
            Uri uriInsert = Uri.parse(emailBaseUri + BluetoothMapContract.TABLE_MESSAGE);
            if (D) Log.d(TAG, "pushMessage - uriInsert= " + uriInsert.toString() +
                    ", intoFolder id=" + folderElement.getEmailFolderId());

            synchronized(mMsgListEmail) {
                // Now insert the empty message into folder
                ContentValues values = new ContentValues();
                folderId = folderElement.getEmailFolderId();
                values.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId);
                Uri uriNew = mProviderClient.insert(uriInsert, values);
                if (D) Log.d(TAG, "pushMessage - uriNew= " + uriNew.toString());
                handle =  Long.parseLong(uriNew.getLastPathSegment());

                try {
                    fdOut = mProviderClient.openFile(uriNew, "w");
                    os = new FileOutputStream(fdOut.getFileDescriptor());
                    // Write Email to DB
                    os.write(msgBody.getBytes(), 0, msgBody.getBytes().length);
                } catch (FileNotFoundException e) {
                    Log.w(TAG, e);
                    throw(new IOException("Unable to open file stream"));
                } catch (NullPointerException e) {
                    Log.w(TAG, e);
                    throw(new IllegalArgumentException("Unable to parse message."));
                } finally {
                    try {
                        if(os != null)
                            os.close();
                    } catch (IOException e) {Log.w(TAG, e);}
                    try {
                        if(fdOut != null)
                             fdOut.close();
                    } catch (IOException e) {Log.w(TAG, e);}
                }

                /* Extract the data for the inserted message, and store in local mirror, to
                 * avoid sending a NewMessage Event. */
                Msg newMsg = new Msg(handle, folderId);
                newMsg.transparent = (transparent == 1) ? true : false;
                if ( folderId == folderElement.getEmailFolderByName(
                        BluetoothMapContract.FOLDER_NAME_OUTBOX).getEmailFolderId() ) {
                    newMsg.localInitiatedSend = true;
                }
                mMsgListEmail.put(handle, newMsg);
            }
        } else { // type SMS_* of MMS
            for (BluetoothMapbMessage.vCard recipient : recipientList) {
                if(recipient.getEnvLevel() == 0) // Only send the message to the top level recipient
                {
                    /* Only send to first address */
                    String phone = recipient.getFirstPhoneNumber();
                    String email = recipient.getFirstEmail();
                    String folder = folderElement.getName();
                    boolean read = false;
                    boolean deliveryReport = true;
                    String msgBody = null;

                    /* If MMS contains text only and the size is less than ten SMS's
                     * then convert the MMS to type SMS and then proceed
                     */
                    if (msg.getType().equals(TYPE.MMS) &&
                            (((BluetoothMapbMessageMms) msg).getTextOnly() == true)) {
                        msgBody = ((BluetoothMapbMessageMms) msg).getMessageAsText();
                        SmsManager smsMng = SmsManager.getDefault();
                        ArrayList<String> parts = smsMng.divideMessage(msgBody);
                        int smsParts = parts.size();
                        if (smsParts  <= CONVERT_MMS_TO_SMS_PART_COUNT ) {
                            if (D) Log.d(TAG, "pushMessage - converting MMS to SMS, sms parts=" + smsParts );
                            msg.setType(mSmsType);
                        } else {
                            if (D) Log.d(TAG, "pushMessage - MMS text only but to big to convert to SMS");
                            msgBody = null;
                        }

                    }

                    if (msg.getType().equals(TYPE.MMS)) {
                        /* Send message if folder is outbox else just store in draft*/
                        handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMms)msg);
                    } else if (msg.getType().equals(TYPE.SMS_GSM) ||
                            msg.getType().equals(TYPE.SMS_CDMA) ) {
                        /* Add the message to the database */
                        if(msgBody == null)
                            msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody();

                        /* We need to lock the SMS list while updating the database, to avoid sending
                         * events on MCE initiated operation. */
                        Uri contentUri = Uri.parse(Sms.CONTENT_URI+ "/" + folder);
                        Uri uri;
                        synchronized(mMsgListSms) {
                            uri = Sms.addMessageToUri(mResolver, contentUri, phone, msgBody,
                                "", System.currentTimeMillis(), read, deliveryReport);

                            if(V) Log.v(TAG, "Sms.addMessageToUri() returned: " + uri);
                            if (uri == null) {
                                if (D) Log.d(TAG, "pushMessage - failure on add to uri " + contentUri);
                                return -1;
                            }
                            Cursor c = mResolver.query(uri, SMS_PROJECTION_SHORT, null, null, null);
                            try {
                                /* Extract the data for the inserted message, and store in local mirror, to
                                * avoid sending a NewMessage Event. */
                                if (c != null && c.moveToFirst()) {
                                    long id = c.getLong(c.getColumnIndex(Sms._ID));
                                    int type = c.getInt(c.getColumnIndex(Sms.TYPE));
                                    int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
                                    Msg newMsg = new Msg(id, type, threadId);
                                    mMsgListSms.put(id, newMsg);
                                } else {
                                    return -1; // This can only happen, if the message is deleted just as it is added
                                }
                            } finally {
                                close(c);
                            }

                            handle = Long.parseLong(uri.getLastPathSegment());

                            /* Send message if folder is outbox */
                            if (folder.equals(BluetoothMapContract.FOLDER_NAME_OUTBOX)) {
                                PushMsgInfo msgInfo = new PushMsgInfo(handle, transparent,
                                    retry, phone, uri);
                                mPushMsgList.put(handle, msgInfo);
                                sendMessage(msgInfo, msgBody);
                                if(V) Log.v(TAG, "sendMessage returned...");
                            }
                            /* sendMessage causes the message to be deleted and reinserted, hence we need to lock
                             * the list while this is happening. */
                        }
                    } else {
                        if (D) Log.d(TAG, "pushMessage - failure on type " );
                        return -1;
                    }
                }
            }
        }

        /* If multiple recipients return handle of last */
        return handle;
    }

    public long sendMmsMessage(String folder, String to_address, BluetoothMapbMessageMms msg) {
        /*
         *strategy:
         *1) parse message into parts
         *if folder is outbox/drafts:
         *2) push message to draft
         *if folder is outbox:
         *3) move message to outbox (to trigger the mms app to add msg to pending_messages list)
         *4) send intent to mms app in order to wake it up.
         *else if folder !outbox:
         *1) push message to folder
         * */
        if (folder != null && (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)
                ||  folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT))) {
            long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, to_address, msg);
            /* if invalid handle (-1) then just return the handle - else continue sending (if folder is outbox) */
            if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)) {
                moveDraftToOutbox(handle);
                Intent sendIntent = new Intent("android.intent.action.MMS_SEND_OUTBOX_MSG");
                if (D) Log.d(TAG, "broadcasting intent: "+sendIntent.toString());
                mContext.sendBroadcast(sendIntent);
            }
            return handle;
        } else {
            /* not allowed to push mms to anything but outbox/draft */
            throw  new IllegalArgumentException("Cannot push message to other folders than outbox/draft");
        }
    }

    private void moveDraftToOutbox(long handle) {
        /*Move message by changing the msg_box value in the content provider database */
        if (handle != -1) return;

        String whereClause = " _id= " + handle;
        Uri uri = Mms.CONTENT_URI;
        Cursor queryResult = mResolver.query(uri, null, whereClause, null, null);
        try {
            if (queryResult != null && queryResult.moveToFirst()) {
                ContentValues data = new ContentValues();
                /* set folder to be outbox */
                data.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_OUTBOX);
                mResolver.update(uri, data, whereClause, null);
                if (D) Log.d(TAG, "Moved draft MMS to outbox");
            } else {
                if (D) Log.d(TAG, "Could not move draft to outbox ");
            }
        } finally {
            queryResult.close();
        }
    }

    private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMms msg) {
        /**
         * strategy:
         * 1) parse msg into parts + header
         * 2) create thread id (abuse the ease of adding an SMS to get id for thread)
         * 3) push parts into content://mms/parts/ table
         * 3)
         */

        ContentValues values = new ContentValues();
        values.put(Mms.MESSAGE_BOX, folder);
        values.put(Mms.READ, 0);
        values.put(Mms.SEEN, 0);
        if(msg.getSubject() != null) {
            values.put(Mms.SUBJECT, msg.getSubject());
        } else {
            values.put(Mms.SUBJECT, "");
        }

        if(msg.getSubject() != null && msg.getSubject().length() > 0) {
            values.put(Mms.SUBJECT_CHARSET, 106);
        }
        values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related");
        values.put(Mms.EXPIRY, 604800);
        values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
        values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
        values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION);
        values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL);
        values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO);
        values.put(Mms.TRANSACTION_ID, "T"+ Long.toHexString(System.currentTimeMillis()));
        values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO);
        values.put(Mms.LOCKED, 0);
        if(msg.getTextOnly() == true)
            values.put(Mms.TEXT_ONLY, true);
        values.put(Mms.MESSAGE_SIZE, msg.getSize());

        // Get thread id
        Set<String> recipients = new HashSet<String>();
        recipients.addAll(Arrays.asList(to_address));
        values.put(Mms.THREAD_ID, Telephony.Threads.getOrCreateThreadId(mContext, recipients));
        Uri uri = Mms.CONTENT_URI;

        synchronized (mMsgListMms) {
            uri = mResolver.insert(uri, values);

            if (uri == null) {
                // unable to insert MMS
                Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri);
                return -1;
            }
            /* As we already have all the values we need, we could skip the query, but
               doing the query ensures we get any changes made by the content provider
               at insert. */
            Cursor c = mResolver.query(uri, MMS_PROJECTION_SHORT, null, null, null);
            try {
                if (c != null && c.moveToFirst()) {
                    long id = c.getLong(c.getColumnIndex(Mms._ID));
                    int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
                    int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));

                    /* We must filter out any actions made by the MCE. Add the new message to
                     * the list of known messages. */

                    Msg newMsg = new Msg(id, type, threadId);
                    newMsg.localInitiatedSend = true;
                    mMsgListMms.put(id, newMsg);
                }
            } finally {
                close(c);
            }
        } // Done adding changes, unlock access to mMsgListMms to allow sending MMS events again

        long handle = Long.parseLong(uri.getLastPathSegment());
        if (V) Log.v(TAG, " NEW URI " + uri.toString());

        try {
            if(msg.getMimeParts() == null) {
                /* Perhaps this message have been deleted, and no longer have any content, but only headers */
                Log.w(TAG, "No MMS parts present...");
            } else {
                if(V) Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base.");
                int count = 0;
                for(MimePart part : msg.getMimeParts()) {
                    ++count;
                    values.clear();
                    if(part.mContentType != null &&  part.mContentType.toUpperCase().contains("TEXT")) {
                        values.put(Mms.Part.CONTENT_TYPE, "text/plain");
                        values.put(Mms.Part.CHARSET, 106);
                        if(part.mPartName != null) {
                            values.put(Mms.Part.FILENAME, part.mPartName);
                            values.put(Mms.Part.NAME, part.mPartName);
                        } else {
                            values.put(Mms.Part.FILENAME, "text_" + count +".txt");
                            values.put(Mms.Part.NAME, "text_" + count +".txt");
                        }
                        // Ensure we have "ci" set
                        if(part.mContentId != null) {
                            values.put(Mms.Part.CONTENT_ID, part.mContentId);
                        } else {
                            if(part.mPartName != null) {
                                values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">");
                            } else {
                                values.put(Mms.Part.CONTENT_ID, "<text_" + count + ">");
                            }
                        }
                        // Ensure we have "cl" set
                        if(part.mContentLocation != null) {
                            values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
                        } else {
                            if(part.mPartName != null) {
                                values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".txt");
                            } else {
                                values.put(Mms.Part.CONTENT_LOCATION, "text_" + count + ".txt");
                            }
                        }

                        if(part.mContentDisposition != null) {
                            values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
                        }
                        values.put(Mms.Part.TEXT, part.getDataAsString());
                        uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part");
                        uri = mResolver.insert(uri, values);
                        if(V) Log.v(TAG, "Added TEXT part");

                    } else if (part.mContentType != null &&  part.mContentType.toUpperCase().contains("SMIL")){

                        values.put(Mms.Part.SEQ, -1);
                        values.put(Mms.Part.CONTENT_TYPE, "application/smil");
                        if(part.mContentId != null) {
                            values.put(Mms.Part.CONTENT_ID, part.mContentId);
                        } else {
                            values.put(Mms.Part.CONTENT_ID, "<smil_" + count + ">");
                        }
                        if(part.mContentLocation != null) {
                            values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
                        } else {
                            values.put(Mms.Part.CONTENT_LOCATION, "smil_" + count + ".xml");
                        }

                        if(part.mContentDisposition != null)
                            values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
                        values.put(Mms.Part.FILENAME, "smil.xml");
                        values.put(Mms.Part.NAME, "smil.xml");
                        values.put(Mms.Part.TEXT, new String(part.mData, "UTF-8"));

                        uri = Uri.parse(Mms.CONTENT_URI+ "/" + handle + "/part");
                        uri = mResolver.insert(uri, values);
                        if (V) Log.v(TAG, "Added SMIL part");

                    }else /*VIDEO/AUDIO/IMAGE*/ {
                        writeMmsDataPart(handle, part, count);
                        if (V) Log.v(TAG, "Added OTHER part");
                    }
                    if (uri != null){
                        if (V) Log.v(TAG, "Added part with content-type: "+ part.mContentType + " to Uri: " + uri.toString());
                    }
                }
            }
        } catch (UnsupportedEncodingException e) {
            Log.w(TAG, e);
        } catch (IOException e) {
            Log.w(TAG, e);
        }

        values.clear();
        values.put(Mms.Addr.CONTACT_ID, "null");
        values.put(Mms.Addr.ADDRESS, "insert-address-token");
        values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_FROM);
        values.put(Mms.Addr.CHARSET, 106);

        uri = Uri.parse(Mms.CONTENT_URI + "/"  + handle + "/addr");
        uri = mResolver.insert(uri, values);
        if (uri != null && V){
            Log.v(TAG, " NEW URI " + uri.toString());
        }

        values.clear();
        values.put(Mms.Addr.CONTACT_ID, "null");
        values.put(Mms.Addr.ADDRESS, to_address);
        values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_TO);
        values.put(Mms.Addr.CHARSET, 106);

        uri = Uri.parse(Mms.CONTENT_URI + "/"  + handle + "/addr");
        uri = mResolver.insert(uri, values);
        if (uri != null && V){
            Log.v(TAG, " NEW URI " + uri.toString());
        }
        return handle;
    }


    private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException{
        ContentValues values = new ContentValues();
        values.put(Mms.Part.MSG_ID, handle);
        if(part.mContentType != null) {
            values.put(Mms.Part.CONTENT_TYPE, part.mContentType);
        } else {
            Log.w(TAG, "MMS has no CONTENT_TYPE for part " + count);
        }
        if(part.mContentId != null) {
            values.put(Mms.Part.CONTENT_ID, part.mContentId);
        } else {
            if(part.mPartName != null) {
                values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">");
            } else {
                values.put(Mms.Part.CONTENT_ID, "<part_" + count + ">");
            }
        }

        if(part.mContentLocation != null) {
            values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
        } else {
            if(part.mPartName != null) {
                values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".dat");
            } else {
                values.put(Mms.Part.CONTENT_LOCATION, "part_" + count + ".dat");
            }
        }
        if(part.mContentDisposition != null)
            values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
        if(part.mPartName != null) {
            values.put(Mms.Part.FILENAME, part.mPartName);
            values.put(Mms.Part.NAME, part.mPartName);
        } else {
            /* We must set at least one part identifier */
            values.put(Mms.Part.FILENAME, "part_" + count + ".dat");
            values.put(Mms.Part.NAME, "part_" + count + ".dat");
        }
        Uri partUri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part");
        Uri res = mResolver.insert(partUri, values);

        // Add data to part
        OutputStream os = mResolver.openOutputStream(res);
        os.write(part.mData);
        os.close();
    }


    public void sendMessage(PushMsgInfo msgInfo, String msgBody) {

        SmsManager smsMng = SmsManager.getDefault();
        ArrayList<String> parts = smsMng.divideMessage(msgBody);
        msgInfo.parts = parts.size();
        // We add a time stamp to differentiate delivery reports from each other for resent messages
        msgInfo.timestamp = Calendar.getInstance().getTime().getTime();
        msgInfo.partsDelivered = 0;
        msgInfo.partsSent = 0;

        ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts);
        ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts);

        /*       We handle the SENT intent in the MAP service, as this object
         *       is destroyed at disconnect, hence if a disconnect occur while sending
         *       a message, there is no intent handler to move the message from outbox
         *       to the correct folder.
         *       The correct solution would be to create a service that will start based on
         *       the intent, if BT is turned off. */

        for (int i = 0; i < msgInfo.parts; i++) {
            Intent intentDelivery, intentSent;

            intentDelivery = new Intent(ACTION_MESSAGE_DELIVERY, null);
            /* Add msgId and part number to ensure the intents are different, and we
             * thereby get an intent for each msg part.
             * setType is needed to create different intents for each message id/ time stamp,
             * as the extras are not used when comparing. */
            intentDelivery.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i);
            intentDelivery.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id);
            intentDelivery.putExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, msgInfo.timestamp);
            PendingIntent pendingIntentDelivery = PendingIntent.getBroadcast(mContext, 0,
                    intentDelivery, PendingIntent.FLAG_UPDATE_CURRENT);

            intentSent = new Intent(ACTION_MESSAGE_SENT, null);
            /* Add msgId and part number to ensure the intents are different, and we
             * thereby get an intent for each msg part.
             * setType is needed to create different intents for each message id/ time stamp,
             * as the extras are not used when comparing. */
            intentSent.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i);
            intentSent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id);
            intentSent.putExtra(EXTRA_MESSAGE_SENT_URI, msgInfo.uri.toString());
            intentSent.putExtra(EXTRA_MESSAGE_SENT_RETRY, msgInfo.retry);
            intentSent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, msgInfo.transparent);

            PendingIntent pendingIntentSent = PendingIntent.getBroadcast(mContext, 0,
                    intentSent, PendingIntent.FLAG_UPDATE_CURRENT);

            // We use the same pending intent for all parts, but do not set the one shot flag.
            deliveryIntents.add(pendingIntentDelivery);
            sentIntents.add(pendingIntentSent);
        }

        Log.d(TAG, "sendMessage to " + msgInfo.phone);

        smsMng.sendMultipartTextMessage(msgInfo.phone, null, parts, sentIntents,
            deliveryIntents);
    }

    private static final String ACTION_MESSAGE_DELIVERY =
        "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY";
    public static final String ACTION_MESSAGE_SENT =
        "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT";

    public static final String EXTRA_MESSAGE_SENT_HANDLE = "HANDLE";
    public static final String EXTRA_MESSAGE_SENT_RESULT = "result";
    public static final String EXTRA_MESSAGE_SENT_URI = "uri";
    public static final String EXTRA_MESSAGE_SENT_RETRY = "retry";
    public static final String EXTRA_MESSAGE_SENT_TRANSPARENT = "transparent";
    public static final String EXTRA_MESSAGE_SENT_TIMESTAMP = "timestamp";

    private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver();

    private boolean mInitialized = false;

    private class SmsBroadcastReceiver extends BroadcastReceiver {
        private final String[] ID_PROJECTION = new String[] { Sms._ID };
        private final Uri UPDATE_STATUS_URI = Uri.withAppendedPath(Sms.CONTENT_URI, "/status");

        public void register() {
            Handler handler = new Handler(Looper.getMainLooper());

            IntentFilter intentFilter = new IntentFilter();
            intentFilter.addAction(ACTION_MESSAGE_DELIVERY);
            try{
                intentFilter.addDataType("message/*");
            } catch (MalformedMimeTypeException e) {
                Log.e(TAG, "Wrong mime type!!!", e);
            }

            mContext.registerReceiver(this, intentFilter, null, handler);
        }

        public void unregister() {
            try {
                mContext.unregisterReceiver(this);
            } catch (IllegalArgumentException e) {
                /* do nothing */
            }
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1);
            PushMsgInfo msgInfo = mPushMsgList.get(handle);

            Log.d(TAG, "onReceive: action"  + action);

            if (msgInfo == null) {
                Log.d(TAG, "onReceive: no msgInfo found for handle " + handle);
                return;
            }

            if (action.equals(ACTION_MESSAGE_DELIVERY)) {
                long timestamp = intent.getLongExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, 0);
                int status = -1;
                if(msgInfo.timestamp == timestamp) {
                    msgInfo.partsDelivered++;
                    byte[] pdu = intent.getByteArrayExtra("pdu");
                    String format = intent.getStringExtra("format");

                    SmsMessage message = SmsMessage.createFromPdu(pdu, format);
                    if (message == null) {
                        Log.d(TAG, "actionMessageDelivery: Can't get message from pdu");
                        return;
                    }
                    status = message.getStatus();
                    if(status != 0/*0 is success*/) {
                        msgInfo.statusDelivered = status;
                    }
                }
                if (msgInfo.partsDelivered == msgInfo.parts) {
                    actionMessageDelivery(context, intent, msgInfo);
                }
            } else {
                Log.d(TAG, "onReceive: Unknown action " + action);
            }
        }

        private void actionMessageDelivery(Context context, Intent intent, PushMsgInfo msgInfo) {
            Uri messageUri = intent.getData();
            msgInfo.sendInProgress = false;

            Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null);

            try {
                if (cursor.moveToFirst()) {
                    int messageId = cursor.getInt(0);

                    Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId);

                    if(D) Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + msgInfo.statusDelivered);

                    ContentValues contentValues = new ContentValues(2);

                    contentValues.put(Sms.STATUS, msgInfo.statusDelivered);
                    contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis());
                    mResolver.update(updateUri, contentValues, null, null);
                } else {
                    Log.d(TAG, "Can't find message for status update: " + messageUri);
                }
            } finally {
                cursor.close();
            }

            if (msgInfo.statusDelivered == 0) {
                Event evt = new Event(EVENT_TYPE_DELEVERY_SUCCESS, msgInfo.id,
                    folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
                sendEvent(evt);
            } else {
                Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id,
                    folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
                sendEvent(evt);
            }

            mPushMsgList.remove(msgInfo.id);
        }
    }

    static public void actionMessageSentDisconnected(Context context, Intent intent, int result) {
        /* Check permission for message deletion. */
        if (context.checkCallingOrSelfPermission(android.Manifest.permission.WRITE_SMS)
              != PackageManager.PERMISSION_GRANTED) {
            Log.w(TAG, "actionMessageSentDisconnected: Not allowed to delete SMS/MMS messages");
            return;
        }

        boolean delete = false;
        //int retry = intent.getIntExtra(EXTRA_MESSAGE_SENT_RETRY, 0);
        int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0);
        String uriString = intent.getStringExtra(EXTRA_MESSAGE_SENT_URI);
        if(uriString == null) {
            // Nothing we can do about it, just bail out
            return;
        }
        Uri uri = Uri.parse(uriString);

        if (result == Activity.RESULT_OK) {
            Log.d(TAG, "actionMessageSentDisconnected: result OK");
            if (transparent == 0) {
                if (!Sms.moveMessageToFolder(context, uri,
                        Sms.MESSAGE_TYPE_SENT, 0)) {
                    Log.d(TAG, "Failed to move " + uri + " to SENT");
                }
            } else {
                delete = true;
            }
        } else {
            /*if (retry == 1) {
                 The retry feature only works while connected, else we fail the send,
                 * and move the message to failed, to let the user/app resend manually later.
            } else */{
                if (transparent == 0) {
                    if (!Sms.moveMessageToFolder(context, uri,
                            Sms.MESSAGE_TYPE_FAILED, 0)) {
                        Log.d(TAG, "Failed to move " + uri + " to FAILED");
                    }
                } else {
                    delete = true;
                }
            }
        }

        if (delete) {
            /* Delete from DB */
            ContentResolver resolver = context.getContentResolver();
            if (resolver != null) {
                resolver.delete(uri, null, null);
            } else {
                Log.w(TAG, "Unable to get resolver");
            }
        }
    }

    private void registerPhoneServiceStateListener() {
        TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
        tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE);
    }

    private void unRegisterPhoneServiceStateListener() {
        TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
        tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE);
    }

    private void resendPendingMessages() {
        /* Send pending messages in outbox */
        String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
        Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null);

        try {
            while (c!= null && c.moveToNext()) {
                long id = c.getLong(c.getColumnIndex(Sms._ID));
                String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
                PushMsgInfo msgInfo = mPushMsgList.get(id);
                if (msgInfo == null || msgInfo.resend == false || msgInfo.sendInProgress == true) {
                    continue;
                }
                msgInfo.sendInProgress = true;
                sendMessage(msgInfo, msgBody);
            }
        } finally {
            close(c);
        }
    }

    private void failPendingMessages() {
        /* Move pending messages from outbox to failed */
        String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
        Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null);
        if (c == null) return;

        try {
            while (c!= null && c.moveToNext()) {
                long id = c.getLong(c.getColumnIndex(Sms._ID));
                String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
                PushMsgInfo msgInfo = mPushMsgList.get(id);
                if (msgInfo == null || msgInfo.resend == false) {
                    continue;
                }
                Sms.moveMessageToFolder(mContext, msgInfo.uri,
                    Sms.MESSAGE_TYPE_FAILED, 0);
            }
        } finally {
            close(c);
        }
    }

    private void removeDeletedMessages() {
        /* Remove messages from virtual "deleted" folder (thread_id -1) */
        mResolver.delete(Sms.CONTENT_URI,
                "thread_id = " + DELETED_THREAD_ID, null);
    }

    private PhoneStateListener mPhoneListener = new PhoneStateListener() {
        @Override
        public void onServiceStateChanged(ServiceState serviceState) {
            Log.d(TAG, "Phone service state change: " + serviceState.getState());
            if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
                resendPendingMessages();
            }
        }
    };

    public void init() {
        mSmsBroadcastReceiver.register();
        registerPhoneServiceStateListener();
        mInitialized = true;
    }

    public void deinit() {
        mInitialized = false;
        unregisterObserver();
        mSmsBroadcastReceiver.unregister();
        unRegisterPhoneServiceStateListener();
        failPendingMessages();
        removeDeletedMessages();
    }

    public boolean handleSmsSendIntent(Context context, Intent intent){
        if(mInitialized) {
            mSmsBroadcastReceiver.onReceive(context, intent);
            return true;
        }
        return false;
    }
}
