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