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