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