Home | History | Annotate | Download | only in map
      1 /*
      2 * Copyright (C) 2014 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.Closeable;
     18 import java.io.FileNotFoundException;
     19 import java.io.FileOutputStream;
     20 import java.io.IOException;
     21 import java.io.OutputStream;
     22 import java.io.StringWriter;
     23 import java.io.UnsupportedEncodingException;
     24 import java.util.ArrayList;
     25 import java.util.Arrays;
     26 import java.util.Calendar;
     27 import java.util.Collections;
     28 import java.util.HashMap;
     29 import java.util.HashSet;
     30 import java.util.Map;
     31 import java.util.Set;
     32 
     33 import javax.obex.ResponseCodes;
     34 
     35 import org.xmlpull.v1.XmlSerializer;
     36 
     37 import android.app.Activity;
     38 import android.app.PendingIntent;
     39 import android.content.BroadcastReceiver;
     40 import android.content.ContentProviderClient;
     41 import android.content.ContentResolver;
     42 import android.content.ContentUris;
     43 import android.content.ContentValues;
     44 import android.content.Context;
     45 import android.content.Intent;
     46 import android.content.IntentFilter;
     47 import android.content.IntentFilter.MalformedMimeTypeException;
     48 import android.database.ContentObserver;
     49 import android.database.Cursor;
     50 import android.net.Uri;
     51 import android.os.Build;
     52 import android.os.Handler;
     53 import android.os.Message;
     54 import android.os.ParcelFileDescriptor;
     55 import android.os.RemoteException;
     56 import android.provider.BaseColumns;
     57 import com.android.bluetooth.mapapi.BluetoothMapContract;
     58 import com.android.bluetooth.mapapi.BluetoothMapContract.MessageColumns;
     59 import android.provider.Telephony;
     60 import android.provider.Telephony.Mms;
     61 import android.provider.Telephony.MmsSms;
     62 import android.provider.Telephony.Sms;
     63 import android.provider.Telephony.Sms.Inbox;
     64 import android.telephony.PhoneStateListener;
     65 import android.telephony.ServiceState;
     66 import android.telephony.SmsManager;
     67 import android.telephony.SmsMessage;
     68 import android.telephony.TelephonyManager;
     69 import android.text.format.DateUtils;
     70 import android.util.Log;
     71 import android.util.Xml;
     72 import android.os.Looper;
     73 
     74 import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
     75 import com.android.bluetooth.map.BluetoothMapbMessageMms.MimePart;
     76 import com.google.android.mms.pdu.PduHeaders;
     77 
     78 public class BluetoothMapContentObserver {
     79     private static final String TAG = "BluetoothMapContentObserver";
     80 
     81     private static final boolean D = BluetoothMapService.DEBUG;
     82     private static final boolean V = BluetoothMapService.VERBOSE;
     83 
     84     private static final String EVENT_TYPE_DELETE = "MessageDeleted";
     85     private static final String EVENT_TYPE_SHIFT  = "MessageShift";
     86     private static final String EVENT_TYPE_NEW    = "NewMessage";
     87     private static final String EVENT_TYPE_DELEVERY_SUCCESS = "DeliverySuccess";
     88     private static final String EVENT_TYPE_SENDING_SUCCESS  = "SendingSuccess";
     89     private static final String EVENT_TYPE_SENDING_FAILURE  = "SendingFailure";
     90     private static final String EVENT_TYPE_DELIVERY_FAILURE = "DeliveryFailure";
     91 
     92 
     93     private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
     94 
     95     private Context mContext;
     96     private ContentResolver mResolver;
     97     private ContentProviderClient mProviderClient = null;
     98     private BluetoothMnsObexClient mMnsClient;
     99     private BluetoothMapMasInstance mMasInstance = null;
    100     private int mMasId;
    101     private boolean mEnableSmsMms = false;
    102     private boolean mObserverRegistered = false;
    103     private BluetoothMapEmailSettingsItem mAccount;
    104     private String mAuthority = null;
    105 
    106     private BluetoothMapFolderElement mFolders =
    107             new BluetoothMapFolderElement("DUMMY", null); // Will be set by the MAS when generated.
    108     private Uri mMessageUri = null;
    109 
    110     public static final int DELETED_THREAD_ID = -1;
    111 
    112     // X-Mms-Message-Type field types. These are from PduHeaders.java
    113     public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84;
    114 
    115     // Text only MMS converted to SMS if sms parts less than or equal to defined count
    116     private static final int CONVERT_MMS_TO_SMS_PART_COUNT = 10;
    117 
    118     private TYPE mSmsType;
    119 
    120     private static void close(Closeable c) {
    121         try {
    122             if (c != null) c.close();
    123         } catch (IOException e) {
    124         }
    125     }
    126 
    127     static final String[] SMS_PROJECTION = new String[] {
    128         Sms._ID,
    129         Sms.THREAD_ID,
    130         Sms.ADDRESS,
    131         Sms.BODY,
    132         Sms.DATE,
    133         Sms.READ,
    134         Sms.TYPE,
    135         Sms.STATUS,
    136         Sms.LOCKED,
    137         Sms.ERROR_CODE
    138     };
    139 
    140     static final String[] SMS_PROJECTION_SHORT = new String[] {
    141         Sms._ID,
    142         Sms.THREAD_ID,
    143         Sms.TYPE
    144     };
    145 
    146     static final String[] MMS_PROJECTION_SHORT = new String[] {
    147         Mms._ID,
    148         Mms.THREAD_ID,
    149         Mms.MESSAGE_TYPE,
    150         Mms.MESSAGE_BOX
    151     };
    152 
    153     static final String[] EMAIL_PROJECTION_SHORT = new String[] {
    154         BluetoothMapContract.MessageColumns._ID,
    155         BluetoothMapContract.MessageColumns.FOLDER_ID,
    156         BluetoothMapContract.MessageColumns.FLAG_READ
    157     };
    158 
    159 
    160     public BluetoothMapContentObserver(final Context context,
    161                                        BluetoothMnsObexClient mnsClient,
    162                                        BluetoothMapMasInstance masInstance,
    163                                        BluetoothMapEmailSettingsItem account,
    164                                        boolean enableSmsMms) throws RemoteException {
    165         mContext = context;
    166         mResolver = mContext.getContentResolver();
    167         mAccount = account;
    168         mMasInstance = masInstance;
    169         mMasId = mMasInstance.getMasId();
    170         if(account != null) {
    171             mAuthority = Uri.parse(account.mBase_uri).getAuthority();
    172             mMessageUri = Uri.parse(account.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE);
    173             mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority);
    174             if (mProviderClient == null) {
    175                 throw new RemoteException("Failed to acquire provider for " + mAuthority);
    176             }
    177             mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
    178         }
    179 
    180         mEnableSmsMms = enableSmsMms;
    181         mSmsType = getSmsType();
    182         mMnsClient = mnsClient;
    183     }
    184 
    185     /**
    186      * Set the folder structure to be used for this instance.
    187      * @param folderStructure
    188      */
    189     public void setFolderStructure(BluetoothMapFolderElement folderStructure) {
    190         this.mFolders = folderStructure;
    191     }
    192 
    193     private TYPE getSmsType() {
    194         TYPE smsType = null;
    195         TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
    196 
    197         if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) {
    198             smsType = TYPE.SMS_GSM;
    199         } else if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
    200             smsType = TYPE.SMS_CDMA;
    201         }
    202 
    203         return smsType;
    204     }
    205 
    206     private final ContentObserver mObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
    207         @Override
    208         public void onChange(boolean selfChange) {
    209             onChange(selfChange, null);
    210         }
    211 
    212         @Override
    213         public void onChange(boolean selfChange, Uri uri) {
    214             if (V) Log.d(TAG, "onChange on thread: " + Thread.currentThread().getId()
    215                 + " Uri: " + uri.toString() + " selfchange: " + selfChange);
    216 
    217             handleMsgListChanges(uri);
    218         }
    219     };
    220 
    221     private static final String folderSms[] = {
    222         "",
    223         BluetoothMapContract.FOLDER_NAME_INBOX,
    224         BluetoothMapContract.FOLDER_NAME_SENT,
    225         BluetoothMapContract.FOLDER_NAME_DRAFT,
    226         BluetoothMapContract.FOLDER_NAME_OUTBOX,
    227         BluetoothMapContract.FOLDER_NAME_OUTBOX,
    228         BluetoothMapContract.FOLDER_NAME_OUTBOX,
    229         BluetoothMapContract.FOLDER_NAME_INBOX,
    230         BluetoothMapContract.FOLDER_NAME_INBOX,
    231     };
    232 
    233     private static final String folderMms[] = {
    234         "",
    235         BluetoothMapContract.FOLDER_NAME_INBOX,
    236         BluetoothMapContract.FOLDER_NAME_SENT,
    237         BluetoothMapContract.FOLDER_NAME_DRAFT,
    238         BluetoothMapContract.FOLDER_NAME_OUTBOX,
    239     };
    240 
    241     private class Event {
    242         String eventType;
    243         long handle;
    244         String folder;
    245         String oldFolder;
    246         TYPE msgType;
    247 
    248         final static String PATH = "telecom/msg/";
    249 
    250         public Event(String eventType, long handle, String folder,
    251             String oldFolder, TYPE msgType) {
    252 
    253             this.eventType = eventType;
    254             this.handle = handle;
    255             if (folder != null) {
    256                 if(msgType == TYPE.EMAIL) {
    257                     this.folder = folder;
    258                 } else {
    259                     this.folder = PATH + folder;
    260                 }
    261             } else {
    262                 this.folder = null;
    263             }
    264             if (oldFolder != null) {
    265                 if(msgType == TYPE.EMAIL) {
    266                     this.oldFolder = oldFolder;
    267                 } else {
    268                     this.oldFolder = PATH + oldFolder;
    269                 }
    270             } else {
    271                 this.oldFolder = null;
    272             }
    273             this.msgType = msgType;
    274         }
    275 
    276         public byte[] encode() throws UnsupportedEncodingException {
    277             StringWriter sw = new StringWriter();
    278             XmlSerializer xmlEvtReport = Xml.newSerializer();
    279             try {
    280                 xmlEvtReport.setOutput(sw);
    281                 xmlEvtReport.startDocument(null, null);
    282                 xmlEvtReport.text("\r\n");
    283                 xmlEvtReport.startTag("", "MAP-event-report");
    284                 xmlEvtReport.attribute("", "version", "1.0");
    285 
    286                 xmlEvtReport.startTag("", "event");
    287                 xmlEvtReport.attribute("", "type", eventType);
    288                 xmlEvtReport.attribute("", "handle", BluetoothMapUtils.getMapHandle(handle, msgType));
    289                 if (folder != null) {
    290                     xmlEvtReport.attribute("", "folder", folder);
    291                 }
    292                 if (oldFolder != null) {
    293                     xmlEvtReport.attribute("", "old_folder", oldFolder);
    294                 }
    295                 xmlEvtReport.attribute("", "msg_type", msgType.name());
    296                 xmlEvtReport.endTag("", "event");
    297 
    298                 xmlEvtReport.endTag("", "MAP-event-report");
    299                 xmlEvtReport.endDocument();
    300             } catch (IllegalArgumentException e) {
    301                 if(D) Log.w(TAG,e);
    302             } catch (IllegalStateException e) {
    303                 if(D) Log.w(TAG,e);
    304             } catch (IOException e) {
    305                 if(D) Log.w(TAG,e);
    306             }
    307 
    308             if (V) Log.d(TAG, sw.toString());
    309 
    310             return sw.toString().getBytes("UTF-8");
    311         }
    312     }
    313 
    314     private class Msg {
    315         long id;
    316         int type;               // Used as folder for SMS/MMS
    317         int threadId;           // Used for SMS/MMS at delete
    318         long folderId = -1;     // Email folder ID
    319         long oldFolderId = -1;  // Used for email undelete
    320         boolean localInitiatedSend = false; // Used for MMS to filter out events
    321         boolean transparent = false; // Used for EMAIL to delete message sent with transparency
    322 
    323         public Msg(long id, int type, int threadId) {
    324             this.id = id;
    325             this.type = type;
    326             this.threadId = threadId;
    327         }
    328         public Msg(long id, long folderId) {
    329             this.id = id;
    330             this.folderId = folderId;
    331         }
    332 
    333         /* Eclipse generated hashCode() and equals() to make
    334          * hashMap lookup work independent of whether the obj
    335          * is used for email or SMS/MMS and whether or not the
    336          * oldFolder is set. */
    337         @Override
    338         public int hashCode() {
    339             final int prime = 31;
    340             int result = 1;
    341             result = prime * result + (int) (id ^ (id >>> 32));
    342             return result;
    343         }
    344 
    345         @Override
    346         public boolean equals(Object obj) {
    347             if (this == obj)
    348                 return true;
    349             if (obj == null)
    350                 return false;
    351             if (getClass() != obj.getClass())
    352                 return false;
    353             Msg other = (Msg) obj;
    354             if (id != other.id)
    355                 return false;
    356             return true;
    357         }
    358     }
    359 
    360     private Map<Long, Msg> mMsgListSms = new HashMap<Long, Msg>();
    361 
    362     private Map<Long, Msg> mMsgListMms = new HashMap<Long, Msg>();
    363 
    364     private Map<Long, Msg> mMsgListEmail = new HashMap<Long, Msg>();
    365 
    366     public int setNotificationRegistration(int notificationStatus) throws RemoteException {
    367         // Forward the request to the MNS thread as a message - including the MAS instance ID.
    368         if(D) Log.d(TAG,"setNotificationRegistration() enter");
    369         Handler mns = mMnsClient.getMessageHandler();
    370         if(mns != null) {
    371             Message msg = mns.obtainMessage();
    372             msg.what = BluetoothMnsObexClient.MSG_MNS_NOTIFICATION_REGISTRATION;
    373             msg.arg1 = mMasId;
    374             msg.arg2 = notificationStatus;
    375             mns.sendMessageDelayed(msg, 10); // Send message without forcing a context switch
    376             /* Some devices - e.g. PTS needs to get the unregister confirm before we actually
    377              * disconnect the MNS. */
    378             if(D) Log.d(TAG,"setNotificationRegistration() MSG_MNS_NOTIFICATION_REGISTRATION send to MNS");
    379         } else {
    380             // This should not happen except at shutdown.
    381             if(D) Log.d(TAG,"setNotificationRegistration() Unable to send registration request");
    382             return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
    383         }
    384         if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) {
    385             registerObserver();
    386         } else {
    387             unregisterObserver();
    388         }
    389         return ResponseCodes.OBEX_HTTP_OK;
    390     }
    391 
    392     public void registerObserver() throws RemoteException{
    393         if (V) Log.d(TAG, "registerObserver");
    394 
    395         if (mObserverRegistered)
    396             return;
    397 
    398         /* Use MmsSms Uri since the Sms Uri is not notified on deletes */
    399         if(mEnableSmsMms){
    400             //this is sms/mms
    401             mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver);
    402             mObserverRegistered = true;
    403         }
    404         if(mAccount != null) {
    405 
    406             mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority);
    407             if (mProviderClient == null) {
    408                 throw new RemoteException("Failed to acquire provider for " + mAuthority);
    409             }
    410             mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
    411 
    412             /* For URI's without account ID */
    413             Uri uri = Uri.parse(mAccount.mBase_uri_no_account + "/" + BluetoothMapContract.TABLE_MESSAGE);
    414             if(D) Log.d(TAG, "Registering observer for: " + uri);
    415             mResolver.registerContentObserver(uri, true, mObserver);
    416 
    417             /* For URI's with account ID - is handled the same way as without ID, but is
    418              * only triggered for MAS instances with matching account ID. */
    419             uri = Uri.parse(mAccount.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE);
    420             if(D) Log.d(TAG, "Registering observer for: " + uri);
    421             mResolver.registerContentObserver(uri, true, mObserver);
    422             mObserverRegistered = true;
    423         }
    424         initMsgList();
    425     }
    426 
    427     public void unregisterObserver() {
    428         if (V) Log.d(TAG, "unregisterObserver");
    429         mResolver.unregisterContentObserver(mObserver);
    430         mObserverRegistered = false;
    431         if(mProviderClient != null){
    432             mProviderClient.release();
    433             mProviderClient = null;
    434         }
    435     }
    436 
    437     private void sendEvent(Event evt) {
    438         Log.d(TAG, "sendEvent: " + evt.eventType + " " + evt.handle + " "
    439         + evt.folder + " " + evt.oldFolder + " " + evt.msgType.name());
    440 
    441         if (mMnsClient == null || mMnsClient.isConnected() == false) {
    442             Log.d(TAG, "sendEvent: No MNS client registered or connected- don't send event");
    443             return;
    444         }
    445 
    446         try {
    447             mMnsClient.sendEvent(evt.encode(), mMasId);
    448         } catch (UnsupportedEncodingException ex) {
    449             /* do nothing */
    450         }
    451     }
    452 
    453     private void initMsgList() throws RemoteException {
    454         if (V) Log.d(TAG, "initMsgList");
    455 
    456         if(mEnableSmsMms) {
    457 
    458             HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();
    459 
    460             Cursor c = mResolver.query(Sms.CONTENT_URI,
    461                 SMS_PROJECTION_SHORT, null, null, null);
    462 
    463             try {
    464                 while (c != null && c.moveToNext()) {
    465                     long id = c.getLong(c.getColumnIndex(Sms._ID));
    466                     int type = c.getInt(c.getColumnIndex(Sms.TYPE));
    467                     int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
    468 
    469                     Msg msg = new Msg(id, type, threadId);
    470                     msgListSms.put(id, msg);
    471                 }
    472             } finally {
    473                 close(c);
    474             }
    475 
    476             synchronized(mMsgListSms) {
    477                 mMsgListSms.clear();
    478                 mMsgListSms = msgListSms;
    479             }
    480 
    481             HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();
    482 
    483             c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT, null, null, null);
    484 
    485             try {
    486                 while (c != null && c.moveToNext()) {
    487                     long id = c.getLong(c.getColumnIndex(Mms._ID));
    488                     int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
    489                     int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
    490 
    491                     Msg msg = new Msg(id, type, threadId);
    492                     msgListMms.put(id, msg);
    493                 }
    494             } finally {
    495                 close(c);
    496             }
    497 
    498             synchronized(mMsgListMms) {
    499                 mMsgListMms.clear();
    500                 mMsgListMms = msgListMms;
    501             }
    502         }
    503 
    504         if(mAccount != null) {
    505             HashMap<Long, Msg> msgListEmail = new HashMap<Long, Msg>();
    506             Uri uri = mMessageUri;
    507             Cursor c = mProviderClient.query(uri, EMAIL_PROJECTION_SHORT, null, null, null);
    508 
    509             try {
    510                 while (c != null && c.moveToNext()) {
    511                     long id = c.getLong(c.getColumnIndex(MessageColumns._ID));
    512                     long folderId = c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.FOLDER_ID));
    513 
    514                     Msg msg = new Msg(id, folderId);
    515                     msgListEmail.put(id, msg);
    516                 }
    517             } finally {
    518                 close(c);
    519             }
    520 
    521             synchronized(mMsgListEmail) {
    522                 mMsgListEmail.clear();
    523                 mMsgListEmail = msgListEmail;
    524             }
    525         }
    526     }
    527 
    528     private void handleMsgListChangesSms() {
    529         if (V) Log.d(TAG, "handleMsgListChangesSms");
    530 
    531         HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();
    532 
    533         Cursor c = mResolver.query(Sms.CONTENT_URI,
    534             SMS_PROJECTION_SHORT, null, null, null);
    535 
    536         synchronized(mMsgListSms) {
    537             try {
    538                 while (c != null && c.moveToNext()) {
    539                     long id = c.getLong(c.getColumnIndex(Sms._ID));
    540                     int type = c.getInt(c.getColumnIndex(Sms.TYPE));
    541                     int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
    542 
    543                     Msg msg = mMsgListSms.remove(id);
    544 
    545                     /* We must filter out any actions made by the MCE, hence do not send e.g. a message
    546                      * deleted and/or MessageShift for messages deleted by the MCE. */
    547 
    548                     if (msg == null) {
    549                         /* New message */
    550                         msg = new Msg(id, type, threadId);
    551                         msgListSms.put(id, msg);
    552 
    553                         /* Incoming message from the network */
    554                         Event evt = new Event(EVENT_TYPE_NEW, id, folderSms[type],
    555                             null, mSmsType);
    556                         sendEvent(evt);
    557                     } else {
    558                         /* Existing message */
    559                         if (type != msg.type) {
    560                             Log.d(TAG, "new type: " + type + " old type: " + msg.type);
    561                             String oldFolder = folderSms[msg.type];
    562                             String newFolder = folderSms[type];
    563                             // Filter out the intermediate outbox steps
    564                             if(!oldFolder.equals(newFolder)) {
    565                                 Event evt = new Event(EVENT_TYPE_SHIFT, id, folderSms[type],
    566                                     oldFolder, mSmsType);
    567                                 sendEvent(evt);
    568                             }
    569                             msg.type = type;
    570                         } else if(threadId != msg.threadId) {
    571                             Log.d(TAG, "Message delete change: type: " + type + " old type: " + msg.type
    572                                     + "\n    threadId: " + threadId + " old threadId: " + msg.threadId);
    573                             if(threadId == DELETED_THREAD_ID) { // Message deleted
    574                                 Event evt = new Event(EVENT_TYPE_DELETE, id, BluetoothMapContract.FOLDER_NAME_DELETED,
    575                                     folderSms[msg.type], mSmsType);
    576                                 sendEvent(evt);
    577                                 msg.threadId = threadId;
    578                             } else { // Undelete
    579                                 Event evt = new Event(EVENT_TYPE_SHIFT, id, folderSms[msg.type],
    580                                     BluetoothMapContract.FOLDER_NAME_DELETED, mSmsType);
    581                                 sendEvent(evt);
    582                                 msg.threadId = threadId;
    583                             }
    584                         }
    585                         msgListSms.put(id, msg);
    586                     }
    587                 }
    588             } finally {
    589                 close(c);
    590             }
    591 
    592             for (Msg msg : mMsgListSms.values()) {
    593                 Event evt = new Event(EVENT_TYPE_DELETE, msg.id,
    594                                         BluetoothMapContract.FOLDER_NAME_DELETED,
    595                                         folderSms[msg.type], mSmsType);
    596                 sendEvent(evt);
    597             }
    598 
    599             mMsgListSms = msgListSms;
    600         }
    601     }
    602 
    603     private void handleMsgListChangesMms() {
    604         if (V) Log.d(TAG, "handleMsgListChangesMms");
    605 
    606         HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();
    607 
    608         Cursor c = mResolver.query(Mms.CONTENT_URI,
    609             MMS_PROJECTION_SHORT, null, null, null);
    610 
    611         synchronized(mMsgListMms) {
    612             try {
    613                 while (c != null && c.moveToNext()) {
    614                     long id = c.getLong(c.getColumnIndex(Mms._ID));
    615                     int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
    616                     int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE));
    617                     int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
    618 
    619                     Msg msg = mMsgListMms.remove(id);
    620 
    621                     /* We must filter out any actions made by the MCE, hence do not send e.g. a message
    622                      * deleted and/or MessageShift for messages deleted by the MCE. */
    623 
    624                     if (msg == null) {
    625                         /* New message - only notify on retrieve conf */
    626                         if (folderMms[type].equals(BluetoothMapContract.FOLDER_NAME_INBOX) &&
    627                             mtype != MESSAGE_TYPE_RETRIEVE_CONF) {
    628                                 continue;
    629                         }
    630 
    631                         msg = new Msg(id, type, threadId);
    632                         msgListMms.put(id, msg);
    633 
    634                         /* Incoming message from the network */
    635                         Event evt = new Event(EVENT_TYPE_NEW, id, folderMms[type],
    636                                 null, TYPE.MMS);
    637                         sendEvent(evt);
    638                     } else {
    639                         /* Existing message */
    640                         if (type != msg.type) {
    641                             Log.d(TAG, "new type: " + type + " old type: " + msg.type);
    642                             Event evt;
    643                             if(msg.localInitiatedSend == false) {
    644                                 // Only send events about local initiated changes
    645                                 evt = new Event(EVENT_TYPE_SHIFT, id, folderMms[type],
    646                                         folderMms[msg.type], TYPE.MMS);
    647                                 sendEvent(evt);
    648                             }
    649                             msg.type = type;
    650 
    651                             if (folderMms[type].equals(BluetoothMapContract.FOLDER_NAME_SENT)
    652                                     && msg.localInitiatedSend == true) {
    653                                 msg.localInitiatedSend = false; // Stop tracking changes for this message
    654                                 evt = new Event(EVENT_TYPE_SENDING_SUCCESS, id,
    655                                     folderSms[type], null, TYPE.MMS);
    656                                 sendEvent(evt);
    657                             }
    658                         } else if(threadId != msg.threadId) {
    659                             Log.d(TAG, "Message delete change: type: " + type + " old type: " + msg.type
    660                                     + "\n    threadId: " + threadId + " old threadId: " + msg.threadId);
    661                             if(threadId == DELETED_THREAD_ID) { // Message deleted
    662                                 Event evt = new Event(EVENT_TYPE_DELETE, id, BluetoothMapContract.FOLDER_NAME_DELETED,
    663                                     folderMms[msg.type], TYPE.MMS);
    664                                 sendEvent(evt);
    665                                 msg.threadId = threadId;
    666                             } else { // Undelete
    667                                 Event evt = new Event(EVENT_TYPE_SHIFT, id, folderMms[msg.type],
    668                                     BluetoothMapContract.FOLDER_NAME_DELETED, TYPE.MMS);
    669                                 sendEvent(evt);
    670                                 msg.threadId = threadId;
    671                             }
    672                         }
    673                         msgListMms.put(id, msg);
    674                     }
    675                 }
    676             } finally {
    677                 close(c);
    678             }
    679 
    680             for (Msg msg : mMsgListMms.values()) {
    681                 Event evt = new Event(EVENT_TYPE_DELETE, msg.id,
    682                                         BluetoothMapContract.FOLDER_NAME_DELETED,
    683                                         folderMms[msg.type], TYPE.MMS);
    684                 sendEvent(evt);
    685             }
    686             mMsgListMms = msgListMms;
    687         }
    688     }
    689 
    690     private void handleMsgListChangesEmail(Uri uri)  throws RemoteException{
    691         if (V) Log.v(TAG, "handleMsgListChangesEmail uri: " + uri.toString());
    692 
    693         // TODO: Change observer to handle accountId and message ID if present
    694 
    695         HashMap<Long, Msg> msgListEmail = new HashMap<Long, Msg>();
    696 
    697         Cursor c = mProviderClient.query(mMessageUri, EMAIL_PROJECTION_SHORT, null, null, null);
    698 
    699         synchronized(mMsgListEmail) {
    700             try {
    701                 while (c != null && c.moveToNext()) {
    702                     long id = c.getLong(c.getColumnIndex(BluetoothMapContract.MessageColumns._ID));
    703                     int folderId = c.getInt(c.getColumnIndex(
    704                             BluetoothMapContract.MessageColumns.FOLDER_ID));
    705                     Msg msg = mMsgListEmail.remove(id);
    706                     BluetoothMapFolderElement folderElement = mFolders.getEmailFolderById(folderId);
    707                     String newFolder;
    708                     if(folderElement != null) {
    709                         newFolder = folderElement.getFullPath();
    710                     } else {
    711                         newFolder = "unknown"; // This can happen if a new folder is created while connected
    712                     }
    713 
    714                     /* We must filter out any actions made by the MCE, hence do not send e.g. a message
    715                      * deleted and/or MessageShift for messages deleted by the MCE. */
    716 
    717                     if (msg == null) {
    718                         /* New message */
    719                         msg = new Msg(id, folderId);
    720                         msgListEmail.put(id, msg);
    721                         Event evt = new Event(EVENT_TYPE_NEW, id, newFolder,
    722                             null, TYPE.EMAIL);
    723                         sendEvent(evt);
    724                     } else {
    725                         /* Existing message */
    726                         if (folderId != msg.folderId) {
    727                             if (D) Log.d(TAG, "new folderId: " + folderId + " old folderId: " + msg.folderId);
    728                             BluetoothMapFolderElement oldFolderElement = mFolders.getEmailFolderById(msg.folderId);
    729                             String oldFolder;
    730                             if(oldFolderElement != null) {
    731                                 oldFolder = oldFolderElement.getFullPath();
    732                             } else {
    733                                 // This can happen if a new folder is created while connected
    734                                 oldFolder = "unknown";
    735                             }
    736                             BluetoothMapFolderElement deletedFolder =
    737                                     mFolders.getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED);
    738                             BluetoothMapFolderElement sentFolder =
    739                                     mFolders.getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_SENT);
    740                             /*
    741                              *  If the folder is now 'deleted', send a deleted-event in stead of a shift
    742                              *  or if message is sent initiated by MAP Client, then send sending-success
    743                              *  otherwise send folderShift
    744                              */
    745                             if(deletedFolder != null && deletedFolder.getEmailFolderId() == folderId) {
    746                                 Event evt = new Event(EVENT_TYPE_DELETE, msg.id, newFolder,
    747                                         oldFolder, TYPE.EMAIL);
    748                                 sendEvent(evt);
    749                             } else if(sentFolder != null
    750                                       && sentFolder.getEmailFolderId() == folderId
    751                                       && msg.localInitiatedSend == true) {
    752                                 if(msg.transparent) {
    753                                     mResolver.delete(ContentUris.withAppendedId(mMessageUri, id), null, null);
    754                                 } else {
    755                                     msg.localInitiatedSend = false;
    756                                     Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id,
    757                                                           oldFolder, null, TYPE.EMAIL);
    758                                     sendEvent(evt);
    759                                 }
    760                             } else {
    761                                 Event evt = new Event(EVENT_TYPE_SHIFT, id, newFolder,
    762                                                       oldFolder, TYPE.EMAIL);
    763                                 sendEvent(evt);
    764                             }
    765                             msg.folderId = folderId;
    766                         }
    767                         msgListEmail.put(id, msg);
    768                     }
    769                 }
    770             } finally {
    771                 close(c);
    772             }
    773 
    774             // For all messages no longer in the database send a delete notification
    775             for (Msg msg : mMsgListEmail.values()) {
    776                 BluetoothMapFolderElement oldFolderElement = mFolders.getEmailFolderById(msg.folderId);
    777                 String oldFolder;
    778                 if(oldFolderElement != null) {
    779                     oldFolder = oldFolderElement.getFullPath();
    780                 } else {
    781                     oldFolder = "unknown";
    782                 }
    783                 /* Some e-mail clients delete the message after sending, and creates a new message in sent.
    784                  * We cannot track the message anymore, hence send both a send success and delete message.
    785                  */
    786                 if(msg.localInitiatedSend == true) {
    787                     msg.localInitiatedSend = false;
    788                     // If message is send with transparency don't set folder as message is deleted
    789                     if (msg.transparent)
    790                         oldFolder = null;
    791                     Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, oldFolder, null, TYPE.EMAIL);
    792                     sendEvent(evt);
    793                 }
    794                 /* As this message deleted is only send on a real delete - don't set folder.
    795                  *  - only send delete event if message is not sent with transparency
    796                  */
    797                 if (!msg.transparent) {
    798 
    799                     Event evt = new Event(EVENT_TYPE_DELETE, msg.id, null, oldFolder, TYPE.EMAIL);
    800                     sendEvent(evt);
    801                 }
    802             }
    803             mMsgListEmail = msgListEmail;
    804         }
    805     }
    806 
    807     private void handleMsgListChanges(Uri uri) {
    808         if(uri.getAuthority().equals(mAuthority)) {
    809             try {
    810                 handleMsgListChangesEmail(uri);
    811             }catch(RemoteException e){
    812                 mMasInstance.restartObexServerSession();
    813                 Log.w(TAG, "Problems contacting the ContentProvider in mas Instance "+mMasId+" restaring ObexServerSession");
    814             }
    815 
    816         } else {
    817             handleMsgListChangesSms();
    818             handleMsgListChangesMms();
    819         }
    820     }
    821 
    822     private boolean setEmailMessageStatusDelete(BluetoothMapFolderElement mCurrentFolder,
    823             String uriStr, long handle, int status) {
    824         boolean res = false;
    825         Uri uri = Uri.parse(uriStr + BluetoothMapContract.TABLE_MESSAGE);
    826 
    827         int updateCount = 0;
    828         ContentValues contentValues = new ContentValues();
    829         BluetoothMapFolderElement deleteFolder = mFolders.
    830                 getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED);
    831         contentValues.put(BluetoothMapContract.MessageColumns._ID, handle);
    832         synchronized(mMsgListEmail) {
    833             Msg msg = mMsgListEmail.get(handle);
    834             if (status == BluetoothMapAppParams.STATUS_VALUE_YES) {
    835                 /* Set deleted folder id */
    836                 long folderId = -1;
    837                 if(deleteFolder != null) {
    838                     folderId = deleteFolder.getEmailFolderId();
    839                 }
    840                 contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID,folderId);
    841                 updateCount = mResolver.update(uri, contentValues, null, null);
    842                 /* The race between updating the value in our cached values and the database
    843                  * is handled by the synchronized statement. */
    844                 if(updateCount > 0) {
    845                     res = true;
    846                     if (msg != null) {
    847                         msg.oldFolderId = msg.folderId;
    848                         // Update the folder ID to avoid triggering an event for MCE initiated actions.
    849                         msg.folderId = folderId;
    850                     }
    851                     if(D) Log.d(TAG, "Deleted MSG: " + handle + " from folderId: " + folderId);
    852                 } else {
    853                     Log.w(TAG, "Msg: " + handle + " - Set delete status " + status
    854                             + " failed for folderId " + folderId);
    855                 }
    856             } else if (status == BluetoothMapAppParams.STATUS_VALUE_NO) {
    857                 /* Undelete message. move to old folder if we know it,
    858                  * else move to inbox - as dictated by the spec. */
    859                 if(msg != null && deleteFolder != null &&
    860                         msg.folderId == deleteFolder.getEmailFolderId()) {
    861                     /* Only modify messages in the 'Deleted' folder */
    862                     long folderId = -1;
    863                     if (msg != null && msg.oldFolderId != -1) {
    864                         folderId = msg.oldFolderId;
    865                     } else {
    866                         BluetoothMapFolderElement inboxFolder = mCurrentFolder.
    867                                 getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_INBOX);
    868                         if(inboxFolder != null) {
    869                             folderId = inboxFolder.getEmailFolderId();
    870                         }
    871                         if(D)Log.d(TAG,"We did not delete the message, hence the old folder is unknown. Moving to inbox.");
    872                     }
    873                     contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId);
    874                     updateCount = mResolver.update(uri, contentValues, null, null);
    875                     if(updateCount > 0) {
    876                         res = true;
    877                         // Update the folder ID to avoid triggering an event for MCE initiated actions.
    878                         msg.folderId = folderId;
    879                     } else {
    880                         if(D)Log.d(TAG,"We did not delete the message, hence the old folder is unknown. Moving to inbox.");
    881                     }
    882                 }
    883             }
    884             if(V) {
    885                 BluetoothMapFolderElement folderElement;
    886                 String folderName = "unknown";
    887                 if (msg != null) {
    888                     folderElement = mCurrentFolder.getEmailFolderById(msg.folderId);
    889                     if(folderElement != null) {
    890                         folderName = folderElement.getName();
    891                     }
    892                 }
    893                 Log.d(TAG,"setEmailMessageStatusDelete: " + handle + " from " + folderName
    894                         + " status: " + status);
    895             }
    896         }
    897         if(res == false) {
    898             Log.w(TAG, "Set delete status " + status + " failed.");
    899         }
    900         return res;
    901     }
    902 
    903     private void updateThreadId(Uri uri, String valueString, long threadId) {
    904         ContentValues contentValues = new ContentValues();
    905         contentValues.put(valueString, threadId);
    906         mResolver.update(uri, contentValues, null, null);
    907     }
    908 
    909     private boolean deleteMessageMms(long handle) {
    910         boolean res = false;
    911         Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
    912         Cursor c = mResolver.query(uri, null, null, null, null);
    913 
    914         try {
    915             if (c != null && c.moveToFirst()) {
    916                 /* Move to deleted folder, or delete if already in deleted folder */
    917                 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
    918                 if (threadId != DELETED_THREAD_ID) {
    919                     /* Set deleted thread id */
    920                     synchronized(mMsgListMms) {
    921                         Msg msg = mMsgListMms.get(handle);
    922                         if(msg != null) { // This will always be the case
    923                             msg.threadId = DELETED_THREAD_ID;
    924                         }
    925                     }
    926                     updateThreadId(uri, Mms.THREAD_ID, DELETED_THREAD_ID);
    927                 } else {
    928                     /* Delete from observer message list to avoid delete notifications */
    929                     synchronized(mMsgListMms) {
    930                         mMsgListMms.remove(handle);
    931                     }
    932                     /* Delete message */
    933                     mResolver.delete(uri, null, null);
    934                 }
    935                 res = true;
    936             }
    937         } finally {
    938             close(c);
    939         }
    940 
    941         return res;
    942     }
    943 
    944     private boolean unDeleteMessageMms(long handle) {
    945         boolean res = false;
    946         Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
    947         Cursor c = mResolver.query(uri, null, null, null, null);
    948 
    949         try {
    950             if (c != null && c.moveToFirst()) {
    951                 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
    952                 if (threadId == DELETED_THREAD_ID) {
    953                     /* Restore thread id from address, or if no thread for address
    954                     * create new thread by insert and remove of fake message */
    955                     String address;
    956                     long id = c.getLong(c.getColumnIndex(Mms._ID));
    957                     int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
    958                     if (msgBox == Mms.MESSAGE_BOX_INBOX) {
    959                         address = BluetoothMapContent.getAddressMms(mResolver, id,
    960                             BluetoothMapContent.MMS_FROM);
    961                     } else {
    962                         address = BluetoothMapContent.getAddressMms(mResolver, id,
    963                             BluetoothMapContent.MMS_TO);
    964                     }
    965                     Set<String> recipients = new HashSet<String>();
    966                     recipients.addAll(Arrays.asList(address));
    967                     Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients);
    968                     synchronized(mMsgListMms) {
    969                         Msg msg = mMsgListMms.get(handle);
    970                         if(msg != null) { // This will always be the case
    971                             msg.threadId = oldThreadId.intValue();
    972                         }
    973                     }
    974                     updateThreadId(uri, Mms.THREAD_ID, oldThreadId);
    975                 } else {
    976                     Log.d(TAG, "Message not in deleted folder: handle " + handle
    977                         + " threadId " + threadId);
    978                 }
    979                 res = true;
    980             }
    981         } finally {
    982             close(c);
    983         }
    984 
    985         return res;
    986     }
    987 
    988     private boolean deleteMessageSms(long handle) {
    989         boolean res = false;
    990         Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
    991         Cursor c = mResolver.query(uri, null, null, null, null);
    992 
    993         try {
    994             if (c != null && c.moveToFirst()) {
    995                 /* Move to deleted folder, or delete if already in deleted folder */
    996                 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
    997                 if (threadId != DELETED_THREAD_ID) {
    998                     synchronized(mMsgListSms) {
    999                         Msg msg = mMsgListSms.get(handle);
   1000                         if(msg != null) { // This will always be the case
   1001                             msg.threadId = DELETED_THREAD_ID;
   1002                         }
   1003                     }
   1004                     /* Set deleted thread id */
   1005                     updateThreadId(uri, Sms.THREAD_ID, DELETED_THREAD_ID);
   1006                 } else {
   1007                     /* Delete from observer message list to avoid delete notifications */
   1008                     synchronized(mMsgListSms) {
   1009                         mMsgListSms.remove(handle);
   1010                     }
   1011                     /* Delete message */
   1012                     mResolver.delete(uri, null, null);
   1013                 }
   1014                 res = true;
   1015             }
   1016         } finally {
   1017             close(c);
   1018         }
   1019 
   1020         return res;
   1021     }
   1022 
   1023     private boolean unDeleteMessageSms(long handle) {
   1024         boolean res = false;
   1025         Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
   1026         Cursor c = mResolver.query(uri, null, null, null, null);
   1027 
   1028         try {
   1029             if (c != null && c.moveToFirst()) {
   1030                 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
   1031                 if (threadId == DELETED_THREAD_ID) {
   1032                     String address = c.getString(c.getColumnIndex(Sms.ADDRESS));
   1033                     Set<String> recipients = new HashSet<String>();
   1034                     recipients.addAll(Arrays.asList(address));
   1035                     Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients);
   1036                     synchronized(mMsgListSms) {
   1037                         Msg msg = mMsgListSms.get(handle);
   1038                         if(msg != null) { // This will always be the case
   1039                             msg.threadId = oldThreadId.intValue(); // The threadId is specified as an int, so it is safe to truncate
   1040                         }
   1041                     }
   1042                     updateThreadId(uri, Sms.THREAD_ID, oldThreadId);
   1043                 } else {
   1044                     Log.d(TAG, "Message not in deleted folder: handle " + handle
   1045                         + " threadId " + threadId);
   1046                 }
   1047                 res = true;
   1048             }
   1049         } finally {
   1050             close(c);
   1051         }
   1052 
   1053         return res;
   1054     }
   1055 
   1056     public boolean setMessageStatusDeleted(long handle, TYPE type,
   1057             BluetoothMapFolderElement mCurrentFolder, String uriStr, int statusValue) {
   1058         boolean res = false;
   1059         if (D) Log.d(TAG, "setMessageStatusDeleted: handle " + handle
   1060             + " type " + type + " value " + statusValue);
   1061 
   1062         if (type == TYPE.EMAIL) {
   1063             res = setEmailMessageStatusDelete(mCurrentFolder, uriStr, handle, statusValue);
   1064         } else {
   1065             if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) {
   1066                 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
   1067                     res = deleteMessageSms(handle);
   1068                 } else if (type == TYPE.MMS) {
   1069                     res = deleteMessageMms(handle);
   1070                 }
   1071             } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) {
   1072                 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
   1073                     res = unDeleteMessageSms(handle);
   1074                 } else if (type == TYPE.MMS) {
   1075                     res = unDeleteMessageMms(handle);
   1076                 }
   1077             }
   1078         }
   1079 
   1080         return res;
   1081     }
   1082 
   1083     /**
   1084      *
   1085      * @param handle
   1086      * @param type
   1087      * @param uriStr
   1088      * @param statusValue
   1089      * @return true at success
   1090      */
   1091     public boolean setMessageStatusRead(long handle, TYPE type, String uriStr, int statusValue) throws RemoteException{
   1092         int count = 0;
   1093 
   1094         if (D) Log.d(TAG, "setMessageStatusRead: handle " + handle
   1095             + " type " + type + " value " + statusValue);
   1096 
   1097         /* Approved MAP spec errata 3445 states that read status initiated */
   1098         /* by the MCE shall change the MSE read status. */
   1099 
   1100         if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
   1101             Uri uri = Sms.Inbox.CONTENT_URI;//ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
   1102             ContentValues contentValues = new ContentValues();
   1103             contentValues.put(Sms.READ, statusValue);
   1104             contentValues.put(Sms.SEEN, statusValue);
   1105             String where = Sms._ID+"="+handle;
   1106             String values = contentValues.toString();
   1107             if (D) Log.d(TAG, " -> SMS Uri: " + uri.toString() + " Where " + where + " values " + values);
   1108             count = mResolver.update(uri, contentValues, where, null);
   1109             if (D) Log.d(TAG, " -> "+count +" rows updated!");
   1110 
   1111         } else if (type == TYPE.MMS) {
   1112             Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
   1113             if (D) Log.d(TAG, " -> MMS Uri: " + uri.toString());
   1114             ContentValues contentValues = new ContentValues();
   1115             contentValues.put(Mms.READ, statusValue);
   1116             count = mResolver.update(uri, contentValues, null, null);
   1117             if (D) Log.d(TAG, " -> "+count +" rows updated!");
   1118 
   1119         } if (type == TYPE.EMAIL) {
   1120             Uri uri = mMessageUri;
   1121             ContentValues contentValues = new ContentValues();
   1122             contentValues.put(BluetoothMapContract.MessageColumns.FLAG_READ, statusValue);
   1123             contentValues.put(BluetoothMapContract.MessageColumns._ID, handle);
   1124             count = mProviderClient.update(uri, contentValues, null, null);
   1125         }
   1126 
   1127         return (count > 0);
   1128     }
   1129 
   1130     private class PushMsgInfo {
   1131         long id;
   1132         int transparent;
   1133         int retry;
   1134         String phone;
   1135         Uri uri;
   1136         long timestamp;
   1137         int parts;
   1138         int partsSent;
   1139         int partsDelivered;
   1140         boolean resend;
   1141         boolean sendInProgress;
   1142         boolean failedSent; // Set to true if a single part sent fail is received.
   1143         int statusDelivered; // Set to != 0 if a single part deliver fail is received.
   1144 
   1145         public PushMsgInfo(long id, int transparent,
   1146             int retry, String phone, Uri uri) {
   1147             this.id = id;
   1148             this.transparent = transparent;
   1149             this.retry = retry;
   1150             this.phone = phone;
   1151             this.uri = uri;
   1152             this.resend = false;
   1153             this.sendInProgress = false;
   1154             this.failedSent = false;
   1155             this.statusDelivered = 0; /* Assume success */
   1156             this.timestamp = 0;
   1157         };
   1158     }
   1159 
   1160     private Map<Long, PushMsgInfo> mPushMsgList =
   1161         Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>());
   1162 
   1163     public long pushMessage(BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement,
   1164             BluetoothMapAppParams ap, String emailBaseUri)
   1165                     throws IllegalArgumentException, RemoteException, IOException {
   1166         if (D) Log.d(TAG, "pushMessage");
   1167         ArrayList<BluetoothMapbMessage.vCard> recipientList = msg.getRecipients();
   1168         int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ?
   1169                 0 : ap.getTransparent();
   1170         int retry = ap.getRetry();
   1171         int charset = ap.getCharset();
   1172         long handle = -1;
   1173         long folderId = -1;
   1174 
   1175         if (recipientList == null) {
   1176             if (D) Log.d(TAG, "empty recipient list");
   1177             return -1;
   1178         }
   1179 
   1180         if ( msg.getType().equals(TYPE.EMAIL) ) {
   1181             /* Write the message to the database */
   1182             String msgBody = ((BluetoothMapbMessageEmail) msg).getEmailBody();
   1183             if (V) {
   1184                 int length = msgBody.length();
   1185                 Log.v(TAG, "pushMessage: message string length = " + length);
   1186                 String messages[] = msgBody.split("\r\n");
   1187                 Log.v(TAG, "pushMessage: messages count=" + messages.length);
   1188                 for(int i = 0; i < messages.length; i++) {
   1189                     Log.v(TAG, "part " + i + ":" + messages[i]);
   1190                 }
   1191             }
   1192             FileOutputStream os = null;
   1193             ParcelFileDescriptor fdOut = null;
   1194             Uri uriInsert = Uri.parse(emailBaseUri + BluetoothMapContract.TABLE_MESSAGE);
   1195             if (D) Log.d(TAG, "pushMessage - uriInsert= " + uriInsert.toString() +
   1196                     ", intoFolder id=" + folderElement.getEmailFolderId());
   1197 
   1198             synchronized(mMsgListEmail) {
   1199                 // Now insert the empty message into folder
   1200                 ContentValues values = new ContentValues();
   1201                 folderId = folderElement.getEmailFolderId();
   1202                 values.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId);
   1203                 Uri uriNew = mProviderClient.insert(uriInsert, values);
   1204                 if (D) Log.d(TAG, "pushMessage - uriNew= " + uriNew.toString());
   1205                 handle =  Long.parseLong(uriNew.getLastPathSegment());
   1206 
   1207                 try {
   1208                     fdOut = mProviderClient.openFile(uriNew, "w");
   1209                     os = new FileOutputStream(fdOut.getFileDescriptor());
   1210                     // Write Email to DB
   1211                     os.write(msgBody.getBytes(), 0, msgBody.getBytes().length);
   1212                 } catch (FileNotFoundException e) {
   1213                     Log.w(TAG, e);
   1214                     throw(new IOException("Unable to open file stream"));
   1215                 } catch (NullPointerException e) {
   1216                     Log.w(TAG, e);
   1217                     throw(new IllegalArgumentException("Unable to parse message."));
   1218                 } finally {
   1219                     try {
   1220                         if(os != null)
   1221                             os.close();
   1222                     } catch (IOException e) {Log.w(TAG, e);}
   1223                     try {
   1224                         if(fdOut != null)
   1225                              fdOut.close();
   1226                     } catch (IOException e) {Log.w(TAG, e);}
   1227                 }
   1228 
   1229                 /* Extract the data for the inserted message, and store in local mirror, to
   1230                  * avoid sending a NewMessage Event. */
   1231                 Msg newMsg = new Msg(handle, folderId);
   1232                 newMsg.transparent = (transparent == 1) ? true : false;
   1233                 if ( folderId == folderElement.getEmailFolderByName(
   1234                         BluetoothMapContract.FOLDER_NAME_OUTBOX).getEmailFolderId() ) {
   1235                     newMsg.localInitiatedSend = true;
   1236                 }
   1237                 mMsgListEmail.put(handle, newMsg);
   1238             }
   1239         } else { // type SMS_* of MMS
   1240             for (BluetoothMapbMessage.vCard recipient : recipientList) {
   1241                 if(recipient.getEnvLevel() == 0) // Only send the message to the top level recipient
   1242                 {
   1243                     /* Only send to first address */
   1244                     String phone = recipient.getFirstPhoneNumber();
   1245                     String email = recipient.getFirstEmail();
   1246                     String folder = folderElement.getName();
   1247                     boolean read = false;
   1248                     boolean deliveryReport = true;
   1249                     String msgBody = null;
   1250 
   1251                     /* If MMS contains text only and the size is less than ten SMS's
   1252                      * then convert the MMS to type SMS and then proceed
   1253                      */
   1254                     if (msg.getType().equals(TYPE.MMS) &&
   1255                             (((BluetoothMapbMessageMms) msg).getTextOnly() == true)) {
   1256                         msgBody = ((BluetoothMapbMessageMms) msg).getMessageAsText();
   1257                         SmsManager smsMng = SmsManager.getDefault();
   1258                         ArrayList<String> parts = smsMng.divideMessage(msgBody);
   1259                         int smsParts = parts.size();
   1260                         if (smsParts  <= CONVERT_MMS_TO_SMS_PART_COUNT ) {
   1261                             if (D) Log.d(TAG, "pushMessage - converting MMS to SMS, sms parts=" + smsParts );
   1262                             msg.setType(mSmsType);
   1263                         } else {
   1264                             if (D) Log.d(TAG, "pushMessage - MMS text only but to big to convert to SMS");
   1265                             msgBody = null;
   1266                         }
   1267 
   1268                     }
   1269 
   1270                     if (msg.getType().equals(TYPE.MMS)) {
   1271                         /* Send message if folder is outbox else just store in draft*/
   1272                         handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMms)msg);
   1273                     } else if (msg.getType().equals(TYPE.SMS_GSM) ||
   1274                             msg.getType().equals(TYPE.SMS_CDMA) ) {
   1275                         /* Add the message to the database */
   1276                         if(msgBody == null)
   1277                             msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody();
   1278 
   1279                         /* We need to lock the SMS list while updating the database, to avoid sending
   1280                          * events on MCE initiated operation. */
   1281                         Uri contentUri = Uri.parse(Sms.CONTENT_URI+ "/" + folder);
   1282                         Uri uri;
   1283                         synchronized(mMsgListSms) {
   1284                             uri = Sms.addMessageToUri(mResolver, contentUri, phone, msgBody,
   1285                                 "", System.currentTimeMillis(), read, deliveryReport);
   1286 
   1287                             if(V) Log.v(TAG, "Sms.addMessageToUri() returned: " + uri);
   1288                             if (uri == null) {
   1289                                 if (D) Log.d(TAG, "pushMessage - failure on add to uri " + contentUri);
   1290                                 return -1;
   1291                             }
   1292                             Cursor c = mResolver.query(uri, SMS_PROJECTION_SHORT, null, null, null);
   1293                             try {
   1294                                 /* Extract the data for the inserted message, and store in local mirror, to
   1295                                 * avoid sending a NewMessage Event. */
   1296                                 if (c != null && c.moveToFirst()) {
   1297                                     long id = c.getLong(c.getColumnIndex(Sms._ID));
   1298                                     int type = c.getInt(c.getColumnIndex(Sms.TYPE));
   1299                                     int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
   1300                                     Msg newMsg = new Msg(id, type, threadId);
   1301                                     mMsgListSms.put(id, newMsg);
   1302                                 } else {
   1303                                     return -1; // This can only happen, if the message is deleted just as it is added
   1304                                 }
   1305                             } finally {
   1306                                 close(c);
   1307                             }
   1308 
   1309                             handle = Long.parseLong(uri.getLastPathSegment());
   1310 
   1311                             /* Send message if folder is outbox */
   1312                             if (folder.equals(BluetoothMapContract.FOLDER_NAME_OUTBOX)) {
   1313                                 PushMsgInfo msgInfo = new PushMsgInfo(handle, transparent,
   1314                                     retry, phone, uri);
   1315                                 mPushMsgList.put(handle, msgInfo);
   1316                                 sendMessage(msgInfo, msgBody);
   1317                                 if(V) Log.v(TAG, "sendMessage returned...");
   1318                             }
   1319                             /* sendMessage causes the message to be deleted and reinserted, hence we need to lock
   1320                              * the list while this is happening. */
   1321                         }
   1322                     } else {
   1323                         if (D) Log.d(TAG, "pushMessage - failure on type " );
   1324                         return -1;
   1325                     }
   1326                 }
   1327             }
   1328         }
   1329 
   1330         /* If multiple recipients return handle of last */
   1331         return handle;
   1332     }
   1333 
   1334     public long sendMmsMessage(String folder, String to_address, BluetoothMapbMessageMms msg) {
   1335         /*
   1336          *strategy:
   1337          *1) parse message into parts
   1338          *if folder is outbox/drafts:
   1339          *2) push message to draft
   1340          *if folder is outbox:
   1341          *3) move message to outbox (to trigger the mms app to add msg to pending_messages list)
   1342          *4) send intent to mms app in order to wake it up.
   1343          *else if folder !outbox:
   1344          *1) push message to folder
   1345          * */
   1346         if (folder != null && (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)
   1347                 ||  folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT))) {
   1348             long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, to_address, msg);
   1349             /* if invalid handle (-1) then just return the handle - else continue sending (if folder is outbox) */
   1350             if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)) {
   1351                 moveDraftToOutbox(handle);
   1352                 Intent sendIntent = new Intent("android.intent.action.MMS_SEND_OUTBOX_MSG");
   1353                 if (D) Log.d(TAG, "broadcasting intent: "+sendIntent.toString());
   1354                 mContext.sendBroadcast(sendIntent);
   1355             }
   1356             return handle;
   1357         } else {
   1358             /* not allowed to push mms to anything but outbox/draft */
   1359             throw  new IllegalArgumentException("Cannot push message to other folders than outbox/draft");
   1360         }
   1361     }
   1362 
   1363     private void moveDraftToOutbox(long handle) {
   1364         /*Move message by changing the msg_box value in the content provider database */
   1365         if (handle != -1) return;
   1366 
   1367         String whereClause = " _id= " + handle;
   1368         Uri uri = Mms.CONTENT_URI;
   1369         Cursor queryResult = mResolver.query(uri, null, whereClause, null, null);
   1370         try {
   1371             if (queryResult != null && queryResult.moveToFirst()) {
   1372                 ContentValues data = new ContentValues();
   1373                 /* set folder to be outbox */
   1374                 data.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_OUTBOX);
   1375                 mResolver.update(uri, data, whereClause, null);
   1376                 if (D) Log.d(TAG, "Moved draft MMS to outbox");
   1377             } else {
   1378                 if (D) Log.d(TAG, "Could not move draft to outbox ");
   1379             }
   1380         } finally {
   1381             queryResult.close();
   1382         }
   1383     }
   1384 
   1385     private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMms msg) {
   1386         /**
   1387          * strategy:
   1388          * 1) parse msg into parts + header
   1389          * 2) create thread id (abuse the ease of adding an SMS to get id for thread)
   1390          * 3) push parts into content://mms/parts/ table
   1391          * 3)
   1392          */
   1393 
   1394         ContentValues values = new ContentValues();
   1395         values.put(Mms.MESSAGE_BOX, folder);
   1396         values.put(Mms.READ, 0);
   1397         values.put(Mms.SEEN, 0);
   1398         if(msg.getSubject() != null) {
   1399             values.put(Mms.SUBJECT, msg.getSubject());
   1400         } else {
   1401             values.put(Mms.SUBJECT, "");
   1402         }
   1403 
   1404         if(msg.getSubject() != null && msg.getSubject().length() > 0) {
   1405             values.put(Mms.SUBJECT_CHARSET, 106);
   1406         }
   1407         values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related");
   1408         values.put(Mms.EXPIRY, 604800);
   1409         values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
   1410         values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
   1411         values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION);
   1412         values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL);
   1413         values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO);
   1414         values.put(Mms.TRANSACTION_ID, "T"+ Long.toHexString(System.currentTimeMillis()));
   1415         values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO);
   1416         values.put(Mms.LOCKED, 0);
   1417         if(msg.getTextOnly() == true)
   1418             values.put(Mms.TEXT_ONLY, true);
   1419         values.put(Mms.MESSAGE_SIZE, msg.getSize());
   1420 
   1421         // Get thread id
   1422         Set<String> recipients = new HashSet<String>();
   1423         recipients.addAll(Arrays.asList(to_address));
   1424         values.put(Mms.THREAD_ID, Telephony.Threads.getOrCreateThreadId(mContext, recipients));
   1425         Uri uri = Mms.CONTENT_URI;
   1426 
   1427         synchronized (mMsgListMms) {
   1428             uri = mResolver.insert(uri, values);
   1429 
   1430             if (uri == null) {
   1431                 // unable to insert MMS
   1432                 Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri);
   1433                 return -1;
   1434             }
   1435             /* As we already have all the values we need, we could skip the query, but
   1436                doing the query ensures we get any changes made by the content provider
   1437                at insert. */
   1438             Cursor c = mResolver.query(uri, MMS_PROJECTION_SHORT, null, null, null);
   1439             try {
   1440                 if (c != null && c.moveToFirst()) {
   1441                     long id = c.getLong(c.getColumnIndex(Mms._ID));
   1442                     int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
   1443                     int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
   1444 
   1445                     /* We must filter out any actions made by the MCE. Add the new message to
   1446                      * the list of known messages. */
   1447 
   1448                     Msg newMsg = new Msg(id, type, threadId);
   1449                     newMsg.localInitiatedSend = true;
   1450                     mMsgListMms.put(id, newMsg);
   1451                 }
   1452             } finally {
   1453                 close(c);
   1454             }
   1455         } // Done adding changes, unlock access to mMsgListMms to allow sending MMS events again
   1456 
   1457         long handle = Long.parseLong(uri.getLastPathSegment());
   1458         if (V) Log.v(TAG, " NEW URI " + uri.toString());
   1459 
   1460         try {
   1461             if(msg.getMimeParts() == null) {
   1462                 /* Perhaps this message have been deleted, and no longer have any content, but only headers */
   1463                 Log.w(TAG, "No MMS parts present...");
   1464             } else {
   1465                 if(V) Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base.");
   1466                 int count = 0;
   1467                 for(MimePart part : msg.getMimeParts()) {
   1468                     ++count;
   1469                     values.clear();
   1470                     if(part.mContentType != null &&  part.mContentType.toUpperCase().contains("TEXT")) {
   1471                         values.put(Mms.Part.CONTENT_TYPE, "text/plain");
   1472                         values.put(Mms.Part.CHARSET, 106);
   1473                         if(part.mPartName != null) {
   1474                             values.put(Mms.Part.FILENAME, part.mPartName);
   1475                             values.put(Mms.Part.NAME, part.mPartName);
   1476                         } else {
   1477                             values.put(Mms.Part.FILENAME, "text_" + count +".txt");
   1478                             values.put(Mms.Part.NAME, "text_" + count +".txt");
   1479                         }
   1480                         // Ensure we have "ci" set
   1481                         if(part.mContentId != null) {
   1482                             values.put(Mms.Part.CONTENT_ID, part.mContentId);
   1483                         } else {
   1484                             if(part.mPartName != null) {
   1485                                 values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">");
   1486                             } else {
   1487                                 values.put(Mms.Part.CONTENT_ID, "<text_" + count + ">");
   1488                             }
   1489                         }
   1490                         // Ensure we have "cl" set
   1491                         if(part.mContentLocation != null) {
   1492                             values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
   1493                         } else {
   1494                             if(part.mPartName != null) {
   1495                                 values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".txt");
   1496                             } else {
   1497                                 values.put(Mms.Part.CONTENT_LOCATION, "text_" + count + ".txt");
   1498                             }
   1499                         }
   1500 
   1501                         if(part.mContentDisposition != null) {
   1502                             values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
   1503                         }
   1504                         values.put(Mms.Part.TEXT, part.getDataAsString());
   1505                         uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part");
   1506                         uri = mResolver.insert(uri, values);
   1507                         if(V) Log.v(TAG, "Added TEXT part");
   1508 
   1509                     } else if (part.mContentType != null &&  part.mContentType.toUpperCase().contains("SMIL")){
   1510 
   1511                         values.put(Mms.Part.SEQ, -1);
   1512                         values.put(Mms.Part.CONTENT_TYPE, "application/smil");
   1513                         if(part.mContentId != null) {
   1514                             values.put(Mms.Part.CONTENT_ID, part.mContentId);
   1515                         } else {
   1516                             values.put(Mms.Part.CONTENT_ID, "<smil_" + count + ">");
   1517                         }
   1518                         if(part.mContentLocation != null) {
   1519                             values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
   1520                         } else {
   1521                             values.put(Mms.Part.CONTENT_LOCATION, "smil_" + count + ".xml");
   1522                         }
   1523 
   1524                         if(part.mContentDisposition != null)
   1525                             values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
   1526                         values.put(Mms.Part.FILENAME, "smil.xml");
   1527                         values.put(Mms.Part.NAME, "smil.xml");
   1528                         values.put(Mms.Part.TEXT, new String(part.mData, "UTF-8"));
   1529 
   1530                         uri = Uri.parse(Mms.CONTENT_URI+ "/" + handle + "/part");
   1531                         uri = mResolver.insert(uri, values);
   1532                         if (V) Log.v(TAG, "Added SMIL part");
   1533 
   1534                     }else /*VIDEO/AUDIO/IMAGE*/ {
   1535                         writeMmsDataPart(handle, part, count);
   1536                         if (V) Log.v(TAG, "Added OTHER part");
   1537                     }
   1538                     if (uri != null){
   1539                         if (V) Log.v(TAG, "Added part with content-type: "+ part.mContentType + " to Uri: " + uri.toString());
   1540                     }
   1541                 }
   1542             }
   1543         } catch (UnsupportedEncodingException e) {
   1544             Log.w(TAG, e);
   1545         } catch (IOException e) {
   1546             Log.w(TAG, e);
   1547         }
   1548 
   1549         values.clear();
   1550         values.put(Mms.Addr.CONTACT_ID, "null");
   1551         values.put(Mms.Addr.ADDRESS, "insert-address-token");
   1552         values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_FROM);
   1553         values.put(Mms.Addr.CHARSET, 106);
   1554 
   1555         uri = Uri.parse(Mms.CONTENT_URI + "/"  + handle + "/addr");
   1556         uri = mResolver.insert(uri, values);
   1557         if (uri != null && V){
   1558             Log.v(TAG, " NEW URI " + uri.toString());
   1559         }
   1560 
   1561         values.clear();
   1562         values.put(Mms.Addr.CONTACT_ID, "null");
   1563         values.put(Mms.Addr.ADDRESS, to_address);
   1564         values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_TO);
   1565         values.put(Mms.Addr.CHARSET, 106);
   1566 
   1567         uri = Uri.parse(Mms.CONTENT_URI + "/"  + handle + "/addr");
   1568         uri = mResolver.insert(uri, values);
   1569         if (uri != null && V){
   1570             Log.v(TAG, " NEW URI " + uri.toString());
   1571         }
   1572         return handle;
   1573     }
   1574 
   1575 
   1576     private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException{
   1577         ContentValues values = new ContentValues();
   1578         values.put(Mms.Part.MSG_ID, handle);
   1579         if(part.mContentType != null) {
   1580             values.put(Mms.Part.CONTENT_TYPE, part.mContentType);
   1581         } else {
   1582             Log.w(TAG, "MMS has no CONTENT_TYPE for part " + count);
   1583         }
   1584         if(part.mContentId != null) {
   1585             values.put(Mms.Part.CONTENT_ID, part.mContentId);
   1586         } else {
   1587             if(part.mPartName != null) {
   1588                 values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">");
   1589             } else {
   1590                 values.put(Mms.Part.CONTENT_ID, "<part_" + count + ">");
   1591             }
   1592         }
   1593 
   1594         if(part.mContentLocation != null) {
   1595             values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
   1596         } else {
   1597             if(part.mPartName != null) {
   1598                 values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".dat");
   1599             } else {
   1600                 values.put(Mms.Part.CONTENT_LOCATION, "part_" + count + ".dat");
   1601             }
   1602         }
   1603         if(part.mContentDisposition != null)
   1604             values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
   1605         if(part.mPartName != null) {
   1606             values.put(Mms.Part.FILENAME, part.mPartName);
   1607             values.put(Mms.Part.NAME, part.mPartName);
   1608         } else {
   1609             /* We must set at least one part identifier */
   1610             values.put(Mms.Part.FILENAME, "part_" + count + ".dat");
   1611             values.put(Mms.Part.NAME, "part_" + count + ".dat");
   1612         }
   1613         Uri partUri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part");
   1614         Uri res = mResolver.insert(partUri, values);
   1615 
   1616         // Add data to part
   1617         OutputStream os = mResolver.openOutputStream(res);
   1618         os.write(part.mData);
   1619         os.close();
   1620     }
   1621 
   1622 
   1623     public void sendMessage(PushMsgInfo msgInfo, String msgBody) {
   1624 
   1625         SmsManager smsMng = SmsManager.getDefault();
   1626         ArrayList<String> parts = smsMng.divideMessage(msgBody);
   1627         msgInfo.parts = parts.size();
   1628         // We add a time stamp to differentiate delivery reports from each other for resent messages
   1629         msgInfo.timestamp = Calendar.getInstance().getTime().getTime();
   1630         msgInfo.partsDelivered = 0;
   1631         msgInfo.partsSent = 0;
   1632 
   1633         ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts);
   1634         ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts);
   1635 
   1636         /*       We handle the SENT intent in the MAP service, as this object
   1637          *       is destroyed at disconnect, hence if a disconnect occur while sending
   1638          *       a message, there is no intent handler to move the message from outbox
   1639          *       to the correct folder.
   1640          *       The correct solution would be to create a service that will start based on
   1641          *       the intent, if BT is turned off. */
   1642 
   1643         for (int i = 0; i < msgInfo.parts; i++) {
   1644             Intent intentDelivery, intentSent;
   1645 
   1646             intentDelivery = new Intent(ACTION_MESSAGE_DELIVERY, null);
   1647             /* Add msgId and part number to ensure the intents are different, and we
   1648              * thereby get an intent for each msg part.
   1649              * setType is needed to create different intents for each message id/ time stamp,
   1650              * as the extras are not used when comparing. */
   1651             intentDelivery.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i);
   1652             intentDelivery.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id);
   1653             intentDelivery.putExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, msgInfo.timestamp);
   1654             PendingIntent pendingIntentDelivery = PendingIntent.getBroadcast(mContext, 0,
   1655                     intentDelivery, PendingIntent.FLAG_UPDATE_CURRENT);
   1656 
   1657             intentSent = new Intent(ACTION_MESSAGE_SENT, null);
   1658             /* Add msgId and part number to ensure the intents are different, and we
   1659              * thereby get an intent for each msg part.
   1660              * setType is needed to create different intents for each message id/ time stamp,
   1661              * as the extras are not used when comparing. */
   1662             intentSent.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i);
   1663             intentSent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id);
   1664             intentSent.putExtra(EXTRA_MESSAGE_SENT_URI, msgInfo.uri.toString());
   1665             intentSent.putExtra(EXTRA_MESSAGE_SENT_RETRY, msgInfo.retry);
   1666             intentSent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, msgInfo.transparent);
   1667 
   1668             PendingIntent pendingIntentSent = PendingIntent.getBroadcast(mContext, 0,
   1669                     intentSent, PendingIntent.FLAG_UPDATE_CURRENT);
   1670 
   1671             // We use the same pending intent for all parts, but do not set the one shot flag.
   1672             deliveryIntents.add(pendingIntentDelivery);
   1673             sentIntents.add(pendingIntentSent);
   1674         }
   1675 
   1676         Log.d(TAG, "sendMessage to " + msgInfo.phone);
   1677 
   1678         smsMng.sendMultipartTextMessage(msgInfo.phone, null, parts, sentIntents,
   1679             deliveryIntents);
   1680     }
   1681 
   1682     private static final String ACTION_MESSAGE_DELIVERY =
   1683         "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY";
   1684     public static final String ACTION_MESSAGE_SENT =
   1685         "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT";
   1686 
   1687     public static final String EXTRA_MESSAGE_SENT_HANDLE = "HANDLE";
   1688     public static final String EXTRA_MESSAGE_SENT_RESULT = "result";
   1689     public static final String EXTRA_MESSAGE_SENT_URI = "uri";
   1690     public static final String EXTRA_MESSAGE_SENT_RETRY = "retry";
   1691     public static final String EXTRA_MESSAGE_SENT_TRANSPARENT = "transparent";
   1692     public static final String EXTRA_MESSAGE_SENT_TIMESTAMP = "timestamp";
   1693 
   1694     private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver();
   1695 
   1696     private boolean mInitialized = false;
   1697 
   1698     private class SmsBroadcastReceiver extends BroadcastReceiver {
   1699         private final String[] ID_PROJECTION = new String[] { Sms._ID };
   1700         private final Uri UPDATE_STATUS_URI = Uri.withAppendedPath(Sms.CONTENT_URI, "/status");
   1701 
   1702         public void register() {
   1703             Handler handler = new Handler(Looper.getMainLooper());
   1704 
   1705             IntentFilter intentFilter = new IntentFilter();
   1706             intentFilter.addAction(ACTION_MESSAGE_DELIVERY);
   1707             /* The reception of ACTION_MESSAGE_SENT have been moved to the MAP
   1708              * service, to be able to handle message sent events after a disconnect. */
   1709             //intentFilter.addAction(ACTION_MESSAGE_SENT);
   1710             try{
   1711                 intentFilter.addDataType("message/*");
   1712             } catch (MalformedMimeTypeException e) {
   1713                 Log.e(TAG, "Wrong mime type!!!", e);
   1714             }
   1715 
   1716             mContext.registerReceiver(this, intentFilter, null, handler);
   1717         }
   1718 
   1719         public void unregister() {
   1720             try {
   1721                 mContext.unregisterReceiver(this);
   1722             } catch (IllegalArgumentException e) {
   1723                 /* do nothing */
   1724             }
   1725         }
   1726 
   1727         @Override
   1728         public void onReceive(Context context, Intent intent) {
   1729             String action = intent.getAction();
   1730             long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1);
   1731             PushMsgInfo msgInfo = mPushMsgList.get(handle);
   1732 
   1733             Log.d(TAG, "onReceive: action"  + action);
   1734 
   1735             if (msgInfo == null) {
   1736                 Log.d(TAG, "onReceive: no msgInfo found for handle " + handle);
   1737                 return;
   1738             }
   1739 
   1740             if (action.equals(ACTION_MESSAGE_SENT)) {
   1741                 int result = intent.getIntExtra(EXTRA_MESSAGE_SENT_RESULT, Activity.RESULT_CANCELED);
   1742                 msgInfo.partsSent++;
   1743                 if(result != Activity.RESULT_OK) {
   1744                     // If just one of the parts in the message fails, we need to send the entire message again
   1745                     msgInfo.failedSent = true;
   1746                 }
   1747                 if(D) Log.d(TAG, "onReceive: msgInfo.partsSent = " + msgInfo.partsSent
   1748                         + ", msgInfo.parts = " + msgInfo.parts + " result = " + result);
   1749 
   1750                 if (msgInfo.partsSent == msgInfo.parts) {
   1751                     actionMessageSent(context, intent, msgInfo);
   1752                 }
   1753             } else if (action.equals(ACTION_MESSAGE_DELIVERY)) {
   1754                 long timestamp = intent.getLongExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, 0);
   1755                 int status = -1;
   1756                 if(msgInfo.timestamp == timestamp) {
   1757                     msgInfo.partsDelivered++;
   1758                     byte[] pdu = intent.getByteArrayExtra("pdu");
   1759                     String format = intent.getStringExtra("format");
   1760 
   1761                     SmsMessage message = SmsMessage.createFromPdu(pdu, format);
   1762                     if (message == null) {
   1763                         Log.d(TAG, "actionMessageDelivery: Can't get message from pdu");
   1764                         return;
   1765                     }
   1766                     status = message.getStatus();
   1767                     if(status != 0/*0 is success*/) {
   1768                         msgInfo.statusDelivered = status;
   1769                     }
   1770                 }
   1771                 if (msgInfo.partsDelivered == msgInfo.parts) {
   1772                     actionMessageDelivery(context, intent, msgInfo);
   1773                 }
   1774             } else {
   1775                 Log.d(TAG, "onReceive: Unknown action " + action);
   1776             }
   1777         }
   1778 
   1779         private void actionMessageSent(Context context, Intent intent, PushMsgInfo msgInfo) {
   1780             /* As the MESSAGE_SENT intent is forwarded from the MAP service, we use the intent
   1781              * to carry the result, as getResult() will not return the correct value.
   1782              */
   1783             boolean delete = false;
   1784 
   1785             if(D) Log.d(TAG,"actionMessageSent(): msgInfo.failedSent = " + msgInfo.failedSent);
   1786 
   1787             msgInfo.sendInProgress = false;
   1788 
   1789             if (msgInfo.failedSent == false) {
   1790                 if(D) Log.d(TAG, "actionMessageSent: result OK");
   1791                 if (msgInfo.transparent == 0) {
   1792                     if (!Sms.moveMessageToFolder(context, msgInfo.uri,
   1793                             Sms.MESSAGE_TYPE_SENT, 0)) {
   1794                         Log.w(TAG, "Failed to move " + msgInfo.uri + " to SENT");
   1795                     }
   1796                 } else {
   1797                     delete = true;
   1798                 }
   1799 
   1800                 Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msgInfo.id,
   1801                     folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
   1802                 sendEvent(evt);
   1803 
   1804             } else {
   1805                 if (msgInfo.retry == 1) {
   1806                     /* Notify failure, but keep message in outbox for resending */
   1807                     msgInfo.resend = true;
   1808                     msgInfo.partsSent = 0; // Reset counter for the retry
   1809                     msgInfo.failedSent = false;
   1810                     Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id,
   1811                         folderSms[Sms.MESSAGE_TYPE_OUTBOX], null, mSmsType);
   1812                     sendEvent(evt);
   1813                 } else {
   1814                     if (msgInfo.transparent == 0) {
   1815                         if (!Sms.moveMessageToFolder(context, msgInfo.uri,
   1816                                 Sms.MESSAGE_TYPE_FAILED, 0)) {
   1817                             Log.w(TAG, "Failed to move " + msgInfo.uri + " to FAILED");
   1818                         }
   1819                     } else {
   1820                         delete = true;
   1821                     }
   1822 
   1823                     Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id,
   1824                         folderSms[Sms.MESSAGE_TYPE_FAILED], null, mSmsType);
   1825                     sendEvent(evt);
   1826                 }
   1827             }
   1828 
   1829             if (delete == true) {
   1830                 /* Delete from Observer message list to avoid delete notifications */
   1831                 synchronized(mMsgListSms) {
   1832                     mMsgListSms.remove(msgInfo.id);
   1833                 }
   1834 
   1835                 /* Delete from DB */
   1836                 mResolver.delete(msgInfo.uri, null, null);
   1837             }
   1838         }
   1839 
   1840         private void actionMessageDelivery(Context context, Intent intent, PushMsgInfo msgInfo) {
   1841             Uri messageUri = intent.getData();
   1842             msgInfo.sendInProgress = false;
   1843 
   1844             Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null);
   1845 
   1846             try {
   1847                 if (cursor.moveToFirst()) {
   1848                     int messageId = cursor.getInt(0);
   1849 
   1850                     Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId);
   1851 
   1852                     if(D) Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + msgInfo.statusDelivered);
   1853 
   1854                     ContentValues contentValues = new ContentValues(2);
   1855 
   1856                     contentValues.put(Sms.STATUS, msgInfo.statusDelivered);
   1857                     contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis());
   1858                     mResolver.update(updateUri, contentValues, null, null);
   1859                 } else {
   1860                     Log.d(TAG, "Can't find message for status update: " + messageUri);
   1861                 }
   1862             } finally {
   1863                 cursor.close();
   1864             }
   1865 
   1866             if (msgInfo.statusDelivered == 0) {
   1867                 Event evt = new Event(EVENT_TYPE_DELEVERY_SUCCESS, msgInfo.id,
   1868                     folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
   1869                 sendEvent(evt);
   1870             } else {
   1871                 Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id,
   1872                     folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
   1873                 sendEvent(evt);
   1874             }
   1875 
   1876             mPushMsgList.remove(msgInfo.id);
   1877         }
   1878     }
   1879 
   1880     static public void actionMessageSentDisconnected(Context context, Intent intent, int result) {
   1881         boolean delete = false;
   1882         //int retry = intent.getIntExtra(EXTRA_MESSAGE_SENT_RETRY, 0);
   1883         int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0);
   1884         String uriString = intent.getStringExtra(EXTRA_MESSAGE_SENT_URI);
   1885         if(uriString == null) {
   1886             // Nothing we can do about it, just bail out
   1887             return;
   1888         }
   1889         Uri uri = Uri.parse(uriString);
   1890 
   1891         if (result == Activity.RESULT_OK) {
   1892             Log.d(TAG, "actionMessageSentDisconnected: result OK");
   1893             if (transparent == 0) {
   1894                 if (!Sms.moveMessageToFolder(context, uri,
   1895                         Sms.MESSAGE_TYPE_SENT, 0)) {
   1896                     Log.d(TAG, "Failed to move " + uri + " to SENT");
   1897                 }
   1898             } else {
   1899                 delete = true;
   1900             }
   1901         } else {
   1902             /*if (retry == 1) {
   1903                  The retry feature only works while connected, else we fail the send,
   1904                  * and move the message to failed, to let the user/app resend manually later.
   1905             } else */{
   1906                 if (transparent == 0) {
   1907                     if (!Sms.moveMessageToFolder(context, uri,
   1908                             Sms.MESSAGE_TYPE_FAILED, 0)) {
   1909                         Log.d(TAG, "Failed to move " + uri + " to FAILED");
   1910                     }
   1911                 } else {
   1912                     delete = true;
   1913                 }
   1914             }
   1915         }
   1916 
   1917         if (delete == true) {
   1918             /* Delete from DB */
   1919             ContentResolver resolver = context.getContentResolver();
   1920             if(resolver != null) {
   1921                 resolver.delete(uri, null, null);
   1922             } else {
   1923                 Log.w(TAG, "Unable to get resolver");
   1924             }
   1925         }
   1926     }
   1927 
   1928     private void registerPhoneServiceStateListener() {
   1929         TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
   1930         tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE);
   1931     }
   1932 
   1933     private void unRegisterPhoneServiceStateListener() {
   1934         TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
   1935         tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE);
   1936     }
   1937 
   1938     private void resendPendingMessages() {
   1939         /* Send pending messages in outbox */
   1940         String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
   1941         Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null);
   1942 
   1943         try {
   1944             while (c!= null && c.moveToNext()) {
   1945                 long id = c.getLong(c.getColumnIndex(Sms._ID));
   1946                 String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
   1947                 PushMsgInfo msgInfo = mPushMsgList.get(id);
   1948                 if (msgInfo == null || msgInfo.resend == false || msgInfo.sendInProgress == true) {
   1949                     continue;
   1950                 }
   1951                 msgInfo.sendInProgress = true;
   1952                 sendMessage(msgInfo, msgBody);
   1953             }
   1954         } finally {
   1955             close(c);
   1956         }
   1957     }
   1958 
   1959     private void failPendingMessages() {
   1960         /* Move pending messages from outbox to failed */
   1961         String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX;
   1962         Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null);
   1963         if (c == null) return;
   1964 
   1965         try {
   1966             while (c!= null && c.moveToNext()) {
   1967                 long id = c.getLong(c.getColumnIndex(Sms._ID));
   1968                 String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
   1969                 PushMsgInfo msgInfo = mPushMsgList.get(id);
   1970                 if (msgInfo == null || msgInfo.resend == false) {
   1971                     continue;
   1972                 }
   1973                 Sms.moveMessageToFolder(mContext, msgInfo.uri,
   1974                     Sms.MESSAGE_TYPE_FAILED, 0);
   1975             }
   1976         } finally {
   1977             close(c);
   1978         }
   1979     }
   1980 
   1981     private void removeDeletedMessages() {
   1982         /* Remove messages from virtual "deleted" folder (thread_id -1) */
   1983         mResolver.delete(Sms.CONTENT_URI,
   1984                 "thread_id = " + DELETED_THREAD_ID, null);
   1985     }
   1986 
   1987     private PhoneStateListener mPhoneListener = new PhoneStateListener() {
   1988         @Override
   1989         public void onServiceStateChanged(ServiceState serviceState) {
   1990             Log.d(TAG, "Phone service state change: " + serviceState.getState());
   1991             if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
   1992                 resendPendingMessages();
   1993             }
   1994         }
   1995     };
   1996 
   1997     public void init() {
   1998         mSmsBroadcastReceiver.register();
   1999         registerPhoneServiceStateListener();
   2000         mInitialized = true;
   2001     }
   2002 
   2003     public void deinit() {
   2004         mInitialized = false;
   2005         unregisterObserver();
   2006         mSmsBroadcastReceiver.unregister();
   2007         unRegisterPhoneServiceStateListener();
   2008         failPendingMessages();
   2009         removeDeletedMessages();
   2010     }
   2011 
   2012     public boolean handleSmsSendIntent(Context context, Intent intent){
   2013         if(mInitialized) {
   2014             mSmsBroadcastReceiver.onReceive(context, intent);
   2015             return true;
   2016         }
   2017         return false;
   2018     }
   2019 }
   2020