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