BluetoothMapContentObserver.java revision ed0c6ae1773ad1f4249fe3cf7447d7033195f222
1/* 2* Copyright (C) 2013 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*/ 15package com.android.bluetooth.map; 16 17import java.io.ByteArrayInputStream; 18import java.io.FileNotFoundException; 19import java.io.IOException; 20import java.io.OutputStream; 21import java.io.StringWriter; 22import java.io.UnsupportedEncodingException; 23import java.util.ArrayList; 24import java.util.Arrays; 25import java.util.Collections; 26import java.util.HashMap; 27import java.util.HashSet; 28import java.util.Map; 29import java.util.Set; 30 31import org.xmlpull.v1.XmlSerializer; 32 33import android.app.Activity; 34import android.app.PendingIntent; 35import android.content.BroadcastReceiver; 36import android.content.ContentResolver; 37import android.content.ContentUris; 38import android.content.ContentValues; 39import android.content.Context; 40import android.content.Intent; 41import android.content.IntentFilter; 42import android.database.ContentObserver; 43import android.database.Cursor; 44import android.net.Uri; 45import android.os.Handler; 46import android.provider.BaseColumns; 47import android.provider.Telephony; 48import android.provider.Telephony.Mms; 49import android.provider.Telephony.MmsSms; 50import android.provider.Telephony.Sms; 51import android.provider.Telephony.Sms.Inbox; 52import android.telephony.PhoneStateListener; 53import android.telephony.ServiceState; 54import android.telephony.SmsManager; 55import android.telephony.SmsMessage; 56import android.telephony.TelephonyManager; 57import android.util.Log; 58import android.util.Xml; 59 60import com.android.bluetooth.map.BluetoothMapUtils.TYPE; 61import com.android.bluetooth.map.BluetoothMapbMessageMmsEmail.MimePart; 62import com.google.android.mms.pdu.PduHeaders; 63 64public class BluetoothMapContentObserver { 65 private static final String TAG = "BluetoothMapContentObserver"; 66 67 private static final boolean D = false; 68 private static final boolean V = false; 69 70 private Context mContext; 71 private ContentResolver mResolver; 72 private BluetoothMnsObexClient mMnsClient; 73 private int mMasId; 74 75 public static final int DELETED_THREAD_ID = -1; 76 77 /* X-Mms-Message-Type field types. These are from PduHeaders.java */ 78 public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84; 79 80 private TYPE mSmsType; 81 82 static final String[] SMS_PROJECTION = new String[] { 83 BaseColumns._ID, 84 Sms.THREAD_ID, 85 Sms.ADDRESS, 86 Sms.BODY, 87 Sms.DATE, 88 Sms.READ, 89 Sms.TYPE, 90 Sms.STATUS, 91 Sms.LOCKED, 92 Sms.ERROR_CODE, 93 }; 94 95 static final String[] MMS_PROJECTION = new String[] { 96 BaseColumns._ID, 97 Mms.THREAD_ID, 98 Mms.MESSAGE_ID, 99 Mms.MESSAGE_SIZE, 100 Mms.SUBJECT, 101 Mms.CONTENT_TYPE, 102 Mms.TEXT_ONLY, 103 Mms.DATE, 104 Mms.DATE_SENT, 105 Mms.READ, 106 Mms.MESSAGE_BOX, 107 Mms.MESSAGE_TYPE, 108 Mms.STATUS, 109 }; 110 111 public BluetoothMapContentObserver(final Context context) { 112 mContext = context; 113 mResolver = mContext.getContentResolver(); 114 115 mSmsType = getSmsType(); 116 } 117 118 private TYPE getSmsType() { 119 TYPE smsType = null; 120 TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); 121 122 if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) { 123 smsType = TYPE.SMS_GSM; 124 } else if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) { 125 smsType = TYPE.SMS_CDMA; 126 } 127 128 return smsType; 129 } 130 131 private final ContentObserver mObserver = new ContentObserver(new Handler()) { 132 @Override 133 public void onChange(boolean selfChange) { 134 onChange(selfChange, null); 135 } 136 137 @Override 138 public void onChange(boolean selfChange, Uri uri) { 139 if (V) Log.d(TAG, "onChange on thread: " + Thread.currentThread().getId() 140 + " Uri: " + uri.toString() + " selfchange: " + selfChange); 141 142 handleMsgListChanges(); 143 } 144 }; 145 146 private static final String folderSms[] = { 147 "", 148 "inbox", 149 "sent", 150 "draft", 151 "outbox", 152 "outbox", 153 "outbox", 154 "inbox", 155 "inbox", 156 }; 157 158 private static final String folderMms[] = { 159 "", 160 "inbox", 161 "sent", 162 "draft", 163 "outbox", 164 }; 165 166 private class Event { 167 String eventType; 168 long handle; 169 String folder; 170 String oldFolder; 171 TYPE msgType; 172 173 public Event(String eventType, long handle, String folder, 174 String oldFolder, TYPE msgType) { 175 String PATH = "telecom/msg/"; 176 this.eventType = eventType; 177 this.handle = handle; 178 if (folder != null) { 179 this.folder = PATH + folder; 180 } else { 181 this.folder = null; 182 } 183 if (oldFolder != null) { 184 this.oldFolder = PATH + oldFolder; 185 } else { 186 this.oldFolder = null; 187 } 188 this.msgType = msgType; 189 } 190 191 public byte[] encode() throws UnsupportedEncodingException { 192 StringWriter sw = new StringWriter(); 193 XmlSerializer xmlEvtReport = Xml.newSerializer(); 194 try { 195 xmlEvtReport.setOutput(sw); 196 xmlEvtReport.startDocument(null, null); 197 xmlEvtReport.text("\n"); 198 xmlEvtReport.startTag("", "MAP-event-report"); 199 xmlEvtReport.attribute("", "version", "1.0"); 200 201 xmlEvtReport.startTag("", "event"); 202 xmlEvtReport.attribute("", "type", eventType); 203 xmlEvtReport.attribute("", "handle", BluetoothMapUtils.getMapHandle(handle, msgType)); 204 if (folder != null) { 205 xmlEvtReport.attribute("", "folder", folder); 206 } 207 if (oldFolder != null) { 208 xmlEvtReport.attribute("", "old_folder", oldFolder); 209 } 210 xmlEvtReport.attribute("", "msg_type", msgType.name()); 211 xmlEvtReport.endTag("", "event"); 212 213 xmlEvtReport.endTag("", "MAP-event-report"); 214 xmlEvtReport.endDocument(); 215 } catch (IllegalArgumentException e) { 216 e.printStackTrace(); 217 } catch (IllegalStateException e) { 218 e.printStackTrace(); 219 } catch (IOException e) { 220 e.printStackTrace(); 221 } 222 223 if (V) System.out.println(sw.toString()); 224 225 return sw.toString().getBytes("UTF-8"); 226 } 227 } 228 229 private class Msg { 230 long id; 231 int type; 232 233 public Msg(long id, int type) { 234 this.id = id; 235 this.type = type; 236 } 237 } 238 239 private Map<Long, Msg> mMsgListSms = 240 Collections.synchronizedMap(new HashMap<Long, Msg>()); 241 242 private Map<Long, Msg> mMsgListMms = 243 Collections.synchronizedMap(new HashMap<Long, Msg>()); 244 245 public void registerObserver(BluetoothMnsObexClient mns, int masId) { 246 if (V) Log.d(TAG, "registerObserver"); 247 /* Use MmsSms Uri since the Sms Uri is not notified on deletes */ 248 mMasId = masId; 249 mMnsClient = mns; 250 mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver); 251 initMsgList(); 252 } 253 254 public void unregisterObserver() { 255 if (V) Log.d(TAG, "unregisterObserver"); 256 mResolver.unregisterContentObserver(mObserver); 257 mMnsClient = null; 258 } 259 260 private void sendEvent(Event evt) { 261 Log.d(TAG, "sendEvent: " + evt.eventType + " " + evt.handle + " " 262 + evt.folder + " " + evt.oldFolder + " " + evt.msgType.name()); 263 264 if (mMnsClient == null) { 265 Log.d(TAG, "sendEvent: No MNS client registered - don't send event"); 266 return; 267 } 268 269 try { 270 mMnsClient.sendEvent(evt.encode(), mMasId); 271 } catch (UnsupportedEncodingException ex) { 272 /* do nothing */ 273 } 274 } 275 276 private void initMsgList() { 277 if (V) Log.d(TAG, "initMsgList"); 278 279 mMsgListSms.clear(); 280 mMsgListMms.clear(); 281 282 HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>(); 283 284 Cursor c = mResolver.query(Sms.CONTENT_URI, 285 SMS_PROJECTION, null, null, null); 286 287 if (c != null && c.moveToFirst()) { 288 do { 289 long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); 290 int type = c.getInt(c.getColumnIndex(Sms.TYPE)); 291 292 Msg msg = new Msg(id, type); 293 msgListSms.put(id, msg); 294 } while (c.moveToNext()); 295 c.close(); 296 } 297 298 mMsgListSms = msgListSms; 299 300 HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>(); 301 302 c = mResolver.query(Mms.CONTENT_URI, 303 MMS_PROJECTION, null, null, null); 304 305 if (c != null && c.moveToFirst()) { 306 do { 307 long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); 308 int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); 309 310 Msg msg = new Msg(id, type); 311 msgListMms.put(id, msg); 312 } while (c.moveToNext()); 313 c.close(); 314 } 315 316 mMsgListMms = msgListMms; 317 } 318 319 private void handleMsgListChangesSms() { 320 if (V) Log.d(TAG, "handleMsgListChangesSms"); 321 322 HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>(); 323 324 Cursor c = mResolver.query(Sms.CONTENT_URI, 325 SMS_PROJECTION, null, null, null); 326 327 synchronized(mMsgListSms) { 328 if (c != null && c.moveToFirst()) { 329 do { 330 long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); 331 int type = c.getInt(c.getColumnIndex(Sms.TYPE)); 332 333 Msg msg = mMsgListSms.remove(id); 334 335 if (msg == null) { 336 /* New message */ 337 msg = new Msg(id, type); 338 msgListSms.put(id, msg); 339 340 if (folderSms[type].equals("inbox")) { 341 Event evt = new Event("NewMessage", id, folderSms[type], 342 null, mSmsType); 343 sendEvent(evt); 344 } 345 } else { 346 /* Existing message */ 347 if (type != msg.type) { 348 Log.d(TAG, "new type: " + type + " old type: " + msg.type); 349 Event evt = new Event("MessageShift", id, folderSms[type], 350 folderSms[msg.type], mSmsType); 351 sendEvent(evt); 352 msg.type = type; 353 } 354 msgListSms.put(id, msg); 355 } 356 } while (c.moveToNext()); 357 c.close(); 358 } 359 360 for (Msg msg : mMsgListSms.values()) { 361 Event evt = new Event("MessageDeleted", msg.id, "deleted", 362 folderSms[msg.type], mSmsType); 363 sendEvent(evt); 364 } 365 366 mMsgListSms = msgListSms; 367 } 368 } 369 370 private void handleMsgListChangesMms() { 371 if (V) Log.d(TAG, "handleMsgListChangesMms"); 372 373 HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>(); 374 375 Cursor c = mResolver.query(Mms.CONTENT_URI, 376 MMS_PROJECTION, null, null, null); 377 378 synchronized(mMsgListMms) { 379 if (c != null && c.moveToFirst()) { 380 do { 381 long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); 382 int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); 383 int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE)); 384 385 Msg msg = mMsgListMms.remove(id); 386 387 if (msg == null) { 388 /* New message - only notify on retrieve conf */ 389 if (folderMms[type].equals("inbox") && 390 mtype != MESSAGE_TYPE_RETRIEVE_CONF) { 391 continue; 392 } 393 394 msg = new Msg(id, type); 395 msgListMms.put(id, msg); 396 397 if (folderMms[type].equals("inbox")) { 398 Event evt = new Event("NewMessage", id, folderMms[type], 399 null, TYPE.MMS); 400 sendEvent(evt); 401 } 402 } else { 403 /* Existing message */ 404 if (type != msg.type) { 405 Log.d(TAG, "new type: " + type + " old type: " + msg.type); 406 Event evt = new Event("MessageShift", id, folderMms[type], 407 folderMms[msg.type], TYPE.MMS); 408 sendEvent(evt); 409 msg.type = type; 410 411 if (folderMms[type].equals("sent")) { 412 evt = new Event("SendingSuccess", id, 413 folderSms[type], null, TYPE.MMS); 414 sendEvent(evt); 415 } 416 } 417 msgListMms.put(id, msg); 418 } 419 } while (c.moveToNext()); 420 c.close(); 421 } 422 423 for (Msg msg : mMsgListMms.values()) { 424 Event evt = new Event("MessageDeleted", msg.id, "deleted", 425 folderMms[msg.type], TYPE.MMS); 426 sendEvent(evt); 427 } 428 429 mMsgListMms = msgListMms; 430 } 431 } 432 433 private void handleMsgListChanges() { 434 handleMsgListChangesSms(); 435 handleMsgListChangesMms(); 436 } 437 438 private boolean deleteMessageMms(long handle) { 439 boolean res = false; 440 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); 441 Cursor c = mResolver.query(uri, null, null, null, null); 442 if (c != null && c.moveToFirst()) { 443 /* Move to deleted folder, or delete if already in deleted folder */ 444 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); 445 if (threadId != DELETED_THREAD_ID) { 446 /* Set deleted thread id */ 447 ContentValues contentValues = new ContentValues(); 448 contentValues.put(Mms.THREAD_ID, DELETED_THREAD_ID); 449 mResolver.update(uri, contentValues, null, null); 450 } else { 451 /* Delete from observer message list to avoid delete notifications */ 452 mMsgListMms.remove(handle); 453 /* Delete message */ 454 mResolver.delete(uri, null, null); 455 } 456 res = true; 457 } 458 if (c != null) { 459 c.close(); 460 } 461 return res; 462 } 463 464 private void updateThreadIdMms(Uri uri, long threadId) { 465 ContentValues contentValues = new ContentValues(); 466 contentValues.put(Mms.THREAD_ID, threadId); 467 mResolver.update(uri, contentValues, null, null); 468 } 469 470 private boolean unDeleteMessageMms(long handle) { 471 boolean res = false; 472 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); 473 Cursor c = mResolver.query(uri, null, null, null, null); 474 475 if (c != null && c.moveToFirst()) { 476 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); 477 if (threadId == DELETED_THREAD_ID) { 478 /* Restore thread id from address, or if no thread for address 479 * create new thread by insert and remove of fake message */ 480 String address; 481 long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); 482 int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); 483 if (msgBox == Mms.MESSAGE_BOX_INBOX) { 484 address = BluetoothMapContent.getAddressMms(mResolver, id, 485 BluetoothMapContent.MMS_FROM); 486 } else { 487 address = BluetoothMapContent.getAddressMms(mResolver, id, 488 BluetoothMapContent.MMS_TO); 489 } 490 Set<String> recipients = new HashSet<String>(); 491 recipients.addAll(Arrays.asList(address)); 492 updateThreadIdMms(uri, Telephony.Threads.getOrCreateThreadId(mContext, recipients)); 493 } else { 494 Log.d(TAG, "Message not in deleted folder: handle " + handle 495 + " threadId " + threadId); 496 } 497 res = true; 498 } 499 if (c != null) { 500 c.close(); 501 } 502 return res; 503 } 504 505 private boolean deleteMessageSms(long handle) { 506 boolean res = false; 507 Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); 508 Cursor c = mResolver.query(uri, null, null, null, null); 509 510 if (c != null && c.moveToFirst()) { 511 /* Move to deleted folder, or delete if already in deleted folder */ 512 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); 513 if (threadId != DELETED_THREAD_ID) { 514 /* Set deleted thread id */ 515 ContentValues contentValues = new ContentValues(); 516 contentValues.put(Sms.THREAD_ID, DELETED_THREAD_ID); 517 mResolver.update(uri, contentValues, null, null); 518 } else { 519 /* Delete from observer message list to avoid delete notifications */ 520 mMsgListSms.remove(handle); 521 /* Delete message */ 522 mResolver.delete(uri, null, null); 523 } 524 res = true; 525 } 526 if (c != null) { 527 c.close(); 528 } 529 return res; 530 } 531 532 private void updateThreadIdSms(Uri uri, long threadId) { 533 ContentValues contentValues = new ContentValues(); 534 contentValues.put(Sms.THREAD_ID, threadId); 535 mResolver.update(uri, contentValues, null, null); 536 } 537 538 private boolean unDeleteMessageSms(long handle) { 539 boolean res = false; 540 Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); 541 Cursor c = mResolver.query(uri, null, null, null, null); 542 543 if (c != null && c.moveToFirst()) { 544 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); 545 if (threadId == DELETED_THREAD_ID) { 546 String address = c.getString(c.getColumnIndex(Sms.ADDRESS)); 547 Set<String> recipients = new HashSet<String>(); 548 recipients.addAll(Arrays.asList(address)); 549 updateThreadIdSms(uri, Telephony.Threads.getOrCreateThreadId(mContext, recipients)); 550 } else { 551 Log.d(TAG, "Message not in deleted folder: handle " + handle 552 + " threadId " + threadId); 553 } 554 res = true; 555 } 556 if (c != null) { 557 c.close(); 558 } 559 return res; 560 } 561 562 public boolean setMessageStatusDeleted(long handle, TYPE type, int statusValue) { 563 boolean res = false; 564 if (D) Log.d(TAG, "setMessageStatusDeleted: handle " + handle 565 + " type " + type + " value " + statusValue); 566 567 if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) { 568 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { 569 res = deleteMessageSms(handle); 570 } else if (type == TYPE.MMS) { 571 res = deleteMessageMms(handle); 572 } 573 } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) { 574 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { 575 res = unDeleteMessageSms(handle); 576 } else if (type == TYPE.MMS) { 577 res = unDeleteMessageMms(handle); 578 } 579 } 580 return res; 581 } 582 583 public boolean setMessageStatusRead(long handle, TYPE type, int statusValue) { 584 boolean res = true; 585 586 if (D) Log.d(TAG, "setMessageStatusRead: handle " + handle 587 + " type " + type + " value " + statusValue); 588 589 /* Approved MAP spec errata 3445 states that read status initiated */ 590 /* by the MCE shall change the MSE read status. */ 591 592 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { 593 Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); 594 Cursor c = mResolver.query(uri, null, null, null, null); 595 596 ContentValues contentValues = new ContentValues(); 597 contentValues.put(Sms.READ, statusValue); 598 mResolver.update(uri, contentValues, null, null); 599 } else if (type == TYPE.MMS) { 600 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); 601 Cursor c = mResolver.query(uri, null, null, null, null); 602 603 ContentValues contentValues = new ContentValues(); 604 contentValues.put(Mms.READ, statusValue); 605 mResolver.update(uri, contentValues, null, null); 606 } 607 608 return res; 609 } 610 611 private class PushMsgInfo { 612 long id; 613 int transparent; 614 int retry; 615 String phone; 616 Uri uri; 617 int parts; 618 int partsSent; 619 int partsDelivered; 620 boolean resend; 621 622 public PushMsgInfo(long id, int transparent, 623 int retry, String phone, Uri uri) { 624 this.id = id; 625 this.transparent = transparent; 626 this.retry = retry; 627 this.phone = phone; 628 this.uri = uri; 629 this.resend = false; 630 }; 631 } 632 633 private Map<Long, PushMsgInfo> mPushMsgList = 634 Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>()); 635 636 public long pushMessage(BluetoothMapbMessage msg, String folder, 637 BluetoothMapAppParams ap) throws IllegalArgumentException { 638 if (D) Log.d(TAG, "pushMessage"); 639 ArrayList<BluetoothMapbMessage.vCard> recipientList = msg.getRecipients(); 640 int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ? 641 0 : ap.getTransparent(); 642 int retry = ap.getRetry(); 643 int charset = ap.getCharset(); 644 long handle = -1; 645 646 if (recipientList == null) { 647 Log.d(TAG, "empty recipient list"); 648 return -1; 649 } 650 651 for (BluetoothMapbMessage.vCard recipient : recipientList) { 652 if(recipient.getEnvLevel() == 0) // Only send the message to the top level recipient 653 { 654 /* Only send to first address */ 655 String phone = recipient.getFirstPhoneNumber(); 656 boolean read = false; 657 boolean deliveryReport = true; 658 659 switch(msg.getType()){ 660 case MMS: 661 { 662 /* Send message if folder is outbox */ 663 /* to do, support MMS in the future */ 664 /* 665 if (folder.equals("outbox")) { 666 handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMmsEmail)msg); 667 } 668 */ 669 break; 670 } 671 case SMS_GSM: //fall-through 672 case SMS_CDMA: 673 { 674 /* Add the message to the database */ 675 String msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody(); 676 Uri contentUri = Uri.parse("content://sms/" + folder); 677 Uri uri = Sms.addMessageToUri(mResolver, contentUri, phone, msgBody, 678 "", System.currentTimeMillis(), read, deliveryReport); 679 680 if (uri == null) { 681 Log.d(TAG, "pushMessage - failure on add to uri " + contentUri); 682 return -1; 683 } 684 685 handle = Long.parseLong(uri.getLastPathSegment()); 686 687 /* Send message if folder is outbox */ 688 if (folder.equals("outbox")) { 689 PushMsgInfo msgInfo = new PushMsgInfo(handle, transparent, 690 retry, phone, uri); 691 mPushMsgList.put(handle, msgInfo); 692 sendMessage(msgInfo, msgBody); 693 } 694 break; 695 } 696 case EMAIL: 697 { 698 break; 699 } 700 } 701 702 } 703 } 704 705 /* If multiple recipients return handle of last */ 706 return handle; 707 } 708 709 710 711 public long sendMmsMessage(String folder,String to_address, BluetoothMapbMessageMmsEmail msg) { 712 /* 713 *strategy: 714 *1) parse message into parts 715 *if folder is outbox/drafts: 716 *2) push message to draft 717 *if folder is outbox: 718 *3) move message to outbox (to trigger the mms app to add msg to pending_messages list) 719 *4) send intent to mms app in order to wake it up. 720 *else if folder !outbox: 721 *1) push message to folder 722 * */ 723 if (folder != null && (folder.equalsIgnoreCase("outbox")|| folder.equalsIgnoreCase("drafts"))) { 724 long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, to_address, msg); 725 /* if invalid handle (-1) then just return the handle - else continue sending (if folder is outbox) */ 726 if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase("outbox")) { 727 moveDraftToOutbox(handle); 728 729 Intent sendIntent = new Intent("android.intent.action.MMS_SEND_OUTBOX_MSG"); 730 Log.d(TAG, "broadcasting intent: "+sendIntent.toString()); 731 mContext.sendBroadcast(sendIntent); 732 } 733 return handle; 734 } else { 735 /* not allowed to push mms to anything but outbox/drafts */ 736 throw new IllegalArgumentException("Cannot push message to other folders than outbox/drafts"); 737 } 738 739 } 740 741 742 private void moveDraftToOutbox(long handle) { 743 ContentResolver contentResolver = mContext.getContentResolver(); 744 /*Move message by changing the msg_box value in the content provider database */ 745 if (handle != -1) { 746 String whereClause = " _id= " + handle; 747 Uri uri = Uri.parse("content://mms"); 748 Cursor queryResult = contentResolver.query(uri, null, whereClause, null, null); 749 if (queryResult != null) { 750 if (queryResult.getCount() > 0) { 751 queryResult.moveToFirst(); 752 ContentValues data = new ContentValues(); 753 /* set folder to be outbox */ 754 data.put("msg_box", Mms.MESSAGE_BOX_OUTBOX); 755 contentResolver.update(uri, data, whereClause, null); 756 Log.d(TAG, "moved draft MMS to outbox"); 757 } 758 queryResult.close(); 759 }else { 760 Log.d(TAG, "Could not move draft to outbox "); 761 } 762 } 763 } 764 private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMmsEmail msg) { 765 /** 766 * strategy: 767 * 1) parse msg into parts + header 768 * 2) create thread id (abuse the ease of adding an SMS to get id for thread) 769 * 3) push parts into content://mms/parts/ table 770 * 3) 771 */ 772 773 ContentValues values = new ContentValues(); 774 values.put("msg_box", folder); 775 776 values.put("read", 0); 777 values.put("seen", 0); 778 values.put("sub", msg.getSubject()); 779 values.put("sub_cs", 106); 780 values.put("ct_t", "application/vnd.wap.multipart.related"); 781 values.put("exp", 604800); 782 values.put("m_cls", PduHeaders.MESSAGE_CLASS_PERSONAL_STR); 783 values.put("m_type", PduHeaders.MESSAGE_TYPE_SEND_REQ); 784 values.put("v", PduHeaders.CURRENT_MMS_VERSION); 785 values.put("pri", PduHeaders.PRIORITY_NORMAL); 786 values.put("rr", PduHeaders.VALUE_NO); 787 values.put("tr_id", "T"+ Long.toHexString(System.currentTimeMillis())); 788 values.put("d_rpt", PduHeaders.VALUE_NO); 789 values.put("locked", 0); 790 if(msg.getTextOnly() == true) 791 values.put("text_only", true); 792 793 values.put("m_size", msg.getSize()); 794 795 // Get thread id 796 Set<String> recipients = new HashSet<String>(); 797 recipients.addAll(Arrays.asList(to_address)); 798 values.put("thread_id", Telephony.Threads.getOrCreateThreadId(mContext, recipients)); 799 Uri uri = Uri.parse("content://mms"); 800 801 ContentResolver cr = mContext.getContentResolver(); 802 uri = cr.insert(uri, values); 803 804 if (uri == null) { 805 // unable to insert MMS 806 Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri); 807 return -1; 808 } 809 810 long handle = Long.parseLong(uri.getLastPathSegment()); 811 if (V){ 812 Log.v(TAG, " NEW URI " + uri.toString()); 813 } 814 try { 815 if(V) Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base."); 816 for(MimePart part : msg.getMimeParts()) { 817 int count = 0; 818 count++; 819 values.clear(); 820 if(part.contentType != null && part.contentType.toUpperCase().contains("TEXT")) { 821 values.put("ct", "text/plain"); 822 values.put("chset", 106); 823 if(part.partName != null) { 824 values.put("fn", part.partName); 825 values.put("name", part.partName); 826 } else if(part.contentId == null && part.contentLocation == null) { 827 /* We must set at least one part identifier */ 828 values.put("fn", "text_" + count +".txt"); 829 values.put("name", "text_" + count +".txt"); 830 } 831 if(part.contentId != null) { 832 values.put("cid", part.contentId); 833 } 834 if(part.contentLocation != null) 835 values.put("cl", part.contentLocation); 836 if(part.contentDisposition != null) 837 values.put("cd", part.contentDisposition); 838 values.put("text", new String(part.data, "UTF-8")); 839 uri = Uri.parse("content://mms/" + handle + "/part"); 840 uri = cr.insert(uri, values); 841 if(V) Log.v(TAG, "Added TEXT part"); 842 843 } else if (part.contentType != null && part.contentType.toUpperCase().contains("SMIL")){ 844 845 values.put("seq", -1); 846 values.put("ct", "application/smil"); 847 if(part.contentId != null) 848 values.put("cid", part.contentId); 849 if(part.contentLocation != null) 850 values.put("cl", part.contentLocation); 851 if(part.contentDisposition != null) 852 values.put("cd", part.contentDisposition); 853 values.put("fn", "smil.xml"); 854 values.put("name", "smil.xml"); 855 values.put("text", new String(part.data, "UTF-8")); 856 857 uri = Uri.parse("content://mms/" + handle + "/part"); 858 uri = cr.insert(uri, values); 859 if(V) Log.v(TAG, "Added SMIL part"); 860 861 }else /*VIDEO/AUDIO/IMAGE*/ { 862 writeMmsDataPart(handle, part, count); 863 if(V) Log.v(TAG, "Added OTHER part"); 864 } 865 if (uri != null && V){ 866 Log.v(TAG, "Added part with content-type: "+ part.contentType + " to Uri: " + uri.toString()); 867 } 868 } 869 } catch (UnsupportedEncodingException e) { 870 Log.w(TAG, e); 871 } catch (IOException e) { 872 Log.w(TAG, e); 873 } 874 875 values.clear(); 876 values.put("contact_id", "null"); 877 values.put("address", "insert-address-token"); 878 values.put("type", BluetoothMapContent.MMS_FROM); 879 values.put("charset", 106); 880 881 uri = Uri.parse("content://mms/" + handle + "/addr"); 882 uri = cr.insert(uri, values); 883 if (uri != null && V){ 884 Log.v(TAG, " NEW URI " + uri.toString()); 885 } 886 887 values.clear(); 888 values.put("contact_id", "null"); 889 values.put("address", to_address); 890 values.put("type", BluetoothMapContent.MMS_TO); 891 values.put("charset", 106); 892 893 uri = Uri.parse("content://mms/" + handle + "/addr"); 894 uri = cr.insert(uri, values); 895 if (uri != null && V){ 896 Log.v(TAG, " NEW URI " + uri.toString()); 897 } 898 return handle; 899 } 900 901 902 private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException{ 903 ContentValues values = new ContentValues(); 904 values.put("mid", handle); 905 if(part.contentType != null){ 906 //Remove last char if ';' from contentType 907 if(part.contentType.charAt(part.contentType.length() - 1) == ';') { 908 part.contentType = part.contentType.substring(0,part.contentType.length() -1); 909 } 910 values.put("ct", part.contentType); 911 } 912 if(part.contentId != null) 913 values.put("cid", part.contentId); 914 if(part.contentLocation != null) 915 values.put("cl", part.contentLocation); 916 if(part.contentDisposition != null) 917 values.put("cd", part.contentDisposition); 918 if(part.partName != null) { 919 values.put("fn", part.partName); 920 values.put("name", part.partName); 921 } else if(part.contentId == null && part.contentLocation == null) { 922 /* We must set at least one part identifier */ 923 values.put("fn", "part_" + count + ".dat"); 924 values.put("name", "part_" + count + ".dat"); 925 } 926 Uri partUri = Uri.parse("content://mms/" + handle + "/part"); 927 Uri res = mResolver.insert(partUri, values); 928 929 // Add data to part 930 OutputStream os = mResolver.openOutputStream(res); 931 os.write(part.data); 932 os.close(); 933 } 934 935 936 public void sendMessage(PushMsgInfo msgInfo, String msgBody) { 937 938 SmsManager smsMng = SmsManager.getDefault(); 939 ArrayList<String> parts = smsMng.divideMessage(msgBody); 940 msgInfo.parts = parts.size(); 941 942 ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts); 943 ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts); 944 945 for (int i = 0; i < msgInfo.parts; i++) { 946 Intent intent; 947 intent = new Intent(ACTION_MESSAGE_DELIVERY, null); 948 intent.putExtra("HANDLE", msgInfo.id); 949 deliveryIntents.add(PendingIntent.getBroadcast(mContext, 0, intent, 950 PendingIntent.FLAG_UPDATE_CURRENT)); 951 952 intent = new Intent(ACTION_MESSAGE_SENT, null); 953 intent.putExtra("HANDLE", msgInfo.id); 954 sentIntents.add(PendingIntent.getBroadcast(mContext, 0, intent, 955 PendingIntent.FLAG_UPDATE_CURRENT)); 956 } 957 958 Log.d(TAG, "sendMessage to " + msgInfo.phone); 959 960 smsMng.sendMultipartTextMessage(msgInfo.phone, null, parts, sentIntents, 961 deliveryIntents); 962 } 963 964 private static final String ACTION_MESSAGE_DELIVERY = 965 "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY"; 966 private static final String ACTION_MESSAGE_SENT = 967 "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT"; 968 969 private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver(); 970 971 private class SmsBroadcastReceiver extends BroadcastReceiver { 972 private final String[] ID_PROJECTION = new String[] { Sms._ID }; 973 private final Uri UPDATE_STATUS_URI = Uri.parse("content://sms/status"); 974 975 public void register() { 976 Handler handler = new Handler(); 977 978 IntentFilter intentFilter = new IntentFilter(); 979 intentFilter.addAction(ACTION_MESSAGE_DELIVERY); 980 intentFilter.addAction(ACTION_MESSAGE_SENT); 981 mContext.registerReceiver(this, intentFilter, null, handler); 982 } 983 984 public void unregister() { 985 try { 986 mContext.unregisterReceiver(this); 987 } catch (IllegalArgumentException e) { 988 /* do nothing */ 989 } 990 } 991 992 @Override 993 public void onReceive(Context context, Intent intent) { 994 String action = intent.getAction(); 995 long handle = intent.getLongExtra("HANDLE", -1); 996 PushMsgInfo msgInfo = mPushMsgList.get(handle); 997 998 Log.d(TAG, "onReceive: action" + action); 999 1000 if (msgInfo == null) { 1001 Log.d(TAG, "onReceive: no msgInfo found for handle " + handle); 1002 return; 1003 } 1004 1005 if (action.equals(ACTION_MESSAGE_SENT)) { 1006 msgInfo.partsSent++; 1007 if (msgInfo.partsSent == msgInfo.parts) { 1008 actionMessageSent(context, intent, msgInfo); 1009 } 1010 } else if (action.equals(ACTION_MESSAGE_DELIVERY)) { 1011 msgInfo.partsDelivered++; 1012 if (msgInfo.partsDelivered == msgInfo.parts) { 1013 actionMessageDelivery(context, intent, msgInfo); 1014 } 1015 } else { 1016 Log.d(TAG, "onReceive: Unknown action " + action); 1017 } 1018 } 1019 1020 private void actionMessageSent(Context context, Intent intent, 1021 PushMsgInfo msgInfo) { 1022 int result = getResultCode(); 1023 boolean delete = false; 1024 1025 if (result == Activity.RESULT_OK) { 1026 Log.d(TAG, "actionMessageSent: result OK"); 1027 if (msgInfo.transparent == 0) { 1028 if (!Sms.moveMessageToFolder(context, msgInfo.uri, 1029 Sms.MESSAGE_TYPE_SENT, 0)) { 1030 Log.d(TAG, "Failed to move " + msgInfo.uri + " to SENT"); 1031 } 1032 } else { 1033 delete = true; 1034 } 1035 1036 Event evt = new Event("SendingSuccess", msgInfo.id, 1037 folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType); 1038 sendEvent(evt); 1039 1040 } else { 1041 if (msgInfo.retry == 1) { 1042 /* Notify failure, but keep message in outbox for resending */ 1043 msgInfo.resend = true; 1044 Event evt = new Event("SendingFailure", msgInfo.id, 1045 folderSms[Sms.MESSAGE_TYPE_OUTBOX], null, mSmsType); 1046 sendEvent(evt); 1047 } else { 1048 if (msgInfo.transparent == 0) { 1049 if (!Sms.moveMessageToFolder(context, msgInfo.uri, 1050 Sms.MESSAGE_TYPE_FAILED, 0)) { 1051 Log.d(TAG, "Failed to move " + msgInfo.uri + " to FAILED"); 1052 } 1053 } else { 1054 delete = true; 1055 } 1056 1057 Event evt = new Event("SendingFailure", msgInfo.id, 1058 folderSms[Sms.MESSAGE_TYPE_FAILED], null, mSmsType); 1059 sendEvent(evt); 1060 } 1061 } 1062 1063 if (delete == true) { 1064 /* Delete from Observer message list to avoid delete notifications */ 1065 mMsgListSms.remove(msgInfo.id); 1066 1067 /* Delete from DB */ 1068 mResolver.delete(msgInfo.uri, null, null); 1069 } 1070 } 1071 1072 private void actionMessageDelivery(Context context, Intent intent, 1073 PushMsgInfo msgInfo) { 1074 Uri messageUri = intent.getData(); 1075 byte[] pdu = intent.getByteArrayExtra("pdu"); 1076 String format = intent.getStringExtra("format"); 1077 1078 SmsMessage message = SmsMessage.createFromPdu(pdu, format); 1079 if (message == null) { 1080 Log.d(TAG, "actionMessageDelivery: Can't get message from pdu"); 1081 return; 1082 } 1083 int status = message.getStatus(); 1084 1085 Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null); 1086 1087 try { 1088 if (cursor.moveToFirst()) { 1089 int messageId = cursor.getInt(0); 1090 1091 Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId); 1092 boolean isStatusReport = message.isStatusReportMessage(); 1093 1094 Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + status + 1095 ", isStatusReport=" + isStatusReport); 1096 1097 ContentValues contentValues = new ContentValues(2); 1098 1099 contentValues.put(Sms.STATUS, status); 1100 contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis()); 1101 mResolver.update(updateUri, contentValues, null, null); 1102 } else { 1103 Log.d(TAG, "Can't find message for status update: " + messageUri); 1104 } 1105 } finally { 1106 cursor.close(); 1107 } 1108 1109 if (status == 0) { 1110 Event evt = new Event("DeliverySuccess", msgInfo.id, 1111 folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType); 1112 sendEvent(evt); 1113 } else { 1114 Event evt = new Event("DeliveryFailure", msgInfo.id, 1115 folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType); 1116 sendEvent(evt); 1117 } 1118 1119 mPushMsgList.remove(msgInfo.id); 1120 } 1121 } 1122 1123 private void registerPhoneServiceStateListener() { 1124 TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); 1125 tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE); 1126 } 1127 1128 private void unRegisterPhoneServiceStateListener() { 1129 TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); 1130 tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE); 1131 } 1132 1133 private void resendPendingMessages() { 1134 /* Send pending messages in outbox */ 1135 String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX; 1136 Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, 1137 null); 1138 1139 if (c != null && c.moveToFirst()) { 1140 do { 1141 long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); 1142 String msgBody = c.getString(c.getColumnIndex(Sms.BODY)); 1143 PushMsgInfo msgInfo = mPushMsgList.get(id); 1144 if (msgInfo == null || msgInfo.resend == false) { 1145 continue; 1146 } 1147 sendMessage(msgInfo, msgBody); 1148 } while (c.moveToNext()); 1149 c.close(); 1150 } 1151 } 1152 1153 private void failPendingMessages() { 1154 /* Move pending messages from outbox to failed */ 1155 String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX; 1156 Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, 1157 null); 1158 1159 if (c != null && c.moveToFirst()) { 1160 do { 1161 long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); 1162 String msgBody = c.getString(c.getColumnIndex(Sms.BODY)); 1163 PushMsgInfo msgInfo = mPushMsgList.get(id); 1164 if (msgInfo == null || msgInfo.resend == false) { 1165 continue; 1166 } 1167 Sms.moveMessageToFolder(mContext, msgInfo.uri, 1168 Sms.MESSAGE_TYPE_FAILED, 0); 1169 } while (c.moveToNext()); 1170 } 1171 if (c != null) c.close(); 1172 } 1173 1174 private void removeDeletedMessages() { 1175 /* Remove messages from virtual "deleted" folder (thread_id -1) */ 1176 mResolver.delete(Uri.parse("content://sms/"), 1177 "thread_id = " + DELETED_THREAD_ID, null); 1178 } 1179 1180 private PhoneStateListener mPhoneListener = new PhoneStateListener() { 1181 @Override 1182 public void onServiceStateChanged(ServiceState serviceState) { 1183 Log.d(TAG, "Phone service state change: " + serviceState.getState()); 1184 if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) { 1185 resendPendingMessages(); 1186 } 1187 } 1188 }; 1189 1190 public void init() { 1191 mSmsBroadcastReceiver.register(); 1192 registerPhoneServiceStateListener(); 1193 } 1194 1195 public void deinit() { 1196 mSmsBroadcastReceiver.unregister(); 1197 unRegisterPhoneServiceStateListener(); 1198 failPendingMessages(); 1199 removeDeletedMessages(); 1200 } 1201} 1202