summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorCasper Bonde <c.bonde@samsung.com>2014-07-24 13:47:23 +0200
committerMatthew Xie <mattx@google.com>2014-08-06 00:06:12 -0700
commit326b5e610063ac24c0ba467ac585bd4c7f618a67 (patch)
tree863e6fa83714e668d7fc9eeab1c693942763b219 /lib
parentf021c4ee6ba53c8512807c1469b2432278cf6cca (diff)
downloadandroid_packages_apps_Bluetooth-326b5e610063ac24c0ba467ac585bd4c7f618a67.tar.gz
android_packages_apps_Bluetooth-326b5e610063ac24c0ba467ac585bd4c7f618a67.tar.bz2
android_packages_apps_Bluetooth-326b5e610063ac24c0ba467ac585bd4c7f618a67.zip
BT MAP: added support for email sharing over BT
- added support for Emails - added activity to do setup of the email accounts to share - added improved handling of MMS, SMS and Email - Many optimizations to speed (especially getMessageListing) - fixed wakelock problem - fixed user timeout problem when user do not react to msg access request - added missing privileges - support for Quoted Printable format - added accountId in test case URIs - fixed problem with service numbers being strings - fixed problem with read flag in getMessage - added support for transparent flag in Email Push - added missing send-event for non-imap accounts - set attachment size to 0 if text-only message - fixed double send for sms messages with retry - removed secondary phone numbers from recipient/originator - removed insert-address-token in MMS messages - fixed null-pointer exception in settings (missing extra in intent) - send text-only mms as sms (workaround to make it cheaper) - fixed rejection of native and fraction requests - better handling of unknown message types in push - fixed problem with possible illigal xml chars in message listing - added missing WRITE_APN_SETTINGS permission to manifest - fixed problem with notifications when pushing to folders other than OUTBOX - removed debugging code - added support for threadId - fixed permission problems - changed to use ContentProviderClients for Email app access - fixed names for member vars UPDATE: Moved the MAP E-mail API to the bluetooth package. UPDATE: Added check for the presense of MMS parts. This is needed due to a change in the MMS app/subsystem, where deleted messages gets corrupted. Signed-off-by: Casper Bonde <c.bonde@samsung.com> Change-Id: Ib5dbe7c2d5c0ba8d978ae843d840028592e3cab4
Diffstat (limited to 'lib')
-rw-r--r--lib/mapapi/BluetoothMapContract.java557
-rw-r--r--lib/mapapi/BluetoothMapEmailProvider.java696
2 files changed, 1253 insertions, 0 deletions
diff --git a/lib/mapapi/BluetoothMapContract.java b/lib/mapapi/BluetoothMapContract.java
new file mode 100644
index 000000000..32018ba95
--- /dev/null
+++ b/lib/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">&lt;provider android:authorities="[PROVIDER AUTHORITY]"
+ android:exported="true"
+ android:enabled="true"
+ android:permission="android.permission.BLUETOOTH_MAP"&gt;
+ * ...
+ * &lt;intent-filter&gt;
+ &lt;action android:name="android.content.action.BLEUETOOT_MAP_PROVIDER" /&gt;
+ &lt;/intent-filter&gt;
+ * ...
+ * &lt;/provider&gt;
+ * [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/BluetoothMapEmailProvider.java b/lib/mapapi/BluetoothMapEmailProvider.java
new file mode 100644
index 000000000..d95148f07
--- /dev/null
+++ b/lib/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);
+ }
+}