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