Home | History | Annotate | Download | only in map
      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 */
     15 package com.android.bluetooth.map;
     16 
     17 import java.io.ByteArrayInputStream;
     18 import java.io.FileNotFoundException;
     19 import java.io.IOException;
     20 import java.io.OutputStream;
     21 import java.io.StringWriter;
     22 import java.io.UnsupportedEncodingException;
     23 import java.util.ArrayList;
     24 import java.util.Arrays;
     25 import java.util.Collections;
     26 import java.util.HashMap;
     27 import java.util.HashSet;
     28 import java.util.Map;
     29 import java.util.Set;
     30 
     31 import org.xmlpull.v1.XmlSerializer;
     32 
     33 import android.app.Activity;
     34 import android.app.PendingIntent;
     35 import android.content.BroadcastReceiver;
     36 import android.content.ContentResolver;
     37 import android.content.ContentUris;
     38 import android.content.ContentValues;
     39 import android.content.Context;
     40 import android.content.Intent;
     41 import android.content.IntentFilter;
     42 import android.database.ContentObserver;
     43 import android.database.Cursor;
     44 import android.net.Uri;
     45 import android.os.Handler;
     46 import android.provider.BaseColumns;
     47 import android.provider.Telephony;
     48 import android.provider.Telephony.Mms;
     49 import android.provider.Telephony.MmsSms;
     50 import android.provider.Telephony.Sms;
     51 import android.provider.Telephony.Sms.Inbox;
     52 import android.telephony.PhoneStateListener;
     53 import android.telephony.ServiceState;
     54 import android.telephony.SmsManager;
     55 import android.telephony.SmsMessage;
     56 import android.telephony.TelephonyManager;
     57 import android.util.Log;
     58 import android.util.Xml;
     59 
     60 import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
     61 import com.android.bluetooth.map.BluetoothMapbMessageMmsEmail.MimePart;
     62 import com.google.android.mms.pdu.PduHeaders;
     63 
     64 public 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             values.put("ct", part.contentType);
    907         if(part.contentId != null)
    908             values.put("cid", part.contentId);
    909         if(part.contentLocation != null)
    910             values.put("cl", part.contentLocation);
    911         if(part.contentDisposition != null)
    912             values.put("cd", part.contentDisposition);
    913         if(part.partName != null) {
    914             values.put("fn", part.partName);
    915             values.put("name", part.partName);
    916         } else if(part.contentId == null && part.contentLocation == null) {
    917             /* We must set at least one part identifier */
    918             values.put("fn", "part_" + count + ".dat");
    919             values.put("name", "part_" + count + ".dat");
    920         }
    921         Uri partUri = Uri.parse("content://mms/" + handle + "/part");
    922         Uri res = mResolver.insert(partUri, values);
    923 
    924         // Add data to part
    925         OutputStream os = mResolver.openOutputStream(res);
    926         os.write(part.data);
    927         os.close();
    928     }
    929 
    930 
    931     public void sendMessage(PushMsgInfo msgInfo, String msgBody) {
    932 
    933         SmsManager smsMng = SmsManager.getDefault();
    934         ArrayList<String> parts = smsMng.divideMessage(msgBody);
    935         msgInfo.parts = parts.size();
    936 
    937         ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts);
    938         ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts);
    939 
    940         for (int i = 0; i < msgInfo.parts; i++) {
    941             Intent intent;
    942             intent = new Intent(ACTION_MESSAGE_DELIVERY, null);
    943             intent.putExtra("HANDLE", msgInfo.id);
    944             deliveryIntents.add(PendingIntent.getBroadcast(mContext, 0, intent,
    945                 PendingIntent.FLAG_UPDATE_CURRENT));
    946 
    947             intent = new Intent(ACTION_MESSAGE_SENT, null);
    948             intent.putExtra("HANDLE", msgInfo.id);
    949             sentIntents.add(PendingIntent.getBroadcast(mContext, 0, intent,
    950                 PendingIntent.FLAG_UPDATE_CURRENT));
    951         }
    952 
    953         Log.d(TAG, "sendMessage to " + msgInfo.phone);
    954 
    955         smsMng.sendMultipartTextMessage(msgInfo.phone, null, parts, sentIntents,
    956             deliveryIntents);
    957     }
    958 
    959     private static final String ACTION_MESSAGE_DELIVERY =
    960         "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY";
    961     private static final String ACTION_MESSAGE_SENT =
    962         "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT";
    963 
    964     private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver();
    965 
    966     private class SmsBroadcastReceiver extends BroadcastReceiver {
    967         private final String[] ID_PROJECTION = new String[] { Sms._ID };
    968         private final Uri UPDATE_STATUS_URI = Uri.parse("content://sms/status");
    969 
    970         public void register() {
    971             Handler handler = new Handler();
    972 
    973             IntentFilter intentFilter = new IntentFilter();
    974             intentFilter.addAction(ACTION_MESSAGE_DELIVERY);
    975             intentFilter.addAction(ACTION_MESSAGE_SENT);
    976             mContext.registerReceiver(this, intentFilter, null, handler);
    977         }
    978 
    979         public void unregister() {
    980             try {
    981                 mContext.unregisterReceiver(this);
    982             } catch (IllegalArgumentException e) {
    983                 /* do nothing */
    984             }
    985         }
    986 
    987         @Override
    988         public void onReceive(Context context, Intent intent) {
    989             String action = intent.getAction();
    990             long handle = intent.getLongExtra("HANDLE", -1);
    991             PushMsgInfo msgInfo = mPushMsgList.get(handle);
    992 
    993             Log.d(TAG, "onReceive: action"  + action);
    994 
    995             if (msgInfo == null) {
    996                 Log.d(TAG, "onReceive: no msgInfo found for handle " + handle);
    997                 return;
    998             }
    999 
   1000             if (action.equals(ACTION_MESSAGE_SENT)) {
   1001                 msgInfo.partsSent++;
   1002                 if (msgInfo.partsSent == msgInfo.parts) {
   1003                     actionMessageSent(context, intent, msgInfo);
   1004                 }
   1005             } else if (action.equals(ACTION_MESSAGE_DELIVERY)) {
   1006                 msgInfo.partsDelivered++;
   1007                 if (msgInfo.partsDelivered == msgInfo.parts) {
   1008                     actionMessageDelivery(context, intent, msgInfo);
   1009                 }
   1010             } else {
   1011                 Log.d(TAG, "onReceive: Unknown action " + action);
   1012             }
   1013         }
   1014 
   1015         private void actionMessageSent(Context context, Intent intent,
   1016             PushMsgInfo msgInfo) {
   1017             int result = getResultCode();
   1018             boolean delete = false;
   1019 
   1020             if (result == Activity.RESULT_OK) {
   1021                 Log.d(TAG, "actionMessageSent: result OK");
   1022                 if (msgInfo.transparent == 0) {
   1023                     if (!Sms.moveMessageToFolder(context, msgInfo.uri,
   1024                             Sms.MESSAGE_TYPE_SENT, 0)) {
   1025                         Log.d(TAG, "Failed to move " + msgInfo.uri + " to SENT");
   1026                     }
   1027                 } else {
   1028                     delete = true;
   1029                 }
   1030 
   1031                 Event evt = new Event("SendingSuccess", msgInfo.id,
   1032                     folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
   1033                 sendEvent(evt);
   1034 
   1035             } else {
   1036                 if (msgInfo.retry == 1) {
   1037                     /* Notify failure, but keep message in outbox for resending */
   1038                     msgInfo.resend = true;
   1039                     Event evt = new Event("SendingFailure", msgInfo.id,
   1040                         folderSms[Sms.MESSAGE_TYPE_OUTBOX], null, mSmsType);
   1041                     sendEvent(evt);
   1042                 } else {
   1043                     if (msgInfo.transparent == 0) {
   1044                         if (!Sms.moveMessageToFolder(context, msgInfo.uri,
   1045                                 Sms.MESSAGE_TYPE_FAILED, 0)) {
   1046                             Log.d(TAG, "Failed to move " + msgInfo.uri + " to FAILED");
   1047                         }
   1048                     } else {
   1049                         delete = true;
   1050                     }
   1051 
   1052                     Event evt = new Event("SendingFailure", msgInfo.id,
   1053                         folderSms[Sms.MESSAGE_TYPE_FAILED], null, mSmsType);
   1054                     sendEvent(evt);
   1055                 }
   1056             }
   1057 
   1058             if (delete == true) {
   1059                 /* Delete from Observer message list to avoid delete notifications */
   1060                 mMsgListSms.remove(msgInfo.id);
   1061 
   1062                 /* Delete from DB */
   1063                 mResolver.delete(msgInfo.uri, null, null);
   1064             }
   1065         }
   1066 
   1067         private void actionMessageDelivery(Context context, Intent intent,
   1068             PushMsgInfo msgInfo) {
   1069             Uri messageUri = intent.getData();
   1070             byte[] pdu = intent.getByteArrayExtra("pdu");
   1071             String format = intent.getStringExtra("format");
   1072 
   1073             SmsMessage message = SmsMessage.createFromPdu(pdu, format);
   1074             if (message == null) {
   1075                 Log.d(TAG, "actionMessageDelivery: Can't get message from pdu");
   1076                 return;
   1077             }
   1078             int status = message.getStatus();
   1079 
   1080             Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null);
   1081 
   1082             try {
   1083                 if (cursor.moveToFirst()) {
   1084                     int messageId = cursor.getInt(0);
   1085 
   1086                     Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId);
   1087                     boolean isStatusReport = message.isStatusReportMessage();
   1088 
   1089                     Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + status +
   1090                                 ", isStatusReport=" + isStatusReport);
   1091 
   1092                     ContentValues contentValues = new ContentValues(2);
   1093 
   1094                     contentValues.put(Sms.STATUS, status);
   1095                     contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis());
   1096                     mResolver.update(updateUri, contentValues, null, null);
   1097                 } else {
   1098                     Log.d(TAG, "Can't find message for status update: " + messageUri);
   1099                 }
   1100             } finally {
   1101                 cursor.close();
   1102             }
   1103 
   1104             if (status == 0) {
   1105                 Event evt = new Event("DeliverySuccess", msgInfo.id,
   1106                     folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
   1107                 sendEvent(evt);
   1108             } else {
   1109                 Event evt = new Event("DeliveryFailure", msgInfo.id,
   1110                     folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
   1111                 sendEvent(evt);
   1112             }
   1113 
   1114             mPushMsgList.remove(msgInfo.id);
   1115         }
   1116     }
   1117 
   1118     private void registerPhoneServiceStateListener() {
   1119         TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
   1120         tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE);
   1121     }
   1122 
   1123     private void unRegisterPhoneServiceStateListener() {
   1124         TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
   1125         tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE);
   1126     }
   1127 
   1128     private void resendPendingMessages() {
   1129         /* Send pending messages in outbox */
   1130         String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
   1131         Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null,
   1132             null);
   1133 
   1134         if (c != null && c.moveToFirst()) {
   1135             do {
   1136                 long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
   1137                 String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
   1138                 PushMsgInfo msgInfo = mPushMsgList.get(id);
   1139                 if (msgInfo == null || msgInfo.resend == false) {
   1140                     continue;
   1141                 }
   1142                 sendMessage(msgInfo, msgBody);
   1143             } while (c.moveToNext());
   1144             c.close();
   1145         }
   1146     }
   1147 
   1148     private void failPendingMessages() {
   1149         /* Move pending messages from outbox to failed */
   1150         String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
   1151         Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null,
   1152             null);
   1153 
   1154         if (c != null && c.moveToFirst()) {
   1155             do {
   1156                 long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
   1157                 String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
   1158                 PushMsgInfo msgInfo = mPushMsgList.get(id);
   1159                 if (msgInfo == null || msgInfo.resend == false) {
   1160                     continue;
   1161                 }
   1162                 Sms.moveMessageToFolder(mContext, msgInfo.uri,
   1163                     Sms.MESSAGE_TYPE_FAILED, 0);
   1164             } while (c.moveToNext());
   1165         }
   1166         if (c != null) c.close();
   1167     }
   1168 
   1169     private void removeDeletedMessages() {
   1170         /* Remove messages from virtual "deleted" folder (thread_id -1) */
   1171         mResolver.delete(Uri.parse("content://sms/"),
   1172                 "thread_id = " + DELETED_THREAD_ID, null);
   1173     }
   1174 
   1175     private PhoneStateListener mPhoneListener = new PhoneStateListener() {
   1176         @Override
   1177         public void onServiceStateChanged(ServiceState serviceState) {
   1178             Log.d(TAG, "Phone service state change: " + serviceState.getState());
   1179             if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
   1180                 resendPendingMessages();
   1181             }
   1182         }
   1183     };
   1184 
   1185     public void init() {
   1186         mSmsBroadcastReceiver.register();
   1187         registerPhoneServiceStateListener();
   1188     }
   1189 
   1190     public void deinit() {
   1191         mSmsBroadcastReceiver.unregister();
   1192         unRegisterPhoneServiceStateListener();
   1193         failPendingMessages();
   1194         removeDeletedMessages();
   1195     }
   1196 }
   1197