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 Log.w(TAG, "handleMsgListChangesSms, ID is null"); 1416 continue; 1417 } 1418 long id = c.getLong(idIndex); 1419 int type = c.getInt(c.getColumnIndex(Sms.TYPE)); 1420 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); 1421 int read = c.getInt(c.getColumnIndex(Sms.READ)); 1422 1423 Msg msg = getMsgListSms().remove(id); 1424 1425 /* We must filter out any actions made by the MCE, hence do not send e.g. 1426 * a message deleted and/or MessageShift for messages deleted by the MCE. */ 1427 1428 if (msg == null) { 1429 /* New message */ 1430 msg = new Msg(id, type, threadId, read); 1431 msgListSms.put(id, msg); 1432 listChanged = true; 1433 Event evt; 1434 if (mTransmitEvents && // extract contact details only if needed 1435 mMapEventReportVersion 1436 > BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1437 String date = BluetoothMapUtils.getDateTimeString( 1438 c.getLong(c.getColumnIndex(Sms.DATE))); 1439 String subject = c.getString(c.getColumnIndex(Sms.BODY)); 1440 if (subject == null) { 1441 subject = ""; 1442 } 1443 String name = ""; 1444 String phone = ""; 1445 if (type == 1) { //inbox 1446 phone = c.getString(c.getColumnIndex(Sms.ADDRESS)); 1447 if (phone != null && !phone.isEmpty()) { 1448 name = BluetoothMapContent.getContactNameFromPhone(phone, 1449 mResolver); 1450 if (name == null || name.isEmpty()) { 1451 name = phone; 1452 } 1453 } else { 1454 name = phone; 1455 } 1456 } else { 1457 TelephonyManager tm = 1458 (TelephonyManager) mContext.getSystemService( 1459 Context.TELEPHONY_SERVICE); 1460 if (tm != null) { 1461 phone = tm.getLine1Number(); 1462 name = phone; 1463 } 1464 } 1465 String priority = "no"; // no priority for sms 1466 /* Incoming message from the network */ 1467 if (mMapEventReportVersion 1468 == BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 1469 evt = new Event(EVENT_TYPE_NEW, id, getSmsFolderName(type), 1470 mSmsType, date, subject, name, priority); 1471 } else { 1472 evt = new Event(EVENT_TYPE_NEW, id, getSmsFolderName(type), 1473 mSmsType, date, subject, name, priority, 1474 (long) threadId, null); 1475 } 1476 } else { 1477 /* Incoming message from the network */ 1478 evt = new Event(EVENT_TYPE_NEW, id, getSmsFolderName(type), null, 1479 mSmsType); 1480 } 1481 sendEvent(evt); 1482 } else { 1483 /* Existing message */ 1484 if (type != msg.type) { 1485 listChanged = true; 1486 Log.d(TAG, "new type: " + type + " old type: " + msg.type); 1487 String oldFolder = getSmsFolderName(msg.type); 1488 String newFolder = getSmsFolderName(type); 1489 // Filter out the intermediate outbox steps 1490 if (!oldFolder.equalsIgnoreCase(newFolder)) { 1491 Event evt = 1492 new Event(EVENT_TYPE_SHIFT, id, getSmsFolderName(type), 1493 oldFolder, mSmsType); 1494 sendEvent(evt); 1495 } 1496 msg.type = type; 1497 } else if (threadId != msg.threadId) { 1498 listChanged = true; 1499 Log.d(TAG, "Message delete change: type: " + type + " old type: " 1500 + msg.type + "\n threadId: " + threadId 1501 + " old threadId: " + msg.threadId); 1502 if (threadId == DELETED_THREAD_ID) { // Message deleted 1503 // TODO: 1504 // We shall only use the folder attribute, but can't remember 1505 // wether to set it to "deleted" or the name of the folder 1506 // from which the message have been deleted. 1507 // "old_folder" used only for MessageShift event 1508 Event evt = new Event(EVENT_TYPE_DELETE, id, 1509 getSmsFolderName(msg.type), null, mSmsType); 1510 sendEvent(evt); 1511 msg.threadId = threadId; 1512 } else { // Undelete 1513 Event evt = new Event(EVENT_TYPE_SHIFT, id, 1514 getSmsFolderName(msg.type), 1515 BluetoothMapContract.FOLDER_NAME_DELETED, mSmsType); 1516 sendEvent(evt); 1517 msg.threadId = threadId; 1518 } 1519 } 1520 if (read != msg.flagRead) { 1521 listChanged = true; 1522 msg.flagRead = read; 1523 if (mMapEventReportVersion 1524 > BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1525 Event evt = new Event(EVENT_TYPE_READ_STATUS, id, 1526 getSmsFolderName(msg.type), mSmsType); 1527 sendEvent(evt); 1528 } 1529 } 1530 msgListSms.put(id, msg); 1531 } 1532 } while (c.moveToNext()); 1533 } 1534 } finally { 1535 if (c != null) { 1536 c.close(); 1537 } 1538 } 1539 String eventType = EVENT_TYPE_DELETE; 1540 for (Msg msg : getMsgListSms().values()) { 1541 // "old_folder" used only for MessageShift event 1542 if (mMapEventReportVersion >= BluetoothMapUtils.MAP_EVENT_REPORT_V12) { 1543 eventType = EVENT_TYPE_REMOVED; 1544 if (V) Log.v(TAG," sent EVENT_TYPE_REMOVED"); 1545 } 1546 Event evt = new Event(eventType, msg.id, getSmsFolderName(msg.type), null, 1547 mSmsType); 1548 sendEvent(evt); 1549 listChanged = true; 1550 } 1551 1552 setMsgListSms(msgListSms, listChanged); 1553 } 1554 } 1555 handleMsgListChangesMms()1556 private void handleMsgListChangesMms() { 1557 if (V) { 1558 Log.d(TAG, "handleMsgListChangesMms"); 1559 } 1560 1561 HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>(); 1562 boolean listChanged = false; 1563 Cursor c; 1564 synchronized (getMsgListMms()) { 1565 if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1566 c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT, null, null, null); 1567 } else { 1568 c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT_EXT, null, null, null); 1569 } 1570 1571 try { 1572 if (c != null && c.moveToFirst()) { 1573 do { 1574 int idIndex = c.getColumnIndexOrThrow(Mms._ID); 1575 if (c.isNull(idIndex)) { 1576 Log.w(TAG, "handleMsgListChangesMms, ID is null"); 1577 continue; 1578 } 1579 long id = c.getLong(idIndex); 1580 int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); 1581 int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE)); 1582 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); 1583 // TODO: Go through code to see if we have an issue with mismatch in types 1584 // for threadId. Seems to be a long in DB?? 1585 int read = c.getInt(c.getColumnIndex(Mms.READ)); 1586 1587 Msg msg = getMsgListMms().remove(id); 1588 1589 /* We must filter out any actions made by the MCE, hence do not send 1590 * e.g. a message deleted and/or MessageShift for messages deleted by the 1591 * MCE.*/ 1592 1593 if (msg == null) { 1594 /* New message - only notify on retrieve conf */ 1595 listChanged = true; 1596 if (getMmsFolderName(type).equalsIgnoreCase( 1597 BluetoothMapContract.FOLDER_NAME_INBOX) 1598 && mtype != MESSAGE_TYPE_RETRIEVE_CONF) { 1599 continue; 1600 } 1601 msg = new Msg(id, type, threadId, read); 1602 msgListMms.put(id, msg); 1603 Event evt; 1604 if (mTransmitEvents && // extract contact details only if needed 1605 mMapEventReportVersion 1606 != BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1607 String date = BluetoothMapUtils.getDateTimeString( 1608 c.getLong(c.getColumnIndex(Mms.DATE))); 1609 String subject = c.getString(c.getColumnIndex(Mms.SUBJECT)); 1610 if (subject == null || subject.length() == 0) { 1611 /* Get subject from mms text body parts - if any exists */ 1612 subject = BluetoothMapContent.getTextPartsMms(mResolver, id); 1613 if (subject == null) { 1614 subject = ""; 1615 } 1616 } 1617 int tmpPri = c.getInt(c.getColumnIndex(Mms.PRIORITY)); 1618 Log.d(TAG, "TEMP handleMsgListChangesMms, " 1619 + "newMessage 'read' state: " + read + "priority: " 1620 + tmpPri); 1621 1622 String address = BluetoothMapContent.getAddressMms(mResolver, id, 1623 BluetoothMapContent.MMS_FROM); 1624 if (address == null) { 1625 address = ""; 1626 } 1627 1628 String priority = "no"; 1629 if (tmpPri == PduHeaders.PRIORITY_HIGH) { 1630 priority = "yes"; 1631 } 1632 1633 /* Incoming message from the network */ 1634 if (mMapEventReportVersion 1635 == BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 1636 evt = new Event(EVENT_TYPE_NEW, id, getMmsFolderName(type), 1637 TYPE.MMS, date, subject, address, priority); 1638 } else { 1639 evt = new Event(EVENT_TYPE_NEW, id, getMmsFolderName(type), 1640 TYPE.MMS, date, subject, address, priority, 1641 (long) threadId, null); 1642 } 1643 1644 } else { 1645 /* Incoming message from the network */ 1646 evt = new Event(EVENT_TYPE_NEW, id, getMmsFolderName(type), null, 1647 TYPE.MMS); 1648 } 1649 1650 sendEvent(evt); 1651 } else { 1652 /* Existing message */ 1653 if (type != msg.type) { 1654 Log.d(TAG, "new type: " + type + " old type: " + msg.type); 1655 Event evt; 1656 listChanged = true; 1657 if (!msg.localInitiatedSend) { 1658 // Only send events about local initiated changes 1659 evt = new Event(EVENT_TYPE_SHIFT, id, getMmsFolderName(type), 1660 getMmsFolderName(msg.type), TYPE.MMS); 1661 sendEvent(evt); 1662 } 1663 msg.type = type; 1664 1665 if (getMmsFolderName(type).equalsIgnoreCase( 1666 BluetoothMapContract.FOLDER_NAME_SENT) 1667 && msg.localInitiatedSend) { 1668 // Stop tracking changes for this message 1669 msg.localInitiatedSend = false; 1670 evt = new Event(EVENT_TYPE_SENDING_SUCCESS, id, 1671 getMmsFolderName(type), null, TYPE.MMS); 1672 sendEvent(evt); 1673 } 1674 } else if (threadId != msg.threadId) { 1675 Log.d(TAG, "Message delete change: type: " + type + " old type: " 1676 + msg.type + "\n threadId: " + threadId 1677 + " old threadId: " + msg.threadId); 1678 listChanged = true; 1679 if (threadId == DELETED_THREAD_ID) { // Message deleted 1680 // "old_folder" used only for MessageShift event 1681 Event evt = new Event(EVENT_TYPE_DELETE, id, 1682 getMmsFolderName(msg.type), null, TYPE.MMS); 1683 sendEvent(evt); 1684 msg.threadId = threadId; 1685 } else { // Undelete 1686 Event evt = new Event(EVENT_TYPE_SHIFT, id, 1687 getMmsFolderName(msg.type), 1688 BluetoothMapContract.FOLDER_NAME_DELETED, TYPE.MMS); 1689 sendEvent(evt); 1690 msg.threadId = threadId; 1691 } 1692 } 1693 if (read != msg.flagRead) { 1694 listChanged = true; 1695 msg.flagRead = read; 1696 if (mMapEventReportVersion 1697 > BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1698 Event evt = new Event(EVENT_TYPE_READ_STATUS, id, 1699 getMmsFolderName(msg.type), TYPE.MMS); 1700 sendEvent(evt); 1701 } 1702 } 1703 msgListMms.put(id, msg); 1704 } 1705 } while (c.moveToNext()); 1706 1707 } 1708 } finally { 1709 if (c != null) { 1710 c.close(); 1711 } 1712 } 1713 for (Msg msg : getMsgListMms().values()) { 1714 // "old_folder" used only for MessageShift event 1715 Event evt = new Event(EVENT_TYPE_DELETE, msg.id, getMmsFolderName(msg.type), null, 1716 TYPE.MMS); 1717 sendEvent(evt); 1718 listChanged = true; 1719 } 1720 setMsgListMms(msgListMms, listChanged); 1721 } 1722 } 1723 handleMsgListChangesMsg(Uri uri)1724 private void handleMsgListChangesMsg(Uri uri) throws RemoteException { 1725 if (V) { 1726 Log.v(TAG, "handleMsgListChangesMsg uri: " + uri.toString()); 1727 } 1728 1729 // TODO: Change observer to handle accountId and message ID if present 1730 1731 HashMap<Long, Msg> msgList = new HashMap<Long, Msg>(); 1732 Cursor c; 1733 boolean listChanged = false; 1734 if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1735 c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT, null, null, null); 1736 } else if (mMapEventReportVersion == BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 1737 c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT_EXT, null, null, null); 1738 } else { 1739 c = mProviderClient.query(mMessageUri, MSG_PROJECTION_SHORT_EXT2, null, null, null); 1740 } 1741 synchronized (getMsgListMsg()) { 1742 try { 1743 if (c != null && c.moveToFirst()) { 1744 do { 1745 long id = c.getLong( 1746 c.getColumnIndex(BluetoothMapContract.MessageColumns._ID)); 1747 int folderId = c.getInt( 1748 c.getColumnIndex(BluetoothMapContract.MessageColumns.FOLDER_ID)); 1749 int readFlag = c.getInt( 1750 c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_READ)); 1751 Msg msg = getMsgListMsg().remove(id); 1752 BluetoothMapFolderElement folderElement = mFolders.getFolderById(folderId); 1753 String newFolder; 1754 if (folderElement != null) { 1755 newFolder = folderElement.getFullPath(); 1756 } else { 1757 // This can happen if a new folder is created while connected 1758 newFolder = "unknown"; 1759 } 1760 /* We must filter out any actions made by the MCE, hence do not send e.g. 1761 * a message deleted and/or MessageShift for messages deleted by the MCE. */ 1762 if (msg == null) { 1763 listChanged = true; 1764 /* New message - created with message unread */ 1765 msg = new Msg(id, folderId, 0, readFlag); 1766 msgList.put(id, msg); 1767 Event evt; 1768 /* Incoming message from the network */ 1769 if (mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1770 String date = BluetoothMapUtils.getDateTimeString(c.getLong( 1771 c.getColumnIndex( 1772 BluetoothMapContract.MessageColumns.DATE))); 1773 String subject = c.getString(c.getColumnIndex( 1774 BluetoothMapContract.MessageColumns.SUBJECT)); 1775 String address = c.getString(c.getColumnIndex( 1776 BluetoothMapContract.MessageColumns.FROM_LIST)); 1777 String priority = "no"; 1778 if (c.getInt(c.getColumnIndex( 1779 BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY)) 1780 == 1) { 1781 priority = "yes"; 1782 } 1783 if (mMapEventReportVersion 1784 == BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 1785 evt = new Event(EVENT_TYPE_NEW, id, newFolder, 1786 mAccount.getType(), date, subject, address, priority); 1787 } else { 1788 long threadId = c.getLong(c.getColumnIndex( 1789 BluetoothMapContract.MessageColumns.THREAD_ID)); 1790 String threadName = c.getString(c.getColumnIndex( 1791 BluetoothMapContract.MessageColumns.THREAD_NAME)); 1792 evt = new Event(EVENT_TYPE_NEW, id, newFolder, 1793 mAccount.getType(), date, subject, address, priority, 1794 threadId, threadName); 1795 } 1796 } else { 1797 evt = new Event(EVENT_TYPE_NEW, id, newFolder, null, TYPE.EMAIL); 1798 } 1799 sendEvent(evt); 1800 } else { 1801 /* Existing message */ 1802 if (folderId != msg.folderId && msg.folderId != -1) { 1803 if (D) { 1804 Log.d(TAG, "new folderId: " + folderId + " old folderId: " 1805 + msg.folderId); 1806 } 1807 BluetoothMapFolderElement oldFolderElement = 1808 mFolders.getFolderById(msg.folderId); 1809 String oldFolder; 1810 listChanged = true; 1811 if (oldFolderElement != null) { 1812 oldFolder = oldFolderElement.getFullPath(); 1813 } else { 1814 // This can happen if a new folder is created while connected 1815 oldFolder = "unknown"; 1816 } 1817 BluetoothMapFolderElement deletedFolder = mFolders.getFolderByName( 1818 BluetoothMapContract.FOLDER_NAME_DELETED); 1819 BluetoothMapFolderElement sentFolder = mFolders.getFolderByName( 1820 BluetoothMapContract.FOLDER_NAME_SENT); 1821 /* 1822 * If the folder is now 'deleted', send a deleted-event in stead of 1823 * a shift or if message is sent initiated by MAP Client, then send 1824 * sending-success otherwise send folderShift 1825 */ 1826 if (deletedFolder != null 1827 && deletedFolder.getFolderId() == folderId) { 1828 // "old_folder" used only for MessageShift event 1829 Event evt = 1830 new Event(EVENT_TYPE_DELETE, msg.id, oldFolder, null, 1831 mAccount.getType()); 1832 sendEvent(evt); 1833 } else if (sentFolder != null 1834 && sentFolder.getFolderId() == folderId 1835 && msg.localInitiatedSend) { 1836 if (msg.transparent) { 1837 mResolver.delete( 1838 ContentUris.withAppendedId(mMessageUri, id), null, 1839 null); 1840 } else { 1841 msg.localInitiatedSend = false; 1842 Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, 1843 oldFolder, null, mAccount.getType()); 1844 sendEvent(evt); 1845 } 1846 } else { 1847 if (!oldFolder.equalsIgnoreCase("root")) { 1848 Event evt = new Event(EVENT_TYPE_SHIFT, id, newFolder, 1849 oldFolder, mAccount.getType()); 1850 sendEvent(evt); 1851 } 1852 } 1853 msg.folderId = folderId; 1854 } 1855 if (readFlag != msg.flagRead) { 1856 listChanged = true; 1857 1858 if (mMapEventReportVersion 1859 > BluetoothMapUtils.MAP_EVENT_REPORT_V10) { 1860 Event evt = new Event(EVENT_TYPE_READ_STATUS, id, newFolder, 1861 mAccount.getType()); 1862 sendEvent(evt); 1863 msg.flagRead = readFlag; 1864 } 1865 } 1866 1867 msgList.put(id, msg); 1868 } 1869 } while (c.moveToNext()); 1870 } 1871 } finally { 1872 if (c != null) { 1873 c.close(); 1874 } 1875 } 1876 // For all messages no longer in the database send a delete notification 1877 for (Msg msg : getMsgListMsg().values()) { 1878 BluetoothMapFolderElement oldFolderElement = mFolders.getFolderById(msg.folderId); 1879 String oldFolder; 1880 listChanged = true; 1881 if (oldFolderElement != null) { 1882 oldFolder = oldFolderElement.getFullPath(); 1883 } else { 1884 oldFolder = "unknown"; 1885 } 1886 /* Some e-mail clients delete the message after sending, and creates a 1887 * new message in sent. We cannot track the message anymore, hence send both a 1888 * send success and delete message. 1889 */ 1890 if (msg.localInitiatedSend) { 1891 msg.localInitiatedSend = false; 1892 // If message is send with transparency don't set folder as message is deleted 1893 if (msg.transparent) { 1894 oldFolder = null; 1895 } 1896 Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, oldFolder, null, 1897 mAccount.getType()); 1898 sendEvent(evt); 1899 } 1900 /* As this message deleted is only send on a real delete - don't set folder. 1901 * - only send delete event if message is not sent with transparency 1902 */ 1903 if (!msg.transparent) { 1904 1905 // "old_folder" used only for MessageShift event 1906 Event evt = new Event(EVENT_TYPE_DELETE, msg.id, oldFolder, null, 1907 mAccount.getType()); 1908 sendEvent(evt); 1909 } 1910 } 1911 setMsgListMsg(msgList, listChanged); 1912 } 1913 } 1914 handleMsgListChanges(Uri uri)1915 private void handleMsgListChanges(Uri uri) { 1916 if (uri.getAuthority().equals(mAuthority)) { 1917 try { 1918 if (D) { 1919 Log.d(TAG, "handleMsgListChanges: account type = " + mAccount.getType() 1920 .toString()); 1921 } 1922 handleMsgListChangesMsg(uri); 1923 } catch (RemoteException e) { 1924 mMasInstance.restartObexServerSession(); 1925 Log.w(TAG, "Problems contacting the ContentProvider in mas Instance " + mMasId 1926 + " restaring ObexServerSession"); 1927 } 1928 1929 } 1930 // TODO: check to see if there could be problem with IM and SMS in one instance 1931 if (mEnableSmsMms) { 1932 handleMsgListChangesSms(); 1933 handleMsgListChangesMms(); 1934 } 1935 } 1936 handleContactListChanges(Uri uri)1937 private void handleContactListChanges(Uri uri) { 1938 if (uri.getAuthority().equals(mAuthority)) { 1939 try { 1940 if (V) { 1941 Log.v(TAG, "handleContactListChanges uri: " + uri.toString()); 1942 } 1943 Cursor c = null; 1944 boolean listChanged = false; 1945 try { 1946 ConvoContactInfo cInfo = new ConvoContactInfo(); 1947 1948 if (mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V10 1949 && mMapEventReportVersion != BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 1950 c = mProviderClient.query(mContactUri, 1951 BluetoothMapContract.BT_CONTACT_CHATSTATE_PRESENCE_PROJECTION, 1952 null, null, null); 1953 cInfo.setConvoColunms(c); 1954 } else { 1955 if (V) { 1956 Log.v(TAG, "handleContactListChanges MAP version does not" 1957 + "support convocontact notifications"); 1958 } 1959 return; 1960 } 1961 1962 HashMap<String, BluetoothMapConvoContactElement> contactList = 1963 new HashMap<String, BluetoothMapConvoContactElement>( 1964 getContactList().size()); 1965 1966 synchronized (getContactList()) { 1967 if (c != null && c.moveToFirst()) { 1968 do { 1969 String uci = c.getString(cInfo.mContactColUci); 1970 long convoId = c.getLong(cInfo.mContactColConvoId); 1971 if (convoId == 0) { 1972 continue; 1973 } 1974 1975 if (V) { 1976 BluetoothMapUtils.printCursor(c); 1977 } 1978 1979 BluetoothMapConvoContactElement contact = 1980 getContactList().remove(uci); 1981 1982 /* 1983 * We must filter out any actions made by the 1984 * MCE, hence do not send e.g. a message deleted 1985 * and/or MessageShift for messages deleted by 1986 * the MCE. 1987 */ 1988 if (contact == null) { 1989 listChanged = true; 1990 /* 1991 * New contact - added to conversation and 1992 * tracked here 1993 */ 1994 if (mMapEventReportVersion 1995 != BluetoothMapUtils.MAP_EVENT_REPORT_V10 1996 && mMapEventReportVersion 1997 != BluetoothMapUtils.MAP_EVENT_REPORT_V11) { 1998 Event evt; 1999 String name = c.getString(cInfo.mContactColName); 2000 String displayName = c.getString(cInfo.mContactColNickname); 2001 String presenceStatus = 2002 c.getString(cInfo.mContactColPresenceText); 2003 int presenceState = 2004 c.getInt(cInfo.mContactColPresenceState); 2005 long lastActivity = c.getLong(cInfo.mContactColLastActive); 2006 int chatState = c.getInt(cInfo.mContactColChatState); 2007 int priority = c.getInt(cInfo.mContactColPriority); 2008 String btUid = c.getString(cInfo.mContactColBtUid); 2009 2010 // Get Conversation information for 2011 // event 2012 // Uri convoUri = Uri 2013 // .parse(mAccount.mBase_uri 2014 // + "/" 2015 // + BluetoothMapContract 2016 // .TABLE_CONVERSATION); 2017 // String whereClause = "contacts._id = " 2018 // + convoId; 2019 // Cursor cConvo = mProviderClient 2020 // .query(convoUri, 2021 // BluetoothMapContract 2022 // .BT_CONVERSATION_PROJECTION, 2023 // whereClause, null, null); 2024 // TODO: will move out of the loop when merged with CB's 2025 // changes make sure to look up col index out side loop 2026 String convoName = null; 2027 // if (cConvo != null 2028 // && cConvo.moveToFirst()) { 2029 // convoName = cConvo 2030 // .getString(cConvo 2031 // .getColumnIndex 2032 // (BluetoothMapContract.ConvoContactColumns.NAME)); 2033 // } 2034 2035 contact = new BluetoothMapConvoContactElement(uci, name, 2036 displayName, presenceStatus, presenceState, 2037 lastActivity, chatState, priority, btUid); 2038 2039 contactList.put(uci, contact); 2040 2041 evt = new Event(EVENT_TYPE_CONVERSATION, uci, 2042 mAccount.getType(), name, String.valueOf(priority), 2043 BluetoothMapUtils.getDateTimeString(lastActivity), 2044 convoId, convoName, presenceState, presenceStatus, 2045 chatState); 2046 2047 sendEvent(evt); 2048 } 2049 2050 } else { 2051 // Not new - compare updated content 2052 // Uri convoUri = Uri 2053 // .parse(mAccount.mBase_uri 2054 // + "/" 2055 // + BluetoothMapContract.TABLE_CONVERSATION); 2056 // TODO: Should be changed to own provider interface name 2057 // String whereClause = "contacts._id = " 2058 // + convoId; 2059 // Cursor cConvo = mProviderClient 2060 // .query(convoUri, 2061 // BluetoothMapContract 2062 // .BT_CONVERSATION_PROJECTION, 2063 // whereClause, null, null); 2064 // // TODO: will move out of the loop when merged with CB's 2065 // // changes make sure to look up col index out side loop 2066 String convoName = null; 2067 // if (cConvo != null && cConvo.moveToFirst()) { 2068 // convoName = cConvo 2069 // .getString(cConvo 2070 // .getColumnIndex(BluetoothMapContract 2071 // .ConvoContactColumns.NAME)); 2072 // } 2073 2074 // Check if presence is updated 2075 int presenceState = c.getInt(cInfo.mContactColPresenceState); 2076 String presenceStatus = 2077 c.getString(cInfo.mContactColPresenceText); 2078 String currentPresenceStatus = contact.getPresenceStatus(); 2079 if (contact.getPresenceAvailability() != presenceState 2080 || !Objects.equals(currentPresenceStatus, 2081 presenceStatus)) { 2082 long lastOnline = c.getLong(cInfo.mContactColLastOnline); 2083 contact.setPresenceAvailability(presenceState); 2084 contact.setLastActivity(lastOnline); 2085 if (currentPresenceStatus != null 2086 && !currentPresenceStatus.equals(presenceStatus)) { 2087 contact.setPresenceStatus(presenceStatus); 2088 } 2089 Event evt = new Event(EVENT_TYPE_PRESENCE, uci, 2090 mAccount.getType(), contact.getName(), 2091 String.valueOf(contact.getPriority()), 2092 BluetoothMapUtils.getDateTimeString(lastOnline), 2093 convoId, convoName, presenceState, presenceStatus, 2094 0); 2095 sendEvent(evt); 2096 } 2097 2098 // Check if chat state is updated 2099 int chatState = c.getInt(cInfo.mContactColChatState); 2100 if (contact.getChatState() != chatState) { 2101 // Get DB timestamp 2102 long lastActivity = c.getLong(cInfo.mContactColLastActive); 2103 contact.setLastActivity(lastActivity); 2104 contact.setChatState(chatState); 2105 Event evt = new Event(EVENT_TYPE_CHAT_STATE, uci, 2106 mAccount.getType(), contact.getName(), 2107 String.valueOf(contact.getPriority()), 2108 BluetoothMapUtils.getDateTimeString(lastActivity), 2109 convoId, convoName, 0, null, chatState); 2110 sendEvent(evt); 2111 } 2112 contactList.put(uci, contact); 2113 } 2114 } while (c.moveToNext()); 2115 } 2116 if (getContactList().size() > 0) { 2117 // one or more contacts were deleted, hence the conversation listing 2118 // version counter should change. 2119 listChanged = true; 2120 } 2121 setContactList(contactList, listChanged); 2122 } // end synchronized 2123 } finally { 2124 if (c != null) { 2125 c.close(); 2126 } 2127 } 2128 } catch (RemoteException e) { 2129 mMasInstance.restartObexServerSession(); 2130 Log.w(TAG, "Problems contacting the ContentProvider in mas Instance " + mMasId 2131 + " restaring ObexServerSession"); 2132 } 2133 2134 } 2135 // TODO: conversation contact updates if IM and SMS(MMS in one instance 2136 } 2137 setEmailMessageStatusDelete(BluetoothMapFolderElement mCurrentFolder, String uriStr, long handle, int status)2138 private boolean setEmailMessageStatusDelete(BluetoothMapFolderElement mCurrentFolder, 2139 String uriStr, long handle, int status) { 2140 boolean res = false; 2141 Uri uri = Uri.parse(uriStr + BluetoothMapContract.TABLE_MESSAGE); 2142 2143 int updateCount = 0; 2144 ContentValues contentValues = new ContentValues(); 2145 BluetoothMapFolderElement deleteFolder = 2146 mFolders.getFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED); 2147 contentValues.put(BluetoothMapContract.MessageColumns._ID, handle); 2148 synchronized (getMsgListMsg()) { 2149 Msg msg = getMsgListMsg().get(handle); 2150 if (status == BluetoothMapAppParams.STATUS_VALUE_YES) { 2151 /* Set deleted folder id */ 2152 long folderId = -1; 2153 if (deleteFolder != null) { 2154 folderId = deleteFolder.getFolderId(); 2155 } 2156 contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId); 2157 updateCount = mResolver.update(uri, contentValues, null, null); 2158 /* The race between updating the value in our cached values and the database 2159 * is handled by the synchronized statement. */ 2160 if (updateCount > 0) { 2161 res = true; 2162 if (msg != null) { 2163 msg.oldFolderId = msg.folderId; 2164 /* Update the folder ID to avoid triggering an event for MCE 2165 * initiated actions. */ 2166 msg.folderId = folderId; 2167 } 2168 if (D) { 2169 Log.d(TAG, "Deleted MSG: " + handle + " from folderId: " + folderId); 2170 } 2171 } else { 2172 Log.w(TAG, "Msg: " + handle + " - Set delete status " + status 2173 + " failed for folderId " + folderId); 2174 } 2175 } else if (status == BluetoothMapAppParams.STATUS_VALUE_NO) { 2176 /* Undelete message. move to old folder if we know it, 2177 * else move to inbox - as dictated by the spec. */ 2178 if (msg != null && deleteFolder != null 2179 && msg.folderId == deleteFolder.getFolderId()) { 2180 /* Only modify messages in the 'Deleted' folder */ 2181 long folderId = -1; 2182 BluetoothMapFolderElement inboxFolder = 2183 mCurrentFolder.getFolderByName(BluetoothMapContract.FOLDER_NAME_INBOX); 2184 if (msg != null && msg.oldFolderId != -1) { 2185 folderId = msg.oldFolderId; 2186 } else { 2187 if (inboxFolder != null) { 2188 folderId = inboxFolder.getFolderId(); 2189 } 2190 if (D) { 2191 Log.d(TAG, "We did not delete the message, hence the old folder " 2192 + "is unknown. Moving to inbox."); 2193 } 2194 } 2195 contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId); 2196 updateCount = mResolver.update(uri, contentValues, null, null); 2197 if (updateCount > 0) { 2198 res = true; 2199 /* Update the folder ID to avoid triggering an event for MCE 2200 * initiated actions. */ 2201 /* UPDATE: Actually the BT-Spec. states that an undelete is a move of the 2202 * message to INBOX - clearified in errata 5591. 2203 * Therefore we update the cache to INBOX-folderId - to trigger a message 2204 * shift event to the old-folder. */ 2205 if (inboxFolder != null) { 2206 msg.folderId = inboxFolder.getFolderId(); 2207 } else { 2208 msg.folderId = folderId; 2209 } 2210 } else { 2211 if (D) { 2212 Log.d(TAG, "We did not delete the message, hence the old folder " 2213 + "is unknown. Moving to inbox."); 2214 } 2215 } 2216 } 2217 } 2218 if (V) { 2219 BluetoothMapFolderElement folderElement; 2220 String folderName = "unknown"; 2221 if (msg != null) { 2222 folderElement = mCurrentFolder.getFolderById(msg.folderId); 2223 if (folderElement != null) { 2224 folderName = folderElement.getName(); 2225 } 2226 } 2227 Log.d(TAG, "setEmailMessageStatusDelete: " + handle + " from " + folderName 2228 + " status: " + status); 2229 } 2230 } 2231 if (!res) { 2232 Log.w(TAG, "Set delete status " + status + " failed."); 2233 } 2234 return res; 2235 } 2236 updateThreadId(Uri uri, String valueString, long threadId)2237 private void updateThreadId(Uri uri, String valueString, long threadId) { 2238 ContentValues contentValues = new ContentValues(); 2239 contentValues.put(valueString, threadId); 2240 mResolver.update(uri, contentValues, null, null); 2241 } 2242 deleteMessageMms(long handle)2243 private boolean deleteMessageMms(long handle) { 2244 boolean res = false; 2245 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); 2246 Cursor c = mResolver.query(uri, null, null, null, null); 2247 try { 2248 if (c != null && c.moveToFirst()) { 2249 /* Move to deleted folder, or delete if already in deleted folder */ 2250 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); 2251 if (threadId != DELETED_THREAD_ID) { 2252 /* Set deleted thread id */ 2253 synchronized (getMsgListMms()) { 2254 Msg msg = getMsgListMms().get(handle); 2255 if (msg != null) { // This will always be the case 2256 msg.threadId = DELETED_THREAD_ID; 2257 } 2258 } 2259 updateThreadId(uri, Mms.THREAD_ID, DELETED_THREAD_ID); 2260 } else { 2261 /* Delete from observer message list to avoid delete notifications */ 2262 synchronized (getMsgListMms()) { 2263 getMsgListMms().remove(handle); 2264 } 2265 /* Delete message */ 2266 mResolver.delete(uri, null, null); 2267 } 2268 res = true; 2269 } 2270 } finally { 2271 if (c != null) { 2272 c.close(); 2273 } 2274 } 2275 2276 return res; 2277 } 2278 unDeleteMessageMms(long handle)2279 private boolean unDeleteMessageMms(long handle) { 2280 boolean res = false; 2281 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); 2282 Cursor c = mResolver.query(uri, null, null, null, null); 2283 try { 2284 if (c != null && c.moveToFirst()) { 2285 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); 2286 if (threadId == DELETED_THREAD_ID) { 2287 /* Restore thread id from address, or if no thread for address 2288 * create new thread by insert and remove of fake message */ 2289 String address; 2290 long id = c.getLong(c.getColumnIndex(Mms._ID)); 2291 int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); 2292 if (msgBox == Mms.MESSAGE_BOX_INBOX) { 2293 address = BluetoothMapContent.getAddressMms(mResolver, id, 2294 BluetoothMapContent.MMS_FROM); 2295 } else { 2296 address = BluetoothMapContent.getAddressMms(mResolver, id, 2297 BluetoothMapContent.MMS_TO); 2298 } 2299 Set<String> recipients = new HashSet<String>(); 2300 recipients.addAll(Arrays.asList(address)); 2301 Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients); 2302 synchronized (getMsgListMms()) { 2303 Msg msg = getMsgListMms().get(handle); 2304 if (msg != null) { // This will always be the case 2305 msg.threadId = oldThreadId.intValue(); 2306 // Spec. states that undelete shall shift the message to Inbox. 2307 // Hence we need to trigger a message shift from INBOX to old-folder 2308 // after undelete. 2309 // We do this by changing the cached folder value to being inbox - hence 2310 // the event handler will se the update as the message have been shifted 2311 // from INBOX to old-folder. (Errata 5591 clearifies this) 2312 msg.type = Mms.MESSAGE_BOX_INBOX; 2313 } 2314 } 2315 updateThreadId(uri, Mms.THREAD_ID, oldThreadId); 2316 } else { 2317 Log.d(TAG, "Message not in deleted folder: handle " + handle + " threadId " 2318 + threadId); 2319 } 2320 res = true; 2321 } 2322 } finally { 2323 if (c != null) { 2324 c.close(); 2325 } 2326 } 2327 return res; 2328 } 2329 deleteMessageSms(long handle)2330 private boolean deleteMessageSms(long handle) { 2331 boolean res = false; 2332 Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); 2333 Cursor c = mResolver.query(uri, null, null, null, null); 2334 try { 2335 if (c != null && c.moveToFirst()) { 2336 /* Move to deleted folder, or delete if already in deleted folder */ 2337 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); 2338 if (threadId != DELETED_THREAD_ID) { 2339 synchronized (getMsgListSms()) { 2340 Msg msg = getMsgListSms().get(handle); 2341 if (msg != null) { // This will always be the case 2342 msg.threadId = DELETED_THREAD_ID; 2343 } 2344 } 2345 /* Set deleted thread id */ 2346 updateThreadId(uri, Sms.THREAD_ID, DELETED_THREAD_ID); 2347 } else { 2348 /* Delete from observer message list to avoid delete notifications */ 2349 synchronized (getMsgListSms()) { 2350 getMsgListSms().remove(handle); 2351 } 2352 /* Delete message */ 2353 mResolver.delete(uri, null, null); 2354 } 2355 res = true; 2356 } 2357 } finally { 2358 if (c != null) { 2359 c.close(); 2360 } 2361 } 2362 return res; 2363 } 2364 unDeleteMessageSms(long handle)2365 private boolean unDeleteMessageSms(long handle) { 2366 boolean res = false; 2367 Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); 2368 Cursor c = mResolver.query(uri, null, null, null, null); 2369 try { 2370 if (c != null && c.moveToFirst()) { 2371 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); 2372 if (threadId == DELETED_THREAD_ID) { 2373 String address = c.getString(c.getColumnIndex(Sms.ADDRESS)); 2374 Set<String> recipients = new HashSet<String>(); 2375 recipients.addAll(Arrays.asList(address)); 2376 Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients); 2377 synchronized (getMsgListSms()) { 2378 Msg msg = getMsgListSms().get(handle); 2379 if (msg != null) { 2380 msg.threadId = oldThreadId.intValue(); 2381 /* This will always be the case 2382 * The threadId is specified as an int, so it is safe to truncate 2383 * TODO: Test that this will trigger a message-shift from Inbox 2384 * to old-folder 2385 **/ 2386 /* Spec. states that undelete shall shift the message to Inbox. 2387 * Hence we need to trigger a message shift from INBOX to old-folder 2388 * after undelete. 2389 * We do this by changing the cached folder value to being inbox - hence 2390 * the event handler will se the update as the message have been shifted 2391 * from INBOX to old-folder. (Errata 5591 clearifies this) 2392 * */ 2393 msg.type = Sms.MESSAGE_TYPE_INBOX; 2394 } 2395 } 2396 updateThreadId(uri, Sms.THREAD_ID, oldThreadId); 2397 } else { 2398 Log.d(TAG, "Message not in deleted folder: handle " + handle + " threadId " 2399 + threadId); 2400 } 2401 res = true; 2402 } 2403 } finally { 2404 if (c != null) { 2405 c.close(); 2406 } 2407 } 2408 return res; 2409 } 2410 2411 /** 2412 * 2413 * @param handle 2414 * @param type 2415 * @param mCurrentFolder 2416 * @param uriStr 2417 * @param statusValue 2418 * @return true is success 2419 */ setMessageStatusDeleted(long handle, TYPE type, BluetoothMapFolderElement mCurrentFolder, String uriStr, int statusValue)2420 public boolean setMessageStatusDeleted(long handle, TYPE type, 2421 BluetoothMapFolderElement mCurrentFolder, String uriStr, int statusValue) { 2422 boolean res = false; 2423 if (D) { 2424 Log.d(TAG, "setMessageStatusDeleted: handle " + handle + " type " + type + " value " 2425 + statusValue); 2426 } 2427 2428 if (type == TYPE.EMAIL) { 2429 res = setEmailMessageStatusDelete(mCurrentFolder, uriStr, handle, statusValue); 2430 } else if (type == TYPE.IM) { 2431 // TODO: to do when deleting IM message 2432 if (D) { 2433 Log.d(TAG, "setMessageStatusDeleted: IM not handled"); 2434 } 2435 } else { 2436 if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) { 2437 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { 2438 res = deleteMessageSms(handle); 2439 } else if (type == TYPE.MMS) { 2440 res = deleteMessageMms(handle); 2441 } 2442 } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) { 2443 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { 2444 res = unDeleteMessageSms(handle); 2445 } else if (type == TYPE.MMS) { 2446 res = unDeleteMessageMms(handle); 2447 } 2448 } 2449 } 2450 return res; 2451 } 2452 2453 /** 2454 * 2455 * @param handle 2456 * @param type 2457 * @param uriStr 2458 * @param statusValue 2459 * @return true at success 2460 */ setMessageStatusRead(long handle, TYPE type, String uriStr, int statusValue)2461 public boolean setMessageStatusRead(long handle, TYPE type, String uriStr, int statusValue) 2462 throws RemoteException { 2463 int count = 0; 2464 2465 if (D) { 2466 Log.d(TAG, "setMessageStatusRead: handle " + handle + " type " + type + " value " 2467 + statusValue); 2468 } 2469 2470 /* Approved MAP spec errata 3445 states that read status initiated 2471 * by the MCE shall change the MSE read status. */ 2472 if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { 2473 Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); 2474 ContentValues contentValues = new ContentValues(); 2475 contentValues.put(Sms.READ, statusValue); 2476 contentValues.put(Sms.SEEN, statusValue); 2477 String values = contentValues.toString(); 2478 if (D) { 2479 Log.d(TAG, " -> SMS Uri: " + uri.toString() + " values " + values); 2480 } 2481 synchronized (getMsgListSms()) { 2482 Msg msg = getMsgListSms().get(handle); 2483 if (msg != null) { // This will always be the case 2484 msg.flagRead = statusValue; 2485 } 2486 } 2487 count = mResolver.update(uri, contentValues, null, null); 2488 if (D) { 2489 Log.d(TAG, " -> " + count + " rows updated!"); 2490 } 2491 2492 } else if (type == TYPE.MMS) { 2493 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); 2494 if (D) { 2495 Log.d(TAG, " -> MMS Uri: " + uri.toString()); 2496 } 2497 ContentValues contentValues = new ContentValues(); 2498 contentValues.put(Mms.READ, statusValue); 2499 synchronized (getMsgListMms()) { 2500 Msg msg = getMsgListMms().get(handle); 2501 if (msg != null) { // This will always be the case 2502 msg.flagRead = statusValue; 2503 } 2504 } 2505 count = mResolver.update(uri, contentValues, null, null); 2506 if (D) { 2507 Log.d(TAG, " -> " + count + " rows updated!"); 2508 } 2509 } else if (type == TYPE.EMAIL || type == TYPE.IM) { 2510 Uri uri = mMessageUri; 2511 ContentValues contentValues = new ContentValues(); 2512 contentValues.put(BluetoothMapContract.MessageColumns.FLAG_READ, statusValue); 2513 contentValues.put(BluetoothMapContract.MessageColumns._ID, handle); 2514 synchronized (getMsgListMsg()) { 2515 Msg msg = getMsgListMsg().get(handle); 2516 if (msg != null) { // This will always be the case 2517 msg.flagRead = statusValue; 2518 } 2519 } 2520 count = mProviderClient.update(uri, contentValues, null, null); 2521 } 2522 2523 return (count > 0); 2524 } 2525 2526 private class PushMsgInfo { 2527 public long id; 2528 public int transparent; 2529 public int retry; 2530 public String phone; 2531 public Uri uri; 2532 public long timestamp; 2533 public int parts; 2534 public int partsSent; 2535 public int partsDelivered; 2536 public boolean resend; 2537 public boolean sendInProgress; 2538 public boolean failedSent; // Set to true if a single part sent fail is received. 2539 public int statusDelivered; // Set to != 0 if a single part deliver fail is received. 2540 PushMsgInfo(long id, int transparent, int retry, String phone, Uri uri)2541 PushMsgInfo(long id, int transparent, int retry, String phone, Uri uri) { 2542 this.id = id; 2543 this.transparent = transparent; 2544 this.retry = retry; 2545 this.phone = phone; 2546 this.uri = uri; 2547 this.resend = false; 2548 this.sendInProgress = false; 2549 this.failedSent = false; 2550 this.statusDelivered = 0; /* Assume success */ 2551 this.timestamp = 0; 2552 } 2553 2554 ; 2555 } 2556 2557 private Map<Long, PushMsgInfo> mPushMsgList = 2558 Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>()); 2559 2560 /** 2561 * Add an SMS to the given URI. 2562 * 2563 * @param resolver the content resolver to use 2564 * @param uri the URI to add the message to 2565 * @param address the address of the sender 2566 * @param body the body of the message 2567 * @param subject the pseudo-subject of the message 2568 * @param date the timestamp for the message 2569 * @return the URI for the new message 2570 */ addMessageToUri(ContentResolver resolver, Uri uri, String address, String body, String subject, Long date)2571 private static Uri addMessageToUri(ContentResolver resolver, Uri uri, 2572 String address, String body, String subject, 2573 Long date) { 2574 ContentValues values = new ContentValues(7); 2575 final int statusPending = 32; 2576 final int subId = SubscriptionManager.getDefaultSmsSubscriptionId(); 2577 Log.v(TAG, "Telephony addMessageToUri sub id: " + subId); 2578 2579 values.put(Telephony.Sms.SUBSCRIPTION_ID, subId); 2580 values.put(Telephony.Sms.ADDRESS, address); 2581 if (date != null) { 2582 values.put(Telephony.Sms.DATE, date); 2583 } 2584 values.put(Telephony.Sms.READ, 0); 2585 values.put(Telephony.Sms.SUBJECT, subject); 2586 values.put(Telephony.Sms.BODY, body); 2587 values.put(Telephony.Sms.STATUS, statusPending); 2588 return resolver.insert(uri, values); 2589 } 2590 pushMessage(BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement, BluetoothMapAppParams ap, String emailBaseUri)2591 public long pushMessage(BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement, 2592 BluetoothMapAppParams ap, String emailBaseUri) 2593 throws IllegalArgumentException, RemoteException, IOException { 2594 if (D) { 2595 Log.d(TAG, "pushMessage"); 2596 } 2597 ArrayList<BluetoothMapbMessage.VCard> recipientList = msg.getRecipients(); 2598 int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ? 0 2599 : ap.getTransparent(); 2600 int retry = ap.getRetry(); 2601 int charset = ap.getCharset(); 2602 long handle = -1; 2603 long folderId = -1; 2604 2605 if (recipientList == null) { 2606 if (folderElement.getName().equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT)) { 2607 BluetoothMapbMessage.VCard empty = 2608 new BluetoothMapbMessage.VCard("", "", null, null, 0); 2609 recipientList = new ArrayList<BluetoothMapbMessage.VCard>(); 2610 recipientList.add(empty); 2611 Log.w(TAG, "Added empty recipient to draft message"); 2612 } else { 2613 Log.e(TAG, "Trying to send a message with no recipients"); 2614 return -1; 2615 } 2616 } 2617 2618 if (msg.getType().equals(TYPE.EMAIL)) { 2619 /* Write the message to the database */ 2620 String msgBody = ((BluetoothMapbMessageEmail) msg).getEmailBody(); 2621 if (V) { 2622 int length = msgBody.length(); 2623 Log.v(TAG, "pushMessage: message string length = " + length); 2624 String[] messages = msgBody.split("\r\n"); 2625 Log.v(TAG, "pushMessage: messages count=" + messages.length); 2626 for (int i = 0; i < messages.length; i++) { 2627 Log.v(TAG, "part " + i + ":" + messages[i]); 2628 } 2629 } 2630 FileOutputStream os = null; 2631 ParcelFileDescriptor fdOut = null; 2632 Uri uriInsert = Uri.parse(emailBaseUri + BluetoothMapContract.TABLE_MESSAGE); 2633 if (D) { 2634 Log.d(TAG, "pushMessage - uriInsert= " + uriInsert.toString() + ", intoFolder id=" 2635 + folderElement.getFolderId()); 2636 } 2637 2638 synchronized (getMsgListMsg()) { 2639 // Now insert the empty message into folder 2640 ContentValues values = new ContentValues(); 2641 folderId = folderElement.getFolderId(); 2642 values.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId); 2643 Uri uriNew = mProviderClient.insert(uriInsert, values); 2644 if (D) { 2645 Log.d(TAG, "pushMessage - uriNew= " + uriNew.toString()); 2646 } 2647 handle = Long.parseLong(uriNew.getLastPathSegment()); 2648 2649 try { 2650 fdOut = mProviderClient.openFile(uriNew, "w"); 2651 os = new FileOutputStream(fdOut.getFileDescriptor()); 2652 // Write Email to DB 2653 os.write(msgBody.getBytes(), 0, msgBody.getBytes().length); 2654 } catch (FileNotFoundException e) { 2655 Log.w(TAG, e); 2656 throw (new IOException("Unable to open file stream")); 2657 } catch (NullPointerException e) { 2658 Log.w(TAG, e); 2659 throw (new IllegalArgumentException("Unable to parse message.")); 2660 } finally { 2661 try { 2662 if (os != null) { 2663 os.close(); 2664 } 2665 } catch (IOException e) { 2666 Log.w(TAG, e); 2667 } 2668 try { 2669 if (fdOut != null) { 2670 fdOut.close(); 2671 } 2672 } catch (IOException e) { 2673 Log.w(TAG, e); 2674 } 2675 } 2676 2677 /* Extract the data for the inserted message, and store in local mirror, to 2678 * avoid sending a NewMessage Event. */ 2679 /*TODO: We need to add the new 1.1 parameter as well:-) e.g. read*/ 2680 Msg newMsg = new Msg(handle, folderId, 1); // TODO: Create define for read-state 2681 newMsg.transparent = transparent == 1; 2682 if (folderId == folderElement.getFolderByName( 2683 BluetoothMapContract.FOLDER_NAME_OUTBOX).getFolderId()) { 2684 newMsg.localInitiatedSend = true; 2685 } 2686 getMsgListMsg().put(handle, newMsg); 2687 } 2688 } else if (msg.getType().equals(TYPE.MMS) && (recipientList.size() > 1)) { 2689 // Group MMS 2690 String folder = folderElement.getName(); 2691 ArrayList<String> telNums = new ArrayList<String>(); 2692 for (BluetoothMapbMessage.VCard recipient : recipientList) { 2693 // Only send the message to the top level recipient 2694 if (recipient.getEnvLevel() == 0) { 2695 // Only send to recipient's first phone number 2696 telNums.add(recipient.getFirstPhoneNumber()); 2697 } 2698 } 2699 // Send message if folder is outbox else just store in draft 2700 handle = sendMmsMessage(folder, telNums.toArray(new String[telNums.size()]), 2701 (BluetoothMapbMessageMime) msg, transparent, retry); 2702 } else { // type SMS_* (single or mass text) or single MMS 2703 for (BluetoothMapbMessage.VCard recipient : recipientList) { 2704 // Only send the message to the top level recipient 2705 if (recipient.getEnvLevel() == 0) { 2706 /* Only send to first address */ 2707 String phone = recipient.getFirstPhoneNumber(); 2708 String email = recipient.getFirstEmail(); 2709 String folder = folderElement.getName(); 2710 String msgBody = null; 2711 2712 /* If MMS contains text only and the size is less than ten SMS's 2713 * then convert the MMS to type SMS and then proceed 2714 */ 2715 if (msg.getType().equals(TYPE.MMS) 2716 && (((BluetoothMapbMessageMime) msg).getTextOnly())) { 2717 msgBody = ((BluetoothMapbMessageMime) msg).getMessageAsText(); 2718 SmsManager smsMng = SmsManager.getDefault(); 2719 ArrayList<String> parts = smsMng.divideMessage(msgBody); 2720 int smsParts = parts.size(); 2721 if (smsParts <= CONVERT_MMS_TO_SMS_PART_COUNT) { 2722 if (D) { 2723 Log.d(TAG, "pushMessage - converting MMS to SMS, sms parts=" 2724 + smsParts); 2725 } 2726 msg.setType(mSmsType); 2727 } else { 2728 if (D) { 2729 Log.d(TAG, "pushMessage - MMS text only but to big to " 2730 + "convert to SMS"); 2731 } 2732 msgBody = null; 2733 } 2734 2735 } 2736 2737 if (msg.getType().equals(TYPE.MMS)) { 2738 /* Send message if folder is outbox else just store in draft*/ 2739 handle = sendMmsMessage(folder, new String[] {phone}, 2740 (BluetoothMapbMessageMime) msg, transparent, retry); 2741 } else if (msg.getType().equals(TYPE.SMS_GSM) || msg.getType() 2742 .equals(TYPE.SMS_CDMA)) { 2743 /* Add the message to the database */ 2744 if (msgBody == null) { 2745 msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody(); 2746 } 2747 2748 if (TextUtils.isEmpty(msgBody)) { 2749 Log.d(TAG, "PushMsg: Empty msgBody "); 2750 /* not allowed to push empty message */ 2751 throw new IllegalArgumentException("push EMPTY message: Invalid Body"); 2752 } 2753 /* We need to lock the SMS list while updating the database, 2754 * to avoid sending events on MCE initiated operation. */ 2755 Uri contentUri = Uri.parse(Sms.CONTENT_URI + "/" + folder); 2756 Uri uri; 2757 synchronized (getMsgListSms()) { 2758 uri = addMessageToUri(mResolver, contentUri, phone, msgBody, "", 2759 System.currentTimeMillis()); 2760 2761 if (V) { 2762 Log.v(TAG, "Sms.addMessageToUri() returned: " + uri); 2763 } 2764 if (uri == null) { 2765 if (D) { 2766 Log.d(TAG, "pushMessage - failure on add to uri " + contentUri); 2767 } 2768 return -1; 2769 } 2770 Cursor c = mResolver.query(uri, SMS_PROJECTION_SHORT, null, null, null); 2771 2772 /* Extract the data for the inserted message, and store in local mirror, 2773 * to avoid sending a NewMessage Event. */ 2774 try { 2775 if (c != null && c.moveToFirst()) { 2776 long id = c.getLong(c.getColumnIndex(Sms._ID)); 2777 int type = c.getInt(c.getColumnIndex(Sms.TYPE)); 2778 int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); 2779 int readFlag = c.getInt(c.getColumnIndex(Sms.READ)); 2780 if (V) { 2781 Log.v(TAG, "add message with id=" + id + " type=" + type 2782 + " threadId=" + threadId + " readFlag=" + readFlag 2783 + "to mMsgListSms"); 2784 } 2785 Msg newMsg = new Msg(id, type, threadId, readFlag); 2786 getMsgListSms().put(id, newMsg); 2787 c.close(); 2788 } else { 2789 Log.w(TAG, "Message: " + uri + " no longer exist!"); 2790 /* This can only happen, if the message is deleted 2791 * just as it is added */ 2792 return -1; 2793 } 2794 } finally { 2795 if (c != null) { 2796 c.close(); 2797 } 2798 } 2799 2800 handle = Long.parseLong(uri.getLastPathSegment()); 2801 2802 /* Send message if folder is outbox */ 2803 if (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)) { 2804 PushMsgInfo msgInfo = 2805 new PushMsgInfo(handle, transparent, retry, phone, uri); 2806 mPushMsgList.put(handle, msgInfo); 2807 sendMessage(msgInfo, msgBody); 2808 if (V) { 2809 Log.v(TAG, "sendMessage returned..."); 2810 } 2811 } /* else just added to draft */ 2812 2813 /* sendMessage causes the message to be deleted and reinserted, 2814 * hence we need to lock the list while this is happening. */ 2815 } 2816 } else { 2817 if (D) { 2818 Log.d(TAG, "pushMessage - failure on type "); 2819 } 2820 return -1; 2821 } 2822 } 2823 } 2824 } 2825 2826 /* If multiple recipients return handle of last */ 2827 return handle; 2828 } 2829 sendMmsMessage(String folder, String[] toAddress, BluetoothMapbMessageMime msg, int transparent, int retry)2830 public long sendMmsMessage(String folder, String[] toAddress, BluetoothMapbMessageMime msg, 2831 int transparent, int retry) { 2832 /* 2833 *strategy: 2834 *1) parse message into parts 2835 *if folder is outbox/drafts: 2836 *2) push message to draft 2837 *if folder is outbox: 2838 *3) move message to outbox (to trigger the mms app to add msg to pending_messages list) 2839 *4) send intent to mms app in order to wake it up. 2840 *else if folder !outbox: 2841 *1) push message to folder 2842 * */ 2843 if (folder != null && (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX) 2844 || folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT))) { 2845 long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, toAddress, msg); 2846 /* if invalid handle (-1) then just return the handle 2847 * - else continue sending (if folder is outbox) */ 2848 if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase( 2849 BluetoothMapContract.FOLDER_NAME_OUTBOX)) { 2850 Uri btMmsUri = MmsFileProvider.CONTENT_URI.buildUpon() 2851 .appendPath(Long.toString(handle)) 2852 .build(); 2853 Intent sentIntent = new Intent(ACTION_MESSAGE_SENT); 2854 // TODO: update the mmsMsgList <- done in pushMmsToFolder() but check 2855 sentIntent.setType("message/" + Long.toString(handle)); 2856 sentIntent.putExtra(EXTRA_MESSAGE_SENT_MSG_TYPE, TYPE.MMS.ordinal()); 2857 sentIntent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, handle); // needed for notification 2858 sentIntent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, transparent); 2859 sentIntent.putExtra(EXTRA_MESSAGE_SENT_RETRY, retry); 2860 //sentIntent.setDataAndNormalize(btMmsUri); 2861 // TODO(b/171825892) Please replace FLAG_MUTABLE_UNAUDITED below 2862 // with either FLAG_IMMUTABLE (recommended) or FLAG_MUTABLE. 2863 PendingIntent pendingSendIntent = 2864 PendingIntent.getBroadcast(mContext, 0, sentIntent, 2865 PendingIntent.FLAG_IMMUTABLE); 2866 SmsManager.getDefault() 2867 .sendMultimediaMessage(mContext, btMmsUri, null/*locationUrl*/, 2868 null/*configOverrides*/, 2869 pendingSendIntent); 2870 } 2871 return handle; 2872 } else { 2873 /* not allowed to push mms to anything but outbox/draft */ 2874 throw new IllegalArgumentException( 2875 "Cannot push message to other " + "folders than outbox/draft"); 2876 } 2877 } 2878 moveDraftToOutbox(long handle)2879 private void moveDraftToOutbox(long handle) { 2880 moveMmsToFolder(handle, mResolver, Mms.MESSAGE_BOX_OUTBOX); 2881 } 2882 2883 /** 2884 * Move a MMS to another folder. 2885 * @param handle the CP handle of the message to move 2886 * @param resolver the ContentResolver to use 2887 * @param folder the destination folder - use Mms.MESSAGE_BOX_xxx 2888 */ moveMmsToFolder(long handle, ContentResolver resolver, int folder)2889 private static void moveMmsToFolder(long handle, ContentResolver resolver, int folder) { 2890 /*Move message by changing the msg_box value in the content provider database */ 2891 if (handle != -1) { 2892 String whereClause = " _id= " + handle; 2893 Uri uri = Mms.CONTENT_URI; 2894 Cursor queryResult = resolver.query(uri, null, whereClause, null, null); 2895 try { 2896 if (queryResult != null) { 2897 if (queryResult.getCount() > 0) { 2898 queryResult.moveToFirst(); 2899 ContentValues data = new ContentValues(); 2900 /* set folder to be outbox */ 2901 data.put(Mms.MESSAGE_BOX, folder); 2902 resolver.update(uri, data, whereClause, null); 2903 if (D) { 2904 Log.d(TAG, "moved MMS message to " + getMmsFolderName(folder)); 2905 } 2906 } 2907 } else { 2908 Log.w(TAG, "Could not move MMS message to " + getMmsFolderName(folder)); 2909 } 2910 } finally { 2911 if (queryResult != null) { 2912 queryResult.close(); 2913 } 2914 } 2915 } 2916 } 2917 pushMmsToFolder(int folder, String[] toAddress, BluetoothMapbMessageMime msg)2918 private long pushMmsToFolder(int folder, String[] toAddress, BluetoothMapbMessageMime msg) { 2919 /** 2920 * strategy: 2921 * 1) parse msg into parts + header 2922 * 2) create thread id (abuse the ease of adding an SMS to get id for thread) 2923 * 3) push parts into content://mms/parts/ table 2924 * 3) 2925 */ 2926 2927 ContentValues values = new ContentValues(); 2928 values.put(Mms.MESSAGE_BOX, folder); 2929 values.put(Mms.READ, 0); 2930 values.put(Mms.SEEN, 0); 2931 if (msg.getSubject() != null) { 2932 values.put(Mms.SUBJECT, msg.getSubject()); 2933 } else { 2934 values.put(Mms.SUBJECT, ""); 2935 } 2936 2937 if (msg.getSubject() != null && msg.getSubject().length() > 0) { 2938 values.put(Mms.SUBJECT_CHARSET, 106); 2939 } 2940 values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related"); 2941 values.put(Mms.EXPIRY, 604800); 2942 values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR); 2943 values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ); 2944 values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION); 2945 values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL); 2946 values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO); 2947 values.put(Mms.TRANSACTION_ID, "T" + Long.toHexString(System.currentTimeMillis())); 2948 values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO); 2949 values.put(Mms.LOCKED, 0); 2950 if (msg.getTextOnly()) { 2951 values.put(Mms.TEXT_ONLY, true); 2952 } 2953 values.put(Mms.MESSAGE_SIZE, msg.getSize()); 2954 2955 // Get thread id 2956 Set<String> recipients = new HashSet<String>(); 2957 recipients.addAll(Arrays.asList(toAddress)); 2958 values.put(Mms.THREAD_ID, Telephony.Threads.getOrCreateThreadId(mContext, recipients)); 2959 Uri uri = Mms.CONTENT_URI; 2960 2961 synchronized (getMsgListMms()) { 2962 2963 uri = mResolver.insert(uri, values); 2964 2965 if (uri == null) { 2966 // unable to insert MMS 2967 Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri); 2968 return -1; 2969 } 2970 /* As we already have all the values we need, we could skip the query, but 2971 doing the query ensures we get any changes made by the content provider 2972 at insert. */ 2973 Cursor c = mResolver.query(uri, MMS_PROJECTION_SHORT, null, null, null); 2974 try { 2975 if (c != null && c.moveToFirst()) { 2976 long id = c.getLong(c.getColumnIndex(Mms._ID)); 2977 int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); 2978 int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); 2979 int readStatus = c.getInt(c.getColumnIndex(Mms.READ)); 2980 2981 /* We must filter out any actions made by the MCE. Add the new message to 2982 * the list of known messages. */ 2983 2984 Msg newMsg = new Msg(id, type, threadId, readStatus); 2985 newMsg.localInitiatedSend = true; 2986 getMsgListMms().put(id, newMsg); 2987 c.close(); 2988 } 2989 } finally { 2990 if (c != null) { 2991 c.close(); 2992 } 2993 } 2994 } // Done adding changes, unlock access to mMsgListMms to allow sending MMS events again 2995 2996 long handle = Long.parseLong(uri.getLastPathSegment()); 2997 if (V) { 2998 Log.v(TAG, " NEW URI " + uri.toString()); 2999 } 3000 3001 try { 3002 if (msg.getMimeParts() == null) { 3003 /* Perhaps this message have been deleted, and no longer have any content, 3004 * but only headers */ 3005 Log.w(TAG, "No MMS parts present..."); 3006 } else { 3007 if (V) { 3008 Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base."); 3009 } 3010 for (MimePart part : msg.getMimeParts()) { 3011 int count = 0; 3012 count++; 3013 values.clear(); 3014 if (part.mContentType != null && part.mContentType.toUpperCase() 3015 .contains("TEXT")) { 3016 values.put(Mms.Part.CONTENT_TYPE, "text/plain"); 3017 values.put(Mms.Part.CHARSET, 106); 3018 if (part.mPartName != null) { 3019 values.put(Mms.Part.FILENAME, part.mPartName); 3020 values.put(Mms.Part.NAME, part.mPartName); 3021 } else { 3022 values.put(Mms.Part.FILENAME, "text_" + count + ".txt"); 3023 values.put(Mms.Part.NAME, "text_" + count + ".txt"); 3024 } 3025 // Ensure we have "ci" set 3026 if (part.mContentId != null) { 3027 values.put(Mms.Part.CONTENT_ID, part.mContentId); 3028 } else { 3029 if (part.mPartName != null) { 3030 values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">"); 3031 } else { 3032 values.put(Mms.Part.CONTENT_ID, "<text_" + count + ">"); 3033 } 3034 } 3035 // Ensure we have "cl" set 3036 if (part.mContentLocation != null) { 3037 values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); 3038 } else { 3039 if (part.mPartName != null) { 3040 values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".txt"); 3041 } else { 3042 values.put(Mms.Part.CONTENT_LOCATION, "text_" + count + ".txt"); 3043 } 3044 } 3045 3046 if (part.mContentDisposition != null) { 3047 values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); 3048 } 3049 values.put(Mms.Part.TEXT, part.getDataAsString()); 3050 uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part"); 3051 uri = mResolver.insert(uri, values); 3052 if (V) { 3053 Log.v(TAG, "Added TEXT part"); 3054 } 3055 3056 } else if (part.mContentType != null && part.mContentType.toUpperCase() 3057 .contains("SMIL")) { 3058 values.put(Mms.Part.SEQ, -1); 3059 values.put(Mms.Part.CONTENT_TYPE, "application/smil"); 3060 if (part.mContentId != null) { 3061 values.put(Mms.Part.CONTENT_ID, part.mContentId); 3062 } else { 3063 values.put(Mms.Part.CONTENT_ID, "<smil_" + count + ">"); 3064 } 3065 if (part.mContentLocation != null) { 3066 values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); 3067 } else { 3068 values.put(Mms.Part.CONTENT_LOCATION, "smil_" + count + ".xml"); 3069 } 3070 3071 if (part.mContentDisposition != null) { 3072 values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); 3073 } 3074 values.put(Mms.Part.FILENAME, "smil.xml"); 3075 values.put(Mms.Part.NAME, "smil.xml"); 3076 values.put(Mms.Part.TEXT, new String(part.mData, "UTF-8")); 3077 3078 uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part"); 3079 uri = mResolver.insert(uri, values); 3080 if (V) { 3081 Log.v(TAG, "Added SMIL part"); 3082 } 3083 3084 } else /*VIDEO/AUDIO/IMAGE*/ { 3085 writeMmsDataPart(handle, part, count); 3086 if (V) { 3087 Log.v(TAG, "Added OTHER part"); 3088 } 3089 } 3090 if (uri != null) { 3091 if (V) { 3092 Log.v(TAG, "Added part with content-type: " + part.mContentType 3093 + " to Uri: " + uri.toString()); 3094 } 3095 } 3096 } 3097 } 3098 } catch (UnsupportedEncodingException e) { 3099 Log.w(TAG, e); 3100 } catch (IOException e) { 3101 Log.w(TAG, e); 3102 } 3103 3104 values.clear(); 3105 values.put(Mms.Addr.CONTACT_ID, "null"); 3106 values.put(Mms.Addr.ADDRESS, "insert-address-token"); 3107 values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_FROM); 3108 values.put(Mms.Addr.CHARSET, 106); 3109 3110 uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr"); 3111 uri = mResolver.insert(uri, values); 3112 if (uri != null && V) { 3113 Log.v(TAG, " NEW URI " + uri.toString()); 3114 } 3115 3116 values.clear(); 3117 values.put(Mms.Addr.CONTACT_ID, "null"); 3118 values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_TO); 3119 values.put(Mms.Addr.CHARSET, 106); 3120 for (String address : toAddress) { 3121 values.put(Mms.Addr.ADDRESS, address); 3122 uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr"); 3123 uri = mResolver.insert(uri, values); 3124 if (uri != null && V) { 3125 Log.v(TAG, " NEW URI " + uri.toString()); 3126 } 3127 } 3128 return handle; 3129 } 3130 3131 writeMmsDataPart(long handle, MimePart part, int count)3132 private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException { 3133 ContentValues values = new ContentValues(); 3134 values.put(Mms.Part.MSG_ID, handle); 3135 if (part.mContentType != null) { 3136 values.put(Mms.Part.CONTENT_TYPE, part.mContentType); 3137 } else { 3138 Log.w(TAG, "MMS has no CONTENT_TYPE for part " + count); 3139 } 3140 if (part.mContentId != null) { 3141 values.put(Mms.Part.CONTENT_ID, part.mContentId); 3142 } else { 3143 if (part.mPartName != null) { 3144 values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">"); 3145 } else { 3146 values.put(Mms.Part.CONTENT_ID, "<part_" + count + ">"); 3147 } 3148 } 3149 3150 if (part.mContentLocation != null) { 3151 values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); 3152 } else { 3153 if (part.mPartName != null) { 3154 values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".dat"); 3155 } else { 3156 values.put(Mms.Part.CONTENT_LOCATION, "part_" + count + ".dat"); 3157 } 3158 } 3159 if (part.mContentDisposition != null) { 3160 values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); 3161 } 3162 if (part.mPartName != null) { 3163 values.put(Mms.Part.FILENAME, part.mPartName); 3164 values.put(Mms.Part.NAME, part.mPartName); 3165 } else { 3166 /* We must set at least one part identifier */ 3167 values.put(Mms.Part.FILENAME, "part_" + count + ".dat"); 3168 values.put(Mms.Part.NAME, "part_" + count + ".dat"); 3169 } 3170 Uri partUri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part"); 3171 Uri res = mResolver.insert(partUri, values); 3172 3173 // Add data to part 3174 OutputStream os = mResolver.openOutputStream(res); 3175 os.write(part.mData); 3176 os.close(); 3177 } 3178 3179 sendMessage(PushMsgInfo msgInfo, String msgBody)3180 public void sendMessage(PushMsgInfo msgInfo, String msgBody) { 3181 3182 SmsManager smsMng = SmsManager.getDefault(); 3183 ArrayList<String> parts = smsMng.divideMessage(msgBody); 3184 msgInfo.parts = parts.size(); 3185 // We add a time stamp to differentiate delivery reports from each other for resent messages 3186 msgInfo.timestamp = Calendar.getInstance().getTime().getTime(); 3187 msgInfo.partsDelivered = 0; 3188 msgInfo.partsSent = 0; 3189 3190 ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts); 3191 ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts); 3192 3193 /* We handle the SENT intent in the MAP service, as this object 3194 * is destroyed at disconnect, hence if a disconnect occur while sending 3195 * a message, there is no intent handler to move the message from outbox 3196 * to the correct folder. 3197 * The correct solution would be to create a service that will start based on 3198 * the intent, if BT is turned off. */ 3199 3200 if (parts != null && parts.size() > 0) { 3201 for (int i = 0; i < msgInfo.parts; i++) { 3202 Intent intentDelivery, intentSent; 3203 3204 intentDelivery = new Intent(ACTION_MESSAGE_DELIVERY, null); 3205 /* Add msgId and part number to ensure the intents are different, and we 3206 * thereby get an intent for each msg part. 3207 * setType is needed to create different intents for each message id/ time stamp, 3208 * as the extras are not used when comparing. */ 3209 intentDelivery.setType( 3210 "message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i); 3211 intentDelivery.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id); 3212 intentDelivery.putExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, msgInfo.timestamp); 3213 // TODO(b/171825892) Please replace FLAG_MUTABLE_UNAUDITED below 3214 // with either FLAG_IMMUTABLE (recommended) or FLAG_MUTABLE. 3215 PendingIntent pendingIntentDelivery = 3216 PendingIntent.getBroadcast(mContext, 0, intentDelivery, 3217 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 3218 intentSent = new Intent(ACTION_MESSAGE_SENT, null); 3219 /* Add msgId and part number to ensure the intents are different, and we 3220 * thereby get an intent for each msg part. 3221 * setType is needed to create different intents for each message id/ time stamp, 3222 * as the extras are not used when comparing. */ 3223 intentSent.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i); 3224 intentSent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id); 3225 intentSent.putExtra(EXTRA_MESSAGE_SENT_URI, msgInfo.uri.toString()); 3226 intentSent.putExtra(EXTRA_MESSAGE_SENT_RETRY, msgInfo.retry); 3227 intentSent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, msgInfo.transparent); 3228 3229 // TODO(b/171825892) Please replace FLAG_MUTABLE_UNAUDITED below 3230 // with either FLAG_IMMUTABLE (recommended) or FLAG_MUTABLE. 3231 PendingIntent pendingIntentSent = 3232 PendingIntent.getBroadcast(mContext, 0, intentSent, 3233 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 3234 // We use the same pending intent for all parts, but do not set the one shot flag. 3235 deliveryIntents.add(pendingIntentDelivery); 3236 sentIntents.add(pendingIntentSent); 3237 } 3238 3239 Log.d(TAG, "sendMessage to " + msgInfo.phone); 3240 3241 if (parts.size() == 1) { 3242 smsMng.sendTextMessageWithoutPersisting(msgInfo.phone, null, parts.get(0), 3243 sentIntents.get(0), deliveryIntents.get(0)); 3244 } else { 3245 smsMng.sendMultipartTextMessageWithoutPersisting(msgInfo.phone, null, parts, 3246 sentIntents, deliveryIntents); 3247 } 3248 } 3249 } 3250 3251 private static final String[] ID_PROJECTION = new String[]{Sms._ID}; 3252 private static final Uri UPDATE_STATUS_URI = Uri.withAppendedPath(Sms.CONTENT_URI, "/status"); 3253 3254 private class SmsBroadcastReceiver extends BroadcastReceiver { register()3255 public void register() { 3256 Handler handler = new Handler(Looper.getMainLooper()); 3257 3258 IntentFilter intentFilter = new IntentFilter(); 3259 intentFilter.addAction(ACTION_MESSAGE_DELIVERY); 3260 try { 3261 intentFilter.addDataType("message/*"); 3262 } catch (MalformedMimeTypeException e) { 3263 Log.e(TAG, "Wrong mime type!!!", e); 3264 } 3265 3266 mContext.registerReceiver(this, intentFilter, null, handler); 3267 } 3268 unregister()3269 public void unregister() { 3270 try { 3271 mContext.unregisterReceiver(this); 3272 } catch (IllegalArgumentException e) { 3273 /* do nothing */ 3274 } 3275 } 3276 3277 @Override onReceive(Context context, Intent intent)3278 public void onReceive(Context context, Intent intent) { 3279 String action = intent.getAction(); 3280 long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1); 3281 PushMsgInfo msgInfo = mPushMsgList.get(handle); 3282 3283 Log.d(TAG, "onReceive: action" + action); 3284 3285 if (msgInfo == null) { 3286 Log.d(TAG, "onReceive: no msgInfo found for handle " + handle); 3287 return; 3288 } 3289 3290 if (action.equals(ACTION_MESSAGE_SENT)) { 3291 int result = 3292 intent.getIntExtra(EXTRA_MESSAGE_SENT_RESULT, Activity.RESULT_CANCELED); 3293 msgInfo.partsSent++; 3294 if (result != Activity.RESULT_OK) { 3295 /* If just one of the parts in the message fails, we need to send the 3296 * entire message again 3297 */ 3298 msgInfo.failedSent = true; 3299 } 3300 if (D) { 3301 Log.d(TAG, "onReceive: msgInfo.partsSent = " + msgInfo.partsSent 3302 + ", msgInfo.parts = " + msgInfo.parts + " result = " + result); 3303 } 3304 3305 if (msgInfo.partsSent == msgInfo.parts) { 3306 actionMessageSent(context, intent, msgInfo, handle); 3307 } 3308 } else if (action.equals(ACTION_MESSAGE_DELIVERY)) { 3309 long timestamp = intent.getLongExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, 0); 3310 int status = -1; 3311 if (msgInfo.timestamp == timestamp) { 3312 msgInfo.partsDelivered++; 3313 } 3314 } else { 3315 Log.d(TAG, "onReceive: Unknown action " + action); 3316 } 3317 } 3318 actionMessageSent( Context context, Intent intent, PushMsgInfo msgInfo, long handle)3319 private void actionMessageSent( 3320 Context context, Intent intent, PushMsgInfo msgInfo, long handle) { 3321 /* As the MESSAGE_SENT intent is forwarded from the MAP service, we use the intent 3322 * to carry the result, as getResult() will not return the correct value. 3323 */ 3324 boolean delete = false; 3325 3326 if (D) { 3327 Log.d(TAG, "actionMessageSent(): msgInfo.failedSent = " + msgInfo.failedSent); 3328 } 3329 3330 msgInfo.sendInProgress = false; 3331 3332 if (!msgInfo.failedSent) { 3333 if (D) { 3334 Log.d(TAG, "actionMessageSent: result OK"); 3335 } 3336 if (msgInfo.transparent == 0) { 3337 if (!Utils.moveMessageToFolder(context, msgInfo.uri, true)) { 3338 Log.w(TAG, "Failed to move " + msgInfo.uri + " to SENT"); 3339 } 3340 } else { 3341 delete = true; 3342 } 3343 3344 Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msgInfo.id, 3345 getSmsFolderName(Sms.MESSAGE_TYPE_SENT), null, mSmsType); 3346 sendEvent(evt); 3347 3348 } else { 3349 if (msgInfo.retry == 1) { 3350 /* Notify failure, but keep message in outbox for resending */ 3351 msgInfo.resend = true; 3352 msgInfo.partsSent = 0; // Reset counter for the retry 3353 msgInfo.failedSent = false; 3354 Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id, 3355 getSmsFolderName(Sms.MESSAGE_TYPE_OUTBOX), null, mSmsType); 3356 sendEvent(evt); 3357 } else { 3358 if (msgInfo.transparent == 0) { 3359 if (!Utils.moveMessageToFolder(context, msgInfo.uri, false)) { 3360 Log.w(TAG, "Failed to move " + msgInfo.uri + " to FAILED"); 3361 } 3362 } else { 3363 delete = true; 3364 } 3365 3366 Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id, 3367 getSmsFolderName(Sms.MESSAGE_TYPE_FAILED), null, mSmsType); 3368 sendEvent(evt); 3369 } 3370 } 3371 3372 if (delete) { 3373 /* Delete from Observer message list to avoid delete notifications */ 3374 synchronized (getMsgListSms()) { 3375 getMsgListSms().remove(msgInfo.id); 3376 } 3377 3378 /* Delete from DB */ 3379 Uri msgUri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); 3380 int nRows = mResolver.delete(msgUri, null, null); 3381 if (V && nRows > 0) Log.v(TAG, "Deleted message with Uri = " + msgUri); 3382 } 3383 } 3384 actionMessageDelivery(Context context, Intent intent, PushMsgInfo msgInfo)3385 private void actionMessageDelivery(Context context, Intent intent, PushMsgInfo msgInfo) { 3386 Uri messageUri = intent.getData(); 3387 msgInfo.sendInProgress = false; 3388 3389 Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null); 3390 3391 try { 3392 if (cursor.moveToFirst()) { 3393 int messageId = cursor.getInt(0); 3394 3395 Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId); 3396 3397 if (D) { 3398 Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" 3399 + msgInfo.statusDelivered); 3400 } 3401 3402 ContentValues contentValues = new ContentValues(2); 3403 3404 contentValues.put(Sms.STATUS, msgInfo.statusDelivered); 3405 contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis()); 3406 mResolver.update(updateUri, contentValues, null, null); 3407 } else { 3408 Log.d(TAG, "Can't find message for status update: " + messageUri); 3409 } 3410 } finally { 3411 if (cursor != null) { 3412 cursor.close(); 3413 } 3414 } 3415 3416 if (msgInfo.statusDelivered == 0) { 3417 Event evt = new Event(EVENT_TYPE_DELEVERY_SUCCESS, msgInfo.id, 3418 getSmsFolderName(Sms.MESSAGE_TYPE_SENT), null, mSmsType); 3419 sendEvent(evt); 3420 } else { 3421 Event evt = new Event(EVENT_TYPE_DELIVERY_FAILURE, msgInfo.id, 3422 getSmsFolderName(Sms.MESSAGE_TYPE_SENT), null, mSmsType); 3423 sendEvent(evt); 3424 } 3425 3426 mPushMsgList.remove(msgInfo.id); 3427 } 3428 } 3429 3430 private class CeBroadcastReceiver extends BroadcastReceiver { register()3431 public void register() { 3432 UserManager manager = UserManager.get(mContext); 3433 if (manager == null || manager.isUserUnlocked()) { 3434 mStorageUnlocked = true; 3435 return; 3436 } 3437 3438 Handler handler = new Handler(Looper.getMainLooper()); 3439 IntentFilter intentFilter = new IntentFilter(); 3440 intentFilter.addAction(Intent.ACTION_BOOT_COMPLETED); 3441 mContext.registerReceiver(this, intentFilter, null, handler); 3442 } 3443 unregister()3444 public void unregister() { 3445 try { 3446 mContext.unregisterReceiver(this); 3447 } catch (IllegalArgumentException e) { 3448 /* do nothing */ 3449 } 3450 } 3451 3452 @Override onReceive(Context context, Intent intent)3453 public void onReceive(Context context, Intent intent) { 3454 String action = intent.getAction(); 3455 Log.d(TAG, "onReceive: action" + action); 3456 3457 if (action.equals(Intent.ACTION_BOOT_COMPLETED)) { 3458 try { 3459 initMsgList(); 3460 } catch (RemoteException e) { 3461 Log.e(TAG, "Error initializing SMS/MMS message lists."); 3462 } 3463 3464 for (String folder : FOLDER_SMS_MAP.values()) { 3465 Event evt = new Event(EVENT_TYPE_NEW, -1, folder, mSmsType); 3466 sendEvent(evt); 3467 } 3468 mStorageUnlocked = true; 3469 /* After unlock this BroadcastReceiver is never needed */ 3470 unregister(); 3471 } else { 3472 Log.d(TAG, "onReceive: Unknown action " + action); 3473 } 3474 } 3475 } 3476 3477 /** 3478 * Handle MMS sent intents in disconnected(MNS) state, where we do not need to send any 3479 * notifications. 3480 * @param context The context to use for provider operations 3481 * @param intent The intent received 3482 * @param result The result 3483 */ actionMmsSent(Context context, Intent intent, int result, Map<Long, Msg> mmsMsgList)3484 public static void actionMmsSent(Context context, Intent intent, int result, 3485 Map<Long, Msg> mmsMsgList) { 3486 /* 3487 * if transparent: 3488 * delete message and send notification(regardless of result) 3489 * else 3490 * Result == Success: 3491 * move to sent folder (will trigger notification) 3492 * Result == Fail: 3493 * move to outbox (send delivery fail notification) 3494 */ 3495 if (D) { 3496 Log.d(TAG, "actionMmsSent()"); 3497 } 3498 int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0); 3499 long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1); 3500 if (handle < 0) { 3501 Log.w(TAG, "Intent received for an invalid handle"); 3502 return; 3503 } 3504 ContentResolver resolver = context.getContentResolver(); 3505 if (transparent == 1) { 3506 /* The specification is a bit unclear about the transparent flag. If it is set 3507 * no copy of the message shall be kept in the send folder after the message 3508 * was send, but in the case of a send error, it is unclear what to do. 3509 * As it will not be transparent if we keep the message in any folder, 3510 * we delete the message regardless of the result. 3511 * If we however do have a MNS connection we need to send a notification. */ 3512 Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); 3513 /* Delete from observer message list to avoid delete notifications */ 3514 if (mmsMsgList != null) { 3515 synchronized (mmsMsgList) { 3516 mmsMsgList.remove(handle); 3517 } 3518 } 3519 /* Delete message */ 3520 if (D) { 3521 Log.d(TAG, "Transparent in use - delete"); 3522 } 3523 resolver.delete(uri, null, null); 3524 } else if (result == Activity.RESULT_OK) { 3525 /* This will trigger a notification */ 3526 moveMmsToFolder(handle, resolver, Mms.MESSAGE_BOX_SENT); 3527 } else { 3528 if (mmsMsgList != null) { 3529 synchronized (mmsMsgList) { 3530 Msg msg = mmsMsgList.get(handle); 3531 if (msg != null) { 3532 msg.type = Mms.MESSAGE_BOX_OUTBOX; 3533 } 3534 } 3535 } 3536 /* Hand further retries over to the MMS application */ 3537 moveMmsToFolder(handle, resolver, Mms.MESSAGE_BOX_OUTBOX); 3538 } 3539 } 3540 actionMessageSentDisconnected(Context context, Intent intent, int result)3541 public static void actionMessageSentDisconnected(Context context, Intent intent, int result) { 3542 TYPE type = TYPE.fromOrdinal( 3543 intent.getIntExtra(EXTRA_MESSAGE_SENT_MSG_TYPE, TYPE.NONE.ordinal())); 3544 if (type == TYPE.MMS) { 3545 actionMmsSent(context, intent, result, null); 3546 } else { 3547 actionSmsSentDisconnected(context, intent, result); 3548 } 3549 } 3550 actionSmsSentDisconnected(Context context, Intent intent, int result)3551 public static void actionSmsSentDisconnected(Context context, Intent intent, int result) { 3552 /* Check permission for message deletion. */ 3553 if ((Binder.getCallingPid() != Process.myPid()) 3554 || !Utils.checkCallerHasWriteSmsPermission(context)) { 3555 Log.w(TAG, "actionSmsSentDisconnected: Not allowed to delete SMS/MMS messages"); 3556 return; 3557 } 3558 3559 boolean delete = false; 3560 //int retry = intent.getIntExtra(EXTRA_MESSAGE_SENT_RETRY, 0); 3561 int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0); 3562 String uriString = intent.getStringExtra(EXTRA_MESSAGE_SENT_URI); 3563 if (uriString == null) { 3564 // Nothing we can do about it, just bail out 3565 return; 3566 } 3567 Uri uri = Uri.parse(uriString); 3568 3569 if (result == Activity.RESULT_OK) { 3570 Log.d(TAG, "actionMessageSentDisconnected: result OK"); 3571 if (transparent == 0) { 3572 if (!Utils.moveMessageToFolder(context, uri, true)) { 3573 Log.d(TAG, "Failed to move " + uri + " to SENT"); 3574 } 3575 } else { 3576 delete = true; 3577 } 3578 } else { 3579 /*if (retry == 1) { 3580 The retry feature only works while connected, else we fail the send, 3581 * and move the message to failed, to let the user/app resend manually later. 3582 } else */ 3583 { 3584 if (transparent == 0) { 3585 if (!Utils.moveMessageToFolder(context, uri, false)) { 3586 Log.d(TAG, "Failed to move " + uri + " to FAILED"); 3587 } 3588 } else { 3589 delete = true; 3590 } 3591 } 3592 } 3593 3594 if (delete) { 3595 /* Delete from DB */ 3596 ContentResolver resolver = context.getContentResolver(); 3597 if (resolver != null) { 3598 resolver.delete(uri, null, null); 3599 } else { 3600 Log.w(TAG, "Unable to get resolver"); 3601 } 3602 } 3603 } 3604 registerPhoneServiceStateListener()3605 private void registerPhoneServiceStateListener() { 3606 TelephonyManager tm = 3607 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); 3608 tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE); 3609 } 3610 unRegisterPhoneServiceStateListener()3611 private void unRegisterPhoneServiceStateListener() { 3612 TelephonyManager tm = 3613 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); 3614 tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE); 3615 } 3616 resendPendingMessages()3617 private void resendPendingMessages() { 3618 /* Send pending messages in outbox */ 3619 String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX; 3620 UserManager manager = UserManager.get(mContext); 3621 if (manager == null || !manager.isUserUnlocked()) { 3622 return; 3623 } 3624 Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null); 3625 try { 3626 if (c != null && c.moveToFirst()) { 3627 do { 3628 long id = c.getLong(c.getColumnIndex(Sms._ID)); 3629 String msgBody = c.getString(c.getColumnIndex(Sms.BODY)); 3630 PushMsgInfo msgInfo = mPushMsgList.get(id); 3631 if (msgInfo == null || !msgInfo.resend || msgInfo.sendInProgress) { 3632 continue; 3633 } 3634 msgInfo.sendInProgress = true; 3635 sendMessage(msgInfo, msgBody); 3636 } while (c.moveToNext()); 3637 } 3638 } finally { 3639 if (c != null) { 3640 c.close(); 3641 } 3642 } 3643 3644 3645 } 3646 failPendingMessages()3647 private void failPendingMessages() { 3648 /* Move pending messages from outbox to failed */ 3649 String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX; 3650 Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null); 3651 try { 3652 if (c != null && c.moveToFirst()) { 3653 do { 3654 long id = c.getLong(c.getColumnIndex(Sms._ID)); 3655 String msgBody = c.getString(c.getColumnIndex(Sms.BODY)); 3656 PushMsgInfo msgInfo = mPushMsgList.get(id); 3657 if (msgInfo == null || !msgInfo.resend) { 3658 continue; 3659 } 3660 Utils.moveMessageToFolder(mContext, msgInfo.uri, false); 3661 } while (c.moveToNext()); 3662 } 3663 } finally { 3664 if (c != null) { 3665 c.close(); 3666 } 3667 } 3668 3669 } 3670 removeDeletedMessages()3671 private void removeDeletedMessages() { 3672 /* Remove messages from virtual "deleted" folder (thread_id -1) */ 3673 mResolver.delete(Sms.CONTENT_URI, "thread_id = " + DELETED_THREAD_ID, null); 3674 } 3675 3676 private PhoneStateListener mPhoneListener = new PhoneStateListener() { 3677 @Override 3678 public void onServiceStateChanged(ServiceState serviceState) { 3679 Log.d(TAG, "Phone service state change: " + serviceState.getState()); 3680 if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) { 3681 resendPendingMessages(); 3682 } 3683 } 3684 }; 3685 init()3686 public void init() { 3687 if (mSmsBroadcastReceiver != null) { 3688 mSmsBroadcastReceiver.register(); 3689 } 3690 3691 if (mCeBroadcastReceiver != null) { 3692 mCeBroadcastReceiver.register(); 3693 } 3694 3695 registerPhoneServiceStateListener(); 3696 mInitialized = true; 3697 } 3698 deinit()3699 public void deinit() { 3700 mInitialized = false; 3701 unregisterObserver(); 3702 if (mSmsBroadcastReceiver != null) { 3703 mSmsBroadcastReceiver.unregister(); 3704 } 3705 unRegisterPhoneServiceStateListener(); 3706 if (UserManager.get(mContext).isUserUnlocked()) { 3707 failPendingMessages(); 3708 removeDeletedMessages(); 3709 } 3710 } 3711 handleSmsSendIntent(Context context, Intent intent)3712 public boolean handleSmsSendIntent(Context context, Intent intent) { 3713 TYPE type = TYPE.fromOrdinal( 3714 intent.getIntExtra(EXTRA_MESSAGE_SENT_MSG_TYPE, TYPE.NONE.ordinal())); 3715 if (type == TYPE.MMS) { 3716 return handleMmsSendIntent(context, intent); 3717 } else { 3718 if (mInitialized) { 3719 mSmsBroadcastReceiver.onReceive(context, intent); 3720 return true; 3721 } 3722 } 3723 return false; 3724 } 3725 handleMmsSendIntent(Context context, Intent intent)3726 public boolean handleMmsSendIntent(Context context, Intent intent) { 3727 if (D) { 3728 Log.w(TAG, "handleMmsSendIntent()"); 3729 } 3730 if (!mMnsClient.isConnected()) { 3731 // No need to handle notifications, just use default handling 3732 if (D) { 3733 Log.w(TAG, "MNS not connected - use static handling"); 3734 } 3735 return false; 3736 } 3737 long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1); 3738 int result = intent.getIntExtra(EXTRA_MESSAGE_SENT_RESULT, Activity.RESULT_CANCELED); 3739 actionMmsSent(context, intent, result, getMsgListMms()); 3740 if (handle < 0) { 3741 Log.w(TAG, "Intent received for an invalid handle"); 3742 return true; 3743 } 3744 if (result != Activity.RESULT_OK) { 3745 if (mObserverRegistered) { 3746 Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, handle, 3747 getMmsFolderName(Mms.MESSAGE_BOX_OUTBOX), null, TYPE.MMS); 3748 sendEvent(evt); 3749 } 3750 } else { 3751 int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0); 3752 if (transparent != 0) { 3753 if (mObserverRegistered) { 3754 Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, handle, 3755 getMmsFolderName(Mms.MESSAGE_BOX_OUTBOX), null, TYPE.MMS); 3756 sendEvent(evt); 3757 } 3758 } 3759 } 3760 return true; 3761 } 3762 3763 } 3764