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