1/* 2* Copyright (C) 2015 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 16package com.android.bluetooth.mapapi; 17 18import android.content.ContentProvider; 19import android.content.ContentResolver; 20import android.content.ContentValues; 21import android.content.Context; 22import android.content.UriMatcher; 23import android.content.pm.ProviderInfo; 24import android.database.Cursor; 25import android.net.Uri; 26import android.os.Binder; 27import android.os.Bundle; 28import android.util.Log; 29 30import java.util.List; 31import java.util.Map; 32import java.util.Map.Entry; 33import java.util.Set; 34 35/** 36 * A base implementation of the BluetoothMapContract. 37 * A base class for a ContentProvider that allows access to Instant messages from a Bluetooth 38 * device through the Message Access Profile. 39 */ 40public abstract class BluetoothMapIMProvider extends ContentProvider { 41 42 private static final String TAG = "BluetoothMapIMProvider"; 43 private static final boolean D = true; 44 45 private static final int MATCH_ACCOUNT = 1; 46 private static final int MATCH_MESSAGE = 3; 47 private static final int MATCH_CONVERSATION = 4; 48 private static final int MATCH_CONVOCONTACT = 5; 49 50 protected ContentResolver mResolver; 51 52 private Uri CONTENT_URI = null; 53 private String mAuthority; 54 private UriMatcher mMatcher; 55 56 /** 57 * @return the CONTENT_URI exposed. This will be used to send out notifications. 58 */ 59 abstract protected Uri getContentUri(); 60 61 /** 62 * Implementation is provided by the parent class. 63 */ 64 @Override 65 public void attachInfo(Context context, ProviderInfo info) { 66 mAuthority = info.authority; 67 68 mMatcher = new UriMatcher(UriMatcher.NO_MATCH); 69 mMatcher.addURI(mAuthority, BluetoothMapContract.TABLE_ACCOUNT, MATCH_ACCOUNT); 70 mMatcher.addURI(mAuthority, "#/"+ BluetoothMapContract.TABLE_MESSAGE, MATCH_MESSAGE); 71 mMatcher.addURI(mAuthority, "#/"+ BluetoothMapContract.TABLE_CONVERSATION, 72 MATCH_CONVERSATION); 73 mMatcher.addURI(mAuthority, "#/"+ BluetoothMapContract.TABLE_CONVOCONTACT, 74 MATCH_CONVOCONTACT); 75 76 // Sanity check our setup 77 if (!info.exported) { 78 throw new SecurityException("Provider must be exported"); 79 } 80 // Enforce correct permissions are used 81 if (!android.Manifest.permission.BLUETOOTH_MAP.equals(info.writePermission)){ 82 throw new SecurityException("Provider must be protected by " + 83 android.Manifest.permission.BLUETOOTH_MAP); 84 } 85 if(D) Log.d(TAG,"attachInfo() mAuthority = " + mAuthority); 86 87 mResolver = context.getContentResolver(); 88 super.attachInfo(context, info); 89 } 90 91 /** 92 * This function shall be called when any Account database content have changed 93 * to Notify any attached observers. 94 * @param accountId the ID of the account that changed. Null is a valid value, 95 * if accountId is unknown or multiple accounts changed. 96 */ 97 protected void onAccountChanged(String accountId) { 98 Uri newUri = null; 99 100 if(mAuthority == null){ 101 return; 102 } 103 if(accountId == null){ 104 newUri = BluetoothMapContract.buildAccountUri(mAuthority); 105 } else { 106 newUri = BluetoothMapContract.buildAccountUriwithId(mAuthority, accountId); 107 } 108 109 if(D) Log.d(TAG,"onAccountChanged() accountId = " + accountId + " URI: " + newUri); 110 mResolver.notifyChange(newUri, null); 111 } 112 113 /** 114 * This function shall be called when any Message database content have changed 115 * to notify any attached observers. 116 * @param accountId Null is a valid value, if accountId is unknown, but 117 * recommended for increased performance. 118 * @param messageId Null is a valid value, if multiple messages changed or the 119 * messageId is unknown, but recommended for increased performance. 120 */ 121 protected void onMessageChanged(String accountId, String messageId) { 122 Uri newUri = null; 123 124 if(mAuthority == null){ 125 return; 126 } 127 if(accountId == null){ 128 newUri = BluetoothMapContract.buildMessageUri(mAuthority); 129 } else { 130 if(messageId == null) 131 { 132 newUri = BluetoothMapContract.buildMessageUri(mAuthority,accountId); 133 } else { 134 newUri = BluetoothMapContract.buildMessageUriWithId(mAuthority,accountId, 135 messageId); 136 } 137 } 138 if(D) Log.d(TAG,"onMessageChanged() accountId = " + accountId 139 + " messageId = " + messageId + " URI: " + newUri); 140 mResolver.notifyChange(newUri, null); 141 } 142 143 144 /** 145 * This function shall be called when any Message database content have changed 146 * to notify any attached observers. 147 * @param accountId Null is a valid value, if accountId is unknown, but 148 * recommended for increased performance. 149 * @param contactId Null is a valid value, if multiple contacts changed or the 150 * contactId is unknown, but recommended for increased performance. 151 */ 152 protected void onContactChanged(String accountId, String contactId) { 153 Uri newUri = null; 154 155 if(mAuthority == null){ 156 return; 157 } 158 if(accountId == null){ 159 newUri = BluetoothMapContract.buildConvoContactsUri(mAuthority); 160 } else { 161 if(contactId == null) 162 { 163 newUri = BluetoothMapContract.buildConvoContactsUri(mAuthority,accountId); 164 } else { 165 newUri = BluetoothMapContract.buildConvoContactsUriWithId(mAuthority, accountId, 166 contactId); 167 } 168 } 169 if(D) Log.d(TAG,"onContactChanged() accountId = " + accountId 170 + " contactId = " + contactId + " URI: " + newUri); 171 mResolver.notifyChange(newUri, null); 172 } 173 174 /** 175 * Not used, this is just a dummy implementation. 176 * TODO: We might need to something intelligent here after introducing IM 177 */ 178 @Override 179 public String getType(Uri uri) { 180 return "InstantMessage"; 181 } 182 183 /** 184 * The MAP specification states that a delete request from MAP client is a folder shift to the 185 * 'deleted' folder. 186 * Only use case of delete() is when transparency is requested for push messages, then 187 * message should not remain in sent folder and therefore must be deleted 188 */ 189 @Override 190 public int delete(Uri uri, String where, String[] selectionArgs) { 191 if (D) Log.d(TAG, "delete(): uri=" + uri.toString() ); 192 int result = 0; 193 194 String table = uri.getPathSegments().get(1); 195 if(table == null) 196 throw new IllegalArgumentException("Table missing in URI"); 197 // the id of the entry to be deleted from the database 198 String messageId = uri.getLastPathSegment(); 199 if (messageId == null) 200 throw new IllegalArgumentException("Message ID missing in update values!"); 201 202 String accountId = getAccountId(uri); 203 if (accountId == null) 204 throw new IllegalArgumentException("Account ID missing in update values!"); 205 206 long callingId = Binder.clearCallingIdentity(); 207 try { 208 if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) { 209 return deleteMessage(accountId, messageId); 210 } else { 211 if (D) Log.w(TAG, "Unknown table name: " + table); 212 return result; 213 } 214 } finally { 215 Binder.restoreCallingIdentity(callingId); 216 } 217 } 218 219 /** 220 * This function deletes a message. 221 * @param accountId the ID of the Account 222 * @param messageId the ID of the message to delete. 223 * @return the number of messages deleted - 0 if the message was not found. 224 */ 225 abstract protected int deleteMessage(String accountId, String messageId); 226 227 /** 228 * Insert is used to add new messages to the data base. 229 * Insert message approach: 230 * - Insert an empty message to get an _id with only a folder_id 231 * - Open the _id for write 232 * - Write the message content 233 * (When the writer completes, this provider should do an update of the message) 234 */ 235 @Override 236 public Uri insert(Uri uri, ContentValues values) { 237 String table = uri.getLastPathSegment(); 238 if(table == null) 239 throw new IllegalArgumentException("Table missing in URI"); 240 241 String accountId = getAccountId(uri); 242 if (accountId == null) 243 throw new IllegalArgumentException("Account ID missing in URI"); 244 245 // TODO: validate values? 246 247 String id; // the id of the entry inserted into the database 248 long callingId = Binder.clearCallingIdentity(); 249 Log.d(TAG, "insert(): uri=" + uri.toString() + " - getLastPathSegment() = " + 250 uri.getLastPathSegment()); 251 try { 252 if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) { 253 id = insertMessage(accountId, values); 254 if(D) Log.i(TAG, "insert() ID: " + id); 255 return Uri.parse(uri.toString() + "/" + id); 256 } else { 257 Log.w(TAG, "Unknown table name: " + table); 258 return null; 259 } 260 } finally { 261 Binder.restoreCallingIdentity(callingId); 262 } 263 } 264 265 266 /** 267 * Inserts an empty message into the Message data base in the specified folder. 268 * This is done before the actual message content is written by fileIO. 269 * @param accountId the ID of the account 270 * @param folderId the ID of the folder to create a new message in. 271 * @return the message id as a string 272 */ 273 abstract protected String insertMessage(String accountId, ContentValues values); 274 275 /** 276 * Utility function to build a projection based on a projectionMap. 277 * 278 * "btColumnName" -> "imColumnName as btColumnName" for each entry. 279 * 280 * This supports SQL statements in the column name entry. 281 * @param projection 282 * @param projectionMap <string, string> 283 * @return the converted projection 284 */ 285 protected String[] convertProjection(String[] projection, Map<String,String> projectionMap) { 286 String[] newProjection = new String[projection.length]; 287 for(int i = 0; i < projection.length; i++) { 288 newProjection[i] = projectionMap.get(projection[i]) + " as " + projection[i]; 289 } 290 return newProjection; 291 } 292 293 /** 294 * This query needs to map from the data used in the e-mail client to 295 * BluetoothMapContract type of data. 296 */ 297 @Override 298 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 299 String sortOrder) { 300 long callingId = Binder.clearCallingIdentity(); 301 try { 302 String accountId = null; 303 if(D)Log.w(TAG, "query(): uri =" + mAuthority + " uri=" + uri.toString()); 304 305 switch (mMatcher.match(uri)) { 306 case MATCH_ACCOUNT: 307 return queryAccount(projection, selection, selectionArgs, sortOrder); 308 case MATCH_MESSAGE: 309 // TODO: Extract account from URI 310 accountId = getAccountId(uri); 311 return queryMessage(accountId, projection, selection, selectionArgs, sortOrder); 312 case MATCH_CONVERSATION: 313 accountId = getAccountId(uri); 314 String value; 315 String searchString = 316 uri.getQueryParameter(BluetoothMapContract.FILTER_ORIGINATOR_SUBSTRING); 317 Long periodBegin = null; 318 value = uri.getQueryParameter(BluetoothMapContract.FILTER_PERIOD_BEGIN); 319 if(value != null) { 320 periodBegin = Long.parseLong(value); 321 } 322 Long periodEnd = null; 323 value = uri.getQueryParameter(BluetoothMapContract.FILTER_PERIOD_END); 324 if(value != null) { 325 periodEnd = Long.parseLong(value); 326 } 327 Boolean read = null; 328 value = uri.getQueryParameter(BluetoothMapContract.FILTER_READ_STATUS); 329 if(value != null) { 330 read = value.equalsIgnoreCase("true"); 331 } 332 Long threadId = null; 333 value = uri.getQueryParameter(BluetoothMapContract.FILTER_THREAD_ID); 334 if(value != null) { 335 threadId = Long.parseLong(value); 336 } 337 return queryConversation(accountId, threadId, read, periodEnd, periodBegin, 338 searchString, projection, sortOrder); 339 case MATCH_CONVOCONTACT: 340 accountId = getAccountId(uri); 341 long contactId = 0; 342 return queryConvoContact(accountId, contactId, projection, 343 selection, selectionArgs, sortOrder); 344 default: 345 throw new UnsupportedOperationException("Unsupported Uri " + uri); 346 } 347 } finally { 348 Binder.restoreCallingIdentity(callingId); 349 } 350 } 351 352 /** 353 * Query account information. 354 * This function shall return only exposable e-mail accounts. Hence shall not 355 * return accounts that has policies suggesting not to be shared them. 356 * @param projection 357 * @param selection 358 * @param selectionArgs 359 * @param sortOrder 360 * @return a cursor to the accounts that are subject to exposure over BT. 361 */ 362 abstract protected Cursor queryAccount(String[] projection, String selection, 363 String[] selectionArgs, String sortOrder); 364 365 /** 366 * For the message table the selection (where clause) can only include the following columns: 367 * date: less than, greater than and equals 368 * flagRead: = 1 or = 0 369 * flagPriority: = 1 or = 0 370 * folder_id: the ID of the folder only equals 371 * toList: partial name/address search 372 * fromList: partial name/address search 373 * Additionally the COUNT and OFFSET shall be supported. 374 * @param accountId the ID of the account 375 * @param projection 376 * @param selection 377 * @param selectionArgs 378 * @param sortOrder 379 * @return a cursor to query result 380 */ 381 abstract protected Cursor queryMessage(String accountId, String[] projection, String selection, 382 String[] selectionArgs, String sortOrder); 383 384 /** 385 * For the Conversation table the selection (where clause) can only include 386 * the following columns: 387 * _id: the ID of the conversation only equals 388 * name: partial name search 389 * last_activity: less than, greater than and equals 390 * version_counter: updated IDs are regenerated 391 * Additionally the COUNT and OFFSET shall be supported. 392 * @param accountId the ID of the account 393 * @param threadId the ID of the conversation 394 * @param projection 395 * @param selection 396 * @param selectionArgs 397 * @param sortOrder 398 * @return a cursor to query result 399 */ 400// abstract protected Cursor queryConversation(Long threadId, String[] projection, 401// String selection, String[] selectionArgs, String sortOrder); 402 403 /** 404 * Query for conversations with contact information. The expected result is a cursor pointing 405 * to one row for each contact in a conversation. 406 * E.g.: 407 * ThreadId | ThreadName | ... | ContactName | ContactPrecence | ... | 408 * 1 | "Bowling" | ... | Hans | 1 | ... | 409 * 1 | "Bowling" | ... | Peter | 2 | ... | 410 * 2 | "" | ... | Peter | 2 | ... | 411 * 3 | "" | ... | Hans | 1 | ... | 412 * 413 * @param accountId the ID of the account 414 * @param threadId filter on a single threadId - null if no filtering is needed. 415 * @param read filter on a read status: 416 * null: no filtering on read is needed. 417 * true: return only threads that has NO unread messages. 418 * false: return only threads that has unread messages. 419 * @param periodEnd last_activity time stamp of the the newest thread to include in the 420 * result. 421 * @param periodBegin last_activity time stamp of the the oldest thread to include in the 422 * result. 423 * @param searchString if not null, include only threads that has contacts that matches the 424 * searchString as part of the contact name or nickName. 425 * @param projection A list of the columns that is needed in the result 426 * @param sortOrder the sort order 427 * @return a Cursor representing the query result. 428 */ 429 abstract protected Cursor queryConversation(String accountId, Long threadId, Boolean read, 430 Long periodEnd, Long periodBegin, String searchString, String[] projection, 431 String sortOrder); 432 433 /** 434 * For the ConvoContact table the selection (where clause) can only include the 435 * following columns: 436 * _id: the ID of the contact only equals 437 * convo_id: id of conversation contact is part of 438 * name: partial name search 439 * x_bt_uid: the ID of the bt uid only equals 440 * chat_state: active, inactive, gone, composing, paused 441 * last_active: less than, greater than and equals 442 * presence_state: online, do_not_disturb, away, offline 443 * priority: level of priority 0 - 100 444 * last_online: less than, greater than and equals 445 * @param accountId the ID of the account 446 * @param contactId the ID of the contact 447 * @param projection 448 * @param selection 449 * @param selectionArgs 450 * @param sortOrder 451 * @return a cursor to query result 452 */ 453 abstract protected Cursor queryConvoContact(String accountId, Long contactId, 454 String[] projection, String selection, String[] selectionArgs, String sortOrder); 455 456 /** 457 * update() 458 * Messages can be modified in the following cases: 459 * - the folder_key of a message - hence the message can be moved to a new folder, 460 * but the content cannot be modified. 461 * - the FLAG_READ state can be changed. 462 * Conversations can be modified in the following cases: 463 * - the read status - changing between read, unread 464 * - the last activity - the time stamp of last message sent of received in the conversation 465 * ConvoContacts can be modified in the following cases: 466 * - the chat_state - chat status of the contact in conversation 467 * - the last_active - the time stamp of last action in the conversation 468 * - the presence_state - the time stamp of last time contact online 469 * - the status - the status text of the contact available in a conversation 470 * - the last_online - the time stamp of last time contact online 471 * The selection statement will always be selection of a message ID, when updating a message, 472 * hence this function will be called multiple times if multiple messages must be updated 473 * due to the nature of the Bluetooth Message Access profile. 474 */ 475 @Override 476 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 477 478 String table = uri.getLastPathSegment(); 479 if(table == null){ 480 throw new IllegalArgumentException("Table missing in URI"); 481 } 482 if(selection != null) { 483 throw new IllegalArgumentException("selection shall not be used, ContentValues " + 484 "shall contain the data"); 485 } 486 487 long callingId = Binder.clearCallingIdentity(); 488 if(D)Log.w(TAG, "update(): uri=" + uri.toString() + " - getLastPathSegment() = " + 489 uri.getLastPathSegment()); 490 try { 491 if(table.equals(BluetoothMapContract.TABLE_ACCOUNT)) { 492 String accountId = values.getAsString(BluetoothMapContract.AccountColumns._ID); 493 if(accountId == null) { 494 throw new IllegalArgumentException("Account ID missing in update values!"); 495 } 496 Integer exposeFlag = values.getAsInteger( 497 BluetoothMapContract.AccountColumns.FLAG_EXPOSE); 498 if(exposeFlag == null){ 499 throw new IllegalArgumentException("Expose flag missing in update values!"); 500 } 501 return updateAccount(accountId, exposeFlag); 502 } else if(table.equals(BluetoothMapContract.TABLE_FOLDER)) { 503 return 0; // We do not support changing folders 504 } else if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) { 505 String accountId = getAccountId(uri); 506 if(accountId == null) { 507 throw new IllegalArgumentException("Account ID missing in update values!"); 508 } 509 Long messageId = values.getAsLong(BluetoothMapContract.MessageColumns._ID); 510 if(messageId == null) { 511 throw new IllegalArgumentException("Message ID missing in update values!"); 512 } 513 Long folderId = values.getAsLong(BluetoothMapContract.MessageColumns.FOLDER_ID); 514 Boolean flagRead = values.getAsBoolean( 515 BluetoothMapContract.MessageColumns.FLAG_READ); 516 return updateMessage(accountId, messageId, folderId, flagRead); 517 } else if(table.equals(BluetoothMapContract.TABLE_CONVERSATION)) { 518 return 0; // We do not support changing conversation 519 } else if(table.equals(BluetoothMapContract.TABLE_CONVOCONTACT)) { 520 return 0; // We do not support changing contacts 521 } else { 522 if(D)Log.w(TAG, "Unknown table name: " + table); 523 return 0; 524 } 525 } finally { 526 Binder.restoreCallingIdentity(callingId); 527 } 528 } 529 530 /** 531 * Update an entry in the account table. Only the expose flag will be 532 * changed through this interface. 533 * @param accountId the ID of the account to change. 534 * @param flagExpose the updated value. 535 * @return the number of entries changed - 0 if account not found or value cannot be changed. 536 */ 537 abstract protected int updateAccount(String accountId, Integer flagExpose); 538 539 /** 540 * Update an entry in the message table. 541 * @param accountId ID of the account to which the messageId relates 542 * @param messageId the ID of the message to update 543 * @param folderId the new folder ID value to set - ignore if null. 544 * @param flagRead the new flagRead value to set - ignore if null. 545 * @return 546 */ 547 abstract protected int updateMessage(String accountId, Long messageId, Long folderId, 548 Boolean flagRead); 549 550 /** 551 * Utility function to Creates a ContentValues object based on a modified valuesSet. 552 * To be used after changing the keys and optionally values of a valueSet obtained 553 * from a ContentValues object received in update(). 554 * @param valueSet the values as received in the contentProvider 555 * @param keyMap the key map <btKey, emailKey> 556 * @return a new ContentValues object with the keys replaced as specified in the 557 * keyMap 558 */ 559 protected ContentValues createContentValues(Set<Entry<String,Object>> valueSet, 560 Map<String, String> keyMap) { 561 ContentValues values = new ContentValues(valueSet.size()); 562 for(Entry<String,Object> ent : valueSet) { 563 String key = keyMap.get(ent.getKey()); // Convert the key name 564 Object value = ent.getValue(); 565 if(value == null) { 566 values.putNull(key); 567 } else if(ent.getValue() instanceof Boolean) { 568 values.put(key, (Boolean) value); 569 } else if(ent.getValue() instanceof Byte) { 570 values.put(key, (Byte) value); 571 } else if(ent.getValue() instanceof byte[]) { 572 values.put(key, (byte[]) value); 573 } else if(ent.getValue() instanceof Double) { 574 values.put(key, (Double) value); 575 } else if(ent.getValue() instanceof Float) { 576 values.put(key, (Float) value); 577 } else if(ent.getValue() instanceof Integer) { 578 values.put(key, (Integer) value); 579 } else if(ent.getValue() instanceof Long) { 580 values.put(key, (Long) value); 581 } else if(ent.getValue() instanceof Short) { 582 values.put(key, (Short) value); 583 } else if(ent.getValue() instanceof String) { 584 values.put(key, (String) value); 585 } else { 586 throw new IllegalArgumentException("Unknown data type in content value"); 587 } 588 } 589 return values; 590 } 591 592 @Override 593 public Bundle call(String method, String arg, Bundle extras) { 594 long callingId = Binder.clearCallingIdentity(); 595 if(D)Log.w(TAG, "call(): method=" + method + " arg=" + arg + "ThreadId: " 596 + Thread.currentThread().getId()); 597 int ret = -1; 598 try { 599 if(method.equals(BluetoothMapContract.METHOD_UPDATE_FOLDER)) { 600 long accountId = extras.getLong(BluetoothMapContract.EXTRA_UPDATE_ACCOUNT_ID, -1); 601 if(accountId == -1) { 602 Log.w(TAG, "No account ID in CALL"); 603 return null; 604 } 605 long folderId = extras.getLong(BluetoothMapContract.EXTRA_UPDATE_FOLDER_ID, -1); 606 if(folderId == -1) { 607 Log.w(TAG, "No folder ID in CALL"); 608 return null; 609 } 610 ret = syncFolder(accountId, folderId); 611 } else if (method.equals(BluetoothMapContract.METHOD_SET_OWNER_STATUS)) { 612 int presenceState = extras.getInt(BluetoothMapContract.EXTRA_PRESENCE_STATE); 613 String presenceStatus = extras.getString( 614 BluetoothMapContract.EXTRA_PRESENCE_STATUS); 615 long lastActive = extras.getLong(BluetoothMapContract.EXTRA_LAST_ACTIVE); 616 int chatState = extras.getInt(BluetoothMapContract.EXTRA_CHAT_STATE); 617 String convoId = extras.getString(BluetoothMapContract.EXTRA_CONVERSATION_ID); 618 ret = setOwnerStatus(presenceState, presenceStatus, lastActive, chatState, convoId); 619 620 } else if (method.equals(BluetoothMapContract.METHOD_SET_BLUETOOTH_STATE)) { 621 boolean bluetoothState = extras.getBoolean( 622 BluetoothMapContract.EXTRA_BLUETOOTH_STATE); 623 ret = setBluetoothStatus(bluetoothState); 624 } 625 } finally { 626 Binder.restoreCallingIdentity(callingId); 627 } 628 if(ret == 0) { 629 return new Bundle(); 630 } 631 return null; 632 } 633 634 /** 635 * Trigger a sync of the specified folder. 636 * @param accountId the ID of the account that owns the folder 637 * @param folderId the ID of the folder. 638 * @return 0 at success 639 */ 640 abstract protected int syncFolder(long accountId, long folderId); 641 642 /** 643 * Set the properties that should change presence or chat state of owner 644 * e.g. when the owner is active on a BT client device but not on the BT server device 645 * where the IM application is installed, it should still be possible to show an active status. 646 * @param presenceState should follow the contract specified values 647 * @param presenceStatus string the owners current status 648 * @param lastActive time stamp of the owners last activity 649 * @param chatState should follow the contract specified values 650 * @param convoId ID to the conversation to change 651 * @return 0 at success 652 */ 653 abstract protected int setOwnerStatus(int presenceState, String presenceStatus, 654 long lastActive, int chatState, String convoId); 655 656 /** 657 * Notify the application of the Bluetooth state 658 * @param bluetoothState 'on' of 'off' 659 * @return 0 at success 660 */ 661 abstract protected int setBluetoothStatus(boolean bluetoothState); 662 663 664 665 /** 666 * Need this to suppress warning in unit tests. 667 */ 668 @Override 669 public void shutdown() { 670 // Don't call super.shutdown(), which emits a warning... 671 } 672 673 /** 674 * Extract the BluetoothMapContract.AccountColumns._ID from the given URI. 675 */ 676 public static String getAccountId(Uri uri) { 677 final List<String> segments = uri.getPathSegments(); 678 if (segments.size() < 1) { 679 throw new IllegalArgumentException("No AccountId pressent in URI: " + uri); 680 } 681 return segments.get(0); 682 } 683} 684