BluetoothMapContentObserver.java revision 5a60e47497f21f64e6d79420dc4c56c1907df22a
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 */ 15package com.android.bluetooth.map; 16 17import android.annotation.TargetApi; 18import android.app.Activity; 19import android.app.PendingIntent; 20import android.content.BroadcastReceiver; 21import android.content.ContentProviderClient; 22import android.content.ContentResolver; 23import android.content.ContentUris; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.Intent; 27import android.content.IntentFilter; 28import android.content.IntentFilter.MalformedMimeTypeException; 29import android.database.ContentObserver; 30import android.database.Cursor; 31import android.net.Uri; 32import android.os.Handler; 33import android.os.Looper; 34import android.os.Message; 35import android.os.ParcelFileDescriptor; 36import android.os.RemoteException; 37import android.provider.Telephony; 38import android.provider.Telephony.Mms; 39import android.provider.Telephony.MmsSms; 40import android.provider.Telephony.Sms; 41import android.provider.Telephony.Sms.Inbox; 42import android.telephony.PhoneStateListener; 43import android.telephony.ServiceState; 44import android.telephony.SmsManager; 45import android.telephony.SmsMessage; 46import android.telephony.TelephonyManager; 47import android.text.format.DateUtils; 48import android.util.Log; 49import android.util.Xml; 50 51import org.xmlpull.v1.XmlSerializer; 52 53import com.android.bluetooth.map.BluetoothMapUtils.TYPE; 54import com.android.bluetooth.map.BluetoothMapbMessageMime.MimePart; 55import com.android.bluetooth.mapapi.BluetoothMapContract; 56import com.android.bluetooth.mapapi.BluetoothMapContract.MessageColumns; 57import com.google.android.mms.pdu.PduHeaders; 58 59import java.io.FileNotFoundException; 60import java.io.FileOutputStream; 61import java.io.IOException; 62import java.io.OutputStream; 63import java.io.StringWriter; 64import java.io.UnsupportedEncodingException; 65import java.util.ArrayList; 66import java.util.Arrays; 67import java.util.Calendar; 68import java.util.Collections; 69import java.util.HashMap; 70import java.util.HashSet; 71import java.util.Map; 72import java.util.Set; 73 74import javax.obex.ResponseCodes; 75 76@TargetApi(19) 77public 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_NEW = "NewMessage"; 84 private static final String EVENT_TYPE_DELETE = "MessageDeleted"; 85 private static final String EVENT_TYPE_REMOVED = "MessageRemoved"; 86 private static final String EVENT_TYPE_SHIFT = "MessageShift"; 87 private static final String EVENT_TYPE_DELEVERY_SUCCESS = "DeliverySuccess"; 88 private static final String EVENT_TYPE_SENDING_SUCCESS = "SendingSuccess"; 89 private static final String EVENT_TYPE_SENDING_FAILURE = "SendingFailure"; 90 private static final String EVENT_TYPE_DELIVERY_FAILURE = "DeliveryFailure"; 91 private static final String EVENT_TYPE_READ_STATUS = "ReadStatusChanged"; 92 private static final String EVENT_TYPE_CONVERSATION = "ConversationChanged"; 93 private static final String EVENT_TYPE_PRESENCE = "ParticipantPresenceChanged"; 94 private static final String EVENT_TYPE_CHAT_STATE = "ParticipantChatStateChanged"; 95 96 private static final long EVENT_FILTER_NEW_MESSAGE = 1L; 97 private static final long EVENT_FILTER_MESSAGE_DELETED = 1L<<1; 98 private static final long EVENT_FILTER_MESSAGE_SHIFT = 1L<<2; 99 private static final long EVENT_FILTER_SENDING_SUCCESS = 1L<<3; 100 private static final long EVENT_FILTER_SENDING_FAILED = 1L<<4; 101 private static final long EVENT_FILTER_DELIVERY_SUCCESS = 1L<<5; 102 private static final long EVENT_FILTER_DELIVERY_FAILED = 1L<<6; 103 private static final long EVENT_FILTER_MEMORY_FULL = 1L<<7; // Unused 104 private static final long EVENT_FILTER_MEMORY_AVAILABLE = 1L<<8; // Unused 105 private static final long EVENT_FILTER_READ_STATUS_CHANGED = 1L<<9; 106 private static final long EVENT_FILTER_CONVERSATION_CHANGED = 1L<<10; 107 private static final long EVENT_FILTER_PARTICIPANT_PRESENCE_CHANGED = 1L<<11; 108 private static final long EVENT_FILTER_PARTICIPANT_CHATSTATE_CHANGED= 1L<<12; 109 private static final long EVENT_FILTER_MESSAGE_REMOVED = 1L<<13; 110 111 // TODO: If we are requesting a large message from the network, on a slow connection 112 // 20 seconds might not be enough... But then again 20 seconds is long for other 113 // cases. 114 private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS; 115 116 private Context mContext; 117 private ContentResolver mResolver; 118 private ContentProviderClient mProviderClient = null; 119 private BluetoothMnsObexClient mMnsClient; 120 private BluetoothMapMasInstance mMasInstance = null; 121 private int mMasId; 122 private boolean mEnableSmsMms = false; 123 private boolean mObserverRegistered = false; 124 private BluetoothMapAccountItem mAccount; 125 private String mAuthority = null; 126 127 // Default supported feature bit mask is 0x1f 128 private int mMapSupportedFeatures = BluetoothMapUtils.MAP_FEATURE_DEFAULT_BITMASK; 129 // Default event report version is 1.0 130 private int mMapEventReportVersion = BluetoothMapUtils.MAP_EVENT_REPORT_V10; 131 132 private BluetoothMapFolderElement mFolders = 133 new BluetoothMapFolderElement("DUMMY", null); // Will be set by the MAS when generated. 134 private Uri mMessageUri = null; 135 private Uri mContactUri = null; 136 137 private boolean mTransmitEvents = true; 138 139 /* To make the filter update atomic, we declare it volatile. 140 * To avoid a penalty when using it, copy the value to a local 141 * non-volatile variable when used more than once. 142 * Actually we only ever use the lower 4 bytes of this variable, 143 * hence we could manage without the volatile keyword, but as 144 * we tend to copy ways of doing things, we better do it right:-) */ 145 private volatile long mEventFilter = 0xFFFFFFFFL; 146 147 public static final int DELETED_THREAD_ID = -1; 148 149 // X-Mms-Message-Type field types. These are from PduHeaders.java 150 public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84; 151 152 // Text only MMS converted to SMS if sms parts less than or equal to defined count 153 private static final int CONVERT_MMS_TO_SMS_PART_COUNT = 10; 154 155 private TYPE mSmsType; 156 157 static final String[] SMS_PROJECTION = new String[] { 158 Sms._ID, 159 Sms.THREAD_ID, 160 Sms.ADDRESS, 161 Sms.BODY, 162 Sms.DATE, 163 Sms.READ, 164 Sms.TYPE, 165 Sms.STATUS, 166 Sms.LOCKED, 167 Sms.ERROR_CODE 168 }; 169 170 static final String[] SMS_PROJECTION_SHORT = new String[] { 171 Sms._ID, 172 Sms.THREAD_ID, 173 Sms.TYPE, 174 Sms.READ 175 }; 176 177 static final String[] SMS_PROJECTION_SHORT_EXT = new String[] { 178 Sms._ID, 179 Sms.THREAD_ID, 180 Sms.ADDRESS, 181 Sms.BODY, 182 Sms.DATE, 183 Sms.READ, 184 Sms.TYPE, 185 }; 186 187 static final String[] MMS_PROJECTION_SHORT = new String[] { 188 Mms._ID, 189 Mms.THREAD_ID, 190 Mms.MESSAGE_TYPE, 191 Mms.MESSAGE_BOX, 192 Mms.READ 193 }; 194 195 static final String[] MMS_PROJECTION_SHORT_EXT = new String[] { 196 Mms._ID, 197 Mms.THREAD_ID, 198 Mms.MESSAGE_TYPE, 199 Mms.MESSAGE_BOX, 200 Mms.READ, 201 Mms.DATE, 202 Mms.SUBJECT, 203 Mms.PRIORITY 204 }; 205 206 static final String[] MSG_PROJECTION_SHORT = new String[] { 207 BluetoothMapContract.MessageColumns._ID, 208 BluetoothMapContract.MessageColumns.FOLDER_ID, 209 BluetoothMapContract.MessageColumns.FLAG_READ 210 }; 211 212 static final String[] MSG_PROJECTION_SHORT_EXT = new String[] { 213 BluetoothMapContract.MessageColumns._ID, 214 BluetoothMapContract.MessageColumns.FOLDER_ID, 215 BluetoothMapContract.MessageColumns.FLAG_READ, 216 BluetoothMapContract.MessageColumns.DATE, 217 BluetoothMapContract.MessageColumns.SUBJECT, 218 BluetoothMapContract.MessageColumns.FROM_LIST, 219 BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY 220 }; 221 222 static final String[] MSG_PROJECTION_SHORT_EXT2 = new String[] { 223 BluetoothMapContract.MessageColumns._ID, 224 BluetoothMapContract.MessageColumns.FOLDER_ID, 225 BluetoothMapContract.MessageColumns.FLAG_READ, 226 BluetoothMapContract.MessageColumns.DATE, 227 BluetoothMapContract.MessageColumns.SUBJECT, 228 BluetoothMapContract.MessageColumns.FROM_LIST, 229 BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY, 230 BluetoothMapContract.MessageColumns.THREAD_ID, 231 BluetoothMapContract.MessageColumns.THREAD_NAME 232 }; 233 234 public BluetoothMapContentObserver(final Context context, 235 BluetoothMnsObexClient mnsClient, 236 BluetoothMapMasInstance masInstance, 237 BluetoothMapAccountItem account, 238 boolean enableSmsMms) throws RemoteException { 239 mContext = context; 240 mResolver = mContext.getContentResolver(); 241 mAccount = account; 242 mMasInstance = masInstance; 243 mMasId = mMasInstance.getMasId(); 244 245 mMapSupportedFeatures = mMasInstance.getRemoteFeatureMask(); 246 if (D) Log.d(TAG, "BluetoothMapContentObserver: Supported features " + 247 Integer.toHexString(mMapSupportedFeatures) ) ; 248 249 if((BluetoothMapUtils.MAP_FEATURE_EXTENDED_EVENT_REPORT_11_BIT 250 & mMapSupportedFeatures) != 0){ 251 mMapEventReportVersion = BluetoothMapUtils.MAP_EVENT_REPORT_V11; 252 } 253 // Make sure support for all formats result in latest version returned 254 if((BluetoothMapUtils.MAP_FEATURE_EVENT_REPORT_V12_BIT 255 & mMapSupportedFeatures) != 0){ 256 mMapEventReportVersion = BluetoothMapUtils.MAP_EVENT_REPORT_V12; 257 } 258 259 if(account != null) { 260 mAuthority = Uri.parse(account.mBase_uri).getAuthority(); 261 mMessageUri = Uri.parse(account.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE); 262 if (mAccount.getType() == TYPE.IM) { 263 mContactUri = Uri.parse(account.mBase_uri + "/" 264 + BluetoothMapContract.TABLE_CONVOCONTACT); 265 } 266 // TODO: We need to release this again! 267 mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority); 268 if (mProviderClient == null) { 269 throw new RemoteException("Failed to acquire provider for " + mAuthority); 270 } 271 mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT); 272 mContactList = mMasInstance.getContactList(); 273 if(mContactList == null) { 274 setContactList(new HashMap<String, BluetoothMapConvoContactElement>(), false); 275 initContactsList(); 276 } 277 } 278 mEnableSmsMms = enableSmsMms; 279 mSmsType = getSmsType(); 280 mMnsClient = mnsClient; 281 /* Get the cached list - if any, else create */ 282 mMsgListSms = mMasInstance.getMsgListSms(); 283 boolean doInit = false; 284 if(mEnableSmsMms) { 285 if(mMsgListSms == null) { 286 setMsgListSms(new HashMap<Long, Msg>(), false); 287 doInit = true; 288 } 289 mMsgListMms = mMasInstance.getMsgListMms(); 290 if(mMsgListMms == null) { 291 setMsgListMms(new HashMap<Long, Msg>(), false); 292 doInit = true; 293 } 294 } 295 if(mAccount != null) { 296 mMsgListMsg = mMasInstance.getMsgListMsg(); 297 if(mMsgListMsg == null) { 298 setMsgListMsg(new HashMap<Long, Msg>(), false); 299 doInit = true; 300 } 301 } 302 if(doInit) { 303 initMsgList(); 304 } 305 } 306 307 308 private Map<Long, Msg> getMsgListSms() { 309 return mMsgListSms; 310 } 311 312 313 private void setMsgListSms(Map<Long, Msg> msgListSms, boolean changesDetected) { 314 mMsgListSms = msgListSms; 315 if(changesDetected) { 316 mMasInstance.updateFolderVersionCounter(); 317 } 318 mMasInstance.setMsgListSms(msgListSms); 319 } 320 321 322 private Map<Long, Msg> getMsgListMms() { 323 return mMsgListMms; 324 } 325 326 327 private void setMsgListMms(Map<Long, Msg> msgListMms, boolean changesDetected) { 328 mMsgListMms = msgListMms; 329 if(changesDetected) { 330 mMasInstance.updateFolderVersionCounter(); 331 } 332 mMasInstance.setMsgListMms(msgListMms); 333 } 334 335 336 private Map<Long, Msg> getMsgListMsg() { 337 return mMsgListMsg; 338 } 339 340 341 private void setMsgListMsg(Map<Long, Msg> msgListMsg, boolean changesDetected) { 342 mMsgListMsg = msgListMsg; 343 if(changesDetected) { 344 mMasInstance.updateFolderVersionCounter(); 345 } 346 mMasInstance.setMsgListMsg(msgListMsg); 347 } 348 349 private Map<String, BluetoothMapConvoContactElement> getContactList() { 350 return mContactList; 351 } 352 353 354 /** 355 * Currently we only have data for IM / email contacts 356 * @param contactList 357 * @param changesDetected that is not chat state changed nor presence state changed. 358 */ 359 private void setContactList(Map<String, BluetoothMapConvoContactElement> contactList, 360 boolean changesDetected) { 361 mContactList = contactList; 362 if(changesDetected) { 363 mMasInstance.updateImEmailConvoListVersionCounter(); 364 } 365 mMasInstance.setContactList(contactList); 366 } 367 368 private static boolean sendEventNewMessage(long eventFilter) { 369 return ((eventFilter & EVENT_FILTER_NEW_MESSAGE) > 0); 370 } 371 372 private static boolean sendEventMessageDeleted(long eventFilter) { 373 return ((eventFilter & EVENT_FILTER_MESSAGE_DELETED) > 0); 374 } 375 376 private static boolean sendEventMessageShift(long eventFilter) { 377 return ((eventFilter & EVENT_FILTER_MESSAGE_SHIFT) > 0); 378 } 379 380 private static boolean sendEventSendingSuccess(long eventFilter) { 381 return ((eventFilter & EVENT_FILTER_SENDING_SUCCESS) > 0); 382 } 383 384 private static boolean sendEventSendingFailed(long eventFilter) { 385 return ((eventFilter & EVENT_FILTER_SENDING_FAILED) > 0); 386 } 387 388 private static boolean sendEventDeliverySuccess(long eventFilter) { 389 return ((eventFilter & EVENT_FILTER_DELIVERY_SUCCESS) > 0); 390 } 391 392 private static boolean sendEventDeliveryFailed(long eventFilter) { 393 return ((eventFilter & EVENT_FILTER_DELIVERY_FAILED) > 0); 394 } 395 396 private static boolean sendEventReadStatusChanged(long eventFilter) { 397 return ((eventFilter & EVENT_FILTER_READ_STATUS_CHANGED) > 0); 398 } 399 400 private static boolean sendEventConversationChanged(long eventFilter) { 401 return ((eventFilter & EVENT_FILTER_CONVERSATION_CHANGED) > 0); 402 } 403 404 private static boolean sendEventParticipantPresenceChanged(long eventFilter) { 405 return ((eventFilter & EVENT_FILTER_PARTICIPANT_PRESENCE_CHANGED) > 0); 406 } 407 408 private static boolean sendEventParticipantChatstateChanged(long eventFilter) { 409 return ((eventFilter & EVENT_FILTER_PARTICIPANT_CHATSTATE_CHANGED) > 0); 410 } 411 412 private static boolean sendEventMessageRemoved(long eventFilter) { 413 return ((eventFilter & EVENT_FILTER_MESSAGE_REMOVED) > 0); 414 } 415 416 private TYPE getSmsType() { 417 TYPE smsType = null; 418 TelephonyManager tm = (TelephonyManager) mContext.getSystemService( 419 Context.TELEPHONY_SERVICE); 420 421 if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) { 422 smsType = TYPE.SMS_GSM; 423 } else if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) { 424 smsType = TYPE.SMS_CDMA; 425 } 426 427 return smsType; 428 } 429 430 private final ContentObserver mObserver = new ContentObserver( 431 new Handler(Looper.getMainLooper())) { 432 @Override 433 public void onChange(boolean selfChange) { 434 onChange(selfChange, null); 435 } 436 437 @Override 438 public void onChange(boolean selfChange, Uri uri) { 439 if(uri == null) { 440 Log.w(TAG, "onChange() with URI == null - not handled."); 441 return; 442 } 443 if (V) Log.d(TAG, "onChange on thread: " + Thread.currentThread().getId() 444 + " Uri: " + uri.toString() + " selfchange: " + selfChange); 445 446 if(uri.toString().contains(BluetoothMapContract.TABLE_CONVOCONTACT)) 447 handleContactListChanges(uri); 448 else 449 handleMsgListChanges(uri); 450 } 451 }; 452 453 private static final HashMap<Integer, String> FOLDER_SMS_MAP; 454 static { 455 FOLDER_SMS_MAP = new HashMap<Integer, String>(); 456 FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_INBOX, BluetoothMapContract.FOLDER_NAME_INBOX); 457 FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_SENT, BluetoothMapContract.FOLDER_NAME_SENT); 458 FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_DRAFT, BluetoothMapContract.FOLDER_NAME_DRAFT); 459 FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_OUTBOX, BluetoothMapContract.FOLDER_NAME_OUTBOX); 460 FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_FAILED, BluetoothMapContract.FOLDER_NAME_OUTBOX); 461 FOLDER_SMS_MAP.put(Sms.MESSAGE_TYPE_QUEUED, BluetoothMapContract.FOLDER_NAME_OUTBOX); 462 } 463 464 private static String getSmsFolderName(int type) { 465 String name = FOLDER_SMS_MAP.get(type); 466 if(name != null) { 467 return name; 468 } 469 Log.e(TAG, "New SMS mailbox types have been introduced, without an update in BT..."); 470 return "Unknown"; 471 } 472 473 474 private static final HashMap<Integer, String> FOLDER_MMS_MAP; 475 static { 476 FOLDER_MMS_MAP = new HashMap<Integer, String>(); 477 FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_INBOX, BluetoothMapContract.FOLDER_NAME_INBOX); 478 FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_SENT, BluetoothMapContract.FOLDER_NAME_SENT); 479 FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_DRAFTS, BluetoothMapContract.FOLDER_NAME_DRAFT); 480 FOLDER_MMS_MAP.put(Mms.MESSAGE_BOX_OUTBOX, BluetoothMapContract.FOLDER_NAME_OUTBOX); 481 } 482 483 private static String getMmsFolderName(int mailbox) { 484 String name = FOLDER_MMS_MAP.get(mailbox); 485 if(name != null) { 486 return name; 487 } 488 Log.e(TAG, "New MMS mailboxes have been introduced, without an update in BT..."); 489 return "Unknown"; 490 } 491 492 /** 493 * Set the folder structure to be used for this instance. 494 * @param folderStructure 495 */ 496 public void setFolderStructure(BluetoothMapFolderElement folderStructure) { 497 this.mFolders = folderStructure; 498 } 499 500 private class ConvoContactInfo { 501 public int mConvoColConvoId = -1; 502 public int mConvoColLastActivity = -1; 503 public int mConvoColName = -1; 504 // public int mConvoColRead = -1; 505 // public int mConvoColVersionCounter = -1; 506 public int mContactColUci = -1; 507 public int mContactColConvoId = -1; 508 public int mContactColName = -1; 509 public int mContactColNickname = -1; 510 public int mContactColBtUid = -1; 511 public int mContactColChatState = -1; 512 public int mContactColContactId = -1; 513 public int mContactColLastActive = -1; 514 public int mContactColPresenceState = -1; 515 public int mContactColPresenceText = -1; 516 public int mContactColPriority = -1; 517 public int mContactColLastOnline = -1; 518 519 public void setConvoColunms(Cursor c) { 520 // mConvoColConvoId = c.getColumnIndex( 521 // BluetoothMapContract.ConversationColumns.THREAD_ID); 522 // mConvoColLastActivity = c.getColumnIndex( 523 // BluetoothMapContract.ConversationColumns.LAST_THREAD_ACTIVITY); 524 // mConvoColName = c.getColumnIndex( 525 // BluetoothMapContract.ConversationColumns.THREAD_NAME); 526 mContactColConvoId = c.getColumnIndex( 527 BluetoothMapContract.ConvoContactColumns.CONVO_ID); 528 mContactColName = c.getColumnIndex( 529 BluetoothMapContract.ConvoContactColumns.NAME); 530 mContactColNickname = c.getColumnIndex( 531 BluetoothMapContract.ConvoContactColumns.NICKNAME); 532 mContactColBtUid = c.getColumnIndex( 533 BluetoothMapContract.ConvoContactColumns.X_BT_UID); 534 mContactColChatState = c.getColumnIndex( 535 BluetoothMapContract.ConvoContactColumns.CHAT_STATE); 536 mContactColUci = c.getColumnIndex( 537 BluetoothMapContract.ConvoContactColumns.UCI); 538 mContactColNickname = c.getColumnIndex( 539 BluetoothMapContract.ConvoContactColumns.NICKNAME); 540 mContactColLastActive = c.getColumnIndex( 541 BluetoothMapContract.ConvoContactColumns.LAST_ACTIVE); 542 mContactColName = c.getColumnIndex( 543 BluetoothMapContract.ConvoContactColumns.NAME); 544 mContactColPresenceState = c.getColumnIndex( 545 BluetoothMapContract.ConvoContactColumns.PRESENCE_STATE); 546 mContactColPresenceText = c.getColumnIndex( 547 BluetoothMapContract.ConvoContactColumns.STATUS_TEXT); 548 mContactColPriority = c.getColumnIndex( 549 BluetoothMapContract.ConvoContactColumns.PRIORITY); 550 mContactColLastOnline = c.getColumnIndex( 551 BluetoothMapContract.ConvoContactColumns.LAST_ONLINE); 552 } 553 } 554 555 private class Event { 556 String eventType; 557 long handle; 558 String folder = null; 559 String oldFolder = null; 560 TYPE msgType; 561 /* Extended event parameters in MAP Event version 1.1 */ 562 String datetime = null; // OBEX time "YYYYMMDDTHHMMSS" 563 String uci = null; 564 String subject = null; 565 String senderName = null; 566 String priority = null; 567 /* Event parameters in MAP Event version 1.2 */ 568 String conversationName = null; 569 long conversationID = -1; 570 int presenceState = BluetoothMapContract.PresenceState.UNKNOWN; 571 String presenceStatus = null; 572 int chatState = BluetoothMapContract.ChatState.UNKNOWN; 573 574 final static String PATH = "telecom/msg/"; 575 576 private void setFolderPath(String name, TYPE type) { 577 if (name != null) { 578 if(type == TYPE.EMAIL || type == TYPE.IM) { 579 this.folder = name; 580 } else { 581 this.folder = PATH + name; 582 } 583 } else { 584 this.folder = null; 585 } 586 } 587 588 public Event(String eventType, long handle, String folder, 589 String oldFolder, TYPE msgType) { 590 this.eventType = eventType; 591 this.handle = handle; 592 setFolderPath(folder, msgType); 593 if (oldFolder != null) { 594 if(msgType == TYPE.EMAIL || msgType == TYPE.IM) { 595 this.oldFolder = oldFolder; 596 } else { 597 this.oldFolder = PATH + oldFolder; 598 } 599 } else { 600 this.oldFolder = null; 601 } 602 this.msgType = msgType; 603 } 604 605 public Event(String eventType, long handle, String folder, TYPE msgType) { 606 this.eventType = eventType; 607 this.handle = handle; 608 setFolderPath(folder, msgType); 609 this.msgType = msgType; 610 } 611 612 /* extended event type 1.1 */ 613 public Event(String eventType, long handle, String folder, TYPE msgType, 614 String datetime, String subject, String senderName, String priority) { 615 this.eventType = eventType; 616 this.handle = handle; 617 setFolderPath(folder, msgType); 618 this.msgType = msgType; 619 this.datetime = datetime; 620 if (subject != null) { 621 this.subject = BluetoothMapUtils.stripInvalidChars(subject); 622 } 623 if (senderName != null) { 624 this.senderName = BluetoothMapUtils.stripInvalidChars(senderName); 625 } 626 this.priority = priority; 627 } 628 629 /* extended event type 1.2 message events */ 630 public Event(String eventType, long handle, String folder, TYPE msgType, 631 String datetime, String subject, String senderName, String priority, 632 long conversationID, String conversationName) { 633 this.eventType = eventType; 634 this.handle = handle; 635 setFolderPath(folder, msgType); 636 this.msgType = msgType; 637 this.datetime = datetime; 638 if (subject != null) { 639 this.subject = BluetoothMapUtils.stripInvalidChars(subject); 640 } 641 if (senderName != null) { 642 this.senderName = BluetoothMapUtils.stripInvalidChars(senderName); 643 } 644 if (conversationID != 0) { 645 this.conversationID = conversationID; 646 } 647 if (conversationName != null) { 648 this.conversationName = BluetoothMapUtils.stripInvalidChars(conversationName); 649 } 650 this.priority = priority; 651 } 652 653 /* extended event type 1.2 for conversation, presence or chat state changed events */ 654 public Event(String eventType, String uci, TYPE msgType, String name, String priority, 655 String lastActivity, long conversationID, String conversationName, 656 int presenceState, String presenceStatus, int chatState) { 657 this.eventType = eventType; 658 this.uci = uci; 659 this.msgType = msgType; 660 if (name != null) { 661 this.senderName = BluetoothMapUtils.stripInvalidChars(name); 662 } 663 this.priority = priority; 664 this.datetime = lastActivity; 665 if (conversationID != 0) { 666 this.conversationID = conversationID; 667 } 668 if (conversationName != null) { 669 this.conversationName = BluetoothMapUtils.stripInvalidChars(conversationName); 670 } 671 if (presenceState != BluetoothMapContract.PresenceState.UNKNOWN) { 672 this.presenceState = presenceState; 673 } 674 if (presenceStatus != null) { 675 this.presenceStatus = BluetoothMapUtils.stripInvalidChars(presenceStatus); 676 } 677 if (chatState != BluetoothMapContract.ChatState.UNKNOWN) { 678 this.chatState = chatState; 679 } 680 } 681 682 public byte[] encode() throws UnsupportedEncodingException { 683 StringWriter sw = new StringWriter(); 684 XmlSerializer xmlEvtReport = Xml.newSerializer(); 685 686 try { 687 xmlEvtReport.setOutput(sw); 688 xmlEvtReport.startDocument("UTF-8", true); 689 xmlEvtReport.text("\r\n"); 690 xmlEvtReport.startTag("", "MAP-event-report"); 691 if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 692 xmlEvtReport.attribute("", "version", BluetoothMapUtils.MAP_V10_STR); 693 } else if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 694 xmlEvtReport.attribute("", "version", BluetoothMapUtils.MAP_V11_STR); 695 } else { 696 xmlEvtReport.attribute("", "version", BluetoothMapUtils.MAP_V12_STR); 697 } 698 xmlEvtReport.startTag("", "event"); 699 xmlEvtReport.attribute("", "type", eventType); 700 if (eventType.equals(EVENT_TYPE_CONVERSATION) || 701 eventType.equals(EVENT_TYPE_PRESENCE) || 702 eventType.equals(EVENT_TYPE_CHAT_STATE)) { 703 xmlEvtReport.attribute("", "participant_uci", uci); 704 } else { 705 xmlEvtReport.attribute("", "handle", 706 BluetoothMapUtils.getMapHandle(handle, msgType)); 707 } 708 709 if (folder != null) { 710 xmlEvtReport.attribute("", "folder", folder); 711 } 712 if (oldFolder != null) { 713 xmlEvtReport.attribute("", "old_folder", oldFolder); 714 } 715 xmlEvtReport.attribute("", "msg_type", msgType.name()); 716 717 /* If MAP event report version is above 1.0 send 718 * extended event report parameters */ 719 if (datetime != null) { 720 xmlEvtReport.attribute("", "datetime", datetime); 721 } 722 if (subject != null) { 723 xmlEvtReport.attribute("", "subject", 724 subject.substring(0,subject.length() < 256 ? subject.length() : 256)); 725 } 726 if (senderName != null) { 727 xmlEvtReport.attribute("", "senderName", senderName); 728 } 729 if (priority != null) { 730 xmlEvtReport.attribute("", "priority", priority); 731 } 732 733 //} 734 /* Include conversation information from event version 1.2 */ 735 if (mMapEventReportVersion > BluetoothMapUtils.MAP_EVENT_REPORT_V11 ) { 736 if (conversationName != null) { 737 xmlEvtReport.attribute("", "conversation_name", conversationName); 738 } 739 if (conversationID != -1) { 740 // Convert provider conversation handle to string incl type 741 xmlEvtReport.attribute("", "conversation_id", 742 BluetoothMapUtils.getMapConvoHandle(conversationID, msgType)); 743 } 744 if (eventType.equals(EVENT_TYPE_PRESENCE)) { 745 if (presenceState != 0) { 746 // Convert provider conversation handle to string incl type 747 xmlEvtReport.attribute("", "presence_availability", 748 String.valueOf(presenceState)); 749 } 750 if (presenceStatus != null) { 751 // Convert provider conversation handle to string incl type 752 xmlEvtReport.attribute("", "presence_status", 753 presenceStatus.substring( 754 0,presenceStatus.length() < 256 ? subject.length() : 256)); 755 } 756 } 757 if (eventType.equals(EVENT_TYPE_PRESENCE)) { 758 if (chatState != 0) { 759 // Convert provider conversation handle to string incl type 760 xmlEvtReport.attribute("", "chat_state", String.valueOf(chatState)); 761 } 762 } 763 764 } 765 xmlEvtReport.endTag("", "event"); 766 xmlEvtReport.endTag("", "MAP-event-report"); 767 xmlEvtReport.endDocument(); 768 } catch (IllegalArgumentException e) { 769 if(D) Log.w(TAG,e); 770 } catch (IllegalStateException e) { 771 if(D) Log.w(TAG,e); 772 } catch (IOException e) { 773 if(D) Log.w(TAG,e); 774 } 775 776 if (V) Log.d(TAG, sw.toString()); 777 778 return sw.toString().getBytes("UTF-8"); 779 } 780 } 781 782 /*package*/ class Msg { 783 long id; 784 int type; // Used as folder for SMS/MMS 785 int threadId; // Used for SMS/MMS at delete 786 long folderId = -1; // Email folder ID 787 long oldFolderId = -1; // Used for email undelete 788 boolean localInitiatedSend = false; // Used for MMS to filter out events 789 boolean transparent = false; // Used for EMAIL to delete message sent with transparency 790 int flagRead = -1; // Message status read/unread 791 792 public Msg(long id, int type, int threadId, int readFlag) { 793 this.id = id; 794 this.type = type; 795 this.threadId = threadId; 796 this.flagRead = readFlag; 797 } 798 public Msg(long id, long folderId, int readFlag) { 799 this.id = id; 800 this.folderId = folderId; 801 this.flagRead = readFlag; 802 } 803 804 /* Eclipse generated hashCode() and equals() to make 805 * hashMap lookup work independent of whether the obj 806 * is used for email or SMS/MMS and whether or not the 807 * oldFolder is set. */ 808 @Override 809 public int hashCode() { 810 final int prime = 31; 811 int result = 1; 812 result = prime * result + (int) (id ^ (id >>> 32)); 813 return result; 814 } 815 816 @Override 817 public boolean equals(Object obj) { 818 if (this == obj) 819 return true; 820 if (obj == null) 821 return false; 822 if (getClass() != obj.getClass()) 823 return false; 824 Msg other = (Msg) obj; 825 if (id != other.id) 826 return false; 827 return true; 828 } 829 } 830 831 private Map<Long, Msg> mMsgListSms = null; 832 833 private Map<Long, Msg> mMsgListMms = null; 834 835 private Map<Long, Msg> mMsgListMsg = null; 836 837 private Map<String, BluetoothMapConvoContactElement> mContactList = null; 838 839 public int setNotificationRegistration(int notificationStatus) throws RemoteException { 840 // Forward the request to the MNS thread as a message - including the MAS instance ID. 841 if(D) Log.d(TAG,"setNotificationRegistration() enter"); 842 Handler mns = mMnsClient.getMessageHandler(); 843 if(mns != null) { 844 Message msg = mns.obtainMessage(); 845 msg.what = BluetoothMnsObexClient.MSG_MNS_NOTIFICATION_REGISTRATION; 846 msg.arg1 = mMasId; 847 msg.arg2 = notificationStatus; 848 mns.sendMessageDelayed(msg, 10); // Send message without forcing a context switch 849 /* Some devices - e.g. PTS needs to get the unregister confirm before we actually 850 * disconnect the MNS. */ 851 if(D) Log.d(TAG,"setNotificationRegistration() MSG_MNS_NOTIFICATION_REGISTRATION " + 852 "send to MNS"); 853 } else { 854 // This should not happen except at shutdown. 855 if(D) Log.d(TAG,"setNotificationRegistration() Unable to send registration request"); 856 return ResponseCodes.OBEX_HTTP_UNAVAILABLE; 857 } 858 if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) { 859 registerObserver(); 860 } else { 861 unregisterObserver(); 862 } 863 return ResponseCodes.OBEX_HTTP_OK; 864 } 865 866 boolean eventMaskContainsContacts(long mask) { 867 return sendEventParticipantPresenceChanged(mask); 868 } 869 870 boolean eventMaskContainsCovo(long mask) { 871 return (sendEventConversationChanged(mask) 872 || sendEventParticipantChatstateChanged(mask)); 873 } 874 875 /* Overwrite the existing notification filter. Will register/deregister observers for 876 * the Contacts and Conversation table as needed. We keep the message observer 877 * at all times. */ 878 /*package*/ synchronized void setNotificationFilter(long newFilter) { 879 long oldFilter = mEventFilter; 880 mEventFilter = newFilter; 881 /* Contacts */ 882 if(!eventMaskContainsContacts(oldFilter) && 883 eventMaskContainsContacts(newFilter)) { 884 // TODO: 885 // Enable the observer 886 // Reset the contacts list 887 } 888 /* Conversations */ 889 if(!eventMaskContainsCovo(oldFilter) && 890 eventMaskContainsCovo(newFilter)) { 891 // TODO: 892 // Enable the observer 893 // Reset the conversations list 894 } 895 } 896 897 public void registerObserver() throws RemoteException{ 898 if (V) Log.d(TAG, "registerObserver"); 899 900 if (mObserverRegistered) 901 return; 902 903 if(mAccount != null) { 904 905 mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority); 906 if (mProviderClient == null) { 907 throw new RemoteException("Failed to acquire provider for " + mAuthority); 908 } 909 mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT); 910 911 // If there is a change in the database before we init the lists we will be sending 912 // loads of events - hence init before register. 913 if(mAccount.getType() == TYPE.IM) { 914 // Further add contact list tracking 915 initContactsList(); 916 } 917 } 918 // If there is a change in the database before we init the lists we will be sending 919 // loads of events - hence init before register. 920 initMsgList(); 921 922 /* Use MmsSms Uri since the Sms Uri is not notified on deletes */ 923 if(mEnableSmsMms){ 924 //this is sms/mms 925 mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver); 926 mObserverRegistered = true; 927 } 928 929 if(mAccount != null) { 930 /* For URI's without account ID */ 931 Uri uri = Uri.parse(mAccount.mBase_uri_no_account + "/" 932 + BluetoothMapContract.TABLE_MESSAGE); 933 if(D) Log.d(TAG, "Registering observer for: " + uri); 934 mResolver.registerContentObserver(uri, true, mObserver); 935 936 /* For URI's with account ID - is handled the same way as without ID, but is 937 * only triggered for MAS instances with matching account ID. */ 938 uri = Uri.parse(mAccount.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE); 939 if(D) Log.d(TAG, "Registering observer for: " + uri); 940 mResolver.registerContentObserver(uri, true, mObserver); 941 942 if(mAccount.getType() == TYPE.IM) { 943 944 uri = Uri.parse(mAccount.mBase_uri_no_account + "/" 945 + BluetoothMapContract.TABLE_CONVOCONTACT); 946 if(D) Log.d(TAG, "Registering observer for: " + uri); 947 mResolver.registerContentObserver(uri, true, mObserver); 948 949 /* For URI's with account ID - is handled the same way as without ID, but is 950 * only triggered for MAS instances with matching account ID. */ 951 uri = Uri.parse(mAccount.mBase_uri + "/" + BluetoothMapContract.TABLE_CONVOCONTACT); 952 if(D) Log.d(TAG, "Registering observer for: " + uri); 953 mResolver.registerContentObserver(uri, true, mObserver); 954 } 955 956 mObserverRegistered = true; 957 } 958 } 959 960 public void unregisterObserver() { 961 if (V) Log.d(TAG, "unregisterObserver"); 962 mResolver.unregisterContentObserver(mObserver); 963 mObserverRegistered = false; 964 if(mProviderClient != null){ 965 mProviderClient.release(); 966 mProviderClient = null; 967 } 968 } 969 970 /** 971 * Per design it is only possible to call the refreshXxxx functions sequentially, hence it 972 * is safe to modify mTransmitEvents without synchronization. 973 */ 974 /* package */ void refreshFolderVersionCounter() { 975 if (mObserverRegistered) { 976 // As we have observers, we already keep the counter up-to-date. 977 return; 978 } 979 /* We need to perform the same functionality, as when we receive a notification change, 980 hence we: 981 - disable the event transmission 982 - triggers the code for updates 983 - enable the event transmission */ 984 mTransmitEvents = false; 985 try { 986 if(mEnableSmsMms) { 987 handleMsgListChangesSms(); 988 handleMsgListChangesMms(); 989 } 990 if(mAccount != null) { 991 try { 992 handleMsgListChangesMsg(mMessageUri); 993 } catch (RemoteException e) { 994 Log.e(TAG, "Unable to update FolderVersionCounter. - Not fatal, but can cause" + 995 " undesirable user experience!", e); 996 } 997 } 998 } finally { 999 // Ensure we always enable events again 1000 mTransmitEvents = true; 1001 } 1002 } 1003 1004 /* package */ void refreshConvoListVersionCounter() { 1005 if (mObserverRegistered) { 1006 // As we have observers, we already keep the counter up-to-date. 1007 return; 1008 } 1009 /* We need to perform the same functionality, as when we receive a notification change, 1010 hence we: 1011 - disable event transmission 1012 - triggers the code for updates 1013 - enable event transmission */ 1014 mTransmitEvents = false; 1015 try { 1016 if((mAccount != null) && (mContactUri != null)) { 1017 handleContactListChanges(mContactUri); 1018 } 1019 } finally { 1020 // Ensure we always enable events again 1021 mTransmitEvents = true; 1022 } 1023 } 1024 1025 private void sendEvent(Event evt) { 1026 1027 if(mTransmitEvents == false) { 1028 if(V) Log.v(TAG, "mTransmitEvents == false - don't send event."); 1029 return; 1030 } 1031 1032 if(D)Log.d(TAG, "sendEvent: " + evt.eventType + " " + evt.handle + " " + evt.folder + " " 1033 + evt.oldFolder + " " + evt.msgType.name() + " " + evt.datetime + " " 1034 + evt.subject + " " + evt.senderName + " " + evt.priority ); 1035 1036 if (mMnsClient == null || mMnsClient.isConnected() == false) { 1037 Log.d(TAG, "sendEvent: No MNS client registered or connected- don't send event"); 1038 return; 1039 } 1040 1041 /* Enable use of the cache for checking the filter */ 1042 long eventFilter = mEventFilter; 1043 1044 /* This should have been a switch on the string, but it is not allowed in Java 1.6 */ 1045 /* WARNING: Here we do pointer compare for the string to speed up things, that is. 1046 * HENCE: always use the EVENT_TYPE_"defines" */ 1047 if(evt.eventType == EVENT_TYPE_NEW) { 1048 if(!sendEventNewMessage(eventFilter)) { 1049 if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); 1050 return; 1051 } 1052 } else if(evt.eventType == EVENT_TYPE_DELETE) { 1053 if(!sendEventMessageDeleted(eventFilter)) { 1054 if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); 1055 return; 1056 } 1057 } else if(evt.eventType == EVENT_TYPE_REMOVED) { 1058 if(!sendEventMessageRemoved(eventFilter)) { 1059 if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); 1060 return; 1061 } 1062 } else if(evt.eventType == EVENT_TYPE_SHIFT) { 1063 if(!sendEventMessageShift(eventFilter)) { 1064 if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); 1065 return; 1066 } 1067 } else if(evt.eventType == EVENT_TYPE_DELEVERY_SUCCESS) { 1068 if(!sendEventDeliverySuccess(eventFilter)) { 1069 if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); 1070 return; 1071 } 1072 } else if(evt.eventType == EVENT_TYPE_SENDING_SUCCESS) { 1073 if(!sendEventSendingSuccess(eventFilter)) { 1074 if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); 1075 return; 1076 } 1077 } else if(evt.eventType == EVENT_TYPE_SENDING_FAILURE) { 1078 if(!sendEventSendingFailed(eventFilter)) { 1079 if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); 1080 return; 1081 } 1082 } else if(evt.eventType == EVENT_TYPE_DELIVERY_FAILURE) { 1083 if(!sendEventDeliveryFailed(eventFilter)) { 1084 if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); 1085 return; 1086 } 1087 } else if(evt.eventType == EVENT_TYPE_READ_STATUS) { 1088 if(!sendEventReadStatusChanged(eventFilter)) { 1089 if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); 1090 return; 1091 } 1092 } else if(evt.eventType == EVENT_TYPE_CONVERSATION) { 1093 if(!sendEventConversationChanged(eventFilter)) { 1094 if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); 1095 return; 1096 } 1097 } else if(evt.eventType == EVENT_TYPE_PRESENCE) { 1098 if(!sendEventParticipantPresenceChanged(eventFilter)) { 1099 if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); 1100 return; 1101 } 1102 } else if(evt.eventType == EVENT_TYPE_CHAT_STATE) { 1103 if(!sendEventParticipantChatstateChanged(eventFilter)) { 1104 if(D)Log.d(TAG, "Skip sending event of type: " + evt.eventType); 1105 return; 1106 } 1107 } 1108 1109 try { 1110 mMnsClient.sendEvent(evt.encode(), mMasId); 1111 } catch (UnsupportedEncodingException ex) { 1112 /* do nothing */ 1113 if (D) Log.e(TAG, "Exception - should not happen: ",ex); 1114 } 1115 } 1116 1117 private void initMsgList() throws RemoteException { 1118 if (V) Log.d(TAG, "initMsgList"); 1119 1120 if(mEnableSmsMms) { 1121 1122 HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>(); 1123 1124 Cursor c = mResolver.query(Sms.CONTENT_URI, 1125 SMS_PROJECTION_SHORT, null, null, null); 1126 try { 1127 if (c != null && c.moveToFirst()) { 1128 do { 1129 long id = c.getLong(c.getColumnIndex(Sms._ID)); 1130 int type = c.getInt(c.getColumnIndex(Sms.TYPE)); 1131 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); 1132 int read = c.getInt(c.getColumnIndex(Sms.READ)); 1133 1134 Msg msg = new Msg(id, type, threadId, read); 1135 msgListSms.put(id, msg); 1136 } while (c.moveToNext()); 1137 } 1138 } finally { 1139 if (c != null) c.close(); 1140 } 1141 1142 synchronized(getMsgListSms()) { 1143 getMsgListSms().clear(); 1144 setMsgListSms(msgListSms, true); // Set initial folder version counter 1145 } 1146 1147 HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>(); 1148 1149 c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT, null, null, null); 1150 try { 1151 if (c != null && c.moveToFirst()) { 1152 do { 1153 long id = c.getLong(c.getColumnIndex(Mms._ID)); 1154 int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); 1155 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); 1156 int read = c.getInt(c.getColumnIndex(Mms.READ)); 1157 1158 Msg msg = new Msg(id, type, threadId, read); 1159 msgListMms.put(id, msg); 1160 } while (c.moveToNext()); 1161 } 1162 } finally { 1163 if (c != null) c.close(); 1164 } 1165 1166 synchronized(getMsgListMms()) { 1167 getMsgListMms().clear(); 1168 setMsgListMms(msgListMms, true); // Set initial folder version counter 1169 } 1170 } 1171 1172 if(mAccount != null) { 1173 HashMap<Long, Msg> msgList = new HashMap<Long, Msg>(); 1174 Uri uri = mMessageUri; 1175 Cursor c = mProviderClient.query(uri, MSG_PROJECTION_SHORT, null, null, null); 1176 1177 try { 1178 if (c != null && c.moveToFirst()) { 1179 do { 1180 long id = c.getLong(c.getColumnIndex(MessageColumns._ID)); 1181 long folderId = c.getInt(c.getColumnIndex( 1182 BluetoothMapContract.MessageColumns.FOLDER_ID)); 1183 int readFlag = c.getInt(c.getColumnIndex( 1184 BluetoothMapContract.MessageColumns.FLAG_READ)); 1185 Msg msg = new Msg(id, folderId, readFlag); 1186 msgList.put(id, msg); 1187 } while (c.moveToNext()); 1188 } 1189 } finally { 1190 if (c != null) c.close(); 1191 } 1192 1193 synchronized(getMsgListMsg()) { 1194 getMsgListMsg().clear(); 1195 setMsgListMsg(msgList, true); 1196 } 1197 } 1198 } 1199 1200 private void initContactsList() throws RemoteException { 1201 if (V) Log.d(TAG, "initContactsList"); 1202 if(mContactUri == null) { 1203 if (D) Log.d(TAG, "initContactsList() no mContactUri - nothing to init"); 1204 return; 1205 } 1206 Uri uri = mContactUri; 1207 Cursor c = mProviderClient.query(uri, 1208 BluetoothMapContract.BT_CONTACT_CHATSTATE_PRESENCE_PROJECTION, 1209 null, null, null); 1210 Map<String, BluetoothMapConvoContactElement> contactList = 1211 new HashMap<String, BluetoothMapConvoContactElement>(); 1212 try { 1213 if (c != null && c.moveToFirst()) { 1214 ConvoContactInfo cInfo = new ConvoContactInfo(); 1215 cInfo.setConvoColunms(c); 1216 do { 1217 long convoId = c.getLong(cInfo.mContactColConvoId); 1218 if (convoId == 0) 1219 continue; 1220 if (V) BluetoothMapUtils.printCursor(c); 1221 String uci = c.getString(cInfo.mContactColUci); 1222 String name = c.getString(cInfo.mContactColName); 1223 String displayName = c.getString(cInfo.mContactColNickname); 1224 String presenceStatus = c.getString(cInfo.mContactColPresenceText); 1225 int presenceState = c.getInt(cInfo.mContactColPresenceState); 1226 long lastActivity = c.getLong(cInfo.mContactColLastActive); 1227 int chatState = c.getInt(cInfo.mContactColChatState); 1228 int priority = c.getInt(cInfo.mContactColPriority); 1229 String btUid = c.getString(cInfo.mContactColBtUid); 1230 BluetoothMapConvoContactElement contact = 1231 new BluetoothMapConvoContactElement(uci, name, displayName, 1232 presenceStatus, presenceState, lastActivity, chatState, 1233 priority, btUid); 1234 contactList.put(uci, contact); 1235 } while (c.moveToNext()); 1236 } 1237 } finally { 1238 if (c != null) c.close(); 1239 } 1240 synchronized(getContactList()) { 1241 getContactList().clear(); 1242 setContactList(contactList, true); 1243 } 1244 } 1245 1246 private void handleMsgListChangesSms() { 1247 if (V) Log.d(TAG, "handleMsgListChangesSms"); 1248 1249 HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>(); 1250 boolean listChanged = false; 1251 1252 Cursor c; 1253 if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1254 c = mResolver.query(Sms.CONTENT_URI, 1255 SMS_PROJECTION_SHORT, null, null, null); 1256 } else { 1257 c = mResolver.query(Sms.CONTENT_URI, 1258 SMS_PROJECTION_SHORT_EXT, null, null, null); 1259 } 1260 synchronized(getMsgListSms()) { 1261 try { 1262 if (c != null && c.moveToFirst()) { 1263 do { 1264 long id = c.getLong(c.getColumnIndex(Sms._ID)); 1265 int type = c.getInt(c.getColumnIndex(Sms.TYPE)); 1266 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); 1267 int read = c.getInt(c.getColumnIndex(Sms.READ)); 1268 1269 Msg msg = getMsgListSms().remove(id); 1270 1271 /* We must filter out any actions made by the MCE, hence do not send e.g. 1272 * a message deleted and/or MessageShift for messages deleted by the MCE. */ 1273 1274 if (msg == null) { 1275 /* New message */ 1276 msg = new Msg(id, type, threadId, read); 1277 msgListSms.put(id, msg); 1278 listChanged = true; 1279 Event evt; 1280 if (mTransmitEvents == true && // extract contact details only if needed 1281 mMapEventReportVersion > 1282 BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1283 String date = BluetoothMapUtils.getDateTimeString( 1284 c.getLong(c.getColumnIndex(Sms.DATE))); 1285 String subject = c.getString(c.getColumnIndex(Sms.BODY)); 1286 String name = ""; 1287 String phone = ""; 1288 if (type == 1) { //inbox 1289 phone = c.getString(c.getColumnIndex(Sms.ADDRESS)); 1290 if (phone != null && !phone.isEmpty()) { 1291 name = BluetoothMapContent.getContactNameFromPhone(phone, 1292 mResolver); 1293 if(name == null || name.isEmpty()){ 1294 name = phone; 1295 } 1296 }else{ 1297 name = phone; 1298 } 1299 } else { 1300 TelephonyManager tm = 1301 (TelephonyManager)mContext.getSystemService( 1302 Context.TELEPHONY_SERVICE); 1303 if (tm != null) { 1304 phone = tm.getLine1Number(); 1305 name = tm.getLine1AlphaTag(); 1306 if(name == null || name.isEmpty()){ 1307 name = phone; 1308 } 1309 } 1310 } 1311 String priority = "no";// no priority for sms 1312 /* Incoming message from the network */ 1313 if (mMapEventReportVersion == 1314 BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 1315 evt = new Event(EVENT_TYPE_NEW, id, getSmsFolderName(type), 1316 mSmsType, date, subject, name, priority); 1317 } else { 1318 evt = new Event(EVENT_TYPE_NEW, id, getSmsFolderName(type), 1319 mSmsType, date, subject, name, priority, 1320 (long)threadId, null); 1321 } 1322 } else { 1323 /* Incoming message from the network */ 1324 evt = new Event(EVENT_TYPE_NEW, id, getSmsFolderName(type), 1325 null, mSmsType); 1326 } 1327 sendEvent(evt); 1328 } else { 1329 /* Existing message */ 1330 if (type != msg.type) { 1331 listChanged = true; 1332 Log.d(TAG, "new type: " + type + " old type: " + msg.type); 1333 String oldFolder = getSmsFolderName(msg.type); 1334 String newFolder = getSmsFolderName(type); 1335 // Filter out the intermediate outbox steps 1336 if(!oldFolder.equalsIgnoreCase(newFolder)) { 1337 Event evt = new Event(EVENT_TYPE_SHIFT, id, 1338 getSmsFolderName(type), oldFolder, mSmsType); 1339 sendEvent(evt); 1340 } 1341 msg.type = type; 1342 } else if(threadId != msg.threadId) { 1343 listChanged = true; 1344 Log.d(TAG, "Message delete change: type: " + type 1345 + " old type: " + msg.type 1346 + "\n threadId: " + threadId 1347 + " old threadId: " + msg.threadId); 1348 if(threadId == DELETED_THREAD_ID) { // Message deleted 1349 // TODO: 1350 // We shall only use the folder attribute, but can't remember 1351 // wether to set it to "deleted" or the name of the folder 1352 // from which the message have been deleted. 1353 Event evt = new Event(EVENT_TYPE_DELETE, id, 1354 BluetoothMapContract.FOLDER_NAME_DELETED, 1355 getSmsFolderName(msg.type), mSmsType); 1356 sendEvent(evt); 1357 msg.threadId = threadId; 1358 } else { // Undelete 1359 Event evt = new Event(EVENT_TYPE_SHIFT, id, 1360 getSmsFolderName(msg.type), 1361 BluetoothMapContract.FOLDER_NAME_DELETED, mSmsType); 1362 sendEvent(evt); 1363 msg.threadId = threadId; 1364 } 1365 } 1366 if(read != msg.flagRead) { 1367 listChanged = true; 1368 msg.flagRead = read; 1369 if (mMapEventReportVersion > 1370 BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1371 Event evt = new Event(EVENT_TYPE_READ_STATUS, id, 1372 getSmsFolderName(msg.type), mSmsType); 1373 sendEvent(evt); 1374 } 1375 } 1376 msgListSms.put(id, msg); 1377 } 1378 } while (c.moveToNext()); 1379 } 1380 } finally { 1381 if (c != null) c.close(); 1382 } 1383 1384 for (Msg msg : getMsgListSms().values()) { 1385 Event evt = new Event(EVENT_TYPE_DELETE, msg.id, 1386 BluetoothMapContract.FOLDER_NAME_DELETED, 1387 getSmsFolderName(msg.type), mSmsType); 1388 sendEvent(evt); 1389 listChanged = true; 1390 } 1391 1392 setMsgListSms(msgListSms, listChanged); 1393 } 1394 } 1395 1396 private void handleMsgListChangesMms() { 1397 if (V) Log.d(TAG, "handleMsgListChangesMms"); 1398 1399 HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>(); 1400 boolean listChanged = false; 1401 Cursor c; 1402 if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1403 c = mResolver.query(Mms.CONTENT_URI, 1404 MMS_PROJECTION_SHORT, null, null, null); 1405 } else { 1406 c = mResolver.query(Mms.CONTENT_URI, 1407 MMS_PROJECTION_SHORT_EXT, null, null, null); 1408 } 1409 1410 synchronized(getMsgListMms()) { 1411 try{ 1412 if (c != null && c.moveToFirst()) { 1413 do { 1414 long id = c.getLong(c.getColumnIndex(Mms._ID)); 1415 int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); 1416 int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE)); 1417 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); 1418 // TODO: Go through code to see if we have an issue with mismatch in types 1419 // for threadId. Seems to be a long in DB?? 1420 int read = c.getInt(c.getColumnIndex(Mms.READ)); 1421 1422 Msg msg = getMsgListMms().remove(id); 1423 1424 /* We must filter out any actions made by the MCE, hence do not send 1425 * e.g. a message deleted and/or MessageShift for messages deleted by the 1426 * MCE.*/ 1427 1428 if (msg == null) { 1429 /* New message - only notify on retrieve conf */ 1430 listChanged = true; 1431 if (getMmsFolderName(type).equalsIgnoreCase( 1432 BluetoothMapContract.FOLDER_NAME_INBOX) && 1433 mtype != MESSAGE_TYPE_RETRIEVE_CONF) { 1434 continue; 1435 } 1436 msg = new Msg(id, type, threadId, read); 1437 msgListMms.put(id, msg); 1438 Event evt; 1439 if (mTransmitEvents == true && // extract contact details only if needed 1440 mMapEventReportVersion != 1441 BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1442 String date = BluetoothMapUtils.getDateTimeString( 1443 c.getLong(c.getColumnIndex(Mms.DATE))); 1444 String subject = c.getString(c.getColumnIndex(Mms.SUBJECT)); 1445 if (subject == null || subject.length() == 0) { 1446 /* Get subject from mms text body parts - if any exists */ 1447 subject = BluetoothMapContent.getTextPartsMms(mResolver, id); 1448 } 1449 int tmpPri = c.getInt(c.getColumnIndex(Mms.PRIORITY)); 1450 Log.d(TAG, "TEMP handleMsgListChangesMms, " + 1451 "newMessage 'read' state: " + read + 1452 "priority: " + tmpPri); 1453 1454 String address = BluetoothMapContent.getAddressMms( 1455 mResolver,id,BluetoothMapContent.MMS_FROM); 1456 String priority = "no"; 1457 if(tmpPri == PduHeaders.PRIORITY_HIGH) 1458 priority = "yes"; 1459 1460 /* Incoming message from the network */ 1461 if (mMapEventReportVersion == 1462 BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 1463 evt = new Event(EVENT_TYPE_NEW, id, getMmsFolderName(type), 1464 TYPE.MMS, date, subject, address, priority); 1465 } else { 1466 evt = new Event(EVENT_TYPE_NEW, id, getMmsFolderName(type), 1467 TYPE.MMS, date, subject, address, priority, 1468 (long)threadId, null); 1469 } 1470 1471 } else { 1472 /* Incoming message from the network */ 1473 evt = new Event(EVENT_TYPE_NEW, id, getMmsFolderName(type), 1474 null, TYPE.MMS); 1475 } 1476 1477 sendEvent(evt); 1478 } else { 1479 /* Existing message */ 1480 if (type != msg.type) { 1481 Log.d(TAG, "new type: " + type + " old type: " + msg.type); 1482 Event evt; 1483 listChanged = true; 1484 if(msg.localInitiatedSend == false) { 1485 // Only send events about local initiated changes 1486 evt = new Event(EVENT_TYPE_SHIFT, id, getMmsFolderName(type), 1487 getMmsFolderName(msg.type), TYPE.MMS); 1488 sendEvent(evt); 1489 } 1490 msg.type = type; 1491 1492 if (getMmsFolderName(type).equalsIgnoreCase( 1493 BluetoothMapContract.FOLDER_NAME_SENT) 1494 && msg.localInitiatedSend == true) { 1495 // Stop tracking changes for this message 1496 msg.localInitiatedSend = false; 1497 evt = new Event(EVENT_TYPE_SENDING_SUCCESS, id, 1498 getMmsFolderName(type), null, TYPE.MMS); 1499 sendEvent(evt); 1500 } 1501 } else if(threadId != msg.threadId) { 1502 Log.d(TAG, "Message delete change: type: " + type + " old type: " 1503 + msg.type 1504 + "\n threadId: " + threadId + " old threadId: " 1505 + msg.threadId); 1506 listChanged = true; 1507 if(threadId == DELETED_THREAD_ID) { // Message deleted 1508 Event evt = new Event(EVENT_TYPE_DELETE, id, 1509 BluetoothMapContract.FOLDER_NAME_DELETED, 1510 getMmsFolderName(msg.type), TYPE.MMS); 1511 sendEvent(evt); 1512 msg.threadId = threadId; 1513 } else { // Undelete 1514 Event evt = new Event(EVENT_TYPE_SHIFT, id, 1515 getMmsFolderName(msg.type), 1516 BluetoothMapContract.FOLDER_NAME_DELETED, TYPE.MMS); 1517 sendEvent(evt); 1518 msg.threadId = threadId; 1519 } 1520 } 1521 if(read != msg.flagRead) { 1522 listChanged = true; 1523 msg.flagRead = read; 1524 if (mMapEventReportVersion > 1525 BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1526 Event evt = new Event(EVENT_TYPE_READ_STATUS, id, 1527 getMmsFolderName(msg.type), TYPE.MMS); 1528 sendEvent(evt); 1529 } 1530 } 1531 msgListMms.put(id, msg); 1532 } 1533 } while (c.moveToNext()); 1534 1535 } 1536 } finally { 1537 if (c != null) c.close(); 1538 } 1539 for (Msg msg : getMsgListMms().values()) { 1540 Event evt = new Event(EVENT_TYPE_DELETE, msg.id, 1541 BluetoothMapContract.FOLDER_NAME_DELETED, 1542 getMmsFolderName(msg.type), TYPE.MMS); 1543 sendEvent(evt); 1544 listChanged = true; 1545 } 1546 setMsgListMms(msgListMms, listChanged); 1547 } 1548 } 1549 1550 private void handleMsgListChangesMsg(Uri uri) throws RemoteException{ 1551 if (V) Log.v(TAG, "handleMsgListChangesMsg uri: " + uri.toString()); 1552 1553 // TODO: Change observer to handle accountId and message ID if present 1554 1555 HashMap<Long, Msg> msgList = new HashMap<Long, Msg>(); 1556 Cursor c; 1557 boolean listChanged = false; 1558 if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1559 c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT, null, null, null); 1560 } else if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 1561 c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT_EXT, null, null, null); 1562 } else { 1563 c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT_EXT2, null, null, null); 1564 } 1565 synchronized(getMsgListMsg()) { 1566 try { 1567 if (c != null && c.moveToFirst()) { 1568 do { 1569 long id = c.getLong(c.getColumnIndex( 1570 BluetoothMapContract.MessageColumns._ID)); 1571 int folderId = c.getInt(c.getColumnIndex( 1572 BluetoothMapContract.MessageColumns.FOLDER_ID)); 1573 int readFlag = c.getInt(c.getColumnIndex( 1574 BluetoothMapContract.MessageColumns.FLAG_READ)); 1575 Msg msg = getMsgListMsg().remove(id); 1576 BluetoothMapFolderElement folderElement = mFolders.getFolderById(folderId); 1577 String newFolder; 1578 if(folderElement != null) { 1579 newFolder = folderElement.getFullPath(); 1580 } else { 1581 // This can happen if a new folder is created while connected 1582 newFolder = "unknown"; 1583 } 1584 /* We must filter out any actions made by the MCE, hence do not send e.g. 1585 * a message deleted and/or MessageShift for messages deleted by the MCE. */ 1586 if (msg == null) { 1587 listChanged = true; 1588 /* New message - created with message unread */ 1589 msg = new Msg(id, folderId, 0, readFlag); 1590 msgList.put(id, msg); 1591 Event evt; 1592 /* Incoming message from the network */ 1593 if (mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1594 String date = BluetoothMapUtils.getDateTimeString( 1595 c.getLong(c.getColumnIndex( 1596 BluetoothMapContract.MessageColumns.DATE))); 1597 String subject = c.getString(c.getColumnIndex( 1598 BluetoothMapContract.MessageColumns.SUBJECT)); 1599 String address = c.getString(c.getColumnIndex( 1600 BluetoothMapContract.MessageColumns.FROM_LIST)); 1601 String priority = "no"; 1602 if(c.getInt(c.getColumnIndex( 1603 BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY)) 1604 == 1) 1605 priority = "yes"; 1606 if (mMapEventReportVersion == 1607 BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 1608 evt = new Event(EVENT_TYPE_NEW, id, newFolder, 1609 mAccount.getType(), date, subject, address, priority); 1610 } else { 1611 long thread_id = c.getLong(c.getColumnIndex( 1612 BluetoothMapContract.MessageColumns.THREAD_ID)); 1613 String thread_name = c.getString(c.getColumnIndex( 1614 BluetoothMapContract.MessageColumns.THREAD_NAME)); 1615 evt = new Event(EVENT_TYPE_NEW, id, newFolder, 1616 mAccount.getType(), date, subject, address, priority, 1617 thread_id, thread_name); 1618 } 1619 } else { 1620 evt = new Event(EVENT_TYPE_NEW, id, newFolder, null, TYPE.EMAIL); 1621 } 1622 sendEvent(evt); 1623 } else { 1624 /* Existing message */ 1625 if (folderId != msg.folderId && msg.folderId != -1) { 1626 if (D) Log.d(TAG, "new folderId: " + folderId + " old folderId: " 1627 + msg.folderId); 1628 BluetoothMapFolderElement oldFolderElement = 1629 mFolders.getFolderById(msg.folderId); 1630 String oldFolder; 1631 listChanged = true; 1632 if(oldFolderElement != null) { 1633 oldFolder = oldFolderElement.getFullPath(); 1634 } else { 1635 // This can happen if a new folder is created while connected 1636 oldFolder = "unknown"; 1637 } 1638 BluetoothMapFolderElement deletedFolder = 1639 mFolders.getFolderByName( 1640 BluetoothMapContract.FOLDER_NAME_DELETED); 1641 BluetoothMapFolderElement sentFolder = 1642 mFolders.getFolderByName( 1643 BluetoothMapContract.FOLDER_NAME_SENT); 1644 /* 1645 * If the folder is now 'deleted', send a deleted-event in stead of 1646 * a shift or if message is sent initiated by MAP Client, then send 1647 * sending-success otherwise send folderShift 1648 */ 1649 if(deletedFolder != null && deletedFolder.getFolderId() 1650 == folderId) { 1651 Event evt = new Event(EVENT_TYPE_DELETE, msg.id, newFolder, 1652 oldFolder, mAccount.getType()); 1653 sendEvent(evt); 1654 } else if(sentFolder != null 1655 && sentFolder.getFolderId() == folderId 1656 && msg.localInitiatedSend == true) { 1657 if(msg.transparent) { 1658 mResolver.delete( 1659 ContentUris.withAppendedId(mMessageUri, id), 1660 null, null); 1661 } else { 1662 msg.localInitiatedSend = false; 1663 Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, 1664 oldFolder, null, mAccount.getType()); 1665 sendEvent(evt); 1666 } 1667 } else { 1668 if (!oldFolder.equalsIgnoreCase("root")) { 1669 Event evt = new Event(EVENT_TYPE_SHIFT, id, newFolder, 1670 oldFolder, mAccount.getType()); 1671 sendEvent(evt); 1672 } 1673 } 1674 msg.folderId = folderId; 1675 } 1676 if(readFlag != msg.flagRead) { 1677 listChanged = true; 1678 1679 if (mMapEventReportVersion > 1680 BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1681 Event evt = new Event(EVENT_TYPE_READ_STATUS, id, newFolder, 1682 mAccount.getType()); 1683 sendEvent(evt); 1684 msg.flagRead = readFlag; 1685 } 1686 } 1687 1688 msgList.put(id, msg); 1689 } 1690 } while (c.moveToNext()); 1691 } 1692 } finally { 1693 if (c != null) c.close(); 1694 } 1695 // For all messages no longer in the database send a delete notification 1696 for (Msg msg : getMsgListMsg().values()) { 1697 BluetoothMapFolderElement oldFolderElement = mFolders.getFolderById(msg.folderId); 1698 String oldFolder; 1699 listChanged = true; 1700 if(oldFolderElement != null) { 1701 oldFolder = oldFolderElement.getFullPath(); 1702 } else { 1703 oldFolder = "unknown"; 1704 } 1705 /* Some e-mail clients delete the message after sending, and creates a 1706 * new message in sent. We cannot track the message anymore, hence send both a 1707 * send success and delete message. 1708 */ 1709 if(msg.localInitiatedSend == true) { 1710 msg.localInitiatedSend = false; 1711 // If message is send with transparency don't set folder as message is deleted 1712 if (msg.transparent) 1713 oldFolder = null; 1714 Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, oldFolder, null, 1715 mAccount.getType()); 1716 sendEvent(evt); 1717 } 1718 /* As this message deleted is only send on a real delete - don't set folder. 1719 * - only send delete event if message is not sent with transparency 1720 */ 1721 if (!msg.transparent) { 1722 1723 Event evt = new Event(EVENT_TYPE_DELETE, msg.id, null, oldFolder, 1724 mAccount.getType()); 1725 sendEvent(evt); 1726 } 1727 } 1728 setMsgListMsg(msgList, listChanged); 1729 } 1730 } 1731 1732 private void handleMsgListChanges(Uri uri) { 1733 if(uri.getAuthority().equals(mAuthority)) { 1734 try { 1735 if(D) Log.d(TAG, "handleMsgListChanges: account type = " 1736 + mAccount.getType().toString()); 1737 handleMsgListChangesMsg(uri); 1738 } catch(RemoteException e) { 1739 mMasInstance.restartObexServerSession(); 1740 Log.w(TAG, "Problems contacting the ContentProvider in mas Instance " 1741 + mMasId + " restaring ObexServerSession"); 1742 } 1743 1744 } 1745 // TODO: check to see if there could be problem with IM and SMS in one instance 1746 if (mEnableSmsMms) { 1747 handleMsgListChangesSms(); 1748 handleMsgListChangesMms(); 1749 } 1750 } 1751 1752 private void handleContactListChanges(Uri uri) { 1753 if (uri.getAuthority().equals(mAuthority)) { 1754 try { 1755 if (V) Log.v(TAG,"handleContactListChanges uri: " + uri.toString()); 1756 Cursor c = null; 1757 boolean listChanged = false; 1758 try { 1759 ConvoContactInfo cInfo = new ConvoContactInfo(); 1760 1761 if (mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V10 1762 && mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 1763 c = mProviderClient 1764 .query(mContactUri, 1765 BluetoothMapContract. 1766 BT_CONTACT_CHATSTATE_PRESENCE_PROJECTION, 1767 null, null, null); 1768 cInfo.setConvoColunms(c); 1769 } else { 1770 if (V) Log.v(TAG,"handleContactListChanges MAP version does not" + 1771 "support convocontact notifications"); 1772 return; 1773 } 1774 1775 HashMap<String, BluetoothMapConvoContactElement> contactList = 1776 new HashMap<String, 1777 BluetoothMapConvoContactElement>(getContactList().size()); 1778 1779 synchronized (getContactList()) { 1780 if (c != null && c.moveToFirst()) { 1781 do { 1782 String uci = c.getString(cInfo.mContactColUci); 1783 long convoId = c.getLong(cInfo.mContactColConvoId); 1784 if (convoId == 0) 1785 continue; 1786 1787 if (V) BluetoothMapUtils.printCursor(c); 1788 1789 BluetoothMapConvoContactElement contact = 1790 getContactList().remove(uci); 1791 1792 /* 1793 * We must filter out any actions made by the 1794 * MCE, hence do not send e.g. a message deleted 1795 * and/or MessageShift for messages deleted by 1796 * the MCE. 1797 */ 1798 if (contact == null) { 1799 listChanged = true; 1800 /* 1801 * New contact - added to conversation and 1802 * tracked here 1803 */ 1804 if (mMapEventReportVersion 1805 != BluetoothMapUtils.MAP_EVENT_REPORT_V10 1806 && mMapEventReportVersion 1807 != BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 1808 Event evt; 1809 String name = c 1810 .getString(cInfo.mContactColName); 1811 String displayName = c 1812 .getString(cInfo.mContactColNickname); 1813 String presenceStatus = c 1814 .getString(cInfo.mContactColPresenceText); 1815 int presenceState = c 1816 .getInt(cInfo.mContactColPresenceState); 1817 long lastActivity = c 1818 .getLong(cInfo.mContactColLastActive); 1819 int chatState = c 1820 .getInt(cInfo.mContactColChatState); 1821 int priority = c 1822 .getInt(cInfo.mContactColPriority); 1823 String btUid = c 1824 .getString(cInfo.mContactColBtUid); 1825 1826 // Get Conversation information for 1827 // event 1828// Uri convoUri = Uri 1829// .parse(mAccount.mBase_uri 1830// + "/" 1831// + BluetoothMapContract.TABLE_CONVERSATION); 1832// String whereClause = "contacts._id = " 1833// + convoId; 1834// Cursor cConvo = mProviderClient 1835// .query(convoUri, 1836// BluetoothMapContract.BT_CONVERSATION_PROJECTION, 1837// whereClause, null, null); 1838 // TODO: will move out of the loop when merged with CB's 1839 // changes make sure to look up col index out side loop 1840 String convoName = null; 1841// if (cConvo != null 1842// && cConvo.moveToFirst()) { 1843// convoName = cConvo 1844// .getString(cConvo 1845// .getColumnIndex(BluetoothMapContract.ConvoContactColumns.NAME)); 1846// } 1847 1848 contact = new BluetoothMapConvoContactElement( 1849 uci, name, displayName, 1850 presenceStatus, presenceState, 1851 lastActivity, chatState, 1852 priority, btUid); 1853 1854 contactList.put(uci, contact); 1855 1856 evt = new Event( 1857 EVENT_TYPE_CONVERSATION, 1858 uci, 1859 mAccount.getType(), 1860 name, 1861 String.valueOf(priority), 1862 BluetoothMapUtils 1863 .getDateTimeString(lastActivity), 1864 convoId, convoName, 1865 presenceState, presenceStatus, 1866 chatState); 1867 1868 sendEvent(evt); 1869 } 1870 1871 } else { 1872 // Not new - compare updated content 1873// Uri convoUri = Uri 1874// .parse(mAccount.mBase_uri 1875// + "/" 1876// + BluetoothMapContract.TABLE_CONVERSATION); 1877 // TODO: Should be changed to own provider interface name 1878// String whereClause = "contacts._id = " 1879// + convoId; 1880// Cursor cConvo = mProviderClient 1881// .query(convoUri, 1882// BluetoothMapContract.BT_CONVERSATION_PROJECTION, 1883// whereClause, null, null); 1884// // TODO: will move out of the loop when merged with CB's 1885// // changes make sure to look up col index out side loop 1886 String convoName = null; 1887// if (cConvo != null && cConvo.moveToFirst()) { 1888// convoName = cConvo 1889// .getString(cConvo 1890// .getColumnIndex(BluetoothMapContract.ConvoContactColumns.NAME)); 1891// } 1892 1893 // Check if presence is updated 1894 int presenceState = c.getInt(cInfo.mContactColPresenceState); 1895 String presenceStatus = c.getString( 1896 cInfo.mContactColPresenceText); 1897 String currentPresenceStatus = contact 1898 .getPresenceStatus(); 1899 if (contact.getPresenceAvailability() != presenceState 1900 || currentPresenceStatus != presenceStatus) { 1901 long lastOnline = c 1902 .getLong(cInfo.mContactColLastOnline); 1903 contact.setPresenceAvailability(presenceState); 1904 contact.setLastActivity(lastOnline); 1905 if (currentPresenceStatus != null 1906 && !currentPresenceStatus 1907 .equals(presenceStatus)) { 1908 contact.setPresenceStatus(presenceStatus); 1909 } 1910 Event evt = new Event( 1911 EVENT_TYPE_PRESENCE, 1912 uci, 1913 mAccount.getType(), 1914 contact.getName(), 1915 String.valueOf(contact 1916 .getPriority()), 1917 BluetoothMapUtils 1918 .getDateTimeString(lastOnline), 1919 convoId, convoName, 1920 presenceState, presenceStatus, 1921 0); 1922 sendEvent(evt); 1923 } 1924 1925 // Check if chat state is updated 1926 int chatState = c.getInt(cInfo.mContactColChatState); 1927 if (contact.getChatState() != chatState) { 1928 // Get DB timestamp 1929 long lastActivity = c.getLong(cInfo.mContactColLastActive); 1930 contact.setLastActivity(lastActivity); 1931 contact.setChatState(chatState); 1932 Event evt = new Event( 1933 EVENT_TYPE_CHAT_STATE, 1934 uci, 1935 mAccount.getType(), 1936 contact.getName(), 1937 String.valueOf(contact 1938 .getPriority()), 1939 BluetoothMapUtils 1940 .getDateTimeString(lastActivity), 1941 convoId, convoName, 0, null, 1942 chatState); 1943 sendEvent(evt); 1944 } 1945 contactList.put(uci, contact); 1946 } 1947 } while (c.moveToNext()); 1948 } 1949 if(getContactList().size() > 0) { 1950 // one or more contacts were deleted, hence the conversation listing 1951 // version counter should change. 1952 listChanged = true; 1953 } 1954 setContactList(contactList, listChanged); 1955 } // end synchronized 1956 } finally { 1957 if (c != null) c.close(); 1958 } 1959 } catch (RemoteException e) { 1960 mMasInstance.restartObexServerSession(); 1961 Log.w(TAG, 1962 "Problems contacting the ContentProvider in mas Instance " 1963 + mMasId + " restaring ObexServerSession"); 1964 } 1965 1966 } 1967 // TODO: conversation contact updates if IM and SMS(MMS in one instance 1968 } 1969 1970 private boolean setEmailMessageStatusDelete(BluetoothMapFolderElement mCurrentFolder, 1971 String uriStr, long handle, int status) { 1972 boolean res = false; 1973 Uri uri = Uri.parse(uriStr + BluetoothMapContract.TABLE_MESSAGE); 1974 1975 int updateCount = 0; 1976 ContentValues contentValues = new ContentValues(); 1977 BluetoothMapFolderElement deleteFolder = mFolders. 1978 getFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED); 1979 contentValues.put(BluetoothMapContract.MessageColumns._ID, handle); 1980 synchronized(getMsgListMsg()) { 1981 Msg msg = getMsgListMsg().get(handle); 1982 if (status == BluetoothMapAppParams.STATUS_VALUE_YES) { 1983 /* Set deleted folder id */ 1984 long folderId = -1; 1985 if(deleteFolder != null) { 1986 folderId = deleteFolder.getFolderId(); 1987 } 1988 contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID,folderId); 1989 updateCount = mResolver.update(uri, contentValues, null, null); 1990 /* The race between updating the value in our cached values and the database 1991 * is handled by the synchronized statement. */ 1992 if(updateCount > 0) { 1993 res = true; 1994 if (msg != null) { 1995 msg.oldFolderId = msg.folderId; 1996 /* Update the folder ID to avoid triggering an event for MCE 1997 * initiated actions. */ 1998 msg.folderId = folderId; 1999 } 2000 if(D) Log.d(TAG, "Deleted MSG: " + handle + " from folderId: " + folderId); 2001 } else { 2002 Log.w(TAG, "Msg: " + handle + " - Set delete status " + status 2003 + " failed for folderId " + folderId); 2004 } 2005 } else if (status == BluetoothMapAppParams.STATUS_VALUE_NO) { 2006 /* Undelete message. move to old folder if we know it, 2007 * else move to inbox - as dictated by the spec. */ 2008 if(msg != null && deleteFolder != null && 2009 msg.folderId == deleteFolder.getFolderId()) { 2010 /* Only modify messages in the 'Deleted' folder */ 2011 long folderId = -1; 2012 BluetoothMapFolderElement inboxFolder = mCurrentFolder. 2013 getFolderByName(BluetoothMapContract.FOLDER_NAME_INBOX); 2014 if (msg != null && msg.oldFolderId != -1) { 2015 folderId = msg.oldFolderId; 2016 } else { 2017 if(inboxFolder != null) { 2018 folderId = inboxFolder.getFolderId(); 2019 } 2020 if(D)Log.d(TAG,"We did not delete the message, hence the old folder " + 2021 "is unknown. Moving to inbox."); 2022 } 2023 contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId); 2024 updateCount = mResolver.update(uri, contentValues, null, null); 2025 if(updateCount > 0) { 2026 res = true; 2027 /* Update the folder ID to avoid triggering an event for MCE 2028 * initiated actions. */ 2029 /* UPDATE: Actually the BT-Spec. states that an undelete is a move of the 2030 * message to INBOX - clearified in errata 5591. 2031 * Therefore we update the cache to INBOX-folderId - to trigger a message 2032 * shift event to the old-folder. */ 2033 if(inboxFolder != null) { 2034 msg.folderId = inboxFolder.getFolderId(); 2035 } else { 2036 msg.folderId = folderId; 2037 } 2038 } else { 2039 if(D)Log.d(TAG,"We did not delete the message, hence the old folder " + 2040 "is unknown. Moving to inbox."); 2041 } 2042 } 2043 } 2044 if(V) { 2045 BluetoothMapFolderElement folderElement; 2046 String folderName = "unknown"; 2047 if (msg != null) { 2048 folderElement = mCurrentFolder.getFolderById(msg.folderId); 2049 if(folderElement != null) { 2050 folderName = folderElement.getName(); 2051 } 2052 } 2053 Log.d(TAG,"setEmailMessageStatusDelete: " + handle + " from " + folderName 2054 + " status: " + status); 2055 } 2056 } 2057 if(res == false) { 2058 Log.w(TAG, "Set delete status " + status + " failed."); 2059 } 2060 return res; 2061 } 2062 2063 private void updateThreadId(Uri uri, String valueString, long threadId) { 2064 ContentValues contentValues = new ContentValues(); 2065 contentValues.put(valueString, threadId); 2066 mResolver.update(uri, contentValues, null, null); 2067 } 2068 2069 private boolean deleteMessageMms(long handle) { 2070 boolean res = false; 2071 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); 2072 Cursor c = mResolver.query(uri, null, null, null, null); 2073 try { 2074 if (c != null && c.moveToFirst()) { 2075 /* Move to deleted folder, or delete if already in deleted folder */ 2076 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); 2077 if (threadId != DELETED_THREAD_ID) { 2078 /* Set deleted thread id */ 2079 synchronized(getMsgListMms()) { 2080 Msg msg = getMsgListMms().get(handle); 2081 if(msg != null) { // This will always be the case 2082 msg.threadId = DELETED_THREAD_ID; 2083 } 2084 } 2085 updateThreadId(uri, Mms.THREAD_ID, DELETED_THREAD_ID); 2086 } else { 2087 /* Delete from observer message list to avoid delete notifications */ 2088 synchronized(getMsgListMms()) { 2089 getMsgListMms().remove(handle); 2090 } 2091 /* Delete message */ 2092 mResolver.delete(uri, null, null); 2093 } 2094 res = true; 2095 } 2096 } finally { 2097 if (c != null) c.close(); 2098 } 2099 2100 return res; 2101 } 2102 2103 private boolean unDeleteMessageMms(long handle) { 2104 boolean res = false; 2105 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); 2106 Cursor c = mResolver.query(uri, null, null, null, null); 2107 try { 2108 if (c != null && c.moveToFirst()) { 2109 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); 2110 if (threadId == DELETED_THREAD_ID) { 2111 /* Restore thread id from address, or if no thread for address 2112 * create new thread by insert and remove of fake message */ 2113 String address; 2114 long id = c.getLong(c.getColumnIndex(Mms._ID)); 2115 int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); 2116 if (msgBox == Mms.MESSAGE_BOX_INBOX) { 2117 address = BluetoothMapContent.getAddressMms(mResolver, id, 2118 BluetoothMapContent.MMS_FROM); 2119 } else { 2120 address = BluetoothMapContent.getAddressMms(mResolver, id, 2121 BluetoothMapContent.MMS_TO); 2122 } 2123 Set<String> recipients = new HashSet<String>(); 2124 recipients.addAll(Arrays.asList(address)); 2125 Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients); 2126 synchronized(getMsgListMms()) { 2127 Msg msg = getMsgListMms().get(handle); 2128 if(msg != null) { // This will always be the case 2129 msg.threadId = oldThreadId.intValue(); 2130 // Spec. states that undelete shall shift the message to Inbox. 2131 // Hence we need to trigger a message shift from INBOX to old-folder 2132 // after undelete. 2133 // We do this by changing the cached folder value to being inbox - hence 2134 // the event handler will se the update as the message have been shifted 2135 // from INBOX to old-folder. (Errata 5591 clearifies this) 2136 msg.type = Mms.MESSAGE_BOX_INBOX; 2137 } 2138 } 2139 updateThreadId(uri, Mms.THREAD_ID, oldThreadId); 2140 } else { 2141 Log.d(TAG, "Message not in deleted folder: handle " + handle 2142 + " threadId " + threadId); 2143 } 2144 res = true; 2145 } 2146 } finally { 2147 if (c != null) c.close(); 2148 } 2149 return res; 2150 } 2151 2152 private boolean deleteMessageSms(long handle) { 2153 boolean res = false; 2154 Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); 2155 Cursor c = mResolver.query(uri, null, null, null, null); 2156 try { 2157 if (c != null && c.moveToFirst()) { 2158 /* Move to deleted folder, or delete if already in deleted folder */ 2159 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); 2160 if (threadId != DELETED_THREAD_ID) { 2161 synchronized(getMsgListSms()) { 2162 Msg msg = getMsgListSms().get(handle); 2163 if(msg != null) { // This will always be the case 2164 msg.threadId = DELETED_THREAD_ID; 2165 } 2166 } 2167 /* Set deleted thread id */ 2168 updateThreadId(uri, Sms.THREAD_ID, DELETED_THREAD_ID); 2169 } else { 2170 /* Delete from observer message list to avoid delete notifications */ 2171 synchronized(getMsgListSms()) { 2172 getMsgListSms().remove(handle); 2173 } 2174 /* Delete message */ 2175 mResolver.delete(uri, null, null); 2176 } 2177 res = true; 2178 } 2179 } finally { 2180 if (c != null) c.close(); 2181 } 2182 return res; 2183 } 2184 2185 private boolean unDeleteMessageSms(long handle) { 2186 boolean res = false; 2187 Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); 2188 Cursor c = mResolver.query(uri, null, null, null, null); 2189 try { 2190 if (c != null && c.moveToFirst()) { 2191 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); 2192 if (threadId == DELETED_THREAD_ID) { 2193 String address = c.getString(c.getColumnIndex(Sms.ADDRESS)); 2194 Set<String> recipients = new HashSet<String>(); 2195 recipients.addAll(Arrays.asList(address)); 2196 Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients); 2197 synchronized(getMsgListSms()) { 2198 Msg msg = getMsgListSms().get(handle); 2199 if(msg != null) { 2200 msg.threadId = oldThreadId.intValue(); 2201 /* This will always be the case 2202 * The threadId is specified as an int, so it is safe to truncate 2203 * TODO: Test that this will trigger a message-shift from Inbox 2204 * to old-folder 2205 **/ 2206 /* Spec. states that undelete shall shift the message to Inbox. 2207 * Hence we need to trigger a message shift from INBOX to old-folder 2208 * after undelete. 2209 * We do this by changing the cached folder value to being inbox - hence 2210 * the event handler will se the update as the message have been shifted 2211 * from INBOX to old-folder. (Errata 5591 clearifies this) 2212 * */ 2213 msg.type = Sms.MESSAGE_TYPE_INBOX; 2214 } 2215 } 2216 updateThreadId(uri, Sms.THREAD_ID, oldThreadId); 2217 } else { 2218 Log.d(TAG, "Message not in deleted folder: handle " + handle 2219 + " threadId " + threadId); 2220 } 2221 res = true; 2222 } 2223 } finally { 2224 if (c != null) c.close(); 2225 } 2226 return res; 2227 } 2228 2229 /** 2230 * 2231 * @param handle 2232 * @param type 2233 * @param mCurrentFolder 2234 * @param uriStr 2235 * @param statusValue 2236 * @return true is success 2237 */ 2238 public boolean setMessageStatusDeleted(long handle, TYPE type, 2239 BluetoothMapFolderElement mCurrentFolder, String uriStr, int statusValue) { 2240 boolean res = false; 2241 if (D) Log.d(TAG, "setMessageStatusDeleted: handle " + handle 2242 + " type " + type + " value " + statusValue); 2243 2244 if (type == TYPE.EMAIL) { 2245 res = setEmailMessageStatusDelete(mCurrentFolder, uriStr, handle, statusValue); 2246 } else if (type == TYPE.IM) { 2247 // TODO: to do when deleting IM message 2248 if (D) Log.d(TAG, "setMessageStatusDeleted: IM not handled" ); 2249 } else { 2250 if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) { 2251 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { 2252 res = deleteMessageSms(handle); 2253 } else if (type == TYPE.MMS) { 2254 res = deleteMessageMms(handle); 2255 } 2256 } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) { 2257 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { 2258 res = unDeleteMessageSms(handle); 2259 } else if (type == TYPE.MMS) { 2260 res = unDeleteMessageMms(handle); 2261 } 2262 } 2263 } 2264 return res; 2265 } 2266 2267 /** 2268 * 2269 * @param handle 2270 * @param type 2271 * @param uriStr 2272 * @param statusValue 2273 * @return true at success 2274 */ 2275 public boolean setMessageStatusRead(long handle, TYPE type, String uriStr, int statusValue) 2276 throws RemoteException{ 2277 int count = 0; 2278 2279 if (D) Log.d(TAG, "setMessageStatusRead: handle " + handle 2280 + " type " + type + " value " + statusValue); 2281 2282 /* Approved MAP spec errata 3445 states that read status initiated 2283 * by the MCE shall change the MSE read status. */ 2284 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { 2285 Uri uri = Sms.Inbox.CONTENT_URI; 2286 ContentValues contentValues = new ContentValues(); 2287 contentValues.put(Sms.READ, statusValue); 2288 contentValues.put(Sms.SEEN, statusValue); 2289 String where = Sms._ID+"="+handle; 2290 String values = contentValues.toString(); 2291 if (D) Log.d(TAG, " -> SMS Uri: " + uri.toString() + 2292 " Where " + where + " values " + values); 2293 synchronized(getMsgListSms()) { 2294 Msg msg = getMsgListSms().get(handle); 2295 if(msg != null) { // This will always be the case 2296 msg.flagRead = statusValue; 2297 } 2298 } 2299 count = mResolver.update(uri, contentValues, where, null); 2300 if (D) Log.d(TAG, " -> "+count +" rows updated!"); 2301 2302 } else if (type == TYPE.MMS) { 2303 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); 2304 if (D) Log.d(TAG, " -> MMS Uri: " + uri.toString()); 2305 ContentValues contentValues = new ContentValues(); 2306 contentValues.put(Mms.READ, statusValue); 2307 synchronized(getMsgListMms()) { 2308 Msg msg = getMsgListMms().get(handle); 2309 if(msg != null) { // This will always be the case 2310 msg.flagRead = statusValue; 2311 } 2312 } 2313 count = mResolver.update(uri, contentValues, null, null); 2314 if (D) Log.d(TAG, " -> "+count +" rows updated!"); 2315 } else if (type == TYPE.EMAIL || 2316 type == TYPE.IM) { 2317 Uri uri = mMessageUri; 2318 ContentValues contentValues = new ContentValues(); 2319 contentValues.put(BluetoothMapContract.MessageColumns.FLAG_READ, statusValue); 2320 contentValues.put(BluetoothMapContract.MessageColumns._ID, handle); 2321 synchronized(getMsgListMsg()) { 2322 Msg msg = getMsgListMsg().get(handle); 2323 if(msg != null) { // This will always be the case 2324 msg.flagRead = statusValue; 2325 } 2326 } 2327 count = mProviderClient.update(uri, contentValues, null, null); 2328 } 2329 2330 return (count > 0); 2331 } 2332 2333 private class PushMsgInfo { 2334 long id; 2335 int transparent; 2336 int retry; 2337 String phone; 2338 Uri uri; 2339 long timestamp; 2340 int parts; 2341 int partsSent; 2342 int partsDelivered; 2343 boolean resend; 2344 boolean sendInProgress; 2345 boolean failedSent; // Set to true if a single part sent fail is received. 2346 int statusDelivered; // Set to != 0 if a single part deliver fail is received. 2347 2348 public PushMsgInfo(long id, int transparent, 2349 int retry, String phone, Uri uri) { 2350 this.id = id; 2351 this.transparent = transparent; 2352 this.retry = retry; 2353 this.phone = phone; 2354 this.uri = uri; 2355 this.resend = false; 2356 this.sendInProgress = false; 2357 this.failedSent = false; 2358 this.statusDelivered = 0; /* Assume success */ 2359 this.timestamp = 0; 2360 }; 2361 } 2362 2363 private Map<Long, PushMsgInfo> mPushMsgList = 2364 Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>()); 2365 2366 public long pushMessage(BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement, 2367 BluetoothMapAppParams ap, String emailBaseUri) 2368 throws IllegalArgumentException, RemoteException, IOException { 2369 if (D) Log.d(TAG, "pushMessage"); 2370 ArrayList<BluetoothMapbMessage.vCard> recipientList = msg.getRecipients(); 2371 int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ? 2372 0 : ap.getTransparent(); 2373 int retry = ap.getRetry(); 2374 int charset = ap.getCharset(); 2375 long handle = -1; 2376 long folderId = -1; 2377 2378 if (recipientList == null) { 2379 if (D) Log.d(TAG, "empty recipient list"); 2380 return -1; 2381 } 2382 2383 if ( msg.getType().equals(TYPE.EMAIL) ) { 2384 /* Write the message to the database */ 2385 String msgBody = ((BluetoothMapbMessageEmail) msg).getEmailBody(); 2386 if (V) { 2387 int length = msgBody.length(); 2388 Log.v(TAG, "pushMessage: message string length = " + length); 2389 String messages[] = msgBody.split("\r\n"); 2390 Log.v(TAG, "pushMessage: messages count=" + messages.length); 2391 for(int i = 0; i < messages.length; i++) { 2392 Log.v(TAG, "part " + i + ":" + messages[i]); 2393 } 2394 } 2395 FileOutputStream os = null; 2396 ParcelFileDescriptor fdOut = null; 2397 Uri uriInsert = Uri.parse(emailBaseUri + BluetoothMapContract.TABLE_MESSAGE); 2398 if (D) Log.d(TAG, "pushMessage - uriInsert= " + uriInsert.toString() + 2399 ", intoFolder id=" + folderElement.getFolderId()); 2400 2401 synchronized(getMsgListMsg()) { 2402 // Now insert the empty message into folder 2403 ContentValues values = new ContentValues(); 2404 folderId = folderElement.getFolderId(); 2405 values.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId); 2406 Uri uriNew = mProviderClient.insert(uriInsert, values); 2407 if (D) Log.d(TAG, "pushMessage - uriNew= " + uriNew.toString()); 2408 handle = Long.parseLong(uriNew.getLastPathSegment()); 2409 2410 try { 2411 fdOut = mProviderClient.openFile(uriNew, "w"); 2412 os = new FileOutputStream(fdOut.getFileDescriptor()); 2413 // Write Email to DB 2414 os.write(msgBody.getBytes(), 0, msgBody.getBytes().length); 2415 } catch (FileNotFoundException e) { 2416 Log.w(TAG, e); 2417 throw(new IOException("Unable to open file stream")); 2418 } catch (NullPointerException e) { 2419 Log.w(TAG, e); 2420 throw(new IllegalArgumentException("Unable to parse message.")); 2421 } finally { 2422 try { 2423 if(os != null) 2424 os.close(); 2425 } catch (IOException e) {Log.w(TAG, e);} 2426 try { 2427 if(fdOut != null) 2428 fdOut.close(); 2429 } catch (IOException e) {Log.w(TAG, e);} 2430 } 2431 2432 /* Extract the data for the inserted message, and store in local mirror, to 2433 * avoid sending a NewMessage Event. */ 2434 /*TODO: We need to add the new 1.1 parameter as well:-) e.g. read*/ 2435 Msg newMsg = new Msg(handle, folderId, 1); // TODO: Create define for read-state 2436 newMsg.transparent = (transparent == 1) ? true : false; 2437 if ( folderId == folderElement.getFolderByName( 2438 BluetoothMapContract.FOLDER_NAME_OUTBOX).getFolderId() ) { 2439 newMsg.localInitiatedSend = true; 2440 } 2441 getMsgListMsg().put(handle, newMsg); 2442 } 2443 } else { // type SMS_* of MMS 2444 for (BluetoothMapbMessage.vCard recipient : recipientList) { 2445 // Only send the message to the top level recipient 2446 if(recipient.getEnvLevel() == 0) 2447 { 2448 /* Only send to first address */ 2449 String phone = recipient.getFirstPhoneNumber(); 2450 String email = recipient.getFirstEmail(); 2451 String folder = folderElement.getName(); 2452 boolean read = false; 2453 boolean deliveryReport = true; 2454 String msgBody = null; 2455 2456 /* If MMS contains text only and the size is less than ten SMS's 2457 * then convert the MMS to type SMS and then proceed 2458 */ 2459 if (msg.getType().equals(TYPE.MMS) && 2460 (((BluetoothMapbMessageMime) msg).getTextOnly() == true)) { 2461 msgBody = ((BluetoothMapbMessageMime) msg).getMessageAsText(); 2462 SmsManager smsMng = SmsManager.getDefault(); 2463 ArrayList<String> parts = smsMng.divideMessage(msgBody); 2464 int smsParts = parts.size(); 2465 if (smsParts <= CONVERT_MMS_TO_SMS_PART_COUNT ) { 2466 if (D) Log.d(TAG, "pushMessage - converting MMS to SMS, sms parts=" 2467 + smsParts ); 2468 msg.setType(mSmsType); 2469 } else { 2470 if (D) Log.d(TAG, "pushMessage - MMS text only but to big to " + 2471 "convert to SMS"); 2472 msgBody = null; 2473 } 2474 2475 } 2476 2477 if (msg.getType().equals(TYPE.MMS)) { 2478 /* Send message if folder is outbox else just store in draft*/ 2479 handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMime)msg); 2480 } else if (msg.getType().equals(TYPE.SMS_GSM) || 2481 msg.getType().equals(TYPE.SMS_CDMA) ) { 2482 /* Add the message to the database */ 2483 if(msgBody == null) 2484 msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody(); 2485 2486 /* We need to lock the SMS list while updating the database, 2487 * to avoid sending events on MCE initiated operation. */ 2488 Uri contentUri = Uri.parse(Sms.CONTENT_URI+ "/" + folder); 2489 Uri uri; 2490 synchronized(getMsgListSms()) { 2491 uri = Sms.addMessageToUri(mResolver, contentUri, phone, msgBody, 2492 "", System.currentTimeMillis(), read, deliveryReport); 2493 2494 if(V) Log.v(TAG, "Sms.addMessageToUri() returned: " + uri); 2495 if (uri == null) { 2496 if (D) Log.d(TAG, "pushMessage - failure on add to uri " 2497 + contentUri); 2498 return -1; 2499 } 2500 Cursor c = mResolver.query(uri, SMS_PROJECTION_SHORT, null, null, null); 2501 2502 /* Extract the data for the inserted message, and store in local mirror, 2503 * to avoid sending a NewMessage Event. */ 2504 try { 2505 if (c != null && c.moveToFirst()) { 2506 long id = c.getLong(c.getColumnIndex(Sms._ID)); 2507 int type = c.getInt(c.getColumnIndex(Sms.TYPE)); 2508 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); 2509 int readFlag = c.getInt(c.getColumnIndex(Sms.READ)); 2510 if(V) Log.v(TAG, "add message with id=" + id + 2511 " type=" + type + " threadId=" + threadId + 2512 " readFlag=" + readFlag + "to mMsgListSms"); 2513 Msg newMsg = new Msg(id, type, threadId, readFlag); 2514 getMsgListSms().put(id, newMsg); 2515 c.close(); 2516 } else { 2517 Log.w(TAG,"Message: " + uri + " no longer exist!"); 2518 /* This can only happen, if the message is deleted 2519 * just as it is added */ 2520 return -1; 2521 } 2522 } finally { 2523 if (c != null) c.close(); 2524 } 2525 2526 handle = Long.parseLong(uri.getLastPathSegment()); 2527 2528 /* Send message if folder is outbox */ 2529 if (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)) { 2530 PushMsgInfo msgInfo = new PushMsgInfo(handle, transparent, 2531 retry, phone, uri); 2532 mPushMsgList.put(handle, msgInfo); 2533 sendMessage(msgInfo, msgBody); 2534 if(V) Log.v(TAG, "sendMessage returned..."); 2535 } /* else just added to draft */ 2536 2537 /* sendMessage causes the message to be deleted and reinserted, 2538 * hence we need to lock the list while this is happening. */ 2539 } 2540 } else { 2541 if (D) Log.d(TAG, "pushMessage - failure on type " ); 2542 return -1; 2543 } 2544 } 2545 } 2546 } 2547 2548 /* If multiple recipients return handle of last */ 2549 return handle; 2550 } 2551 2552 public long sendMmsMessage(String folder, String to_address, BluetoothMapbMessageMime msg) { 2553 /* 2554 *strategy: 2555 *1) parse message into parts 2556 *if folder is outbox/drafts: 2557 *2) push message to draft 2558 *if folder is outbox: 2559 *3) move message to outbox (to trigger the mms app to add msg to pending_messages list) 2560 *4) send intent to mms app in order to wake it up. 2561 *else if folder !outbox: 2562 *1) push message to folder 2563 * */ 2564 if (folder != null && (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX) 2565 || folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT))) { 2566 long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, to_address, msg); 2567 /* if invalid handle (-1) then just return the handle 2568 * - else continue sending (if folder is outbox) */ 2569 if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && 2570 folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)) { 2571 moveDraftToOutbox(handle); 2572 Intent sendIntent = new Intent("android.intent.action.MMS_SEND_OUTBOX_MSG"); 2573 if (D) Log.d(TAG, "broadcasting intent: "+sendIntent.toString()); 2574 mContext.sendBroadcast(sendIntent); 2575 } 2576 return handle; 2577 } else { 2578 /* not allowed to push mms to anything but outbox/draft */ 2579 throw new IllegalArgumentException("Cannot push message to other " + 2580 "folders than outbox/draft"); 2581 } 2582 } 2583 2584 private void moveDraftToOutbox(long handle) { 2585 /*Move message by changing the msg_box value in the content provider database */ 2586 if (handle != -1) { 2587 String whereClause = " _id= " + handle; 2588 Uri uri = Mms.CONTENT_URI; 2589 Cursor queryResult = mResolver.query(uri, null, whereClause, null, null); 2590 try { 2591 if (queryResult != null) { 2592 if (queryResult.getCount() > 0) { 2593 queryResult.moveToFirst(); 2594 ContentValues data = new ContentValues(); 2595 /* set folder to be outbox */ 2596 data.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_OUTBOX); 2597 mResolver.update(uri, data, whereClause, null); 2598 if (D) Log.d(TAG, "moved draft MMS to outbox"); 2599 } 2600 } else { 2601 if (D) Log.d(TAG, "Could not move draft to outbox "); 2602 } 2603 } finally { 2604 if (queryResult != null) queryResult.close(); 2605 } 2606 } 2607 } 2608 private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMime msg) { 2609 /** 2610 * strategy: 2611 * 1) parse msg into parts + header 2612 * 2) create thread id (abuse the ease of adding an SMS to get id for thread) 2613 * 3) push parts into content://mms/parts/ table 2614 * 3) 2615 */ 2616 2617 ContentValues values = new ContentValues(); 2618 values.put(Mms.MESSAGE_BOX, folder); 2619 values.put(Mms.READ, 0); 2620 values.put(Mms.SEEN, 0); 2621 if(msg.getSubject() != null) { 2622 values.put(Mms.SUBJECT, msg.getSubject()); 2623 } else { 2624 values.put(Mms.SUBJECT, ""); 2625 } 2626 2627 if(msg.getSubject() != null && msg.getSubject().length() > 0) { 2628 values.put(Mms.SUBJECT_CHARSET, 106); 2629 } 2630 values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related"); 2631 values.put(Mms.EXPIRY, 604800); 2632 values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR); 2633 values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ); 2634 values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION); 2635 values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL); 2636 values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO); 2637 values.put(Mms.TRANSACTION_ID, "T"+ Long.toHexString(System.currentTimeMillis())); 2638 values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO); 2639 values.put(Mms.LOCKED, 0); 2640 if(msg.getTextOnly() == true) 2641 values.put(Mms.TEXT_ONLY, true); 2642 values.put(Mms.MESSAGE_SIZE, msg.getSize()); 2643 2644 // Get thread id 2645 Set<String> recipients = new HashSet<String>(); 2646 recipients.addAll(Arrays.asList(to_address)); 2647 values.put(Mms.THREAD_ID, Telephony.Threads.getOrCreateThreadId(mContext, recipients)); 2648 Uri uri = Mms.CONTENT_URI; 2649 2650 synchronized (getMsgListMms()) { 2651 2652 uri = mResolver.insert(uri, values); 2653 2654 if (uri == null) { 2655 // unable to insert MMS 2656 Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri); 2657 return -1; 2658 } 2659 /* As we already have all the values we need, we could skip the query, but 2660 doing the query ensures we get any changes made by the content provider 2661 at insert. */ 2662 Cursor c = mResolver.query(uri, MMS_PROJECTION_SHORT, null, null, null); 2663 try { 2664 if (c != null && c.moveToFirst()) { 2665 long id = c.getLong(c.getColumnIndex(Mms._ID)); 2666 int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); 2667 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); 2668 2669 /* We must filter out any actions made by the MCE. Add the new message to 2670 * the list of known messages. */ 2671 2672 Msg newMsg = new Msg(id, type, threadId); 2673 newMsg.localInitiatedSend = true; 2674 getMsgListMms().put(id, newMsg); 2675 c.close(); 2676 } 2677 } finally { 2678 if (c != null) c.close(); 2679 } 2680 } // Done adding changes, unlock access to mMsgListMms to allow sending MMS events again 2681 2682 long handle = Long.parseLong(uri.getLastPathSegment()); 2683 if (V) Log.v(TAG, " NEW URI " + uri.toString()); 2684 2685 try { 2686 if(msg.getMimeParts() == null) { 2687 /* Perhaps this message have been deleted, and no longer have any content, 2688 * but only headers */ 2689 Log.w(TAG, "No MMS parts present..."); 2690 } else { 2691 if(V) Log.v(TAG, "Adding " + msg.getMimeParts().size() 2692 + " parts to the data base."); 2693 for(MimePart part : msg.getMimeParts()) { 2694 int count = 0; 2695 count++; 2696 values.clear(); 2697 if(part.mContentType != null && 2698 part.mContentType.toUpperCase().contains("TEXT")) { 2699 values.put(Mms.Part.CONTENT_TYPE, "text/plain"); 2700 values.put(Mms.Part.CHARSET, 106); 2701 if(part.mPartName != null) { 2702 values.put(Mms.Part.FILENAME, part.mPartName); 2703 values.put(Mms.Part.NAME, part.mPartName); 2704 } else { 2705 values.put(Mms.Part.FILENAME, "text_" + count +".txt"); 2706 values.put(Mms.Part.NAME, "text_" + count +".txt"); 2707 } 2708 // Ensure we have "ci" set 2709 if(part.mContentId != null) { 2710 values.put(Mms.Part.CONTENT_ID, part.mContentId); 2711 } else { 2712 if(part.mPartName != null) { 2713 values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">"); 2714 } else { 2715 values.put(Mms.Part.CONTENT_ID, "<text_" + count + ">"); 2716 } 2717 } 2718 // Ensure we have "cl" set 2719 if(part.mContentLocation != null) { 2720 values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); 2721 } else { 2722 if(part.mPartName != null) { 2723 values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".txt"); 2724 } else { 2725 values.put(Mms.Part.CONTENT_LOCATION, "text_" + count + ".txt"); 2726 } 2727 } 2728 2729 if(part.mContentDisposition != null) { 2730 values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); 2731 } 2732 values.put(Mms.Part.TEXT, part.getDataAsString()); 2733 uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part"); 2734 uri = mResolver.insert(uri, values); 2735 if(V) Log.v(TAG, "Added TEXT part"); 2736 2737 } else if (part.mContentType != null && 2738 part.mContentType.toUpperCase().contains("SMIL")){ 2739 values.put(Mms.Part.SEQ, -1); 2740 values.put(Mms.Part.CONTENT_TYPE, "application/smil"); 2741 if(part.mContentId != null) { 2742 values.put(Mms.Part.CONTENT_ID, part.mContentId); 2743 } else { 2744 values.put(Mms.Part.CONTENT_ID, "<smil_" + count + ">"); 2745 } 2746 if(part.mContentLocation != null) { 2747 values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); 2748 } else { 2749 values.put(Mms.Part.CONTENT_LOCATION, "smil_" + count + ".xml"); 2750 } 2751 2752 if(part.mContentDisposition != null) 2753 values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); 2754 values.put(Mms.Part.FILENAME, "smil.xml"); 2755 values.put(Mms.Part.NAME, "smil.xml"); 2756 values.put(Mms.Part.TEXT, new String(part.mData, "UTF-8")); 2757 2758 uri = Uri.parse(Mms.CONTENT_URI+ "/" + handle + "/part"); 2759 uri = mResolver.insert(uri, values); 2760 if (V) Log.v(TAG, "Added SMIL part"); 2761 2762 }else /*VIDEO/AUDIO/IMAGE*/ { 2763 writeMmsDataPart(handle, part, count); 2764 if (V) Log.v(TAG, "Added OTHER part"); 2765 } 2766 if (uri != null){ 2767 if (V) Log.v(TAG, "Added part with content-type: " + part.mContentType 2768 + " to Uri: " + uri.toString()); 2769 } 2770 } 2771 } 2772 } catch (UnsupportedEncodingException e) { 2773 Log.w(TAG, e); 2774 } catch (IOException e) { 2775 Log.w(TAG, e); 2776 } 2777 2778 values.clear(); 2779 values.put(Mms.Addr.CONTACT_ID, "null"); 2780 values.put(Mms.Addr.ADDRESS, "insert-address-token"); 2781 values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_FROM); 2782 values.put(Mms.Addr.CHARSET, 106); 2783 2784 uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr"); 2785 uri = mResolver.insert(uri, values); 2786 if (uri != null && V){ 2787 Log.v(TAG, " NEW URI " + uri.toString()); 2788 } 2789 2790 values.clear(); 2791 values.put(Mms.Addr.CONTACT_ID, "null"); 2792 values.put(Mms.Addr.ADDRESS, to_address); 2793 values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_TO); 2794 values.put(Mms.Addr.CHARSET, 106); 2795 2796 uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr"); 2797 uri = mResolver.insert(uri, values); 2798 if (uri != null && V){ 2799 Log.v(TAG, " NEW URI " + uri.toString()); 2800 } 2801 return handle; 2802 } 2803 2804 2805 private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException{ 2806 ContentValues values = new ContentValues(); 2807 values.put(Mms.Part.MSG_ID, handle); 2808 if(part.mContentType != null) { 2809 values.put(Mms.Part.CONTENT_TYPE, part.mContentType); 2810 } else { 2811 Log.w(TAG, "MMS has no CONTENT_TYPE for part " + count); 2812 } 2813 if(part.mContentId != null) { 2814 values.put(Mms.Part.CONTENT_ID, part.mContentId); 2815 } else { 2816 if(part.mPartName != null) { 2817 values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">"); 2818 } else { 2819 values.put(Mms.Part.CONTENT_ID, "<part_" + count + ">"); 2820 } 2821 } 2822 2823 if(part.mContentLocation != null) { 2824 values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); 2825 } else { 2826 if(part.mPartName != null) { 2827 values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".dat"); 2828 } else { 2829 values.put(Mms.Part.CONTENT_LOCATION, "part_" + count + ".dat"); 2830 } 2831 } 2832 if(part.mContentDisposition != null) 2833 values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); 2834 if(part.mPartName != null) { 2835 values.put(Mms.Part.FILENAME, part.mPartName); 2836 values.put(Mms.Part.NAME, part.mPartName); 2837 } else { 2838 /* We must set at least one part identifier */ 2839 values.put(Mms.Part.FILENAME, "part_" + count + ".dat"); 2840 values.put(Mms.Part.NAME, "part_" + count + ".dat"); 2841 } 2842 Uri partUri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part"); 2843 Uri res = mResolver.insert(partUri, values); 2844 2845 // Add data to part 2846 OutputStream os = mResolver.openOutputStream(res); 2847 os.write(part.mData); 2848 os.close(); 2849 } 2850 2851 2852 public void sendMessage(PushMsgInfo msgInfo, String msgBody) { 2853 2854 SmsManager smsMng = SmsManager.getDefault(); 2855 ArrayList<String> parts = smsMng.divideMessage(msgBody); 2856 msgInfo.parts = parts.size(); 2857 // We add a time stamp to differentiate delivery reports from each other for resent messages 2858 msgInfo.timestamp = Calendar.getInstance().getTime().getTime(); 2859 msgInfo.partsDelivered = 0; 2860 msgInfo.partsSent = 0; 2861 2862 ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts); 2863 ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts); 2864 2865 /* We handle the SENT intent in the MAP service, as this object 2866 * is destroyed at disconnect, hence if a disconnect occur while sending 2867 * a message, there is no intent handler to move the message from outbox 2868 * to the correct folder. 2869 * The correct solution would be to create a service that will start based on 2870 * the intent, if BT is turned off. */ 2871 2872 for (int i = 0; i < msgInfo.parts; i++) { 2873 Intent intentDelivery, intentSent; 2874 2875 intentDelivery = new Intent(ACTION_MESSAGE_DELIVERY, null); 2876 /* Add msgId and part number to ensure the intents are different, and we 2877 * thereby get an intent for each msg part. 2878 * setType is needed to create different intents for each message id/ time stamp, 2879 * as the extras are not used when comparing. */ 2880 intentDelivery.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i); 2881 intentDelivery.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id); 2882 intentDelivery.putExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, msgInfo.timestamp); 2883 PendingIntent pendingIntentDelivery = PendingIntent.getBroadcast(mContext, 0, 2884 intentDelivery, PendingIntent.FLAG_UPDATE_CURRENT); 2885 2886 intentSent = new Intent(ACTION_MESSAGE_SENT, null); 2887 /* Add msgId and part number to ensure the intents are different, and we 2888 * thereby get an intent for each msg part. 2889 * setType is needed to create different intents for each message id/ time stamp, 2890 * as the extras are not used when comparing. */ 2891 intentSent.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i); 2892 intentSent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id); 2893 intentSent.putExtra(EXTRA_MESSAGE_SENT_URI, msgInfo.uri.toString()); 2894 intentSent.putExtra(EXTRA_MESSAGE_SENT_RETRY, msgInfo.retry); 2895 intentSent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, msgInfo.transparent); 2896 2897 PendingIntent pendingIntentSent = PendingIntent.getBroadcast(mContext, 0, 2898 intentSent, PendingIntent.FLAG_UPDATE_CURRENT); 2899 2900 // We use the same pending intent for all parts, but do not set the one shot flag. 2901 deliveryIntents.add(pendingIntentDelivery); 2902 sentIntents.add(pendingIntentSent); 2903 } 2904 2905 Log.d(TAG, "sendMessage to " + msgInfo.phone); 2906 2907 smsMng.sendMultipartTextMessage(msgInfo.phone, null, parts, sentIntents, 2908 deliveryIntents); 2909 } 2910 2911 private static final String ACTION_MESSAGE_DELIVERY = 2912 "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY"; 2913 public static final String ACTION_MESSAGE_SENT = 2914 "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT"; 2915 2916 public static final String EXTRA_MESSAGE_SENT_HANDLE = "HANDLE"; 2917 public static final String EXTRA_MESSAGE_SENT_RESULT = "result"; 2918 public static final String EXTRA_MESSAGE_SENT_URI = "uri"; 2919 public static final String EXTRA_MESSAGE_SENT_RETRY = "retry"; 2920 public static final String EXTRA_MESSAGE_SENT_TRANSPARENT = "transparent"; 2921 public static final String EXTRA_MESSAGE_SENT_TIMESTAMP = "timestamp"; 2922 2923 private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver(); 2924 2925 private boolean mInitialized = false; 2926 2927 private class SmsBroadcastReceiver extends BroadcastReceiver { 2928 private final String[] ID_PROJECTION = new String[] { Sms._ID }; 2929 private final Uri UPDATE_STATUS_URI = Uri.withAppendedPath(Sms.CONTENT_URI, "/status"); 2930 2931 public void register() { 2932 Handler handler = new Handler(Looper.getMainLooper()); 2933 2934 IntentFilter intentFilter = new IntentFilter(); 2935 intentFilter.addAction(ACTION_MESSAGE_DELIVERY); 2936 /* The reception of ACTION_MESSAGE_SENT have been moved to the MAP 2937 * service, to be able to handle message sent events after a disconnect. */ 2938 //intentFilter.addAction(ACTION_MESSAGE_SENT); 2939 try{ 2940 intentFilter.addDataType("message/*"); 2941 } catch (MalformedMimeTypeException e) { 2942 Log.e(TAG, "Wrong mime type!!!", e); 2943 } 2944 2945 mContext.registerReceiver(this, intentFilter, null, handler); 2946 } 2947 2948 public void unregister() { 2949 try { 2950 mContext.unregisterReceiver(this); 2951 } catch (IllegalArgumentException e) { 2952 /* do nothing */ 2953 } 2954 } 2955 2956 @Override 2957 public void onReceive(Context context, Intent intent) { 2958 String action = intent.getAction(); 2959 long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1); 2960 PushMsgInfo msgInfo = mPushMsgList.get(handle); 2961 2962 Log.d(TAG, "onReceive: action" + action); 2963 2964 if (msgInfo == null) { 2965 Log.d(TAG, "onReceive: no msgInfo found for handle " + handle); 2966 return; 2967 } 2968 2969 if (action.equals(ACTION_MESSAGE_SENT)) { 2970 int result = intent.getIntExtra(EXTRA_MESSAGE_SENT_RESULT, 2971 Activity.RESULT_CANCELED); 2972 msgInfo.partsSent++; 2973 if(result != Activity.RESULT_OK) { 2974 /* If just one of the parts in the message fails, we need to send the 2975 * entire message again 2976 */ 2977 msgInfo.failedSent = true; 2978 } 2979 if(D) Log.d(TAG, "onReceive: msgInfo.partsSent = " + msgInfo.partsSent 2980 + ", msgInfo.parts = " + msgInfo.parts + " result = " + result); 2981 2982 if (msgInfo.partsSent == msgInfo.parts) { 2983 actionMessageSent(context, intent, msgInfo); 2984 } 2985 } else if (action.equals(ACTION_MESSAGE_DELIVERY)) { 2986 long timestamp = intent.getLongExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, 0); 2987 int status = -1; 2988 if(msgInfo.timestamp == timestamp) { 2989 msgInfo.partsDelivered++; 2990 byte[] pdu = intent.getByteArrayExtra("pdu"); 2991 String format = intent.getStringExtra("format"); 2992 2993 SmsMessage message = SmsMessage.createFromPdu(pdu, format); 2994 if (message == null) { 2995 Log.d(TAG, "actionMessageDelivery: Can't get message from pdu"); 2996 return; 2997 } 2998 status = message.getStatus(); 2999 if(status != 0/*0 is success*/) { 3000 msgInfo.statusDelivered = status; 3001 if(D) Log.d(TAG, "msgInfo.statusDelivered = " + status); 3002 } 3003 } 3004 if (msgInfo.partsDelivered == msgInfo.parts) { 3005 actionMessageDelivery(context, intent, msgInfo); 3006 } 3007 } else { 3008 Log.d(TAG, "onReceive: Unknown action " + action); 3009 } 3010 } 3011 3012 private void actionMessageSent(Context context, Intent intent, PushMsgInfo msgInfo) { 3013 /* As the MESSAGE_SENT intent is forwarded from the MAP service, we use the intent 3014 * to carry the result, as getResult() will not return the correct value. 3015 */ 3016 boolean delete = false; 3017 3018 if(D) Log.d(TAG,"actionMessageSent(): msgInfo.failedSent = " + msgInfo.failedSent); 3019 3020 msgInfo.sendInProgress = false; 3021 3022 if (msgInfo.failedSent == false) { 3023 if(D) Log.d(TAG, "actionMessageSent: result OK"); 3024 if (msgInfo.transparent == 0) { 3025 if (!Sms.moveMessageToFolder(context, msgInfo.uri, 3026 Sms.MESSAGE_TYPE_SENT, 0)) { 3027 Log.w(TAG, "Failed to move " + msgInfo.uri + " to SENT"); 3028 } 3029 } else { 3030 delete = true; 3031 } 3032 3033 Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msgInfo.id, 3034 getSmsFolderName(Sms.MESSAGE_TYPE_SENT), null, mSmsType); 3035 sendEvent(evt); 3036 3037 } else { 3038 if (msgInfo.retry == 1) { 3039 /* Notify failure, but keep message in outbox for resending */ 3040 msgInfo.resend = true; 3041 msgInfo.partsSent = 0; // Reset counter for the retry 3042 msgInfo.failedSent = false; 3043 Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id, 3044 getSmsFolderName(Sms.MESSAGE_TYPE_OUTBOX), null, mSmsType); 3045 sendEvent(evt); 3046 } else { 3047 if (msgInfo.transparent == 0) { 3048 if (!Sms.moveMessageToFolder(context, msgInfo.uri, 3049 Sms.MESSAGE_TYPE_FAILED, 0)) { 3050 Log.w(TAG, "Failed to move " + msgInfo.uri + " to FAILED"); 3051 } 3052 } else { 3053 delete = true; 3054 } 3055 3056 Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id, 3057 getSmsFolderName(Sms.MESSAGE_TYPE_FAILED), null, mSmsType); 3058 sendEvent(evt); 3059 } 3060 } 3061 3062 if (delete == true) { 3063 /* Delete from Observer message list to avoid delete notifications */ 3064 synchronized(getMsgListSms()) { 3065 getMsgListSms().remove(msgInfo.id); 3066 } 3067 3068 /* Delete from DB */ 3069 mResolver.delete(msgInfo.uri, null, null); 3070 } 3071 } 3072 3073 private void actionMessageDelivery(Context context, Intent intent, PushMsgInfo msgInfo) { 3074 Uri messageUri = intent.getData(); 3075 msgInfo.sendInProgress = false; 3076 3077 Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null); 3078 3079 try { 3080 if (cursor.moveToFirst()) { 3081 int messageId = cursor.getInt(0); 3082 3083 Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId); 3084 3085 if(D) Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" 3086 + msgInfo.statusDelivered); 3087 3088 ContentValues contentValues = new ContentValues(2); 3089 3090 contentValues.put(Sms.STATUS, msgInfo.statusDelivered); 3091 contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis()); 3092 mResolver.update(updateUri, contentValues, null, null); 3093 } else { 3094 Log.d(TAG, "Can't find message for status update: " + messageUri); 3095 } 3096 } finally { 3097 if (cursor != null) cursor.close(); 3098 } 3099 3100 if (msgInfo.statusDelivered == 0) { 3101 Event evt = new Event(EVENT_TYPE_DELEVERY_SUCCESS, msgInfo.id, 3102 getSmsFolderName(Sms.MESSAGE_TYPE_SENT), null, mSmsType); 3103 sendEvent(evt); 3104 } else { 3105 Event evt = new Event(EVENT_TYPE_DELIVERY_FAILURE, msgInfo.id, 3106 getSmsFolderName(Sms.MESSAGE_TYPE_SENT), null, mSmsType); 3107 sendEvent(evt); 3108 } 3109 3110 mPushMsgList.remove(msgInfo.id); 3111 } 3112 } 3113 3114 static public void actionMessageSentDisconnected(Context context, Intent intent, int result) { 3115 boolean delete = false; 3116 //int retry = intent.getIntExtra(EXTRA_MESSAGE_SENT_RETRY, 0); 3117 int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0); 3118 String uriString = intent.getStringExtra(EXTRA_MESSAGE_SENT_URI); 3119 if(uriString == null) { 3120 // Nothing we can do about it, just bail out 3121 return; 3122 } 3123 Uri uri = Uri.parse(uriString); 3124 3125 if (result == Activity.RESULT_OK) { 3126 Log.d(TAG, "actionMessageSentDisconnected: result OK"); 3127 if (transparent == 0) { 3128 if (!Sms.moveMessageToFolder(context, uri, 3129 Sms.MESSAGE_TYPE_SENT, 0)) { 3130 Log.d(TAG, "Failed to move " + uri + " to SENT"); 3131 } 3132 } else { 3133 delete = true; 3134 } 3135 } else { 3136 /*if (retry == 1) { 3137 The retry feature only works while connected, else we fail the send, 3138 * and move the message to failed, to let the user/app resend manually later. 3139 } else */{ 3140 if (transparent == 0) { 3141 if (!Sms.moveMessageToFolder(context, uri, 3142 Sms.MESSAGE_TYPE_FAILED, 0)) { 3143 Log.d(TAG, "Failed to move " + uri + " to FAILED"); 3144 } 3145 } else { 3146 delete = true; 3147 } 3148 } 3149 } 3150 3151 if (delete == true) { 3152 /* Delete from DB */ 3153 ContentResolver resolver = context.getContentResolver(); 3154 if(resolver != null) { 3155 resolver.delete(uri, null, null); 3156 } else { 3157 Log.w(TAG, "Unable to get resolver"); 3158 } 3159 } 3160 } 3161 3162 private void registerPhoneServiceStateListener() { 3163 TelephonyManager tm = (TelephonyManager)mContext.getSystemService( 3164 Context.TELEPHONY_SERVICE); 3165 tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE); 3166 } 3167 3168 private void unRegisterPhoneServiceStateListener() { 3169 TelephonyManager tm = (TelephonyManager)mContext.getSystemService( 3170 Context.TELEPHONY_SERVICE); 3171 tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE); 3172 } 3173 3174 private void resendPendingMessages() { 3175 /* Send pending messages in outbox */ 3176 String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX; 3177 Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, 3178 null); 3179 try { 3180 if (c != null && c.moveToFirst()) { 3181 do { 3182 long id = c.getLong(c.getColumnIndex(Sms._ID)); 3183 String msgBody = c.getString(c.getColumnIndex(Sms.BODY)); 3184 PushMsgInfo msgInfo = mPushMsgList.get(id); 3185 if (msgInfo == null || msgInfo.resend == false || 3186 msgInfo.sendInProgress == true) { 3187 continue; 3188 } 3189 msgInfo.sendInProgress = true; 3190 sendMessage(msgInfo, msgBody); 3191 } while (c.moveToNext()); 3192 } 3193 } finally { 3194 if (c != null) c.close(); 3195 } 3196 3197 3198 } 3199 3200 private void failPendingMessages() { 3201 /* Move pending messages from outbox to failed */ 3202 String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX; 3203 Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, 3204 null); 3205 try { 3206 if (c != null && c.moveToFirst()) { 3207 do { 3208 long id = c.getLong(c.getColumnIndex(Sms._ID)); 3209 String msgBody = c.getString(c.getColumnIndex(Sms.BODY)); 3210 PushMsgInfo msgInfo = mPushMsgList.get(id); 3211 if (msgInfo == null || msgInfo.resend == false) { 3212 continue; 3213 } 3214 Sms.moveMessageToFolder(mContext, msgInfo.uri, 3215 Sms.MESSAGE_TYPE_FAILED, 0); 3216 } while (c.moveToNext()); 3217 } 3218 } finally { 3219 if (c != null) c.close(); 3220 } 3221 3222 } 3223 3224 private void removeDeletedMessages() { 3225 /* Remove messages from virtual "deleted" folder (thread_id -1) */ 3226 mResolver.delete(Sms.CONTENT_URI, 3227 "thread_id = " + DELETED_THREAD_ID, null); 3228 } 3229 3230 private PhoneStateListener mPhoneListener = new PhoneStateListener() { 3231 @Override 3232 public void onServiceStateChanged(ServiceState serviceState) { 3233 Log.d(TAG, "Phone service state change: " + serviceState.getState()); 3234 if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) { 3235 resendPendingMessages(); 3236 } 3237 } 3238 }; 3239 3240 public void init() { 3241 mSmsBroadcastReceiver.register(); 3242 registerPhoneServiceStateListener(); 3243 mInitialized = true; 3244 } 3245 3246 public void deinit() { 3247 mInitialized = false; 3248 unregisterObserver(); 3249 mSmsBroadcastReceiver.unregister(); 3250 unRegisterPhoneServiceStateListener(); 3251 failPendingMessages(); 3252 removeDeletedMessages(); 3253 } 3254 3255 public boolean handleSmsSendIntent(Context context, Intent intent){ 3256 if(mInitialized) { 3257 mSmsBroadcastReceiver.onReceive(context, intent); 3258 return true; 3259 } 3260 return false; 3261 } 3262} 3263