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