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