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