| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.bluetooth.mapapi; |
| |
| import android.content.ContentProvider; |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.UriMatcher; |
| import android.content.pm.ProviderInfo; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.ParcelFileDescriptor; |
| import android.util.Log; |
| |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| /** |
| * A base implementation of the BluetoothMapEmailContract. |
| * A base class for a ContentProvider that allows access to Email messages from a Bluetooth |
| * device through the Message Access Profile. |
| */ |
| public abstract class BluetoothMapEmailProvider extends ContentProvider { |
| |
| private static final String TAG = "BluetoothMapEmailProvider"; |
| private static final boolean D = true; |
| |
| private static final int MATCH_ACCOUNT = 1; |
| private static final int MATCH_MESSAGE = 2; |
| private static final int MATCH_FOLDER = 3; |
| |
| protected ContentResolver mResolver; |
| |
| private Uri CONTENT_URI = null; |
| private String mAuthority; |
| private UriMatcher mMatcher; |
| |
| |
| private PipeReader mPipeReader = new PipeReader(); |
| private PipeWriter mPipeWriter = new PipeWriter(); |
| |
| /** |
| * Write the content of a message to a stream as MIME encoded RFC-2822 data. |
| * @param accountId the ID of the account to which the message belong |
| * @param messageId the ID of the message to write to the stream |
| * @param includeAttachment true if attachments should be included |
| * @param download true if any missing part of the message shall be downloaded |
| * before written to the stream. The download flag will determine |
| * whether or not attachments shall be downloaded or only the message content. |
| * @param out the FileOurputStream to write to. |
| * @throws IOException |
| */ |
| abstract protected void WriteMessageToStream(long accountId, long messageId, |
| boolean includeAttachment, boolean download, FileOutputStream out) |
| throws IOException; |
| |
| /** |
| * @return the CONTENT_URI exposed. This will be used to send out notifications. |
| */ |
| abstract protected Uri getContentUri(); |
| |
| /** |
| * Implementation is provided by the parent class. |
| */ |
| @Override |
| public void attachInfo(Context context, ProviderInfo info) { |
| mAuthority = info.authority; |
| |
| mMatcher = new UriMatcher(UriMatcher.NO_MATCH); |
| mMatcher.addURI(mAuthority, BluetoothMapContract.TABLE_ACCOUNT, MATCH_ACCOUNT); |
| mMatcher.addURI(mAuthority, "#/"+BluetoothMapContract.TABLE_FOLDER, MATCH_FOLDER); |
| mMatcher.addURI(mAuthority, "#/"+BluetoothMapContract.TABLE_MESSAGE, MATCH_MESSAGE); |
| |
| // Sanity check our setup |
| if (!info.exported) { |
| throw new SecurityException("Provider must be exported"); |
| } |
| // Enforce correct permissions are used |
| if (!android.Manifest.permission.BLUETOOTH_MAP.equals(info.writePermission)){ |
| throw new SecurityException("Provider must be protected by " + |
| android.Manifest.permission.BLUETOOTH_MAP); |
| } |
| mResolver = context.getContentResolver(); |
| super.attachInfo(context, info); |
| } |
| |
| |
| /** |
| * Interface to write a stream of data to a pipe. Use with |
| * {@link ContentProvider#openPipeHelper}. |
| */ |
| public interface PipeDataReader<T> { |
| /** |
| * Called from a background thread to stream data from a pipe. |
| * Note that the pipe is blocking, so this thread can block on |
| * reads for an arbitrary amount of time if the client is slow |
| * at writing. |
| * |
| * @param input The pipe where data should be read. This will be |
| * closed for you upon returning from this function. |
| * @param uri The URI whose data is to be written. |
| * @param mimeType The desired type of data to be written. |
| * @param opts Options supplied by caller. |
| * @param args Your own custom arguments. |
| */ |
| public void readDataFromPipe(ParcelFileDescriptor input, Uri uri, String mimeType, |
| Bundle opts, T args); |
| } |
| |
| public class PipeReader implements PipeDataReader<Cursor> { |
| /** |
| * Read the data from the pipe and generate a message. |
| * Use the message to do an update of the message specified by the URI. |
| */ |
| @Override |
| public void readDataFromPipe(ParcelFileDescriptor input, Uri uri, |
| String mimeType, Bundle opts, Cursor args) { |
| Log.v(TAG, "readDataFromPipe(): uri=" + uri.toString()); |
| FileInputStream fIn = null; |
| try { |
| fIn = new FileInputStream(input.getFileDescriptor()); |
| long messageId = Long.valueOf(uri.getLastPathSegment()); |
| long accountId = Long.valueOf(getAccountId(uri)); |
| UpdateMimeMessageFromStream(fIn, accountId, messageId); |
| } catch (IOException e) { |
| Log.w(TAG,"IOException: ", e); |
| /* TODO: How to signal the error to the calling entity? Had expected readDataFromPipe |
| * to throw IOException? |
| */ |
| } finally { |
| try { |
| if(fIn != null) |
| fIn.close(); |
| } catch (IOException e) { |
| Log.w(TAG,e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Read a MIME encoded RFC-2822 fileStream and update the message content. |
| * The Date and/or From headers may not be present in the MIME encoded |
| * message, and this function shall add appropriate values if the headers |
| * are missing. From should be set to the owner of the account. |
| * |
| * @param input the file stream to read data from |
| * @param accountId the accountId |
| * @param messageId ID of the message to update |
| */ |
| abstract protected void UpdateMimeMessageFromStream(FileInputStream input, long accountId, |
| long messageId) throws IOException; |
| |
| public class PipeWriter implements PipeDataWriter<Cursor> { |
| /** |
| * Generate a message based on the cursor, and write the encoded data to the stream. |
| */ |
| |
| public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType, |
| Bundle opts, Cursor c) { |
| if (D) Log.d(TAG, "writeDataToPipe(): uri=" + uri.toString() + |
| " - getLastPathSegment() = " + uri.getLastPathSegment()); |
| |
| FileOutputStream fout = null; |
| |
| try { |
| fout = new FileOutputStream(output.getFileDescriptor()); |
| |
| boolean includeAttachments = true; |
| boolean download = false; |
| List<String> segments = uri.getPathSegments(); |
| long messageId = Long.parseLong(segments.get(2)); |
| long accountId = Long.parseLong(getAccountId(uri)); |
| if(segments.size() >= 4) { |
| String format = segments.get(3); |
| if(format.equalsIgnoreCase(BluetoothMapContract.FILE_MSG_NO_ATTACHMENTS)) { |
| includeAttachments = false; |
| } else if(format.equalsIgnoreCase(BluetoothMapContract.FILE_MSG_DOWNLOAD_NO_ATTACHMENTS)) { |
| includeAttachments = false; |
| download = true; |
| } else if(format.equalsIgnoreCase(BluetoothMapContract.FILE_MSG_DOWNLOAD)) { |
| download = true; |
| } |
| } |
| |
| WriteMessageToStream(accountId, messageId, includeAttachments, download, fout); |
| } catch (IOException e) { |
| Log.w(TAG, e); |
| /* TODO: How to signal the error to the calling entity? Had expected writeDataToPipe |
| * to throw IOException? |
| */ |
| } finally { |
| try { |
| fout.flush(); |
| } catch (IOException e) { |
| Log.w(TAG, "IOException: ", e); |
| } |
| try { |
| fout.close(); |
| } catch (IOException e) { |
| Log.w(TAG, "IOException: ", e); |
| } |
| } |
| } |
| } |
| |
| /** |
| * This function shall be called when any Account database content have changed |
| * to Notify any attached observers. |
| * @param accountId the ID of the account that changed. Null is a valid value, |
| * if accountId is unknown or multiple accounts changed. |
| */ |
| protected void onAccountChanged(String accountId) { |
| Uri newUri = null; |
| |
| if(mAuthority == null){ |
| return; |
| } |
| if(accountId == null){ |
| newUri = BluetoothMapContract.buildAccountUri(mAuthority); |
| } else { |
| newUri = BluetoothMapContract.buildAccountUriwithId(mAuthority, accountId); |
| } |
| if(D) Log.d(TAG,"onAccountChanged() accountId = " + accountId + " URI: " + newUri); |
| mResolver.notifyChange(newUri, null); |
| } |
| |
| /** |
| * This function shall be called when any Message database content have changed |
| * to notify any attached observers. |
| * @param accountId Null is a valid value, if accountId is unknown, but |
| * recommended for increased performance. |
| * @param messageId Null is a valid value, if multiple messages changed or the |
| * messageId is unknown, but recommended for increased performance. |
| */ |
| protected void onMessageChanged(String accountId, String messageId) { |
| Uri newUri = null; |
| |
| if(mAuthority == null){ |
| return; |
| } |
| |
| if(accountId == null){ |
| newUri = BluetoothMapContract.buildMessageUri(mAuthority); |
| } else { |
| if(messageId == null) |
| { |
| newUri = BluetoothMapContract.buildMessageUri(mAuthority,accountId); |
| } else { |
| newUri = BluetoothMapContract.buildMessageUriWithId(mAuthority,accountId, messageId); |
| } |
| } |
| if(D) Log.d(TAG,"onMessageChanged() accountId = " + accountId + " messageId = " + messageId + " URI: " + newUri); |
| mResolver.notifyChange(newUri, null); |
| } |
| |
| /** |
| * Not used, this is just a dummy implementation. |
| */ |
| @Override |
| public String getType(Uri uri) { |
| return "Email"; |
| } |
| |
| /** |
| * Open a file descriptor to a message. |
| * Two modes supported for read: With and without attachments. |
| * One mode exist for write and the actual content will be with or without |
| * attachments. |
| * |
| * Mode will be "r" or "w". |
| * |
| * URI format: |
| * The URI scheme is as follows. |
| * For messages with attachments: |
| * content://com.android.mail.bluetoothprovider/Messages/msgId# |
| * |
| * For messages without attachments: |
| * content://com.android.mail.bluetoothprovider/Messages/msgId#/NO_ATTACHMENTS |
| * |
| * UPDATE: For write. |
| * First create a message in the DB using insert into the message DB |
| * Then open a file handle to the #id |
| * write the data to a stream created from the fileHandle. |
| * |
| * @param uri the URI to open. ../Messages/#id |
| * @param mode the mode to use. The following modes exist: - UPDATE do not work - use URI |
| * - "read_with_attachments" - to read an e-mail including any attachments |
| * - "read_no_attachments" - to read an e-mail excluding any attachments |
| * - "write" - to add a mime encoded message to the database. This write |
| * should not trigger the message to be send. |
| * @return the ParcelFileDescriptor |
| * @throws FileNotFoundException |
| */ |
| @Override |
| public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { |
| long callingId = Binder.clearCallingIdentity(); |
| if(D)Log.d(TAG, "openFile(): uri=" + uri.toString() + " - getLastPathSegment() = " + |
| uri.getLastPathSegment()); |
| try { |
| /* To be able to do abstraction of the file IO, we simply ignore the URI at this |
| * point and let the read/write function implementations parse the URI. */ |
| if(mode.equals("w")) { |
| return openInversePipeHelper(uri, null, null, null, mPipeReader); |
| } else { |
| return openPipeHelper (uri, null, null, null, mPipeWriter); |
| } |
| } catch (IOException e) { |
| Log.w(TAG,e); |
| } finally { |
| Binder.restoreCallingIdentity(callingId); |
| } |
| return null; |
| } |
| |
| /** |
| * A helper function for implementing {@link #openFile}, for |
| * creating a data pipe and background thread allowing you to stream |
| * data back from the client. This function returns a new |
| * ParcelFileDescriptor that should be returned to the caller (the caller |
| * is responsible for closing it). |
| * |
| * @param uri The URI whose data is to be written. |
| * @param mimeType The desired type of data to be written. |
| * @param opts Options supplied by caller. |
| * @param args Your own custom arguments. |
| * @param func Interface implementing the function that will actually |
| * stream the data. |
| * @return Returns a new ParcelFileDescriptor holding the read side of |
| * the pipe. This should be returned to the caller for reading; the caller |
| * is responsible for closing it when done. |
| */ |
| private <T> ParcelFileDescriptor openInversePipeHelper(final Uri uri, final String mimeType, |
| final Bundle opts, final T args, final PipeDataReader<T> func) |
| throws FileNotFoundException { |
| try { |
| final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe(); |
| |
| AsyncTask<Object, Object, Object> task = new AsyncTask<Object, Object, Object>() { |
| @Override |
| protected Object doInBackground(Object... params) { |
| func.readDataFromPipe(fds[0], uri, mimeType, opts, args); |
| try { |
| fds[0].close(); |
| } catch (IOException e) { |
| Log.w(TAG, "Failure closing pipe", e); |
| } |
| return null; |
| } |
| }; |
| task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Object[])null); |
| |
| return fds[1]; |
| } catch (IOException e) { |
| throw new FileNotFoundException("failure making pipe"); |
| } |
| } |
| |
| /** |
| * The MAP specification states that a delete request from MAP client is a folder shift to the |
| * 'deleted' folder. |
| * Only use case of delete() is when transparency is requested for push messages, then |
| * message should not remain in sent folder and therefore must be deleted |
| */ |
| @Override |
| public int delete(Uri uri, String where, String[] selectionArgs) { |
| if (D) Log.d(TAG, "delete(): uri=" + uri.toString() ); |
| int result = 0; |
| |
| String table = uri.getPathSegments().get(1); |
| if(table == null) |
| throw new IllegalArgumentException("Table missing in URI"); |
| // the id of the entry to be deleted from the database |
| String messageId = uri.getLastPathSegment(); |
| if (messageId == null) |
| throw new IllegalArgumentException("Message ID missing in update values!"); |
| |
| |
| String accountId = getAccountId(uri); |
| if (accountId == null) |
| throw new IllegalArgumentException("Account ID missing in update values!"); |
| |
| long callingId = Binder.clearCallingIdentity(); |
| try { |
| if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) { |
| return deleteMessage(accountId, messageId); |
| } else { |
| if (D) Log.w(TAG, "Unknown table name: " + table); |
| return result; |
| } |
| } finally { |
| Binder.restoreCallingIdentity(callingId); |
| } |
| } |
| |
| /** |
| * This function deletes a message. |
| * @param accountId the ID of the Account |
| * @param messageId the ID of the message to delete. |
| * @return the number of messages deleted - 0 if the message was not found. |
| */ |
| abstract protected int deleteMessage(String accountId, String messageId); |
| |
| /** |
| * Insert is used to add new messages to the data base. |
| * Insert message approach: |
| * - Insert an empty message to get an _id with only a folder_id |
| * - Open the _id for write |
| * - Write the message content |
| * (When the writer completes, this provider should do an update of the message) |
| */ |
| @Override |
| public Uri insert(Uri uri, ContentValues values) { |
| String table = uri.getLastPathSegment(); |
| if(table == null){ |
| throw new IllegalArgumentException("Table missing in URI"); |
| } |
| String accountId = getAccountId(uri); |
| Long folderId = values.getAsLong(BluetoothMapContract.MessageColumns.FOLDER_ID); |
| if(folderId == null) { |
| throw new IllegalArgumentException("FolderId missing in ContentValues"); |
| } |
| |
| String id; // the id of the entry inserted into the database |
| long callingId = Binder.clearCallingIdentity(); |
| Log.d(TAG, "insert(): uri=" + uri.toString() + " - getLastPathSegment() = " + |
| uri.getLastPathSegment()); |
| try { |
| if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) { |
| id = insertMessage(accountId, folderId.toString()); |
| if(D) Log.i(TAG, "insert() ID: " + id); |
| return Uri.parse(uri.toString() + "/" + id); |
| } else { |
| Log.w(TAG, "Unknown table name: " + table); |
| return null; |
| } |
| } finally { |
| Binder.restoreCallingIdentity(callingId); |
| } |
| } |
| |
| |
| /** |
| * Inserts an empty message into the Message data base in the specified folder. |
| * This is done before the actual message content is written by fileIO. |
| * @param accountId the ID of the account |
| * @param folderId the ID of the folder to create a new message in. |
| * @return the message id as a string |
| */ |
| abstract protected String insertMessage(String accountId, String folderId); |
| |
| /** |
| * Utility function to build a projection based on a projectionMap. |
| * |
| * "btColumnName" -> "emailColumnName as btColumnName" for each entry. |
| * |
| * This supports SQL statements in the emailColumnName entry. |
| * @param projection |
| * @param projectionMap <btColumnName, emailColumnName> |
| * @return the converted projection |
| */ |
| protected String[] convertProjection(String[] projection, Map<String,String> projectionMap) { |
| String[] newProjection = new String[projection.length]; |
| for(int i = 0; i < projection.length; i++) { |
| newProjection[i] = projectionMap.get(projection[i]) + " as " + projection[i]; |
| } |
| return newProjection; |
| } |
| |
| /** |
| * This query needs to map from the data used in the e-mail client to BluetoothMapContract type of data. |
| */ |
| @Override |
| public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, |
| String sortOrder) { |
| long callingId = Binder.clearCallingIdentity(); |
| try { |
| String accountId = null; |
| switch (mMatcher.match(uri)) { |
| case MATCH_ACCOUNT: |
| return queryAccount(projection, selection, selectionArgs, sortOrder); |
| case MATCH_FOLDER: |
| accountId = getAccountId(uri); |
| return queryFolder(accountId, projection, selection, selectionArgs, sortOrder); |
| case MATCH_MESSAGE: |
| accountId = getAccountId(uri); |
| return queryMessage(accountId, projection, selection, selectionArgs, sortOrder); |
| default: |
| throw new UnsupportedOperationException("Unsupported Uri " + uri); |
| } |
| } finally { |
| Binder.restoreCallingIdentity(callingId); |
| } |
| } |
| |
| /** |
| * Query account information. |
| * This function shall return only exposable e-mail accounts. Hence shall not |
| * return accounts that has policies suggesting not to be shared them. |
| * @param projection |
| * @param selection |
| * @param selectionArgs |
| * @param sortOrder |
| * @return a cursor to the accounts that are subject to exposure over BT. |
| */ |
| abstract protected Cursor queryAccount(String[] projection, String selection, String[] selectionArgs, |
| String sortOrder); |
| |
| /** |
| * Filter out the non usable folders and ensure to name the mandatory folders |
| * inbox, outbox, sent, deleted and draft. |
| * @param accountId |
| * @param projection |
| * @param selection |
| * @param selectionArgs |
| * @param sortOrder |
| * @return |
| */ |
| abstract protected Cursor queryFolder(String accountId, String[] projection, String selection, String[] selectionArgs, |
| String sortOrder); |
| /** |
| * For the message table the selection (where clause) can only include the following columns: |
| * date: less than, greater than and equals |
| * flagRead: = 1 or = 0 |
| * flagPriority: = 1 or = 0 |
| * folder_id: the ID of the folder only equals |
| * toList: partial name/address search |
| * ccList: partial name/address search |
| * bccList: partial name/address search |
| * fromList: partial name/address search |
| * Additionally the COUNT and OFFSET shall be supported. |
| * @param accountId the ID of the account |
| * @param projection |
| * @param selection |
| * @param selectionArgs |
| * @param sortOrder |
| * @return a cursor to query result |
| */ |
| abstract protected Cursor queryMessage(String accountId, String[] projection, String selection, String[] selectionArgs, |
| String sortOrder); |
| |
| /** |
| * update() |
| * Messages can be modified in the following cases: |
| * - the folder_key of a message - hence the message can be moved to a new folder, |
| * but the content cannot be modified. |
| * - the FLAG_READ state can be changed. |
| * The selection statement will always be selection of a message ID, when updating a message, |
| * hence this function will be called multiple times if multiple messages must be updated |
| * due to the nature of the Bluetooth Message Access profile. |
| */ |
| @Override |
| public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { |
| |
| String table = uri.getLastPathSegment(); |
| if(table == null){ |
| throw new IllegalArgumentException("Table missing in URI"); |
| } |
| if(selection != null) { |
| throw new IllegalArgumentException("selection shall not be used, ContentValues shall contain the data"); |
| } |
| |
| long callingId = Binder.clearCallingIdentity(); |
| if(D)Log.w(TAG, "update(): uri=" + uri.toString() + " - getLastPathSegment() = " + |
| uri.getLastPathSegment()); |
| try { |
| if(table.equals(BluetoothMapContract.TABLE_ACCOUNT)) { |
| String accountId = values.getAsString(BluetoothMapContract.AccountColumns._ID); |
| if(accountId == null) { |
| throw new IllegalArgumentException("Account ID missing in update values!"); |
| } |
| Integer exposeFlag = values.getAsInteger(BluetoothMapContract.AccountColumns.FLAG_EXPOSE); |
| if(exposeFlag == null){ |
| throw new IllegalArgumentException("Expose flag missing in update values!"); |
| } |
| return updateAccount(accountId, exposeFlag); |
| } else if(table.equals(BluetoothMapContract.TABLE_FOLDER)) { |
| return 0; // We do not support changing folders |
| } else if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) { |
| String accountId = getAccountId(uri); |
| Long messageId = values.getAsLong(BluetoothMapContract.MessageColumns._ID); |
| if(messageId == null) { |
| throw new IllegalArgumentException("Message ID missing in update values!"); |
| } |
| Long folderId = values.getAsLong(BluetoothMapContract.MessageColumns.FOLDER_ID); |
| Boolean flagRead = values.getAsBoolean(BluetoothMapContract.MessageColumns.FLAG_READ); |
| return updateMessage(accountId, messageId, folderId, flagRead); |
| } else { |
| if(D)Log.w(TAG, "Unknown table name: " + table); |
| return 0; |
| } |
| } finally { |
| Binder.restoreCallingIdentity(callingId); |
| } |
| } |
| |
| /** |
| * Update an entry in the account table. Only the expose flag will be |
| * changed through this interface. |
| * @param accountId the ID of the account to change. |
| * @param flagExpose the updated value. |
| * @return the number of entries changed - 0 if account not found or value cannot be changed. |
| */ |
| abstract protected int updateAccount(String accountId, int flagExpose); |
| |
| /** |
| * Update an entry in the message table. |
| * @param accountId ID of the account to which the messageId relates |
| * @param messageId the ID of the message to update |
| * @param folderId the new folder ID value to set - ignore if null. |
| * @param flagRead the new flagRead value to set - ignore if null. |
| * @return |
| */ |
| abstract protected int updateMessage(String accountId, Long messageId, Long folderId, Boolean flagRead); |
| |
| |
| @Override |
| public Bundle call(String method, String arg, Bundle extras) { |
| long callingId = Binder.clearCallingIdentity(); |
| if(D)Log.d(TAG, "call(): method=" + method + " arg=" + arg + "ThreadId: " + Thread.currentThread().getId()); |
| |
| try { |
| if(method.equals(BluetoothMapContract.METHOD_UPDATE_FOLDER)) { |
| long accountId = extras.getLong(BluetoothMapContract.EXTRA_UPDATE_ACCOUNT_ID, -1); |
| if(accountId == -1) { |
| Log.w(TAG, "No account ID in CALL"); |
| return null; |
| } |
| long folderId = extras.getLong(BluetoothMapContract.EXTRA_UPDATE_FOLDER_ID, -1); |
| if(folderId == -1) { |
| Log.w(TAG, "No folder ID in CALL"); |
| return null; |
| } |
| int ret = syncFolder(accountId, folderId); |
| if(ret == 0) { |
| return new Bundle(); |
| } |
| return null; |
| } |
| } finally { |
| Binder.restoreCallingIdentity(callingId); |
| } |
| return null; |
| } |
| |
| /** |
| * Trigger a sync of the specified folder. |
| * @param accountId the ID of the account that owns the folder |
| * @param folderId the ID of the folder. |
| * @return 0 at success |
| */ |
| abstract protected int syncFolder(long accountId, long folderId); |
| |
| /** |
| * Need this to suppress warning in unit tests. |
| */ |
| @Override |
| public void shutdown() { |
| // Don't call super.shutdown(), which emits a warning... |
| } |
| |
| /** |
| * Extract the BluetoothMapContract.AccountColumns._ID from the given URI. |
| */ |
| public static String getAccountId(Uri uri) { |
| final List<String> segments = uri.getPathSegments(); |
| if (segments.size() < 1) { |
| throw new IllegalArgumentException("No AccountId pressent in URI: " + uri); |
| } |
| return segments.get(0); |
| } |
| } |