1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.bluetooth.mapapi; 18 19 import android.content.ContentProvider; 20 import android.content.ContentResolver; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.content.UriMatcher; 24 import android.content.pm.ProviderInfo; 25 import android.database.Cursor; 26 import android.net.Uri; 27 import android.os.AsyncTask; 28 import android.os.Binder; 29 import android.os.Bundle; 30 import android.os.ParcelFileDescriptor; 31 import android.util.Log; 32 33 import java.io.FileInputStream; 34 import java.io.FileNotFoundException; 35 import java.io.FileOutputStream; 36 import java.io.IOException; 37 import java.util.List; 38 import java.util.Map; 39 40 /** 41 * A base implementation of the BluetoothMapEmailContract. 42 * A base class for a ContentProvider that allows access to Email messages from a Bluetooth 43 * device through the Message Access Profile. 44 */ 45 public abstract class BluetoothMapEmailProvider extends ContentProvider { 46 47 private static final String TAG = "BluetoothMapEmailProvider"; 48 private static final boolean D = true; 49 50 private static final int MATCH_ACCOUNT = 1; 51 private static final int MATCH_MESSAGE = 2; 52 private static final int MATCH_FOLDER = 3; 53 54 protected ContentResolver mResolver; 55 56 private Uri CONTENT_URI = null; 57 private String mAuthority; 58 private UriMatcher mMatcher; 59 60 61 private PipeReader mPipeReader = new PipeReader(); 62 private PipeWriter mPipeWriter = new PipeWriter(); 63 64 /** 65 * Write the content of a message to a stream as MIME encoded RFC-2822 data. 66 * @param accountId the ID of the account to which the message belong 67 * @param messageId the ID of the message to write to the stream 68 * @param includeAttachment true if attachments should be included 69 * @param download true if any missing part of the message shall be downloaded 70 * before written to the stream. The download flag will determine 71 * whether or not attachments shall be downloaded or only the message content. 72 * @param out the FileOurputStream to write to. 73 * @throws IOException 74 */ WriteMessageToStream(long accountId, long messageId, boolean includeAttachment, boolean download, FileOutputStream out)75 protected abstract void WriteMessageToStream(long accountId, long messageId, 76 boolean includeAttachment, boolean download, FileOutputStream out) throws IOException; 77 78 /** 79 * @return the CONTENT_URI exposed. This will be used to send out notifications. 80 */ getContentUri()81 protected abstract Uri getContentUri(); 82 83 /** 84 * Implementation is provided by the parent class. 85 */ 86 @Override attachInfo(Context context, ProviderInfo info)87 public void attachInfo(Context context, ProviderInfo info) { 88 mAuthority = info.authority; 89 90 mMatcher = new UriMatcher(UriMatcher.NO_MATCH); 91 mMatcher.addURI(mAuthority, BluetoothMapContract.TABLE_ACCOUNT, MATCH_ACCOUNT); 92 mMatcher.addURI(mAuthority, "#/" + BluetoothMapContract.TABLE_FOLDER, MATCH_FOLDER); 93 mMatcher.addURI(mAuthority, "#/" + BluetoothMapContract.TABLE_MESSAGE, MATCH_MESSAGE); 94 95 // Sanity check our setup 96 if (!info.exported) { 97 throw new SecurityException("Provider must be exported"); 98 } 99 // Enforce correct permissions are used 100 if (!android.Manifest.permission.BLUETOOTH_MAP.equals(info.writePermission)) { 101 throw new SecurityException( 102 "Provider must be protected by " + android.Manifest.permission.BLUETOOTH_MAP); 103 } 104 mResolver = context.getContentResolver(); 105 super.attachInfo(context, info); 106 } 107 108 109 /** 110 * Interface to write a stream of data to a pipe. Use with 111 * {@link ContentProvider#openPipeHelper}. 112 */ 113 public interface PipeDataReader<T> { 114 /** 115 * Called from a background thread to stream data from a pipe. 116 * Note that the pipe is blocking, so this thread can block on 117 * reads for an arbitrary amount of time if the client is slow 118 * at writing. 119 * 120 * @param input The pipe where data should be read. This will be 121 * closed for you upon returning from this function. 122 * @param uri The URI whose data is to be written. 123 * @param mimeType The desired type of data to be written. 124 * @param opts Options supplied by caller. 125 * @param args Your own custom arguments. 126 */ readDataFromPipe(ParcelFileDescriptor input, Uri uri, String mimeType, Bundle opts, T args)127 void readDataFromPipe(ParcelFileDescriptor input, Uri uri, String mimeType, Bundle opts, 128 T args); 129 } 130 131 public class PipeReader implements PipeDataReader<Cursor> { 132 /** 133 * Read the data from the pipe and generate a message. 134 * Use the message to do an update of the message specified by the URI. 135 */ 136 @Override readDataFromPipe(ParcelFileDescriptor input, Uri uri, String mimeType, Bundle opts, Cursor args)137 public void readDataFromPipe(ParcelFileDescriptor input, Uri uri, String mimeType, 138 Bundle opts, Cursor args) { 139 Log.v(TAG, "readDataFromPipe(): uri=" + uri.toString()); 140 FileInputStream fIn = null; 141 try { 142 fIn = new FileInputStream(input.getFileDescriptor()); 143 long messageId = Long.valueOf(uri.getLastPathSegment()); 144 long accountId = Long.valueOf(getAccountId(uri)); 145 UpdateMimeMessageFromStream(fIn, accountId, messageId); 146 } catch (IOException e) { 147 Log.w(TAG, "IOException: ", e); 148 /* TODO: How to signal the error to the calling entity? Had expected 149 readDataFromPipe 150 * to throw IOException? 151 */ 152 } finally { 153 try { 154 if (fIn != null) { 155 fIn.close(); 156 } 157 } catch (IOException e) { 158 Log.w(TAG, e); 159 } 160 } 161 } 162 } 163 164 /** 165 * Read a MIME encoded RFC-2822 fileStream and update the message content. 166 * The Date and/or From headers may not be present in the MIME encoded 167 * message, and this function shall add appropriate values if the headers 168 * are missing. From should be set to the owner of the account. 169 * 170 * @param input the file stream to read data from 171 * @param accountId the accountId 172 * @param messageId ID of the message to update 173 */ UpdateMimeMessageFromStream(FileInputStream input, long accountId, long messageId)174 protected abstract void UpdateMimeMessageFromStream(FileInputStream input, long accountId, 175 long messageId) throws IOException; 176 177 public class PipeWriter implements PipeDataWriter<Cursor> { 178 /** 179 * Generate a message based on the cursor, and write the encoded data to the stream. 180 */ 181 182 @Override writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType, Bundle opts, Cursor c)183 public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType, 184 Bundle opts, Cursor c) { 185 if (D) { 186 Log.d(TAG, "writeDataToPipe(): uri=" + uri.toString() + " - getLastPathSegment() = " 187 + uri.getLastPathSegment()); 188 } 189 190 FileOutputStream fout = null; 191 192 try { 193 fout = new FileOutputStream(output.getFileDescriptor()); 194 195 boolean includeAttachments = true; 196 boolean download = false; 197 List<String> segments = uri.getPathSegments(); 198 long messageId = Long.parseLong(segments.get(2)); 199 long accountId = Long.parseLong(getAccountId(uri)); 200 if (segments.size() >= 4) { 201 String format = segments.get(3); 202 if (format.equalsIgnoreCase(BluetoothMapContract.FILE_MSG_NO_ATTACHMENTS)) { 203 includeAttachments = false; 204 } else if (format.equalsIgnoreCase( 205 BluetoothMapContract.FILE_MSG_DOWNLOAD_NO_ATTACHMENTS)) { 206 includeAttachments = false; 207 download = true; 208 } else if (format.equalsIgnoreCase(BluetoothMapContract.FILE_MSG_DOWNLOAD)) { 209 download = true; 210 } 211 } 212 213 WriteMessageToStream(accountId, messageId, includeAttachments, download, fout); 214 } catch (IOException e) { 215 Log.w(TAG, e); 216 /* TODO: How to signal the error to the calling entity? Had expected writeDataToPipe 217 * to throw IOException? 218 */ 219 } finally { 220 try { 221 fout.flush(); 222 } catch (IOException e) { 223 Log.w(TAG, "IOException: ", e); 224 } 225 try { 226 fout.close(); 227 } catch (IOException e) { 228 Log.w(TAG, "IOException: ", e); 229 } 230 } 231 } 232 } 233 234 /** 235 * This function shall be called when any Account database content have changed 236 * to Notify any attached observers. 237 * @param accountId the ID of the account that changed. Null is a valid value, 238 * if accountId is unknown or multiple accounts changed. 239 */ onAccountChanged(String accountId)240 protected void onAccountChanged(String accountId) { 241 Uri newUri = null; 242 243 if (mAuthority == null) { 244 return; 245 } 246 if (accountId == null) { 247 newUri = BluetoothMapContract.buildAccountUri(mAuthority); 248 } else { 249 newUri = BluetoothMapContract.buildAccountUriwithId(mAuthority, accountId); 250 } 251 if (D) { 252 Log.d(TAG, "onAccountChanged() accountId = " + accountId + " URI: " + newUri); 253 } 254 mResolver.notifyChange(newUri, null); 255 } 256 257 /** 258 * This function shall be called when any Message database content have changed 259 * to notify any attached observers. 260 * @param accountId Null is a valid value, if accountId is unknown, but 261 * recommended for increased performance. 262 * @param messageId Null is a valid value, if multiple messages changed or the 263 * messageId is unknown, but recommended for increased performance. 264 */ onMessageChanged(String accountId, String messageId)265 protected void onMessageChanged(String accountId, String messageId) { 266 Uri newUri = null; 267 268 if (mAuthority == null) { 269 return; 270 } 271 272 if (accountId == null) { 273 newUri = BluetoothMapContract.buildMessageUri(mAuthority); 274 } else { 275 if (messageId == null) { 276 newUri = BluetoothMapContract.buildMessageUri(mAuthority, accountId); 277 } else { 278 newUri = BluetoothMapContract.buildMessageUriWithId(mAuthority, accountId, 279 messageId); 280 } 281 } 282 if (D) { 283 Log.d(TAG, "onMessageChanged() accountId = " + accountId + " messageId = " + messageId 284 + " URI: " + newUri); 285 } 286 mResolver.notifyChange(newUri, null); 287 } 288 289 /** 290 * Not used, this is just a dummy implementation. 291 */ 292 @Override getType(Uri uri)293 public String getType(Uri uri) { 294 return "Email"; 295 } 296 297 /** 298 * Open a file descriptor to a message. 299 * Two modes supported for read: With and without attachments. 300 * One mode exist for write and the actual content will be with or without 301 * attachments. 302 * 303 * Mode will be "r" or "w". 304 * 305 * URI format: 306 * The URI scheme is as follows. 307 * For messages with attachments: 308 * content://com.android.mail.bluetoothprovider/Messages/msgId# 309 * 310 * For messages without attachments: 311 * content://com.android.mail.bluetoothprovider/Messages/msgId#/NO_ATTACHMENTS 312 * 313 * UPDATE: For write. 314 * First create a message in the DB using insert into the message DB 315 * Then open a file handle to the #id 316 * write the data to a stream created from the fileHandle. 317 * 318 * @param uri the URI to open. ../Messages/#id 319 * @param mode the mode to use. The following modes exist: - UPDATE do not work - use URI 320 * - "read_with_attachments" - to read an e-mail including any attachments 321 * - "read_no_attachments" - to read an e-mail excluding any attachments 322 * - "write" - to add a mime encoded message to the database. This write 323 * should not trigger the message to be send. 324 * @return the ParcelFileDescriptor 325 * @throws FileNotFoundException 326 */ 327 @Override openFile(Uri uri, String mode)328 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 329 long callingId = Binder.clearCallingIdentity(); 330 if (D) { 331 Log.d(TAG, "openFile(): uri=" + uri.toString() + " - getLastPathSegment() = " 332 + uri.getLastPathSegment()); 333 } 334 try { 335 /* To be able to do abstraction of the file IO, we simply ignore the URI at this 336 * point and let the read/write function implementations parse the URI. */ 337 if (mode.equals("w")) { 338 return openInversePipeHelper(uri, null, null, null, mPipeReader); 339 } else { 340 return openPipeHelper(uri, null, null, null, mPipeWriter); 341 } 342 } catch (IOException e) { 343 Log.w(TAG, e); 344 } finally { 345 Binder.restoreCallingIdentity(callingId); 346 } 347 return null; 348 } 349 350 /** 351 * A helper function for implementing {@link #openFile}, for 352 * creating a data pipe and background thread allowing you to stream 353 * data back from the client. This function returns a new 354 * ParcelFileDescriptor that should be returned to the caller (the caller 355 * is responsible for closing it). 356 * 357 * @param uri The URI whose data is to be written. 358 * @param mimeType The desired type of data to be written. 359 * @param opts Options supplied by caller. 360 * @param args Your own custom arguments. 361 * @param func Interface implementing the function that will actually 362 * stream the data. 363 * @return Returns a new ParcelFileDescriptor holding the read side of 364 * the pipe. This should be returned to the caller for reading; the caller 365 * is responsible for closing it when done. 366 */ openInversePipeHelper(final Uri uri, final String mimeType, final Bundle opts, final T args, final PipeDataReader<T> func)367 private <T> ParcelFileDescriptor openInversePipeHelper(final Uri uri, final String mimeType, 368 final Bundle opts, final T args, final PipeDataReader<T> func) 369 throws FileNotFoundException { 370 try { 371 final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe(); 372 373 AsyncTask<Object, Object, Object> task = new AsyncTask<Object, Object, Object>() { 374 @Override 375 protected Object doInBackground(Object... params) { 376 func.readDataFromPipe(fds[0], uri, mimeType, opts, args); 377 try { 378 fds[0].close(); 379 } catch (IOException e) { 380 Log.w(TAG, "Failure closing pipe", e); 381 } 382 return null; 383 } 384 }; 385 task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Object[]) null); 386 387 return fds[1]; 388 } catch (IOException e) { 389 throw new FileNotFoundException("failure making pipe"); 390 } 391 } 392 393 /** 394 * The MAP specification states that a delete request from MAP client is a folder shift to the 395 * 'deleted' folder. 396 * Only use case of delete() is when transparency is requested for push messages, then 397 * message should not remain in sent folder and therefore must be deleted 398 */ 399 @Override delete(Uri uri, String where, String[] selectionArgs)400 public int delete(Uri uri, String where, String[] selectionArgs) { 401 if (D) { 402 Log.d(TAG, "delete(): uri=" + uri.toString()); 403 } 404 int result = 0; 405 406 String table = uri.getPathSegments().get(1); 407 if (table == null) { 408 throw new IllegalArgumentException("Table missing in URI"); 409 } 410 // the id of the entry to be deleted from the database 411 String messageId = uri.getLastPathSegment(); 412 if (messageId == null) { 413 throw new IllegalArgumentException("Message ID missing in update values!"); 414 } 415 416 417 String accountId = getAccountId(uri); 418 if (accountId == null) { 419 throw new IllegalArgumentException("Account ID missing in update values!"); 420 } 421 422 long callingId = Binder.clearCallingIdentity(); 423 try { 424 if (table.equals(BluetoothMapContract.TABLE_MESSAGE)) { 425 return deleteMessage(accountId, messageId); 426 } else { 427 if (D) { 428 Log.w(TAG, "Unknown table name: " + table); 429 } 430 return result; 431 } 432 } finally { 433 Binder.restoreCallingIdentity(callingId); 434 } 435 } 436 437 /** 438 * This function deletes a message. 439 * @param accountId the ID of the Account 440 * @param messageId the ID of the message to delete. 441 * @return the number of messages deleted - 0 if the message was not found. 442 */ deleteMessage(String accountId, String messageId)443 protected abstract int deleteMessage(String accountId, String messageId); 444 445 /** 446 * Insert is used to add new messages to the data base. 447 * Insert message approach: 448 * - Insert an empty message to get an _id with only a folder_id 449 * - Open the _id for write 450 * - Write the message content 451 * (When the writer completes, this provider should do an update of the message) 452 */ 453 @Override insert(Uri uri, ContentValues values)454 public Uri insert(Uri uri, ContentValues values) { 455 String table = uri.getLastPathSegment(); 456 if (table == null) { 457 throw new IllegalArgumentException("Table missing in URI"); 458 } 459 String accountId = getAccountId(uri); 460 Long folderId = values.getAsLong(BluetoothMapContract.MessageColumns.FOLDER_ID); 461 if (folderId == null) { 462 throw new IllegalArgumentException("FolderId missing in ContentValues"); 463 } 464 465 String id; // the id of the entry inserted into the database 466 long callingId = Binder.clearCallingIdentity(); 467 Log.d(TAG, "insert(): uri=" + uri.toString() + " - getLastPathSegment() = " 468 + uri.getLastPathSegment()); 469 try { 470 if (table.equals(BluetoothMapContract.TABLE_MESSAGE)) { 471 id = insertMessage(accountId, folderId.toString()); 472 if (D) { 473 Log.i(TAG, "insert() ID: " + id); 474 } 475 return Uri.parse(uri.toString() + "/" + id); 476 } else { 477 Log.w(TAG, "Unknown table name: " + table); 478 return null; 479 } 480 } finally { 481 Binder.restoreCallingIdentity(callingId); 482 } 483 } 484 485 486 /** 487 * Inserts an empty message into the Message data base in the specified folder. 488 * This is done before the actual message content is written by fileIO. 489 * @param accountId the ID of the account 490 * @param folderId the ID of the folder to create a new message in. 491 * @return the message id as a string 492 */ insertMessage(String accountId, String folderId)493 protected abstract String insertMessage(String accountId, String folderId); 494 495 /** 496 * Utility function to build a projection based on a projectionMap. 497 * 498 * "btColumnName" -> "emailColumnName as btColumnName" for each entry. 499 * 500 * This supports SQL statements in the emailColumnName entry. 501 * @param projection 502 * @param projectionMap <btColumnName, emailColumnName> 503 * @return the converted projection 504 */ convertProjection(String[] projection, Map<String, String> projectionMap)505 protected String[] convertProjection(String[] projection, Map<String, String> projectionMap) { 506 String[] newProjection = new String[projection.length]; 507 for (int i = 0; i < projection.length; i++) { 508 newProjection[i] = projectionMap.get(projection[i]) + " as " + projection[i]; 509 } 510 return newProjection; 511 } 512 513 /** 514 * This query needs to map from the data used in the e-mail client to BluetoothMapContract 515 * type of data. 516 */ 517 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)518 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 519 String sortOrder) { 520 long callingId = Binder.clearCallingIdentity(); 521 try { 522 String accountId = null; 523 switch (mMatcher.match(uri)) { 524 case MATCH_ACCOUNT: 525 return queryAccount(projection, selection, selectionArgs, sortOrder); 526 case MATCH_FOLDER: 527 accountId = getAccountId(uri); 528 return queryFolder(accountId, projection, selection, selectionArgs, sortOrder); 529 case MATCH_MESSAGE: 530 accountId = getAccountId(uri); 531 return queryMessage(accountId, projection, selection, selectionArgs, sortOrder); 532 default: 533 throw new UnsupportedOperationException("Unsupported Uri " + uri); 534 } 535 } finally { 536 Binder.restoreCallingIdentity(callingId); 537 } 538 } 539 540 /** 541 * Query account information. 542 * This function shall return only exposable e-mail accounts. Hence shall not 543 * return accounts that has policies suggesting not to be shared them. 544 * @param projection 545 * @param selection 546 * @param selectionArgs 547 * @param sortOrder 548 * @return a cursor to the accounts that are subject to exposure over BT. 549 */ queryAccount(String[] projection, String selection, String[] selectionArgs, String sortOrder)550 protected abstract Cursor queryAccount(String[] projection, String selection, 551 String[] selectionArgs, String sortOrder); 552 553 /** 554 * Filter out the non usable folders and ensure to name the mandatory folders 555 * inbox, outbox, sent, deleted and draft. 556 * @param accountId 557 * @param projection 558 * @param selection 559 * @param selectionArgs 560 * @param sortOrder 561 * @return 562 */ queryFolder(String accountId, String[] projection, String selection, String[] selectionArgs, String sortOrder)563 protected abstract Cursor queryFolder(String accountId, String[] projection, String selection, 564 String[] selectionArgs, String sortOrder); 565 566 /** 567 * For the message table the selection (where clause) can only include the following columns: 568 * date: less than, greater than and equals 569 * flagRead: = 1 or = 0 570 * flagPriority: = 1 or = 0 571 * folder_id: the ID of the folder only equals 572 * toList: partial name/address search 573 * ccList: partial name/address search 574 * bccList: partial name/address search 575 * fromList: partial name/address search 576 * Additionally the COUNT and OFFSET shall be supported. 577 * @param accountId the ID of the account 578 * @param projection 579 * @param selection 580 * @param selectionArgs 581 * @param sortOrder 582 * @return a cursor to query result 583 */ queryMessage(String accountId, String[] projection, String selection, String[] selectionArgs, String sortOrder)584 protected abstract Cursor queryMessage(String accountId, String[] projection, String selection, 585 String[] selectionArgs, String sortOrder); 586 587 /** 588 * update() 589 * Messages can be modified in the following cases: 590 * - the folder_key of a message - hence the message can be moved to a new folder, 591 * but the content cannot be modified. 592 * - the FLAG_READ state can be changed. 593 * The selection statement will always be selection of a message ID, when updating a message, 594 * hence this function will be called multiple times if multiple messages must be updated 595 * due to the nature of the Bluetooth Message Access profile. 596 */ 597 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)598 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 599 600 String table = uri.getLastPathSegment(); 601 if (table == null) { 602 throw new IllegalArgumentException("Table missing in URI"); 603 } 604 if (selection != null) { 605 throw new IllegalArgumentException( 606 "selection shall not be used, ContentValues shall contain the data"); 607 } 608 609 long callingId = Binder.clearCallingIdentity(); 610 if (D) { 611 Log.w(TAG, "update(): uri=" + uri.toString() + " - getLastPathSegment() = " 612 + uri.getLastPathSegment()); 613 } 614 try { 615 if (table.equals(BluetoothMapContract.TABLE_ACCOUNT)) { 616 String accountId = values.getAsString(BluetoothMapContract.AccountColumns._ID); 617 if (accountId == null) { 618 throw new IllegalArgumentException("Account ID missing in update values!"); 619 } 620 Integer exposeFlag = 621 values.getAsInteger(BluetoothMapContract.AccountColumns.FLAG_EXPOSE); 622 if (exposeFlag == null) { 623 throw new IllegalArgumentException("Expose flag missing in update values!"); 624 } 625 return updateAccount(accountId, exposeFlag); 626 } else if (table.equals(BluetoothMapContract.TABLE_FOLDER)) { 627 return 0; // We do not support changing folders 628 } else if (table.equals(BluetoothMapContract.TABLE_MESSAGE)) { 629 String accountId = getAccountId(uri); 630 Long messageId = values.getAsLong(BluetoothMapContract.MessageColumns._ID); 631 if (messageId == null) { 632 throw new IllegalArgumentException("Message ID missing in update values!"); 633 } 634 Long folderId = values.getAsLong(BluetoothMapContract.MessageColumns.FOLDER_ID); 635 Boolean flagRead = 636 values.getAsBoolean(BluetoothMapContract.MessageColumns.FLAG_READ); 637 return updateMessage(accountId, messageId, folderId, flagRead); 638 } else { 639 if (D) { 640 Log.w(TAG, "Unknown table name: " + table); 641 } 642 return 0; 643 } 644 } finally { 645 Binder.restoreCallingIdentity(callingId); 646 } 647 } 648 649 /** 650 * Update an entry in the account table. Only the expose flag will be 651 * changed through this interface. 652 * @param accountId the ID of the account to change. 653 * @param flagExpose the updated value. 654 * @return the number of entries changed - 0 if account not found or value cannot be changed. 655 */ updateAccount(String accountId, int flagExpose)656 protected abstract int updateAccount(String accountId, int flagExpose); 657 658 /** 659 * Update an entry in the message table. 660 * @param accountId ID of the account to which the messageId relates 661 * @param messageId the ID of the message to update 662 * @param folderId the new folder ID value to set - ignore if null. 663 * @param flagRead the new flagRead value to set - ignore if null. 664 * @return 665 */ updateMessage(String accountId, Long messageId, Long folderId, Boolean flagRead)666 protected abstract int updateMessage(String accountId, Long messageId, Long folderId, 667 Boolean flagRead); 668 669 670 @Override call(String method, String arg, Bundle extras)671 public Bundle call(String method, String arg, Bundle extras) { 672 long callingId = Binder.clearCallingIdentity(); 673 if (D) { 674 Log.d(TAG, "call(): method=" + method + " arg=" + arg + "ThreadId: " 675 + Thread.currentThread().getId()); 676 } 677 678 try { 679 if (method.equals(BluetoothMapContract.METHOD_UPDATE_FOLDER)) { 680 long accountId = extras.getLong(BluetoothMapContract.EXTRA_UPDATE_ACCOUNT_ID, -1); 681 if (accountId == -1) { 682 Log.w(TAG, "No account ID in CALL"); 683 return null; 684 } 685 long folderId = extras.getLong(BluetoothMapContract.EXTRA_UPDATE_FOLDER_ID, -1); 686 if (folderId == -1) { 687 Log.w(TAG, "No folder ID in CALL"); 688 return null; 689 } 690 int ret = syncFolder(accountId, folderId); 691 if (ret == 0) { 692 return new Bundle(); 693 } 694 return null; 695 } 696 } finally { 697 Binder.restoreCallingIdentity(callingId); 698 } 699 return null; 700 } 701 702 /** 703 * Trigger a sync of the specified folder. 704 * @param accountId the ID of the account that owns the folder 705 * @param folderId the ID of the folder. 706 * @return 0 at success 707 */ syncFolder(long accountId, long folderId)708 protected abstract int syncFolder(long accountId, long folderId); 709 710 /** 711 * Need this to suppress warning in unit tests. 712 */ 713 @Override shutdown()714 public void shutdown() { 715 // Don't call super.shutdown(), which emits a warning... 716 } 717 718 /** 719 * Extract the BluetoothMapContract.AccountColumns._ID from the given URI. 720 */ getAccountId(Uri uri)721 public static String getAccountId(Uri uri) { 722 final List<String> segments = uri.getPathSegments(); 723 if (segments.size() < 1) { 724 throw new IllegalArgumentException("No AccountId pressent in URI: " + uri); 725 } 726 return segments.get(0); 727 } 728 } 729