BluetoothMapContentObserver.java revision ed0c6ae1773ad1f4249fe3cf7447d7033195f222
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                        /* to do, support MMS in the future */
664                        /*
665                        if (folder.equals("outbox")) {
666                           handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMmsEmail)msg);
667                        }
668                        */
669                        break;
670                    }
671                    case SMS_GSM: //fall-through
672                    case SMS_CDMA:
673                    {
674                        /* Add the message to the database */
675                        String msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody();
676                        Uri contentUri = Uri.parse("content://sms/" + folder);
677                        Uri uri = Sms.addMessageToUri(mResolver, contentUri, phone, msgBody,
678                            "", System.currentTimeMillis(), read, deliveryReport);
679
680                        if (uri == null) {
681                            Log.d(TAG, "pushMessage - failure on add to uri " + contentUri);
682                            return -1;
683                        }
684
685                        handle = Long.parseLong(uri.getLastPathSegment());
686
687                        /* Send message if folder is outbox */
688                        if (folder.equals("outbox")) {
689                            PushMsgInfo msgInfo = new PushMsgInfo(handle, transparent,
690                                retry, phone, uri);
691                            mPushMsgList.put(handle, msgInfo);
692                            sendMessage(msgInfo, msgBody);
693                        }
694                        break;
695                    }
696                    case EMAIL:
697                    {
698                        break;
699                    }
700                }
701
702            }
703        }
704
705        /* If multiple recipients return handle of last */
706        return handle;
707    }
708
709
710
711    public long sendMmsMessage(String folder,String to_address, BluetoothMapbMessageMmsEmail msg) {
712        /*
713         *strategy:
714         *1) parse message into parts
715         *if folder is outbox/drafts:
716         *2) push message to draft
717         *if folder is outbox:
718         *3) move message to outbox (to trigger the mms app to add msg to pending_messages list)
719         *4) send intent to mms app in order to wake it up.
720         *else if folder !outbox:
721         *1) push message to folder
722         * */
723        if (folder != null && (folder.equalsIgnoreCase("outbox")||  folder.equalsIgnoreCase("drafts"))) {
724            long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, to_address, msg);
725            /* if invalid handle (-1) then just return the handle - else continue sending (if folder is outbox) */
726            if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase("outbox")) {
727                moveDraftToOutbox(handle);
728
729                Intent sendIntent = new Intent("android.intent.action.MMS_SEND_OUTBOX_MSG");
730                Log.d(TAG, "broadcasting intent: "+sendIntent.toString());
731                mContext.sendBroadcast(sendIntent);
732            }
733            return handle;
734        } else {
735            /* not allowed to push mms to anything but outbox/drafts */
736            throw  new IllegalArgumentException("Cannot push message to other folders than outbox/drafts");
737        }
738
739    }
740
741
742    private void moveDraftToOutbox(long handle) {
743        ContentResolver contentResolver = mContext.getContentResolver();
744        /*Move message by changing the msg_box value in the content provider database */
745        if (handle != -1) {
746            String whereClause = " _id= " + handle;
747            Uri uri = Uri.parse("content://mms");
748            Cursor queryResult = contentResolver.query(uri, null, whereClause, null, null);
749            if (queryResult != null) {
750                if (queryResult.getCount() > 0) {
751                    queryResult.moveToFirst();
752                    ContentValues data = new ContentValues();
753                    /* set folder to be outbox */
754                    data.put("msg_box", Mms.MESSAGE_BOX_OUTBOX);
755                    contentResolver.update(uri, data, whereClause, null);
756                    Log.d(TAG, "moved draft MMS to outbox");
757                }
758                queryResult.close();
759            }else {
760                Log.d(TAG, "Could not move draft to outbox ");
761            }
762        }
763    }
764    private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMmsEmail msg) {
765        /**
766         * strategy:
767         * 1) parse msg into parts + header
768         * 2) create thread id (abuse the ease of adding an SMS to get id for thread)
769         * 3) push parts into content://mms/parts/ table
770         * 3)
771         */
772
773        ContentValues values = new ContentValues();
774        values.put("msg_box", folder);
775
776        values.put("read", 0);
777        values.put("seen", 0);
778        values.put("sub", msg.getSubject());
779        values.put("sub_cs", 106);
780        values.put("ct_t", "application/vnd.wap.multipart.related");
781        values.put("exp", 604800);
782        values.put("m_cls", PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
783        values.put("m_type", PduHeaders.MESSAGE_TYPE_SEND_REQ);
784        values.put("v", PduHeaders.CURRENT_MMS_VERSION);
785        values.put("pri", PduHeaders.PRIORITY_NORMAL);
786        values.put("rr", PduHeaders.VALUE_NO);
787        values.put("tr_id", "T"+ Long.toHexString(System.currentTimeMillis()));
788        values.put("d_rpt", PduHeaders.VALUE_NO);
789        values.put("locked", 0);
790        if(msg.getTextOnly() == true)
791            values.put("text_only", true);
792
793        values.put("m_size", msg.getSize());
794
795     // Get thread id
796        Set<String> recipients = new HashSet<String>();
797        recipients.addAll(Arrays.asList(to_address));
798        values.put("thread_id", Telephony.Threads.getOrCreateThreadId(mContext, recipients));
799        Uri uri = Uri.parse("content://mms");
800
801        ContentResolver cr = mContext.getContentResolver();
802        uri = cr.insert(uri, values);
803
804        if (uri == null) {
805            // unable to insert MMS
806            Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri);
807            return -1;
808        }
809
810        long handle = Long.parseLong(uri.getLastPathSegment());
811        if (V){
812            Log.v(TAG, " NEW URI " + uri.toString());
813        }
814        try {
815            if(V) Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base.");
816        for(MimePart part : msg.getMimeParts()) {
817            int count = 0;
818            count++;
819            values.clear();
820            if(part.contentType != null &&  part.contentType.toUpperCase().contains("TEXT")) {
821                values.put("ct", "text/plain");
822                values.put("chset", 106);
823                if(part.partName != null) {
824                    values.put("fn", part.partName);
825                    values.put("name", part.partName);
826                } else if(part.contentId == null && part.contentLocation == null) {
827                    /* We must set at least one part identifier */
828                    values.put("fn", "text_" + count +".txt");
829                    values.put("name", "text_" + count +".txt");
830                }
831                if(part.contentId != null) {
832                    values.put("cid", part.contentId);
833                }
834                if(part.contentLocation != null)
835                    values.put("cl", part.contentLocation);
836                if(part.contentDisposition != null)
837                    values.put("cd", part.contentDisposition);
838                values.put("text", new String(part.data, "UTF-8"));
839                uri = Uri.parse("content://mms/" + handle + "/part");
840                uri = cr.insert(uri, values);
841                if(V) Log.v(TAG, "Added TEXT part");
842
843            } else if (part.contentType != null &&  part.contentType.toUpperCase().contains("SMIL")){
844
845                values.put("seq", -1);
846                values.put("ct", "application/smil");
847                if(part.contentId != null)
848                    values.put("cid", part.contentId);
849                if(part.contentLocation != null)
850                    values.put("cl", part.contentLocation);
851                if(part.contentDisposition != null)
852                    values.put("cd", part.contentDisposition);
853                values.put("fn", "smil.xml");
854                values.put("name", "smil.xml");
855                values.put("text", new String(part.data, "UTF-8"));
856
857                uri = Uri.parse("content://mms/" + handle + "/part");
858                uri = cr.insert(uri, values);
859                if(V) Log.v(TAG, "Added SMIL part");
860
861            }else /*VIDEO/AUDIO/IMAGE*/ {
862                writeMmsDataPart(handle, part, count);
863                if(V) Log.v(TAG, "Added OTHER part");
864            }
865            if (uri != null && V){
866                Log.v(TAG, "Added part with content-type: "+ part.contentType + " to Uri: " + uri.toString());
867            }
868        }
869        } catch (UnsupportedEncodingException e) {
870            Log.w(TAG, e);
871        } catch (IOException e) {
872            Log.w(TAG, e);
873        }
874
875        values.clear();
876        values.put("contact_id", "null");
877        values.put("address", "insert-address-token");
878        values.put("type", BluetoothMapContent.MMS_FROM);
879        values.put("charset", 106);
880
881        uri = Uri.parse("content://mms/" + handle + "/addr");
882        uri = cr.insert(uri, values);
883        if (uri != null && V){
884            Log.v(TAG, " NEW URI " + uri.toString());
885        }
886
887        values.clear();
888        values.put("contact_id", "null");
889        values.put("address", to_address);
890        values.put("type", BluetoothMapContent.MMS_TO);
891        values.put("charset", 106);
892
893        uri = Uri.parse("content://mms/" + handle + "/addr");
894        uri = cr.insert(uri, values);
895        if (uri != null && V){
896            Log.v(TAG, " NEW URI " + uri.toString());
897        }
898        return handle;
899    }
900
901
902    private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException{
903        ContentValues values = new ContentValues();
904        values.put("mid", handle);
905        if(part.contentType != null){
906            //Remove last char if ';' from contentType
907            if(part.contentType.charAt(part.contentType.length() - 1) == ';') {
908               part.contentType = part.contentType.substring(0,part.contentType.length() -1);
909            }
910            values.put("ct", part.contentType);
911        }
912        if(part.contentId != null)
913            values.put("cid", part.contentId);
914        if(part.contentLocation != null)
915            values.put("cl", part.contentLocation);
916        if(part.contentDisposition != null)
917            values.put("cd", part.contentDisposition);
918        if(part.partName != null) {
919            values.put("fn", part.partName);
920            values.put("name", part.partName);
921        } else if(part.contentId == null && part.contentLocation == null) {
922            /* We must set at least one part identifier */
923            values.put("fn", "part_" + count + ".dat");
924            values.put("name", "part_" + count + ".dat");
925        }
926        Uri partUri = Uri.parse("content://mms/" + handle + "/part");
927        Uri res = mResolver.insert(partUri, values);
928
929        // Add data to part
930        OutputStream os = mResolver.openOutputStream(res);
931        os.write(part.data);
932        os.close();
933    }
934
935
936    public void sendMessage(PushMsgInfo msgInfo, String msgBody) {
937
938        SmsManager smsMng = SmsManager.getDefault();
939        ArrayList<String> parts = smsMng.divideMessage(msgBody);
940        msgInfo.parts = parts.size();
941
942        ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts);
943        ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts);
944
945        for (int i = 0; i < msgInfo.parts; i++) {
946            Intent intent;
947            intent = new Intent(ACTION_MESSAGE_DELIVERY, null);
948            intent.putExtra("HANDLE", msgInfo.id);
949            deliveryIntents.add(PendingIntent.getBroadcast(mContext, 0, intent,
950                PendingIntent.FLAG_UPDATE_CURRENT));
951
952            intent = new Intent(ACTION_MESSAGE_SENT, null);
953            intent.putExtra("HANDLE", msgInfo.id);
954            sentIntents.add(PendingIntent.getBroadcast(mContext, 0, intent,
955                PendingIntent.FLAG_UPDATE_CURRENT));
956        }
957
958        Log.d(TAG, "sendMessage to " + msgInfo.phone);
959
960        smsMng.sendMultipartTextMessage(msgInfo.phone, null, parts, sentIntents,
961            deliveryIntents);
962    }
963
964    private static final String ACTION_MESSAGE_DELIVERY =
965        "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY";
966    private static final String ACTION_MESSAGE_SENT =
967        "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT";
968
969    private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver();
970
971    private class SmsBroadcastReceiver extends BroadcastReceiver {
972        private final String[] ID_PROJECTION = new String[] { Sms._ID };
973        private final Uri UPDATE_STATUS_URI = Uri.parse("content://sms/status");
974
975        public void register() {
976            Handler handler = new Handler();
977
978            IntentFilter intentFilter = new IntentFilter();
979            intentFilter.addAction(ACTION_MESSAGE_DELIVERY);
980            intentFilter.addAction(ACTION_MESSAGE_SENT);
981            mContext.registerReceiver(this, intentFilter, null, handler);
982        }
983
984        public void unregister() {
985            try {
986                mContext.unregisterReceiver(this);
987            } catch (IllegalArgumentException e) {
988                /* do nothing */
989            }
990        }
991
992        @Override
993        public void onReceive(Context context, Intent intent) {
994            String action = intent.getAction();
995            long handle = intent.getLongExtra("HANDLE", -1);
996            PushMsgInfo msgInfo = mPushMsgList.get(handle);
997
998            Log.d(TAG, "onReceive: action"  + action);
999
1000            if (msgInfo == null) {
1001                Log.d(TAG, "onReceive: no msgInfo found for handle " + handle);
1002                return;
1003            }
1004
1005            if (action.equals(ACTION_MESSAGE_SENT)) {
1006                msgInfo.partsSent++;
1007                if (msgInfo.partsSent == msgInfo.parts) {
1008                    actionMessageSent(context, intent, msgInfo);
1009                }
1010            } else if (action.equals(ACTION_MESSAGE_DELIVERY)) {
1011                msgInfo.partsDelivered++;
1012                if (msgInfo.partsDelivered == msgInfo.parts) {
1013                    actionMessageDelivery(context, intent, msgInfo);
1014                }
1015            } else {
1016                Log.d(TAG, "onReceive: Unknown action " + action);
1017            }
1018        }
1019
1020        private void actionMessageSent(Context context, Intent intent,
1021            PushMsgInfo msgInfo) {
1022            int result = getResultCode();
1023            boolean delete = false;
1024
1025            if (result == Activity.RESULT_OK) {
1026                Log.d(TAG, "actionMessageSent: result OK");
1027                if (msgInfo.transparent == 0) {
1028                    if (!Sms.moveMessageToFolder(context, msgInfo.uri,
1029                            Sms.MESSAGE_TYPE_SENT, 0)) {
1030                        Log.d(TAG, "Failed to move " + msgInfo.uri + " to SENT");
1031                    }
1032                } else {
1033                    delete = true;
1034                }
1035
1036                Event evt = new Event("SendingSuccess", msgInfo.id,
1037                    folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
1038                sendEvent(evt);
1039
1040            } else {
1041                if (msgInfo.retry == 1) {
1042                    /* Notify failure, but keep message in outbox for resending */
1043                    msgInfo.resend = true;
1044                    Event evt = new Event("SendingFailure", msgInfo.id,
1045                        folderSms[Sms.MESSAGE_TYPE_OUTBOX], null, mSmsType);
1046                    sendEvent(evt);
1047                } else {
1048                    if (msgInfo.transparent == 0) {
1049                        if (!Sms.moveMessageToFolder(context, msgInfo.uri,
1050                                Sms.MESSAGE_TYPE_FAILED, 0)) {
1051                            Log.d(TAG, "Failed to move " + msgInfo.uri + " to FAILED");
1052                        }
1053                    } else {
1054                        delete = true;
1055                    }
1056
1057                    Event evt = new Event("SendingFailure", msgInfo.id,
1058                        folderSms[Sms.MESSAGE_TYPE_FAILED], null, mSmsType);
1059                    sendEvent(evt);
1060                }
1061            }
1062
1063            if (delete == true) {
1064                /* Delete from Observer message list to avoid delete notifications */
1065                mMsgListSms.remove(msgInfo.id);
1066
1067                /* Delete from DB */
1068                mResolver.delete(msgInfo.uri, null, null);
1069            }
1070        }
1071
1072        private void actionMessageDelivery(Context context, Intent intent,
1073            PushMsgInfo msgInfo) {
1074            Uri messageUri = intent.getData();
1075            byte[] pdu = intent.getByteArrayExtra("pdu");
1076            String format = intent.getStringExtra("format");
1077
1078            SmsMessage message = SmsMessage.createFromPdu(pdu, format);
1079            if (message == null) {
1080                Log.d(TAG, "actionMessageDelivery: Can't get message from pdu");
1081                return;
1082            }
1083            int status = message.getStatus();
1084
1085            Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null);
1086
1087            try {
1088                if (cursor.moveToFirst()) {
1089                    int messageId = cursor.getInt(0);
1090
1091                    Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId);
1092                    boolean isStatusReport = message.isStatusReportMessage();
1093
1094                    Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + status +
1095                                ", isStatusReport=" + isStatusReport);
1096
1097                    ContentValues contentValues = new ContentValues(2);
1098
1099                    contentValues.put(Sms.STATUS, status);
1100                    contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis());
1101                    mResolver.update(updateUri, contentValues, null, null);
1102                } else {
1103                    Log.d(TAG, "Can't find message for status update: " + messageUri);
1104                }
1105            } finally {
1106                cursor.close();
1107            }
1108
1109            if (status == 0) {
1110                Event evt = new Event("DeliverySuccess", msgInfo.id,
1111                    folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
1112                sendEvent(evt);
1113            } else {
1114                Event evt = new Event("DeliveryFailure", msgInfo.id,
1115                    folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
1116                sendEvent(evt);
1117            }
1118
1119            mPushMsgList.remove(msgInfo.id);
1120        }
1121    }
1122
1123    private void registerPhoneServiceStateListener() {
1124        TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
1125        tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE);
1126    }
1127
1128    private void unRegisterPhoneServiceStateListener() {
1129        TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
1130        tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE);
1131    }
1132
1133    private void resendPendingMessages() {
1134        /* Send pending messages in outbox */
1135        String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
1136        Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null,
1137            null);
1138
1139        if (c != null && c.moveToFirst()) {
1140            do {
1141                long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
1142                String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
1143                PushMsgInfo msgInfo = mPushMsgList.get(id);
1144                if (msgInfo == null || msgInfo.resend == false) {
1145                    continue;
1146                }
1147                sendMessage(msgInfo, msgBody);
1148            } while (c.moveToNext());
1149            c.close();
1150        }
1151    }
1152
1153    private void failPendingMessages() {
1154        /* Move pending messages from outbox to failed */
1155        String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
1156        Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null,
1157            null);
1158
1159        if (c != null && c.moveToFirst()) {
1160            do {
1161                long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
1162                String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
1163                PushMsgInfo msgInfo = mPushMsgList.get(id);
1164                if (msgInfo == null || msgInfo.resend == false) {
1165                    continue;
1166                }
1167                Sms.moveMessageToFolder(mContext, msgInfo.uri,
1168                    Sms.MESSAGE_TYPE_FAILED, 0);
1169            } while (c.moveToNext());
1170        }
1171        if (c != null) c.close();
1172    }
1173
1174    private void removeDeletedMessages() {
1175        /* Remove messages from virtual "deleted" folder (thread_id -1) */
1176        mResolver.delete(Uri.parse("content://sms/"),
1177                "thread_id = " + DELETED_THREAD_ID, null);
1178    }
1179
1180    private PhoneStateListener mPhoneListener = new PhoneStateListener() {
1181        @Override
1182        public void onServiceStateChanged(ServiceState serviceState) {
1183            Log.d(TAG, "Phone service state change: " + serviceState.getState());
1184            if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
1185                resendPendingMessages();
1186            }
1187        }
1188    };
1189
1190    public void init() {
1191        mSmsBroadcastReceiver.register();
1192        registerPhoneServiceStateListener();
1193    }
1194
1195    public void deinit() {
1196        mSmsBroadcastReceiver.unregister();
1197        unRegisterPhoneServiceStateListener();
1198        failPendingMessages();
1199        removeDeletedMessages();
1200    }
1201}
1202