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