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