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