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