diff options
author | Jeff Davidson <jpd@google.com> | 2014-08-08 13:59:52 -0700 |
---|---|---|
committer | Jeff Davidson <jpd@google.com> | 2014-08-08 13:59:52 -0700 |
commit | 31db28fcd2e2e1840e03b4bff7f121ed6dd47344 (patch) | |
tree | 0f6c83bcd8d6e0b2377bdfe8172806e27394fba5 /lib/mapapi/com | |
parent | 0d8f1ae3e070d7a13f8e482895bcab428a6c8381 (diff) | |
download | android_packages_apps_Bluetooth-31db28fcd2e2e1840e03b4bff7f121ed6dd47344.tar.gz android_packages_apps_Bluetooth-31db28fcd2e2e1840e03b4bff7f121ed6dd47344.tar.bz2 android_packages_apps_Bluetooth-31db28fcd2e2e1840e03b4bff7f121ed6dd47344.zip |
Make directory structure + Java packages consistent.
Fixes Eclipse compilation errors.
Change-Id: I2898a2aaf2b19b4c64fd523f792bb224c79b5d75
Diffstat (limited to 'lib/mapapi/com')
-rw-r--r-- | lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java | 557 | ||||
-rw-r--r-- | lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapEmailProvider.java | 696 |
2 files changed, 1253 insertions, 0 deletions
diff --git a/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java b/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java new file mode 100644 index 000000000..32018ba95 --- /dev/null +++ b/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java @@ -0,0 +1,557 @@ +/* + * 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.ContentResolver; +import android.net.Uri; + + +/** + * This class defines the minimum sets of data needed for an E-mail client to + * implement to claim support for the Bluetooth Message Access Profile. + * Access to three data sets are needed: + * <ul> + * <li>Message data set containing lists of messages.</li> + * <li>Account data set containing info on the existing accounts, and whether to expose + * these accounts. The content of the account data set is often sensitive information, + * hence care must be taken, not to reveal any personal information nor passwords. + * The accounts in this data base will be exposed in the settings menu, where the user + * is able to enable and disable the EXPOSE_FLAG, and thereby provide access to an + * account from another device, without any password protection the e-mail client + * might provide.</li> + * <li>Folder data set with the folder structure for the messages. Each message is linked to an + * entry in this data set.</li> + * </ul> + * + * To enable that the Bluetooth Message Access Server can detect the content provider implementing + * this interface, the {@code provider} tag for the bluetooth related content provider must + * have an intent-filter like the following in the manifest: + * <pre class="prettyprint"><provider android:authorities="[PROVIDER AUTHORITY]" + android:exported="true" + android:enabled="true" + android:permission="android.permission.BLUETOOTH_MAP"> + * ... + * <intent-filter> + <action android:name="android.content.action.BLEUETOOT_MAP_PROVIDER" /> + </intent-filter> + * ... + * </provider> + * [PROVIDER AUTHORITY] shall be the providers authority value which implements this + * contract. Only a single authority shall be used. The android.permission.BLUETOOTH_MAP + * permission is needed for the provider. + */ +public final class BluetoothMapContract { + /** + * Constructor - should not be used + */ + private BluetoothMapContract(){ + /* class should not be instantiated */ + } + + /** + * Provider interface that should be used as intent-filter action in the provider section + * of the manifest file. + */ + public static final String PROVIDER_INTERFACE = "android.bluetooth.action.BLUETOOTH_MAP_PROVIDER"; + + /** + * The Bluetooth Message Access profile allows a remote BT-MAP client to trigger + * an update of a folder for a specific e-mail account, register for reception + * of new messages from the server. + * + * Additionally the Bluetooth Message Access profile allows a remote BT-MAP client + * to push a message to a folder - e.g. outbox or draft. The Bluetooth profile + * implementation will place a new message in one of these existing folders through + * the content provider. + * + * ContentProvider.call() is used for these purposes, and the METHOD_UPDATE_FOLDER + * method name shall trigger an update of the specified folder for a specified + * account. + * + * This shall be a non blocking call simply starting the update, and the update should + * both send and receive messages, depending on what makes sense for the specified + * folder. + * Bundle extra parameter will carry two INTEGER (long) values: + * EXTRA_UPDATE_ACCOUNT_ID containing the account_id + * EXTRA_UPDATE_FOLDER_ID containing the folder_id of the folder to update + * + * The status for send complete of messages shall be reported by updating the sent-flag + * and e.g. for outbox messages, move them to the sent folder in the message table of the + * content provider and trigger a change notification to any attached content observer. + */ + public static final String METHOD_UPDATE_FOLDER = "UpdateFolder"; + public static final String EXTRA_UPDATE_ACCOUNT_ID = "UpdateAccountId"; + public static final String EXTRA_UPDATE_FOLDER_ID = "UpdateFolderId"; + + /** + * These column names are used as last path segment of the URI (getLastPathSegment()). + * Access to a specific row in the tables is done by using the where-clause, hence + * support for .../#id if not needed for the Email clients. + * The URI format for accessing the tables are as follows: + * content://ProviderAuthority/TABLE_ACCOUNT + * content://ProviderAuthority/account_id/TABLE_MESSAGE + * content://ProviderAuthority/account_id/TABLE_FOLDER + */ + + /** + * Build URI representing the given Accounts data-set in a + * bluetooth provider. When queried, the direct URI for the account + * with the given accountID is returned. + */ + public static Uri buildAccountUri(String authority) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority).appendPath(TABLE_ACCOUNT).build(); + } + /** + * Build URI representing the given Account data-set with specific Id in a + * Bluetooth provider. When queried, the direct URI for the account + * with the given accountID is returned. + */ + public static Uri buildAccountUriwithId(String authority, String accountId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(TABLE_ACCOUNT) + .appendPath(accountId) + .build(); + } + /** + * Build URI representing the entire Message table in a + * bluetooth provider. + */ + public static Uri buildMessageUri(String authority) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(TABLE_MESSAGE) + .build(); + } + /** + * Build URI representing the given Message data-set in a + * bluetooth provider. When queried, the URI for the Messages + * with the given accountID is returned. + */ + public static Uri buildMessageUri(String authority, String accountId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(accountId) + .appendPath(TABLE_MESSAGE) + .build(); + } + /** + * Build URI representing the given Message data-set with specific messageId in a + * bluetooth provider. When queried, the direct URI for the account + * with the given accountID is returned. + */ + public static Uri buildMessageUriWithId(String authority, String accountId,String messageId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(accountId) + .appendPath(TABLE_MESSAGE) + .appendPath(messageId) + .build(); + } + /** + * Build URI representing the given Message data-set in a + * bluetooth provider. When queried, the direct URI for the account + * with the given accountID is returned. + */ + public static Uri buildFolderUri(String authority, String accountId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(accountId) + .appendPath(TABLE_FOLDER) + .build(); + } + + /** + * @hide + */ + public static final String TABLE_ACCOUNT = "Account"; + public static final String TABLE_MESSAGE = "Message"; + public static final String TABLE_FOLDER = "Folder"; + + /** + * Mandatory folders for the Bluetooth message access profile. + * The email client shall at least implement the following root folders. + * E.g. as a mapping for them such that the naming will match the underlying + * matching folder ID's. + */ + public static final String FOLDER_NAME_INBOX = "inbox"; + public static final String FOLDER_NAME_OUTBOX = "outbox"; + public static final String FOLDER_NAME_SENT = "sent"; + public static final String FOLDER_NAME_DELETED = "deleted"; + public static final String FOLDER_NAME_DRAFT = "draft"; + + + /** + * To push RFC2822 encoded messages into a folder and read RFC2822 encoded messages from + * a folder, the openFile() interface will be used as follows: + * 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" for read and "w" for write, never "rw". + * + * URI format: + * The URI scheme is as follows. + * For reading messages with attachments: + * content://ProviderAuthority/account_id/TABLE_MESSAGE/msgId + * Note: This shall be an offline operation, including only message parts and attachments + * already downloaded to the device. + * + * For reading messages without attachments: + * content://ProviderAuthority/account_id/TABLE_MESSAGE/msgId/FILE_MSG_NO_ATTACHMENTS + * Note: This shall be an offline operation, including only message parts already + * downloaded to the device. + * + * For downloading and reading messages with attachments: + * content://ProviderAuthority/account_id/TABLE_MESSAGE/msgId/FILE_MSG_DOWNLOAD + * Note: This shall download the message content and all attachments if possible, + * else throw an IOException. + * + * For downloading and reading messages without attachments: + * content://ProviderAuthority/account_id/TABLE_MESSAGE/msgId/FILE_MSG_DOWNLOAD_NO_ATTACHMENTS + * Note: This shall download the message content if possible, else throw an IOException. + * + * When reading from the file descriptor, the content provider shall return a stream + * of bytes containing a RFC2822 encoded message, as if the message was send to an email + * server. + * + * When a byte stream is written to the file descriptor, the content provider shall + * decode the RFC2822 encoded data and insert the message into the TABLE_MESSAGE at the ID + * supplied in URI - additionally the message content shall be stored in the underlying + * data base structure as if the message was received from an email server. The Message ID + * will be created using a insert on the TABLE_MESSAGE prior to calling openFile(). + * Hence the procedure for inserting a message is: + * - uri/msgId = insert(uri, value: folderId=xxx) + * - fd = openFile(uri/msgId) + * - fd.write (RFC2822 encoded data) + * + * The Bluetooth Message Access Client might not know what to put into the From: + * header nor have a valid time stamp, hence the content provider shall check + * if the From: and Date: headers have been set by the message written, else + * it must fill in appropriate values. + */ + public static final String FILE_MSG_NO_ATTACHMENTS = "NO_ATTACHMENTS"; + public static final String FILE_MSG_DOWNLOAD = "DOWNLOAD"; + public static final String FILE_MSG_DOWNLOAD_NO_ATTACHMENTS = "DOWNLOAD_NO_ATTACHMENTS"; + + /** + * Account Table + * The columns needed to supply account information. + * The e-mail client app may choose to expose all e-mails as being from the same account, + * but it is not recommended, as this would be a violation of the Bluetooth specification. + * The Bluetooth Message Access settings activity will provide the user the ability to + * change the FLAG_EXPOSE values for each account in this table. + * The Bluetooth Message Access service will read the values when Bluetooth is turned on, + * and again on every notified change through the content observer interface. + */ + public interface AccountColumns { + + /** + * The unique ID for a row. + * <P>Type: INTEGER (long)</P> + */ + public static final String _ID = "_id"; + + /** + * The account name to display to the user on the device when selecting whether + * or not to share the account over Bluetooth. + * + * The account display name should not reveal any sensitive information e.g. email- + * address, as it will be added to the Bluetooth SDP record, which can be read by + * any Bluetooth enabled device. (Access to any account content is only provided to + * authenticated devices). It is recommended that if the email client uses the email + * address as account name, then the address should be obfuscated (i.e. replace "@" + * with ".") + * <P>Type: TEXT</P> + * read-only + */ + public static final String ACCOUNT_DISPLAY_NAME = "account_display_name"; + + /** + * Expose this account to other authenticated Bluetooth devices. If the expose flag + * is set, this account will be listed as an available account to access from another + * Bluetooth device. + * + * This is a read/write flag, that can be set either from within the E-mail client + * UI or the Bluetooth settings menu. + * + * It is recommended to either ask the user whether to expose the account, or set this + * to "show" as default. + * + * This setting shall not be used to enforce whether or not an account should be shared + * or not if the account is bound by an administrative security policy. In this case + * the email app should not list the account at all if it is not to be shareable over BT. + * + * <P>Type: INTEGER (boolean) hide = 0, show = 1</P> + */ + public static final String FLAG_EXPOSE = "flag_expose"; + + } + + /** + * The actual message table containing all messages. + * Content that must support filtering using WHERE clauses: + * - To, From, Cc, Bcc, Date, ReadFlag, PriorityFlag, folder_id, account_id + * Additional content that must be supplied: + * - Subject, AttachmentFlag, LoadedState, MessageSize, AttachmentSize + * Content that must support update: + * - FLAG_READ and FOLDER_ID (FOLDER_ID is used to move a message to deleted) + * Additional insert of a new message with the following values shall be supported: + * - FOLDER_ID + * + * When doing an insert on this table, the actual content of the message (subject, + * date etc) written through file-i/o takes precedence over the inserted values and should + * overwrite them. + */ + public interface MessageColumns { + + /** + * The unique ID for a row. + * <P>Type: INTEGER (long)</P> + */ + public static final String _ID = "_id"; + + /** + * The date the message was received as a unix timestamp + * (miliseconds since 00:00:00 UTC 1/1-1970). + * + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String DATE = "date"; + + /** + * Message subject. + * <P>Type: TEXT</P> + * read-only. + */ + public static final String SUBJECT = "subject"; + + /** + * Message Read flag + * <P>Type: INTEGER (boolean) unread = 0, read = 1</P> + * read/write + */ + public static final String FLAG_READ = "flag_read"; + + /** + * Message Priority flag + * <P>Type: INTEGER (boolean) normal priority = 0, high priority = 1</P> + * read-only + */ + public static final String FLAG_HIGH_PRIORITY = "high_priority"; + + /** + * Reception state - the amount of the message that have been loaded from the server. + * <P>Type: INTEGER see RECEPTION_STATE_ constants below </P> + * read-only + */ + public static final String RECEPTION_STATE = "reception_state"; + + /** To be able to filter messages with attachments, we need this flag. + * <P>Type: INTEGER (boolean) no attachment = 0, attachment = 1 </P> + * read-only + */ + public static final String FLAG_ATTACHMENT = "flag_attachment"; + + /** The overall size in bytes of the attachments of the message. + * <P>Type: INTEGER </P> + */ + public static final String ATTACHMENT_SIZE = "attachment_size"; + + /** The overall size in bytes of the message including any attachments. + * This value is informative only and should be the size an email client + * would display as size for the message. + * <P>Type: INTEGER </P> + * read-only + */ + public static final String MESSAGE_SIZE = "message_size"; + + /** Indicates that the message or a part of it is protected by a DRM scheme. + * <P>Type: INTEGER (boolean) no DRM = 0, DRM protected = 1 </P> + * read-only + */ + public static final String FLAG_PROTECTED = "flag_protected"; + + /** + * A comma-delimited list of FROM addresses in RFC2822 format. + * The list must be compatible with Rfc822Tokenizer.tokenize(); + * <P>Type: TEXT</P> + * read-only + */ + public static final String FROM_LIST = "from_list"; + + /** + * A comma-delimited list of TO addresses in RFC2822 format. + * The list must be compatible with Rfc822Tokenizer.tokenize(); + * <P>Type: TEXT</P> + * read-only + */ + public static final String TO_LIST = "to_list"; + + /** + * A comma-delimited list of CC addresses in RFC2822 format. + * The list must be compatible with Rfc822Tokenizer.tokenize(); + * <P>Type: TEXT</P> + * read-only + */ + public static final String CC_LIST = "cc_list"; + + /** + * A comma-delimited list of BCC addresses in RFC2822 format. + * The list must be compatible with Rfc822Tokenizer.tokenize(); + * <P>Type: TEXT</P> + * read-only + */ + public static final String BCC_LIST = "bcc_list"; + + /** + * A comma-delimited list of REPLY-TO addresses in RFC2822 format. + * The list must be compatible with Rfc822Tokenizer.tokenize(); + * <P>Type: TEXT</P> + * read-only + */ + public static final String REPLY_TO_LIST = "reply_to_List"; + + /** + * The unique ID for a row in the folder table in which this message belongs. + * <P>Type: INTEGER (long)</P> + * read/write + */ + public static final String FOLDER_ID = "folder_id"; + + /** + * The unique ID for a row in the account table which owns this message. + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String ACCOUNT_ID = "account_id"; + + /** + * The ID identify the thread a message belongs to. If no thread id is available, + * set value to "-1" + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String THREAD_ID = "thread_id"; + } + + /** + * Indicates that the message, including any attachments, has been received from the + * server to the device. + */ + public static final String RECEPTION_STATE_COMPLETE = "complete"; + /** + * Indicates the message is partially received from the email server. + */ + public static final String RECEPTION_STATE_FRACTIONED = "fractioned"; + /** + * Indicates that only a notification about the message have been received. + */ + public static final String RECEPTION_STATE_NOTIFICATION = "notification"; + + /** + * Message folder structure + * MAP enforces use of a folder structure with mandatory folders: + * - inbox, outbox, sent, deleted, draft + * User defined folders are supported. + * The folder table must provide filtering (use of WHERE clauses) of the following entries: + * - account_id (linking the folder to an e-mail account) + * - parent_id (linking the folders individually) + * The folder table must have a folder name for each entry, and the mandatory folders + * MUST exist for each account_id. The folders may be empty. + * Use the FOLDER_NAME_xxx constants for the mandatory folders. Their names must + * not be translated into other languages, as the folder browsing is string based, and + * many Bluetooth Message Clients will use these strings to navigate to the folders. + */ + public interface FolderColumns { + + /** + * The unique ID for a row. + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String _ID = "_id"; + + /** + * The folder display name to present to the user. + * <P>Type: TEXT</P> + * read-only + */ + public static final String NAME = "name"; + + /** + * The _id-key to the account this folder refers to. + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String ACCOUNT_ID = "account_id"; + + /** + * The _id-key to the parent folder. -1 for root folders. + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String PARENT_FOLDER_ID = "parent_id"; + } + /** + * A projection of all the columns in the Message table + */ + public static final String[] BT_MESSAGE_PROJECTION = new String[] { + MessageColumns._ID, + MessageColumns.DATE, + MessageColumns.SUBJECT, + MessageColumns.FLAG_READ, + MessageColumns.FLAG_ATTACHMENT, + MessageColumns.FOLDER_ID, + MessageColumns.ACCOUNT_ID, + MessageColumns.FROM_LIST, + MessageColumns.TO_LIST, + MessageColumns.CC_LIST, + MessageColumns.BCC_LIST, + MessageColumns.REPLY_TO_LIST, + MessageColumns.FLAG_PROTECTED, + MessageColumns.FLAG_HIGH_PRIORITY, + MessageColumns.MESSAGE_SIZE, + MessageColumns.ATTACHMENT_SIZE, + MessageColumns.RECEPTION_STATE, + MessageColumns.THREAD_ID + }; + + /** + * A projection of all the columns in the Account table + */ + public static final String[] BT_ACCOUNT_PROJECTION = new String[] { + AccountColumns._ID, + AccountColumns.ACCOUNT_DISPLAY_NAME, + AccountColumns.FLAG_EXPOSE, + }; + + /** + * A projection of all the columns in the Folder table + */ + public static final String[] BT_FOLDER_PROJECTION = new String[] { + FolderColumns._ID, + FolderColumns.NAME, + FolderColumns.ACCOUNT_ID, + FolderColumns.PARENT_FOLDER_ID + }; + + +} diff --git a/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapEmailProvider.java b/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapEmailProvider.java new file mode 100644 index 000000000..d95148f07 --- /dev/null +++ b/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapEmailProvider.java @@ -0,0 +1,696 @@ +/* + * 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); + } +} |