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