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