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