BluetoothMapContentObserver.java revision fac695e2dc25510b12684217d066b86dbbbba176
1/*
2* Copyright (C) 2013 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 java.io.ByteArrayInputStream;
18import java.io.FileNotFoundException;
19import java.io.IOException;
20import java.io.OutputStream;
21import java.io.StringWriter;
22import java.io.UnsupportedEncodingException;
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.Collections;
26import java.util.HashMap;
27import java.util.HashSet;
28import java.util.Map;
29import java.util.Set;
30
31import org.xmlpull.v1.XmlSerializer;
32
33import android.app.Activity;
34import android.app.PendingIntent;
35import android.content.BroadcastReceiver;
36import android.content.ContentResolver;
37import android.content.ContentUris;
38import android.content.ContentValues;
39import android.content.Context;
40import android.content.Intent;
41import android.content.IntentFilter;
42import android.database.ContentObserver;
43import android.database.Cursor;
44import android.net.Uri;
45import android.os.Handler;
46import android.provider.BaseColumns;
47import android.provider.Telephony;
48import android.provider.Telephony.Mms;
49import android.provider.Telephony.MmsSms;
50import android.provider.Telephony.Sms;
51import android.provider.Telephony.Sms.Inbox;
52import android.telephony.PhoneStateListener;
53import android.telephony.ServiceState;
54import android.telephony.SmsManager;
55import android.telephony.SmsMessage;
56import android.telephony.TelephonyManager;
57import android.util.Log;
58import android.util.Xml;
59
60import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
61import com.android.bluetooth.map.BluetoothMapbMessageMmsEmail.MimePart;
62import com.google.android.mms.pdu.PduHeaders;
63
64public class BluetoothMapContentObserver {
65    private static final String TAG = "BluetoothMapContentObserver";
66
67    private static final boolean D = false;
68    private static final boolean V = false;
69
70    private Context mContext;
71    private ContentResolver mResolver;
72    private BluetoothMnsObexClient mMnsClient;
73    private int mMasId;
74
75    public static final int DELETED_THREAD_ID = -1;
76
77    /* X-Mms-Message-Type field types. These are from PduHeaders.java */
78    public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84;
79
80    private TYPE mSmsType;
81
82    static final String[] SMS_PROJECTION = new String[] {
83        BaseColumns._ID,
84        Sms.THREAD_ID,
85        Sms.ADDRESS,
86        Sms.BODY,
87        Sms.DATE,
88        Sms.READ,
89        Sms.TYPE,
90        Sms.STATUS,
91        Sms.LOCKED,
92        Sms.ERROR_CODE,
93    };
94
95    static final String[] MMS_PROJECTION = new String[] {
96        BaseColumns._ID,
97        Mms.THREAD_ID,
98        Mms.MESSAGE_ID,
99        Mms.MESSAGE_SIZE,
100        Mms.SUBJECT,
101        Mms.CONTENT_TYPE,
102        Mms.TEXT_ONLY,
103        Mms.DATE,
104        Mms.DATE_SENT,
105        Mms.READ,
106        Mms.MESSAGE_BOX,
107        Mms.MESSAGE_TYPE,
108        Mms.STATUS,
109    };
110
111    public BluetoothMapContentObserver(final Context context) {
112        mContext = context;
113        mResolver = mContext.getContentResolver();
114
115        mSmsType = getSmsType();
116    }
117
118    private TYPE getSmsType() {
119        TYPE smsType = null;
120        TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
121
122        if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) {
123            smsType = TYPE.SMS_GSM;
124        } else if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
125            smsType = TYPE.SMS_CDMA;
126        }
127
128        return smsType;
129    }
130
131    private final ContentObserver mObserver = new ContentObserver(new Handler()) {
132        @Override
133        public void onChange(boolean selfChange) {
134            onChange(selfChange, null);
135        }
136
137        @Override
138        public void onChange(boolean selfChange, Uri uri) {
139            if (V) Log.d(TAG, "onChange on thread: " + Thread.currentThread().getId()
140                + " Uri: " + uri.toString() + " selfchange: " + selfChange);
141
142            handleMsgListChanges();
143        }
144    };
145
146    private static final String folderSms[] = {
147        "",
148        "inbox",
149        "sent",
150        "draft",
151        "outbox",
152        "outbox",
153        "outbox",
154        "inbox",
155        "inbox",
156    };
157
158    private static final String folderMms[] = {
159        "",
160        "inbox",
161        "sent",
162        "draft",
163        "outbox",
164    };
165
166    private class Event {
167        String eventType;
168        long handle;
169        String folder;
170        String oldFolder;
171        TYPE msgType;
172
173        public Event(String eventType, long handle, String folder,
174            String oldFolder, TYPE msgType) {
175            String PATH = "telecom/msg/";
176            this.eventType = eventType;
177            this.handle = handle;
178            if (folder != null) {
179                this.folder = PATH + folder;
180            } else {
181                this.folder = null;
182            }
183            if (oldFolder != null) {
184                this.oldFolder = PATH + oldFolder;
185            } else {
186                this.oldFolder = null;
187            }
188            this.msgType = msgType;
189        }
190
191        public byte[] encode() throws UnsupportedEncodingException {
192            StringWriter sw = new StringWriter();
193            XmlSerializer xmlEvtReport = Xml.newSerializer();
194            try {
195                xmlEvtReport.setOutput(sw);
196                xmlEvtReport.startDocument(null, null);
197                xmlEvtReport.text("\n");
198                xmlEvtReport.startTag("", "MAP-event-report");
199                xmlEvtReport.attribute("", "version", "1.0");
200
201                xmlEvtReport.startTag("", "event");
202                xmlEvtReport.attribute("", "type", eventType);
203                xmlEvtReport.attribute("", "handle", BluetoothMapUtils.getMapHandle(handle, msgType));
204                if (folder != null) {
205                    xmlEvtReport.attribute("", "folder", folder);
206                }
207                if (oldFolder != null) {
208                    xmlEvtReport.attribute("", "old_folder", oldFolder);
209                }
210                xmlEvtReport.attribute("", "msg_type", msgType.name());
211                xmlEvtReport.endTag("", "event");
212
213                xmlEvtReport.endTag("", "MAP-event-report");
214                xmlEvtReport.endDocument();
215            } catch (IllegalArgumentException e) {
216                e.printStackTrace();
217            } catch (IllegalStateException e) {
218                e.printStackTrace();
219            } catch (IOException e) {
220                e.printStackTrace();
221            }
222
223            if (V) System.out.println(sw.toString());
224
225            return sw.toString().getBytes("UTF-8");
226        }
227    }
228
229    private class Msg {
230        long id;
231        int type;
232
233        public Msg(long id, int type) {
234            this.id = id;
235            this.type = type;
236        }
237    }
238
239    private Map<Long, Msg> mMsgListSms =
240        Collections.synchronizedMap(new HashMap<Long, Msg>());
241
242    private Map<Long, Msg> mMsgListMms =
243        Collections.synchronizedMap(new HashMap<Long, Msg>());
244
245    public void registerObserver(BluetoothMnsObexClient mns, int masId) {
246        if (V) Log.d(TAG, "registerObserver");
247        /* Use MmsSms Uri since the Sms Uri is not notified on deletes */
248        mMasId = masId;
249        mMnsClient = mns;
250        mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver);
251        initMsgList();
252    }
253
254    public void unregisterObserver() {
255        if (V) Log.d(TAG, "unregisterObserver");
256        mResolver.unregisterContentObserver(mObserver);
257        mMnsClient = null;
258    }
259
260    private void sendEvent(Event evt) {
261        Log.d(TAG, "sendEvent: " + evt.eventType + " " + evt.handle + " "
262        + evt.folder + " " + evt.oldFolder + " " + evt.msgType.name());
263
264        if (mMnsClient == null) {
265            Log.d(TAG, "sendEvent: No MNS client registered - don't send event");
266            return;
267        }
268
269        try {
270            mMnsClient.sendEvent(evt.encode(), mMasId);
271        } catch (UnsupportedEncodingException ex) {
272            /* do nothing */
273        }
274    }
275
276    private void initMsgList() {
277        if (V) Log.d(TAG, "initMsgList");
278
279        mMsgListSms.clear();
280        mMsgListMms.clear();
281
282        HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();
283
284        Cursor c = mResolver.query(Sms.CONTENT_URI,
285            SMS_PROJECTION, null, null, null);
286
287        if (c != null && c.moveToFirst()) {
288            do {
289                long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
290                int type = c.getInt(c.getColumnIndex(Sms.TYPE));
291
292                Msg msg = new Msg(id, type);
293                msgListSms.put(id, msg);
294            } while (c.moveToNext());
295            c.close();
296        }
297
298        mMsgListSms = msgListSms;
299
300        HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();
301
302        c = mResolver.query(Mms.CONTENT_URI,
303            MMS_PROJECTION, null, null, null);
304
305        if (c != null && c.moveToFirst()) {
306            do {
307                long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
308                int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
309
310                Msg msg = new Msg(id, type);
311                msgListMms.put(id, msg);
312            } while (c.moveToNext());
313            c.close();
314        }
315
316        mMsgListMms = msgListMms;
317    }
318
319    private void handleMsgListChangesSms() {
320        if (V) Log.d(TAG, "handleMsgListChangesSms");
321
322        HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();
323
324        Cursor c = mResolver.query(Sms.CONTENT_URI,
325            SMS_PROJECTION, null, null, null);
326
327        synchronized(mMsgListSms) {
328            if (c != null && c.moveToFirst()) {
329                do {
330                    long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
331                    int type = c.getInt(c.getColumnIndex(Sms.TYPE));
332
333                    Msg msg = mMsgListSms.remove(id);
334
335                    if (msg == null) {
336                        /* New message */
337                        msg = new Msg(id, type);
338                        msgListSms.put(id, msg);
339
340                        if (folderSms[type].equals("inbox")) {
341                            Event evt = new Event("NewMessage", id, folderSms[type],
342                                null, mSmsType);
343                            sendEvent(evt);
344                        }
345                    } else {
346                        /* Existing message */
347                        if (type != msg.type) {
348                            Log.d(TAG, "new type: " + type + " old type: " + msg.type);
349                            Event evt = new Event("MessageShift", id, folderSms[type],
350                                folderSms[msg.type], mSmsType);
351                            sendEvent(evt);
352                            msg.type = type;
353                        }
354                        msgListSms.put(id, msg);
355                    }
356                } while (c.moveToNext());
357                c.close();
358            }
359
360            for (Msg msg : mMsgListSms.values()) {
361                Event evt = new Event("MessageDeleted", msg.id, "deleted",
362                    folderSms[msg.type], mSmsType);
363                sendEvent(evt);
364            }
365
366            mMsgListSms = msgListSms;
367        }
368    }
369
370    private void handleMsgListChangesMms() {
371        if (V) Log.d(TAG, "handleMsgListChangesMms");
372
373        HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();
374
375        Cursor c = mResolver.query(Mms.CONTENT_URI,
376            MMS_PROJECTION, null, null, null);
377
378        synchronized(mMsgListMms) {
379            if (c != null && c.moveToFirst()) {
380                do {
381                    long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
382                    int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
383                    int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE));
384
385                    Msg msg = mMsgListMms.remove(id);
386
387                    if (msg == null) {
388                        /* New message - only notify on retrieve conf */
389                        if (folderMms[type].equals("inbox") &&
390                            mtype != MESSAGE_TYPE_RETRIEVE_CONF) {
391                                continue;
392                        }
393
394                        msg = new Msg(id, type);
395                        msgListMms.put(id, msg);
396
397                        if (folderMms[type].equals("inbox")) {
398                            Event evt = new Event("NewMessage", id, folderMms[type],
399                                null, TYPE.MMS);
400                            sendEvent(evt);
401                        }
402                    } else {
403                        /* Existing message */
404                        if (type != msg.type) {
405                            Log.d(TAG, "new type: " + type + " old type: " + msg.type);
406                            Event evt = new Event("MessageShift", id, folderMms[type],
407                                folderMms[msg.type], TYPE.MMS);
408                            sendEvent(evt);
409                            msg.type = type;
410
411                            if (folderMms[type].equals("sent")) {
412                                evt = new Event("SendingSuccess", id,
413                                    folderSms[type], null, TYPE.MMS);
414                                sendEvent(evt);
415                            }
416                        }
417                        msgListMms.put(id, msg);
418                    }
419                } while (c.moveToNext());
420                c.close();
421            }
422
423            for (Msg msg : mMsgListMms.values()) {
424                Event evt = new Event("MessageDeleted", msg.id, "deleted",
425                    folderMms[msg.type], TYPE.MMS);
426                sendEvent(evt);
427            }
428
429            mMsgListMms = msgListMms;
430        }
431    }
432
433    private void handleMsgListChanges() {
434        handleMsgListChangesSms();
435        handleMsgListChangesMms();
436    }
437
438    private boolean deleteMessageMms(long handle) {
439        boolean res = false;
440        Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
441        Cursor c = mResolver.query(uri, null, null, null, null);
442        if (c != null && c.moveToFirst()) {
443            /* Move to deleted folder, or delete if already in deleted folder */
444            int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
445            if (threadId != DELETED_THREAD_ID) {
446                /* Set deleted thread id */
447                ContentValues contentValues = new ContentValues();
448                contentValues.put(Mms.THREAD_ID, DELETED_THREAD_ID);
449                mResolver.update(uri, contentValues, null, null);
450            } else {
451                /* Delete from observer message list to avoid delete notifications */
452                mMsgListMms.remove(handle);
453                /* Delete message */
454                mResolver.delete(uri, null, null);
455            }
456            res = true;
457        }
458        if (c != null) {
459            c.close();
460        }
461        return res;
462    }
463
464    private void updateThreadIdMms(Uri uri, long threadId) {
465        ContentValues contentValues = new ContentValues();
466        contentValues.put(Mms.THREAD_ID, threadId);
467        mResolver.update(uri, contentValues, null, null);
468    }
469
470    private boolean unDeleteMessageMms(long handle) {
471        boolean res = false;
472        Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
473        Cursor c = mResolver.query(uri, null, null, null, null);
474
475        if (c != null && c.moveToFirst()) {
476            int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
477            if (threadId == DELETED_THREAD_ID) {
478                /* Restore thread id from address, or if no thread for address
479                 * create new thread by insert and remove of fake message */
480                String address;
481                long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
482                int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
483                if (msgBox == Mms.MESSAGE_BOX_INBOX) {
484                    address = BluetoothMapContent.getAddressMms(mResolver, id,
485                        BluetoothMapContent.MMS_FROM);
486                } else {
487                    address = BluetoothMapContent.getAddressMms(mResolver, id,
488                        BluetoothMapContent.MMS_TO);
489                }
490                Set<String> recipients = new HashSet<String>();
491                recipients.addAll(Arrays.asList(address));
492                updateThreadIdMms(uri, Telephony.Threads.getOrCreateThreadId(mContext, recipients));
493            } else {
494                Log.d(TAG, "Message not in deleted folder: handle " + handle
495                    + " threadId " + threadId);
496            }
497            res = true;
498        }
499        if (c != null) {
500            c.close();
501        }
502        return res;
503    }
504
505    private boolean deleteMessageSms(long handle) {
506        boolean res = false;
507        Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
508        Cursor c = mResolver.query(uri, null, null, null, null);
509
510        if (c != null && c.moveToFirst()) {
511            /* Move to deleted folder, or delete if already in deleted folder */
512            int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
513            if (threadId != DELETED_THREAD_ID) {
514                /* Set deleted thread id */
515                ContentValues contentValues = new ContentValues();
516                contentValues.put(Sms.THREAD_ID, DELETED_THREAD_ID);
517                mResolver.update(uri, contentValues, null, null);
518            } else {
519                /* Delete from observer message list to avoid delete notifications */
520                mMsgListSms.remove(handle);
521                /* Delete message */
522                mResolver.delete(uri, null, null);
523            }
524            res = true;
525        }
526        if (c != null) {
527            c.close();
528        }
529        return res;
530    }
531
532    private void updateThreadIdSms(Uri uri, long threadId) {
533        ContentValues contentValues = new ContentValues();
534        contentValues.put(Sms.THREAD_ID, threadId);
535        mResolver.update(uri, contentValues, null, null);
536    }
537
538    private boolean unDeleteMessageSms(long handle) {
539        boolean res = false;
540        Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
541        Cursor c = mResolver.query(uri, null, null, null, null);
542
543        if (c != null && c.moveToFirst()) {
544            int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
545            if (threadId == DELETED_THREAD_ID) {
546                String address = c.getString(c.getColumnIndex(Sms.ADDRESS));
547                Set<String> recipients = new HashSet<String>();
548                recipients.addAll(Arrays.asList(address));
549                updateThreadIdSms(uri, Telephony.Threads.getOrCreateThreadId(mContext, recipients));
550            } else {
551                Log.d(TAG, "Message not in deleted folder: handle " + handle
552                    + " threadId " + threadId);
553            }
554            res = true;
555        }
556        if (c != null) {
557            c.close();
558        }
559        return res;
560    }
561
562    public boolean setMessageStatusDeleted(long handle, TYPE type, int statusValue) {
563        boolean res = false;
564        if (D) Log.d(TAG, "setMessageStatusDeleted: handle " + handle
565            + " type " + type + " value " + statusValue);
566
567        if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) {
568            if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
569                res = deleteMessageSms(handle);
570            } else if (type == TYPE.MMS) {
571                res = deleteMessageMms(handle);
572            }
573        } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) {
574            if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
575                res = unDeleteMessageSms(handle);
576            } else if (type == TYPE.MMS) {
577                res = unDeleteMessageMms(handle);
578            }
579        }
580        return res;
581    }
582
583    public boolean setMessageStatusRead(long handle, TYPE type, int statusValue) {
584        boolean res = true;
585
586        if (D) Log.d(TAG, "setMessageStatusRead: handle " + handle
587            + " type " + type + " value " + statusValue);
588
589        /* Approved MAP spec errata 3445 states that read status initiated */
590        /* by the MCE shall change the MSE read status. */
591
592        if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
593            Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
594            Cursor c = mResolver.query(uri, null, null, null, null);
595
596            ContentValues contentValues = new ContentValues();
597            contentValues.put(Sms.READ, statusValue);
598            mResolver.update(uri, contentValues, null, null);
599        } else if (type == TYPE.MMS) {
600            Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
601            Cursor c = mResolver.query(uri, null, null, null, null);
602
603            ContentValues contentValues = new ContentValues();
604            contentValues.put(Mms.READ, statusValue);
605            mResolver.update(uri, contentValues, null, null);
606        }
607
608        return res;
609    }
610
611    private class PushMsgInfo {
612        long id;
613        int transparent;
614        int retry;
615        String phone;
616        Uri uri;
617        int parts;
618        int partsSent;
619        int partsDelivered;
620        boolean resend;
621
622        public PushMsgInfo(long id, int transparent,
623            int retry, String phone, Uri uri) {
624            this.id = id;
625            this.transparent = transparent;
626            this.retry = retry;
627            this.phone = phone;
628            this.uri = uri;
629            this.resend = false;
630        };
631    }
632
633    private Map<Long, PushMsgInfo> mPushMsgList =
634        Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>());
635
636    public long pushMessage(BluetoothMapbMessage msg, String folder,
637        BluetoothMapAppParams ap) throws IllegalArgumentException {
638        if (D) Log.d(TAG, "pushMessage");
639        ArrayList<BluetoothMapbMessage.vCard> recipientList = msg.getRecipients();
640        int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ?
641                0 : ap.getTransparent();
642        int retry = ap.getRetry();
643        int charset = ap.getCharset();
644        long handle = -1;
645
646        if (recipientList == null) {
647            Log.d(TAG, "empty recipient list");
648            return -1;
649        }
650
651        for (BluetoothMapbMessage.vCard recipient : recipientList) {
652            if(recipient.getEnvLevel() == 0) // Only send the message to the top level recipient
653            {
654                /* Only send to first address */
655                String phone = recipient.getFirstPhoneNumber();
656                boolean read = false;
657                boolean deliveryReport = true;
658
659                switch(msg.getType()){
660                    case MMS:
661                    {
662                        /* Send message if folder is outbox */
663                        if (folder.equals("outbox")) {
664                           handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMmsEmail)msg);
665                        }
666                        break;
667                    }
668                    case SMS_GSM: //fall-through
669                    case SMS_CDMA:
670                    {
671                        /* Add the message to the database */
672                        String msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody();
673                        Uri contentUri = Uri.parse("content://sms/" + folder);
674                        Uri uri = Sms.addMessageToUri(mResolver, contentUri, phone, msgBody,
675                            "", System.currentTimeMillis(), read, deliveryReport);
676
677                        if (uri == null) {
678                            Log.d(TAG, "pushMessage - failure on add to uri " + contentUri);
679                            return -1;
680                        }
681
682                        handle = Long.parseLong(uri.getLastPathSegment());
683
684                        /* Send message if folder is outbox */
685                        if (folder.equals("outbox")) {
686                            PushMsgInfo msgInfo = new PushMsgInfo(handle, transparent,
687                                retry, phone, uri);
688                            mPushMsgList.put(handle, msgInfo);
689                            sendMessage(msgInfo, msgBody);
690                        }
691                        break;
692                    }
693                    case EMAIL:
694                    {
695                        break;
696                    }
697                }
698
699            }
700        }
701
702        /* If multiple recipients return handle of last */
703        return handle;
704    }
705
706
707
708    public long sendMmsMessage(String folder,String to_address, BluetoothMapbMessageMmsEmail msg) {
709        /*
710         *strategy:
711         *1) parse message into parts
712         *if folder is outbox/drafts:
713         *2) push message to draft
714         *if folder is outbox:
715         *3) move message to outbox (to trigger the mms app to add msg to pending_messages list)
716         *4) send intent to mms app in order to wake it up.
717         *else if folder !outbox:
718         *1) push message to folder
719         * */
720        if (folder != null && (folder.equalsIgnoreCase("outbox")||  folder.equalsIgnoreCase("drafts"))) {
721            long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, to_address, msg);
722            /* if invalid handle (-1) then just return the handle - else continue sending (if folder is outbox) */
723            if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase("outbox")) {
724                moveDraftToOutbox(handle);
725
726                Intent sendIntent = new Intent("android.intent.action.MMS_SEND_OUTBOX_MSG");
727                Log.d(TAG, "broadcasting intent: "+sendIntent.toString());
728                mContext.sendBroadcast(sendIntent);
729            }
730            return handle;
731        } else {
732            /* not allowed to push mms to anything but outbox/drafts */
733            throw  new IllegalArgumentException("Cannot push message to other folders than outbox/drafts");
734        }
735
736    }
737
738
739    private void moveDraftToOutbox(long handle) {
740        ContentResolver contentResolver = mContext.getContentResolver();
741        /*Move message by changing the msg_box value in the content provider database */
742        if (handle != -1) {
743            String whereClause = " _id= " + handle;
744            Uri uri = Uri.parse("content://mms");
745            Cursor queryResult = contentResolver.query(uri, null, whereClause, null, null);
746            if (queryResult != null) {
747                if (queryResult.getCount() > 0) {
748                    queryResult.moveToFirst();
749                    ContentValues data = new ContentValues();
750                    /* set folder to be outbox */
751                    data.put("msg_box", Mms.MESSAGE_BOX_OUTBOX);
752                    contentResolver.update(uri, data, whereClause, null);
753                    Log.d(TAG, "moved draft MMS to outbox");
754                }
755                queryResult.close();
756            }else {
757                Log.d(TAG, "Could not move draft to outbox ");
758            }
759        }
760    }
761    private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMmsEmail msg) {
762        /**
763         * strategy:
764         * 1) parse msg into parts + header
765         * 2) create thread id (abuse the ease of adding an SMS to get id for thread)
766         * 3) push parts into content://mms/parts/ table
767         * 3)
768         */
769
770        ContentValues values = new ContentValues();
771        values.put("msg_box", folder);
772
773        values.put("read", 0);
774        values.put("seen", 0);
775        values.put("sub", msg.getSubject());
776        values.put("sub_cs", 106);
777        values.put("ct_t", "application/vnd.wap.multipart.related");
778        values.put("exp", 604800);
779        values.put("m_cls", PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
780        values.put("m_type", PduHeaders.MESSAGE_TYPE_SEND_REQ);
781        values.put("v", PduHeaders.CURRENT_MMS_VERSION);
782        values.put("pri", PduHeaders.PRIORITY_NORMAL);
783        values.put("rr", PduHeaders.VALUE_NO);
784        values.put("tr_id", "T"+ Long.toHexString(System.currentTimeMillis()));
785        values.put("d_rpt", PduHeaders.VALUE_NO);
786        values.put("locked", 0);
787        if(msg.getTextOnly() == true)
788            values.put("text_only", true);
789
790        values.put("m_size", msg.getSize());
791
792     // Get thread id
793        Set<String> recipients = new HashSet<String>();
794        recipients.addAll(Arrays.asList(to_address));
795        values.put("thread_id", Telephony.Threads.getOrCreateThreadId(mContext, recipients));
796        Uri uri = Uri.parse("content://mms");
797
798        ContentResolver cr = mContext.getContentResolver();
799        uri = cr.insert(uri, values);
800
801        if (uri == null) {
802            // unable to insert MMS
803            Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri);
804            return -1;
805        }
806
807        long handle = Long.parseLong(uri.getLastPathSegment());
808        if (V){
809            Log.v(TAG, " NEW URI " + uri.toString());
810        }
811        try {
812            if(V) Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base.");
813        for(MimePart part : msg.getMimeParts()) {
814            int count = 0;
815            count++;
816            values.clear();
817            if(part.contentType != null &&  part.contentType.toUpperCase().contains("TEXT")) {
818                values.put("ct", "text/plain");
819                values.put("chset", 106);
820                if(part.partName != null) {
821                    values.put("fn", part.partName);
822                    values.put("name", part.partName);
823                } else if(part.contentId == null && part.contentLocation == null) {
824                    /* We must set at least one part identifier */
825                    values.put("fn", "text_" + count +".txt");
826                    values.put("name", "text_" + count +".txt");
827                }
828                if(part.contentId != null) {
829                    values.put("cid", part.contentId);
830                }
831                if(part.contentLocation != null)
832                    values.put("cl", part.contentLocation);
833                if(part.contentDisposition != null)
834                    values.put("cd", part.contentDisposition);
835                values.put("text", new String(part.data, "UTF-8"));
836                uri = Uri.parse("content://mms/" + handle + "/part");
837                uri = cr.insert(uri, values);
838                if(V) Log.v(TAG, "Added TEXT part");
839
840            } else if (part.contentType != null &&  part.contentType.toUpperCase().contains("SMIL")){
841
842                values.put("seq", -1);
843                values.put("ct", "application/smil");
844                if(part.contentId != null)
845                    values.put("cid", part.contentId);
846                if(part.contentLocation != null)
847                    values.put("cl", part.contentLocation);
848                if(part.contentDisposition != null)
849                    values.put("cd", part.contentDisposition);
850                values.put("fn", "smil.xml");
851                values.put("name", "smil.xml");
852                values.put("text", new String(part.data, "UTF-8"));
853
854                uri = Uri.parse("content://mms/" + handle + "/part");
855                uri = cr.insert(uri, values);
856                if(V) Log.v(TAG, "Added SMIL part");
857
858            }else /*VIDEO/AUDIO/IMAGE*/ {
859                writeMmsDataPart(handle, part, count);
860                if(V) Log.v(TAG, "Added OTHER part");
861            }
862            if (uri != null && V){
863                Log.v(TAG, "Added part with content-type: "+ part.contentType + " to Uri: " + uri.toString());
864            }
865        }
866        } catch (UnsupportedEncodingException e) {
867            Log.w(TAG, e);
868        } catch (IOException e) {
869            Log.w(TAG, e);
870        }
871
872        values.clear();
873        values.put("contact_id", "null");
874        values.put("address", "insert-address-token");
875        values.put("type", BluetoothMapContent.MMS_FROM);
876        values.put("charset", 106);
877
878        uri = Uri.parse("content://mms/" + handle + "/addr");
879        uri = cr.insert(uri, values);
880        if (uri != null && V){
881            Log.v(TAG, " NEW URI " + uri.toString());
882        }
883
884        values.clear();
885        values.put("contact_id", "null");
886        values.put("address", to_address);
887        values.put("type", BluetoothMapContent.MMS_TO);
888        values.put("charset", 106);
889
890        uri = Uri.parse("content://mms/" + handle + "/addr");
891        uri = cr.insert(uri, values);
892        if (uri != null && V){
893            Log.v(TAG, " NEW URI " + uri.toString());
894        }
895        return handle;
896    }
897
898
899    private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException{
900        ContentValues values = new ContentValues();
901        values.put("mid", handle);
902        if(part.contentType != null)
903            values.put("ct", part.contentType);
904        if(part.contentId != null)
905            values.put("cid", part.contentId);
906        if(part.contentLocation != null)
907            values.put("cl", part.contentLocation);
908        if(part.contentDisposition != null)
909            values.put("cd", part.contentDisposition);
910        if(part.partName != null) {
911            values.put("fn", part.partName);
912            values.put("name", part.partName);
913        } else if(part.contentId == null && part.contentLocation == null) {
914            /* We must set at least one part identifier */
915            values.put("fn", "part_" + count + ".dat");
916            values.put("name", "part_" + count + ".dat");
917        }
918        Uri partUri = Uri.parse("content://mms/" + handle + "/part");
919        Uri res = mResolver.insert(partUri, values);
920
921        // Add data to part
922        OutputStream os = mResolver.openOutputStream(res);
923        os.write(part.data);
924        os.close();
925    }
926
927
928    public void sendMessage(PushMsgInfo msgInfo, String msgBody) {
929
930        SmsManager smsMng = SmsManager.getDefault();
931        ArrayList<String> parts = smsMng.divideMessage(msgBody);
932        msgInfo.parts = parts.size();
933
934        ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts);
935        ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts);
936
937        for (int i = 0; i < msgInfo.parts; i++) {
938            Intent intent;
939            intent = new Intent(ACTION_MESSAGE_DELIVERY, null);
940            intent.putExtra("HANDLE", msgInfo.id);
941            deliveryIntents.add(PendingIntent.getBroadcast(mContext, 0, intent,
942                PendingIntent.FLAG_UPDATE_CURRENT));
943
944            intent = new Intent(ACTION_MESSAGE_SENT, null);
945            intent.putExtra("HANDLE", msgInfo.id);
946            sentIntents.add(PendingIntent.getBroadcast(mContext, 0, intent,
947                PendingIntent.FLAG_UPDATE_CURRENT));
948        }
949
950        Log.d(TAG, "sendMessage to " + msgInfo.phone);
951
952        smsMng.sendMultipartTextMessage(msgInfo.phone, null, parts, sentIntents,
953            deliveryIntents);
954    }
955
956    private static final String ACTION_MESSAGE_DELIVERY =
957        "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY";
958    private static final String ACTION_MESSAGE_SENT =
959        "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT";
960
961    private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver();
962
963    private class SmsBroadcastReceiver extends BroadcastReceiver {
964        private final String[] ID_PROJECTION = new String[] { Sms._ID };
965        private final Uri UPDATE_STATUS_URI = Uri.parse("content://sms/status");
966
967        public void register() {
968            Handler handler = new Handler();
969
970            IntentFilter intentFilter = new IntentFilter();
971            intentFilter.addAction(ACTION_MESSAGE_DELIVERY);
972            intentFilter.addAction(ACTION_MESSAGE_SENT);
973            mContext.registerReceiver(this, intentFilter, null, handler);
974        }
975
976        public void unregister() {
977            try {
978                mContext.unregisterReceiver(this);
979            } catch (IllegalArgumentException e) {
980                /* do nothing */
981            }
982        }
983
984        @Override
985        public void onReceive(Context context, Intent intent) {
986            String action = intent.getAction();
987            long handle = intent.getLongExtra("HANDLE", -1);
988            PushMsgInfo msgInfo = mPushMsgList.get(handle);
989
990            Log.d(TAG, "onReceive: action"  + action);
991
992            if (msgInfo == null) {
993                Log.d(TAG, "onReceive: no msgInfo found for handle " + handle);
994                return;
995            }
996
997            if (action.equals(ACTION_MESSAGE_SENT)) {
998                msgInfo.partsSent++;
999                if (msgInfo.partsSent == msgInfo.parts) {
1000                    actionMessageSent(context, intent, msgInfo);
1001                }
1002            } else if (action.equals(ACTION_MESSAGE_DELIVERY)) {
1003                msgInfo.partsDelivered++;
1004                if (msgInfo.partsDelivered == msgInfo.parts) {
1005                    actionMessageDelivery(context, intent, msgInfo);
1006                }
1007            } else {
1008                Log.d(TAG, "onReceive: Unknown action " + action);
1009            }
1010        }
1011
1012        private void actionMessageSent(Context context, Intent intent,
1013            PushMsgInfo msgInfo) {
1014            int result = getResultCode();
1015            boolean delete = false;
1016
1017            if (result == Activity.RESULT_OK) {
1018                Log.d(TAG, "actionMessageSent: result OK");
1019                if (msgInfo.transparent == 0) {
1020                    if (!Sms.moveMessageToFolder(context, msgInfo.uri,
1021                            Sms.MESSAGE_TYPE_SENT, 0)) {
1022                        Log.d(TAG, "Failed to move " + msgInfo.uri + " to SENT");
1023                    }
1024                } else {
1025                    delete = true;
1026                }
1027
1028                Event evt = new Event("SendingSuccess", msgInfo.id,
1029                    folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
1030                sendEvent(evt);
1031
1032            } else {
1033                if (msgInfo.retry == 1) {
1034                    /* Notify failure, but keep message in outbox for resending */
1035                    msgInfo.resend = true;
1036                    Event evt = new Event("SendingFailure", msgInfo.id,
1037                        folderSms[Sms.MESSAGE_TYPE_OUTBOX], null, mSmsType);
1038                    sendEvent(evt);
1039                } else {
1040                    if (msgInfo.transparent == 0) {
1041                        if (!Sms.moveMessageToFolder(context, msgInfo.uri,
1042                                Sms.MESSAGE_TYPE_FAILED, 0)) {
1043                            Log.d(TAG, "Failed to move " + msgInfo.uri + " to FAILED");
1044                        }
1045                    } else {
1046                        delete = true;
1047                    }
1048
1049                    Event evt = new Event("SendingFailure", msgInfo.id,
1050                        folderSms[Sms.MESSAGE_TYPE_FAILED], null, mSmsType);
1051                    sendEvent(evt);
1052                }
1053            }
1054
1055            if (delete == true) {
1056                /* Delete from Observer message list to avoid delete notifications */
1057                mMsgListSms.remove(msgInfo.id);
1058
1059                /* Delete from DB */
1060                mResolver.delete(msgInfo.uri, null, null);
1061            }
1062        }
1063
1064        private void actionMessageDelivery(Context context, Intent intent,
1065            PushMsgInfo msgInfo) {
1066            Uri messageUri = intent.getData();
1067            byte[] pdu = intent.getByteArrayExtra("pdu");
1068            String format = intent.getStringExtra("format");
1069
1070            SmsMessage message = SmsMessage.createFromPdu(pdu, format);
1071            if (message == null) {
1072                Log.d(TAG, "actionMessageDelivery: Can't get message from pdu");
1073                return;
1074            }
1075            int status = message.getStatus();
1076
1077            Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null);
1078
1079            try {
1080                if (cursor.moveToFirst()) {
1081                    int messageId = cursor.getInt(0);
1082
1083                    Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId);
1084                    boolean isStatusReport = message.isStatusReportMessage();
1085
1086                    Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + status +
1087                                ", isStatusReport=" + isStatusReport);
1088
1089                    ContentValues contentValues = new ContentValues(2);
1090
1091                    contentValues.put(Sms.STATUS, status);
1092                    contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis());
1093                    mResolver.update(updateUri, contentValues, null, null);
1094                } else {
1095                    Log.d(TAG, "Can't find message for status update: " + messageUri);
1096                }
1097            } finally {
1098                cursor.close();
1099            }
1100
1101            if (status == 0) {
1102                Event evt = new Event("DeliverySuccess", msgInfo.id,
1103                    folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
1104                sendEvent(evt);
1105            } else {
1106                Event evt = new Event("DeliveryFailure", msgInfo.id,
1107                    folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
1108                sendEvent(evt);
1109            }
1110
1111            mPushMsgList.remove(msgInfo.id);
1112        }
1113    }
1114
1115    private void registerPhoneServiceStateListener() {
1116        TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
1117        tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE);
1118    }
1119
1120    private void unRegisterPhoneServiceStateListener() {
1121        TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
1122        tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE);
1123    }
1124
1125    private void resendPendingMessages() {
1126        /* Send pending messages in outbox */
1127        String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
1128        Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null,
1129            null);
1130
1131        if (c != null && c.moveToFirst()) {
1132            do {
1133                long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
1134                String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
1135                PushMsgInfo msgInfo = mPushMsgList.get(id);
1136                if (msgInfo == null || msgInfo.resend == false) {
1137                    continue;
1138                }
1139                sendMessage(msgInfo, msgBody);
1140            } while (c.moveToNext());
1141            c.close();
1142        }
1143    }
1144
1145    private void failPendingMessages() {
1146        /* Move pending messages from outbox to failed */
1147        String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
1148        Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null,
1149            null);
1150
1151        if (c != null && c.moveToFirst()) {
1152            do {
1153                long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
1154                String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
1155                PushMsgInfo msgInfo = mPushMsgList.get(id);
1156                if (msgInfo == null || msgInfo.resend == false) {
1157                    continue;
1158                }
1159                Sms.moveMessageToFolder(mContext, msgInfo.uri,
1160                    Sms.MESSAGE_TYPE_FAILED, 0);
1161            } while (c.moveToNext());
1162        }
1163        if (c != null) c.close();
1164    }
1165
1166    private void removeDeletedMessages() {
1167        /* Remove messages from virtual "deleted" folder (thread_id -1) */
1168        mResolver.delete(Uri.parse("content://sms/"),
1169                "thread_id = " + DELETED_THREAD_ID, null);
1170    }
1171
1172    private PhoneStateListener mPhoneListener = new PhoneStateListener() {
1173        @Override
1174        public void onServiceStateChanged(ServiceState serviceState) {
1175            Log.d(TAG, "Phone service state change: " + serviceState.getState());
1176            if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
1177                resendPendingMessages();
1178            }
1179        }
1180    };
1181
1182    public void init() {
1183        mSmsBroadcastReceiver.register();
1184        registerPhoneServiceStateListener();
1185    }
1186
1187    public void deinit() {
1188        mSmsBroadcastReceiver.unregister();
1189        unRegisterPhoneServiceStateListener();
1190        failPendingMessages();
1191        removeDeletedMessages();
1192    }
1193}
1194