diff options
author | kschulz <k.schulz@samsung.com> | 2015-03-17 11:47:46 +0100 |
---|---|---|
committer | Andre Eisenbach <eisenbach@google.com> | 2015-04-10 19:43:37 -0700 |
commit | 5a60e47497f21f64e6d79420dc4c56c1907df22a (patch) | |
tree | 68a80ce2c49692ab9b50204ba8ee8a6aa238df02 /lib/mapapi/com | |
parent | 0dcecb2cfd921916ed586183d64ec9fd250a6e4c (diff) | |
download | android_packages_apps_Bluetooth-5a60e47497f21f64e6d79420dc4c56c1907df22a.tar.gz android_packages_apps_Bluetooth-5a60e47497f21f64e6d79420dc4c56c1907df22a.tar.bz2 android_packages_apps_Bluetooth-5a60e47497f21f64e6d79420dc4c56c1907df22a.zip |
Update to Bluetooth MAP 1.2 (server)
- Change folder name lookup to a map
Replaced the arrays used to convert mailbox ID/msg type to a folder name with a static map.
This is to avoid null pointer exception for unknown values, and to catch any changes in
the ID/type values at compile time in stead of runtime.
Bug-id:16874441
- Added Instance Information support and Extended Event support.
Still missing integration wiht SDP MAP feature bit mask support
- Adding Abstract implementation to support conversations
- added IM account handling, IM type definition, Application paramenters.
- addedgetConversactionList functionality
- added method to strip encoding in headers
- Fixed messagelist showing both email address and name in the name fields.
- Fixed Index out of bounds exception was hit when the subject contained invalid chars.
- Added functionality to support the getConversationListReq
Works for SMS/MMS, Email and IM
For Email/IM it depends on the convoContact table in the contract.
For SMS/MMS it uses the contact number+ name if available in contact database.
- Added new parameters to msgListing also in contract class
- Added Test framework for "near system level" tests
Currently only includes an entry point for single device tests.
- Added support for setOwnerStatus
- Added support for vcard type X-BT-UID
- Introduced type SignedLongLong to handle 128 bit values which needs to be handled as hex-strings.
- Added convocontact notification events for IM
- Added support for IM getMessage
- Added setEventFilter function.
- Added event filtering before enquing an event to be send.
- Added selective observers, depending on the active filter.
- Fixed timestamp to be from seconds to seconds (not from milisec)
- Fixed version number in bMessage if remote featurebit is set for v 1.1
- Added content encoding to QP for text that are not USACII
- Corrected the addresses in to/from for IM messages
- Added btuid and btuci to vcard
- Fixed (some) longlines
- Added extendedData support (empty when sending, just logging when receiving)
- Fixed Email folderName compairison changed to ignore case
- Fixed problem with names containing "null"
- BluetoothMapbMessageMms changed to BluetoothMapbMessageMime
- Fixrf addOriginator in getMessage request
- Add missing subjects in events for SMS
- Don't send ReadStatusChanged when pushing a message
- Temp way of adding names/uci to IM msg listing
- Added messageHandle filtering in msgListing
- Convolisting parameter mask support
- Added support for using handle when filtering in root folder during msgLising
- Added subject to event in sms
- Fixed so attribute_mime_type is only sent when parameter is requested
- Fixed feature bit check to messageListing version
- Fixed leaking cursors
- Added support for database identifier
- Added folder and conversation version counters
Change-Id: I4d2954b795aa7ed2a41dd034384da30f240b518f
Diffstat (limited to 'lib/mapapi/com')
-rw-r--r-- | lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java | 751 | ||||
-rw-r--r-- | lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapIMProvider.java | 689 |
2 files changed, 1395 insertions, 45 deletions
diff --git a/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java b/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java index 32018ba95..6a40a27be 100644 --- a/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java +++ b/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java @@ -21,7 +21,7 @@ import android.net.Uri; /** - * This class defines the minimum sets of data needed for an E-mail client to + * This class defines the minimum sets of data needed for a client to * implement to claim support for the Bluetooth Message Access Profile. * Access to three data sets are needed: * <ul> @@ -35,10 +35,12 @@ import android.net.Uri; * 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> + * <li>Conversation data set with the thread structure of 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 + * 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" @@ -66,8 +68,10 @@ public final class BluetoothMapContract { * 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"; - + public static final String PROVIDER_INTERFACE_EMAIL = + "android.bluetooth.action.BLUETOOTH_MAP_PROVIDER"; + public static final String PROVIDER_INTERFACE_IM = + "android.bluetooth.action.BLUETOOTH_MAP_IM_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 @@ -98,6 +102,48 @@ public final class BluetoothMapContract { public static final String EXTRA_UPDATE_FOLDER_ID = "UpdateFolderId"; /** + * The Bluetooth Message Access profile allows a remote BT-MAP Client to update + * the owners presence and chat state + * + * ContentProvider.call() is used for these purposes, and the METHOD_SET_OWNER_STATUS + * method name shall trigger a change in owner/users presence or chat properties for an + * account or conversation. + * + * This shall be a non blocking call simply setting the properties, and the change should + * be sent to the remote server/users, depending on what property is changed. + * Bundle extra parameter will carry following values: + * EXTRA_ACCOUNT_ID containing the account_id + * EXTRA_PRESENCE_STATE containing the presence state of the owner account + * EXTRA_PRESENCE_STATUS containing the presence status text from the owner + * EXTRA_LAST_ACTIVE containing the last activity time stamp of the owner account + * EXTRA_CHAT_STATE containing the chat state of a specific conversation + * EXTRA_CONVERSATION_ID containing the conversation that is changed + */ + public static final String METHOD_SET_OWNER_STATUS = "SetOwnerStatus"; + public static final String EXTRA_ACCOUNT_ID = "AccountId"; // Is this needed + public static final String EXTRA_PRESENCE_STATE = "PresenceState"; + public static final String EXTRA_PRESENCE_STATUS = "PresenceStatus"; + public static final String EXTRA_LAST_ACTIVE = "LastActive"; + public static final String EXTRA_CHAT_STATE = "ChatState"; + public static final String EXTRA_CONVERSATION_ID = "ConversationId"; + + /** + * The Bluetooth Message Access profile can inform the messaging application of the Bluetooth + * state, whether is is turned 'on' or 'off' + * + * ContentProvider.call() is used for these purposes, and the METHOD_SET_BLUETOOTH_STATE + * method name shall trigger a change in owner/users presence or chat properties for an + * account or conversation. + * + * This shall be a non blocking call simply setting the properties. + * + * Bundle extra parameter will carry following values: + * EXTRA_BLUETOOTH_STATE containing the state of the Bluetooth connectivity + */ + public static final String METHOD_SET_BLUETOOTH_STATE = "SetBtState"; + public static final String EXTRA_BLUETOOTH_STATE = "BluetoothState"; + + /** * 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. @@ -105,11 +151,13 @@ public final class BluetoothMapContract { * content://ProviderAuthority/TABLE_ACCOUNT * content://ProviderAuthority/account_id/TABLE_MESSAGE * content://ProviderAuthority/account_id/TABLE_FOLDER - */ + * content://ProviderAuthority/account_id/TABLE_CONVERSATION + * content://ProviderAuthority/account_id/TABLE_CONVOCONTACT + **/ /** * Build URI representing the given Accounts data-set in a - * bluetooth provider. When queried, the direct URI for the account + * Bluetooth provider. When queried, the direct URI for the account * with the given accountID is returned. */ public static Uri buildAccountUri(String authority) { @@ -130,7 +178,7 @@ public final class BluetoothMapContract { } /** * Build URI representing the entire Message table in a - * bluetooth provider. + * Bluetooth provider. */ public static Uri buildMessageUri(String authority) { return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) @@ -140,7 +188,7 @@ public final class BluetoothMapContract { } /** * Build URI representing the given Message data-set in a - * bluetooth provider. When queried, the URI for the Messages + * Bluetooth provider. When queried, the URI for the Messages * with the given accountID is returned. */ public static Uri buildMessageUri(String authority, String accountId) { @@ -152,7 +200,7 @@ public final class BluetoothMapContract { } /** * Build URI representing the given Message data-set with specific messageId in a - * bluetooth provider. When queried, the direct URI for the account + * 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) { @@ -165,7 +213,7 @@ public final class BluetoothMapContract { } /** * Build URI representing the given Message data-set in a - * bluetooth provider. When queried, the direct URI for the account + * Bluetooth provider. When queried, the direct URI for the folder * with the given accountID is returned. */ public static Uri buildFolderUri(String authority, String accountId) { @@ -177,11 +225,66 @@ public final class BluetoothMapContract { } /** + * Build URI representing the given Message data-set in a + * Bluetooth provider. When queried, the direct URI for the conversation + * with the given accountID is returned. + */ + public static Uri buildConversationUri(String authority, String accountId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(accountId) + .appendPath(TABLE_CONVERSATION) + .build(); + } + + /** + * Build URI representing the given Contact data-set in a + * Bluetooth provider. When queried, the direct URI for the contacts + * with the given accountID is returned. + */ + public static Uri buildConvoContactsUri(String authority) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(TABLE_CONVOCONTACT) + .build(); + } + + /** + * Build URI representing the given Contact data-set in a + * Bluetooth provider. When queried, the direct URI for the contacts + * with the given accountID is returned. + */ + public static Uri buildConvoContactsUri(String authority, String accountId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(accountId) + .appendPath(TABLE_CONVOCONTACT) + .build(); + } + /** + * Build URI representing the given Contact data-set in a + * Bluetooth provider. When queried, the direct URI for the contact + * with the given contactID and accountID is returned. + */ + public static Uri buildConvoContactsUriWithId(String authority, String accountId, + String contactId) { + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(accountId) + .appendPath(TABLE_CONVOCONTACT) + .appendPath(contactId) + .build(); + } + /** * @hide */ - public static final String TABLE_ACCOUNT = "Account"; - public static final String TABLE_MESSAGE = "Message"; - public static final String TABLE_FOLDER = "Folder"; + public static final String TABLE_ACCOUNT = "Account"; + public static final String TABLE_MESSAGE = "Message"; + public static final String TABLE_MESSAGE_PART = "Part"; + public static final String TABLE_FOLDER = "Folder"; + public static final String TABLE_CONVERSATION = "Conversation"; + public static final String TABLE_CONVOCONTACT = "ConvoContact"; + /** * Mandatory folders for the Bluetooth message access profile. @@ -189,11 +292,22 @@ public final class BluetoothMapContract { * 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"; + public static final String FOLDER_NAME_INBOX = "INBOX"; + public static final String FOLDER_NAME_SENT = "SENT"; + public static final String FOLDER_NAME_OUTBOX = "OUTBOX"; + public static final String FOLDER_NAME_DRAFT = "DRAFT"; + public static final String FOLDER_NAME_DELETED = "DELETED"; + public static final String FOLDER_NAME_OTHER = "OTHER"; + + /** + * Folder IDs to be used with Instant Messaging virtual folders + */ + public static final long FOLDER_ID_OTHER = 0; + public static final long FOLDER_ID_INBOX = 1; + public static final long FOLDER_ID_SENT = 2; + public static final long FOLDER_ID_DRAFT = 3; + public static final long FOLDER_ID_OUTBOX = 4; + public static final long FOLDER_ID_DELETED = 5; /** @@ -296,15 +410,107 @@ public final class BluetoothMapContract { * * 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. + * the email app should not list the account at all if it is not to be sharable over BT. * * <P>Type: INTEGER (boolean) hide = 0, show = 1</P> */ public static final String FLAG_EXPOSE = "flag_expose"; + + /** + * The account unique identifier representing this account. For most IM clients this will be + * the fully qualified user name to which an invite message can be sent, from another use. + * + * e.g.: "map_test_user_12345@gmail.com" - for a Hangouts account + * + * This value will only be visible to authenticated Bluetooth devices, and will be + * transmitted using an encrypted link. + * <P>Type: TEXT</P> + * read-only + */ + public static final String ACCOUNT_UCI = "account_uci"; + + + /** + * The Bluetooth SIG maintains a list of assigned numbers(text strings) for IM clients. + * If your client/account has such a string, this is the place to return it. + * If supported by both devices, the presence of this prefix will make it possible to + * respond to a message by making a voice-call, using the same account information. + * (The call will be made using the HandsFree profile) + * https://www.bluetooth.org/en-us/specification/assigned-numbers/uniform-caller-identifiers + * + * e.g.: "hgus" - for Hangouts + * + * <P>Type: TEXT</P> + * read-only + */ + public static final String ACCOUNT_UCI_PREFIX = "account_uci_PREFIX"; + } /** + * Message Data Parts Table + * The columns needed to contain the actual data of the messageparts in IM messages. + * Each "part" has its own row and represent a single mime-part in a multipart-mime + * formatted message. + * + */ + public interface MessagePartColumns { + + /** + * The unique ID for a row. + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String _ID = "_id"; + // FIXME add message parts for IM attachments + /** + * is this a text part yes/no? + * <P>Type: TEXT</P> + * read-only + */ + public static final String TEXT = "text"; + + /** + * The charset used in the content if it is text or 8BIT if it is + * binary data + * + * <P>Type: TEXT</P> + * read-only + */ + public static final String CHARSET = "charset"; + + /** + * The filename representing the data file of the raw data in the database + * If this is empty, then it must be text and part of the message body. + * This is the name that the data will have when it is included as attachment + * + * <P>Type: TEXT</P> + * read-only + */ + + public static final String FILENAME = "filename"; + + /** + * Identifier for the content in the data. This can be used to + * refer directly to the data in the body part. + * + * <P>Type: TEXT</P> + * read-only + */ + + public static final String CONTENT_ID = "cid"; + + /** + * The raw data in either text format or binary format + * + * <P>Type: BLOB</P> + * read-only + */ + public static final String RAW_DATA = "raw_data"; + + } + /** * 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 @@ -319,7 +525,7 @@ public final class BluetoothMapContract { * date etc) written through file-i/o takes precedence over the inserted values and should * overwrite them. */ - public interface MessageColumns { + public interface MessageColumns extends EmailMessageColumns { /** * The unique ID for a row. @@ -336,6 +542,14 @@ public final class BluetoothMapContract { */ public static final String DATE = "date"; + //TODO REMOVE WHEN Parts Table is in place + /** + * Message body. Used by Instant Messaging + * <P>Type: TEXT</P> + * read-only. + */ + public static final String BODY = "body"; + /** * Message subject. * <P>Type: TEXT</P> @@ -359,11 +573,18 @@ public final class BluetoothMapContract { /** * Reception state - the amount of the message that have been loaded from the server. - * <P>Type: INTEGER see RECEPTION_STATE_ constants below </P> + * <P>Type: TEXT see RECEPTION_STATE_* constants below </P> * read-only */ public static final String RECEPTION_STATE = "reception_state"; + /** + * Delivery state - the amount of the message that have been loaded from the server. + * <P>Type: TEXT see DELIVERY_STATE_* constants below </P> + * read-only + */ + public static final String DEVILERY_STATE = "delivery_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 @@ -375,6 +596,12 @@ public final class BluetoothMapContract { */ public static final String ATTACHMENT_SIZE = "attachment_size"; + /** The mine type of the attachments for the message. + * <P>Type: TEXT </P> + * read-only + */ + public static final String ATTACHMENT_MINE_TYPES = "attachment_mime_types"; + /** 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. @@ -406,6 +633,40 @@ public final class BluetoothMapContract { public static final String TO_LIST = "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/conversation 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"; + + /** + * The Name of the thread/conversation a message belongs to. + * <P>Type: TEXT</P> + * read-only + */ + public static final String THREAD_NAME = "thread_name"; + } + + public interface EmailMessageColumns { + + + + /** * A comma-delimited list of CC addresses in RFC2822 format. * The list must be compatible with Rfc822Tokenizer.tokenize(); * <P>Type: TEXT</P> @@ -429,30 +690,19 @@ public final class BluetoothMapContract { */ 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 the complete message has been delivered to the recipient. + */ + public static final String DELIVERY_STATE_DELIVERED = "delivered"; + /** + * Indicates that the complete message has been sent from the MSE to the remote network. + */ + public static final String DELIVERY_STATE_SENT = "sent"; + + /** * Indicates that the message, including any attachments, has been received from the * server to the device. */ @@ -510,6 +760,313 @@ public final class BluetoothMapContract { */ public static final String PARENT_FOLDER_ID = "parent_id"; } + + /** + * Message conversation structure. Enables use of a conversation structure for messages across + * folders, further binding contacts to conversations. + * Content that must be supplied: + * - Name, LastActivity, ReadStatus, VersionCounter + * Content that must support update: + * - READ_STATUS, LAST_ACTIVITY and VERSION_COUNTER (VERSION_COUNTER used to validity of _ID) + * Additional insert of a new conversation with the following values shall be supported: + * - FOLDER_ID + * When querying this table, the cursor returned must contain one row for each contact member + * in a thread. + * For filter/search parameters attributes to the URI will be used. The following columns must + * support filtering: + * - ConvoContactColumns.NAME + * - ConversationColumns.THREAD_ID + * - ConversationColumns.LAST_ACTIVITY + * - ConversationColumns.READ_STATUS + */ + public interface ConversationColumns extends ConvoContactColumns { + + /** + * The unique ID for a row. + * <P>Type: INTEGER (long)</P> + * read-only + */ +// Should not be needed anymore public static final String _ID = "_id"; + + /** + * The unique ID for a Thread. + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String THREAD_ID = "thread_id"; + + /** + * The unique ID for a row. + * <P>Type: INTEGER (long)</P> + * read-only + */ +// TODO: IS THIS NECESSARY - or do we need the thread ID to hold thread Id from message +// or can we be sure we are in control and can use the _ID and put that in the message DB + //public static final String THREAD_ID = "thread_id"; + + /** + * The type of conversation, see {@link ConversationType} + * <P>Type: TEXT</P> + * read-only + */ +// TODO: IS THIS NECESSARY - no conversation type is available in the latest, +// guess it can be found from number of contacts in the conversation + //public static final String TYPE = "type"; + + /** + * The name of the conversation, e.g. group name in case of group chat + * <P>Type: TEXT</P> + * read-only + */ + public static final String THREAD_NAME = "thread_name"; + + /** + * The time stamp of the last activity in the conversation as a unix timestamp + * (miliseconds since 00:00:00 UTC 1/1-1970) + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String LAST_THREAD_ACTIVITY = "last_thread_activity"; + + /** + * The status on the conversation, either 'read' or 'unread' + * <P>Type: INTEGER (boolean) unread = 0, read = 1</P> + * read/write + */ + public static final String READ_STATUS = "read_status"; + + /** + * A counter that keep tack of version of the table content, count up on ID reuse + * <P>Type: INTEGER (long)</P> + * read-only + */ +// TODO: IS THIS NECESSARY - skal den ligge i databasen? + // CB: If we need it, it must be in the database, or initialized with a random value at + // BT-ON + // UPDATE: TODO: Change to the last_activity time stamp (as a long value). This will + // provide the information needed for BT clients - currently unused + public static final String VERSION_COUNTER = "version_counter"; + + /** + * A short description of the latest activity on conversation - typically + * part of the last message. + * <P>Type: TEXT</P> + * read-only + */ + public static final String SUMMARY = "convo_summary"; + + + } + + /** + * MAP enables access to contacts for the conversation + * The conversation table must provide filtering (using WHERE clauses) of following entries: + * - convo_id linking contacts to conversations + * - x_bt_uid linking contacts to PBAP contacts + * The conversation contact table must have a convo_id and a name for each entry. + */ + public interface ConvoContactColumns extends ChatStatusColumns, PresenceColumns { + /** + * The unique ID for a contact in Conversation + * <P>Type: INTEGER (long)</P> + * read-only + */ +// Should not be needed anymore public static final String _ID = "_id"; + + /** + * The ID of the conversation the contact is part of. + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String CONVO_ID = "convo_id"; + + /** + * The name of contact in instant message application + * <P>Type: TEXT</P> + * read-only + */ + public static final String NAME = "name"; + + /** + * The nickname of contact in instant message group chat conversation. + * <P>Type: TEXT</P> + * read-only + */ + public static final String NICKNAME = "nickname"; + + + /** + * The unique ID for all Bluetooth contacts available through PBAP. + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String X_BT_UID = "x_bt_uid"; + + /** + * The unique ID for the contact within the domain of the interfacing service. + * (UCI: Unique Call Identity) + * It is expected that a message send to this ID will reach the recipient regardless + * through which interface the message is send. + * For E-mail this will be the e-mail address, for Google+ this will be the e-mail address + * associated with the contact account. + * This ID + * <P>Type: TEXT</P> + * read-only + */ + public static final String UCI = "x_bt_uci"; + } + + /** + * The name of query parameter used to filter on recipient + */ + public static final String FILTER_RECIPIENT_SUBSTRING = "rec_sub_str"; + + /** + * The name of query parameter used to filter on originator + */ + public static final String FILTER_ORIGINATOR_SUBSTRING = "org_sub_str"; + + /** + * The name of query parameter used to filter on read status. + * - true - return only threads with all messages marked as read + * - false - return only threads with one or more unread messages + * - omitted as query parameter - do not filter on read status + */ + public static final String FILTER_READ_STATUS = "read"; + + /** + * Time in ms since epoch. For conversations this will be for last activity + * as a unix timestamp (miliseconds since 00:00:00 UTC 1/1-1970) + */ + public static final String FILTER_PERIOD_BEGIN = "t_begin"; + + /** + * Time in ms since epoch. For conversations this will be for last activity + * as a unix timestamp (miliseconds since 00:00:00 UTC 1/1-1970) + */ + public static final String FILTER_PERIOD_END = "t_end"; + + /** + * Filter for a specific ThreadId + */ + public static final String FILTER_THREAD_ID = "thread_id"; + + + public interface ChatState { + int UNKNOWN = 0; + int INACITVE = 1; + int ACITVE = 2; + int COMPOSING = 3; + int PAUSED = 4; + int GONE = 5; + } + + /** + * Instant Messaging contact chat state information + * MAP enables access to contacts chat state for the instant messaging application + * The chat state table must provide filtering (use of WHERE clauses) of the following entries: + * - contact_id (linking chat state to contacts) + * - thread_id (linking chat state to conversations and messages) + * The presence table must have a contact_id for each entry. + */ + public interface ChatStatusColumns { + +// /** +// * The contact ID of a instant messaging contact. +// * <P>Type: TEXT </P> +// * read-only +// */ +// public static final String CONTACT_ID = "contact_id"; +// +// /** +// * The thread id for a conversation. +// * <P>Type: INTEGER (long)</P> +// * read-only +// */ +// public static final String CONVO_ID = "convo_id"; + + /** + * The chat state of contact in conversation, see {@link ChatState} + * <P>Type: INTERGER</P> + * read-only + */ + public static final String CHAT_STATE = "chat_state"; + +// /** +// * The geo location of the contact +// * <P>Type: TEXT</P> +// * read-only +// */ +//// TODO: IS THIS NEEDED - not in latest specification +// public static final String GEOLOC = "geoloc"; + + /** + * The time stamp of the last time this contact was active in the conversation + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String LAST_ACTIVE = "last_active"; + + } + + public interface PresenceState { + int UNKNOWN = 0; + int OFFLINE = 1; + int ONLINE = 2; + int AWAY = 3; + int DO_NOT_DISTURB = 4; + int BUSY = 5; + int IN_A_MEETING = 6; + } + + /** + * Instant Messaging contact presence information + * MAP enables access to contacts presences information for the instant messaging application + * The presence table must provide filtering (use of WHERE clauses) of the following entries: + * - contact_id (linking contacts to presence) + * The presence table must have a contact_id for each entry. + */ + public interface PresenceColumns { + +// /** +// * The contact ID of a instant messaging contact. +// * <P>Type: TEXT </P> +// * read-only +// */ +// public static final String CONTACT_ID = "contact_id"; + + /** + * The presence state of contact, see {@link PresenceState} + * <P>Type: INTERGER</P> + * read-only + */ + public static final String PRESENCE_STATE = "presence_state"; + + /** + * The priority of contact presence + * <P>Type: INTERGER</P> + * read-only + */ +// TODO: IS THIS NEEDED - not in latest specification + public static final String PRIORITY = "priority"; + + /** + * The last status text from contact + * <P>Type: TEXT</P> + * read-only + */ + public static final String STATUS_TEXT = "status_text"; + + /** + * The time stamp of the last time the contact was online + * <P>Type: INTEGER (long)</P> + * read-only + */ + public static final String LAST_ONLINE = "last_online"; + + } + + /** * A projection of all the columns in the Message table */ @@ -517,21 +1074,43 @@ public final class BluetoothMapContract { MessageColumns._ID, MessageColumns.DATE, MessageColumns.SUBJECT, + //TODO REMOVE WHEN Parts Table is in place + MessageColumns.BODY, + MessageColumns.MESSAGE_SIZE, + MessageColumns.FOLDER_ID, MessageColumns.FLAG_READ, + MessageColumns.FLAG_PROTECTED, + MessageColumns.FLAG_HIGH_PRIORITY, MessageColumns.FLAG_ATTACHMENT, - MessageColumns.FOLDER_ID, - MessageColumns.ACCOUNT_ID, + MessageColumns.ATTACHMENT_SIZE, MessageColumns.FROM_LIST, MessageColumns.TO_LIST, MessageColumns.CC_LIST, MessageColumns.BCC_LIST, MessageColumns.REPLY_TO_LIST, + MessageColumns.RECEPTION_STATE, + MessageColumns.DEVILERY_STATE, + MessageColumns.THREAD_ID + }; + + public static final String[] BT_INSTANT_MESSAGE_PROJECTION = new String[] { + MessageColumns._ID, + MessageColumns.DATE, + MessageColumns.SUBJECT, + MessageColumns.MESSAGE_SIZE, + MessageColumns.FOLDER_ID, + MessageColumns.FLAG_READ, MessageColumns.FLAG_PROTECTED, MessageColumns.FLAG_HIGH_PRIORITY, - MessageColumns.MESSAGE_SIZE, + MessageColumns.FLAG_ATTACHMENT, MessageColumns.ATTACHMENT_SIZE, + MessageColumns.ATTACHMENT_MINE_TYPES, + MessageColumns.FROM_LIST, + MessageColumns.TO_LIST, MessageColumns.RECEPTION_STATE, - MessageColumns.THREAD_ID + MessageColumns.DEVILERY_STATE, + MessageColumns.THREAD_ID, + MessageColumns.THREAD_NAME }; /** @@ -544,6 +1123,18 @@ public final class BluetoothMapContract { }; /** + * A projection of all the columns in the Account table + * TODO: Is this the way to differentiate + */ + public static final String[] BT_IM_ACCOUNT_PROJECTION = new String[] { + AccountColumns._ID, + AccountColumns.ACCOUNT_DISPLAY_NAME, + AccountColumns.FLAG_EXPOSE, + AccountColumns.ACCOUNT_UCI, + AccountColumns.ACCOUNT_UCI_PREFIX + }; + + /** * A projection of all the columns in the Folder table */ public static final String[] BT_FOLDER_PROJECTION = new String[] { @@ -554,4 +1145,74 @@ public final class BluetoothMapContract { }; + /** + * A projection of all the columns in the Conversation table + */ + public static final String[] BT_CONVERSATION_PROJECTION = new String[] { + /* Thread information */ + ConversationColumns.THREAD_ID, + ConversationColumns.THREAD_NAME, + ConversationColumns.READ_STATUS, + ConversationColumns.LAST_THREAD_ACTIVITY, + ConversationColumns.VERSION_COUNTER, + ConversationColumns.SUMMARY, + /* Contact information */ + ConversationColumns.UCI, + ConversationColumns.NAME, + ConversationColumns.NICKNAME, + ConversationColumns.CHAT_STATE, + ConversationColumns.LAST_ACTIVE, + ConversationColumns.X_BT_UID, + ConversationColumns.PRESENCE_STATE, + ConversationColumns.STATUS_TEXT, + ConversationColumns.PRIORITY + }; + + /** + * A projection of the Contact Info and Presence columns in the Contact Info in table + */ + public static final String[] BT_CONTACT_CHATSTATE_PRESENCE_PROJECTION = new String[] { + ConvoContactColumns.UCI, + ConvoContactColumns.CONVO_ID, + ConvoContactColumns.NAME, + ConvoContactColumns.NICKNAME, + ConvoContactColumns.X_BT_UID, + ConvoContactColumns.CHAT_STATE, + ConvoContactColumns.LAST_ACTIVE, + ConvoContactColumns.PRESENCE_STATE, + ConvoContactColumns.PRIORITY, + ConvoContactColumns.STATUS_TEXT, + ConvoContactColumns.LAST_ONLINE + }; + + /** + * A projection of the Contact Info the columns in Contacts Info table + */ + public static final String[] BT_CONTACT_PROJECTION = new String[] { + ConvoContactColumns.UCI, + ConvoContactColumns.CONVO_ID, + ConvoContactColumns.X_BT_UID, + ConvoContactColumns.NAME, + ConvoContactColumns.NICKNAME + }; + + + /** + * A projection of all the columns in the Chat Status table + */ + public static final String[] BT_CHATSTATUS_PROJECTION = new String[] { + ChatStatusColumns.CHAT_STATE, + ChatStatusColumns.LAST_ACTIVE, + }; + + /** + * A projection of all the columns in the Presence table + */ + public static final String[] BT_PRESENCE_PROJECTION = new String[] { + PresenceColumns.PRESENCE_STATE, + PresenceColumns.PRIORITY, + PresenceColumns.STATUS_TEXT, + PresenceColumns.LAST_ONLINE + }; + } diff --git a/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapIMProvider.java b/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapIMProvider.java new file mode 100644 index 000000000..d0c642ccd --- /dev/null +++ b/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapIMProvider.java @@ -0,0 +1,689 @@ +/* +* Copyright (C) 2015 Samsung System LSI +* 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 BluetoothMapContract. + * A base class for a ContentProvider that allows access to Instant messages from a Bluetooth + * device through the Message Access Profile. + */ +public abstract class BluetoothMapIMProvider extends ContentProvider { + + private static final String TAG = "BluetoothMapIMProvider"; + private static final boolean D = true; + + private static final int MATCH_ACCOUNT = 1; + private static final int MATCH_MESSAGE = 3; + private static final int MATCH_CONVERSATION = 4; + private static final int MATCH_CONVOCONTACT = 5; + + protected ContentResolver mResolver; + + private Uri CONTENT_URI = null; + private String mAuthority; + private UriMatcher mMatcher; + + /** + * @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_MESSAGE, MATCH_MESSAGE); + mMatcher.addURI(mAuthority, "#/"+ BluetoothMapContract.TABLE_CONVERSATION, + MATCH_CONVERSATION); + mMatcher.addURI(mAuthority, "#/"+ BluetoothMapContract.TABLE_CONVOCONTACT, + MATCH_CONVOCONTACT); + + // 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); + } + if(D) Log.d(TAG,"attachInfo() mAuthority = " + mAuthority); + + mResolver = context.getContentResolver(); + super.attachInfo(context, info); + } + + /** + * 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); + } + + + /** + * 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 contactId Null is a valid value, if multiple contacts changed or the + * contactId is unknown, but recommended for increased performance. + */ + protected void onContactChanged(String accountId, String contactId) { + Uri newUri = null; + + if(mAuthority == null){ + return; + } + if(accountId == null){ + newUri = BluetoothMapContract.buildConvoContactsUri(mAuthority); + } else { + if(contactId == null) + { + newUri = BluetoothMapContract.buildConvoContactsUri(mAuthority,accountId); + } else { + newUri = BluetoothMapContract.buildConvoContactsUriWithId(mAuthority, accountId, + contactId); + } + } + if(D) Log.d(TAG,"onContactChanged() accountId = " + accountId + + " contactId = " + contactId + " URI: " + newUri); + mResolver.notifyChange(newUri, null); + } + + /** + * Not used, this is just a dummy implementation. + * TODO: We might need to something intelligent here after introducing IM + */ + @Override + public String getType(Uri uri) { + return "InstantMessage"; + } + + /** + * 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); + if (accountId == null) + throw new IllegalArgumentException("Account ID missing in URI"); + + // TODO: validate values? + + 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, values); + 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, ContentValues values); + + /** + * Utility function to build a projection based on a projectionMap. + * + * "btColumnName" -> "imColumnName as btColumnName" for each entry. + * + * This supports SQL statements in the column name entry. + * @param projection + * @param projectionMap <string, string> + * @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; + if(D)Log.w(TAG, "query(): uri =" + mAuthority + " uri=" + uri.toString()); + + switch (mMatcher.match(uri)) { + case MATCH_ACCOUNT: + return queryAccount(projection, selection, selectionArgs, sortOrder); + case MATCH_MESSAGE: + // TODO: Extract account from URI + accountId = getAccountId(uri); + return queryMessage(accountId, projection, selection, selectionArgs, sortOrder); + case MATCH_CONVERSATION: + accountId = getAccountId(uri); + String value; + String searchString = + uri.getQueryParameter(BluetoothMapContract.FILTER_ORIGINATOR_SUBSTRING); + Long periodBegin = null; + value = uri.getQueryParameter(BluetoothMapContract.FILTER_PERIOD_BEGIN); + if(value != null) { + periodBegin = Long.parseLong(value); + } + Long periodEnd = null; + value = uri.getQueryParameter(BluetoothMapContract.FILTER_PERIOD_END); + if(value != null) { + periodEnd = Long.parseLong(value); + } + Boolean read = null; + value = uri.getQueryParameter(BluetoothMapContract.FILTER_READ_STATUS); + if(value != null) { + read = value.equalsIgnoreCase("true"); + } + Long threadId = null; + value = uri.getQueryParameter(BluetoothMapContract.FILTER_THREAD_ID); + if(value != null) { + threadId = Long.parseLong(value); + } + return queryConversation(accountId, threadId, read, periodEnd, periodBegin, + searchString, projection, sortOrder); + case MATCH_CONVOCONTACT: + accountId = getAccountId(uri); + long contactId = 0; + return queryConvoContact(accountId, contactId, 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); + + /** + * 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 + * 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); + + /** + * For the Conversation table the selection (where clause) can only include + * the following columns: + * _id: the ID of the conversation only equals + * name: partial name search + * last_activity: less than, greater than and equals + * version_counter: updated IDs are regenerated + * Additionally the COUNT and OFFSET shall be supported. + * @param accountId the ID of the account + * @param threadId the ID of the conversation + * @param projection + * @param selection + * @param selectionArgs + * @param sortOrder + * @return a cursor to query result + */ +// abstract protected Cursor queryConversation(Long threadId, String[] projection, +// String selection, String[] selectionArgs, String sortOrder); + + /** + * Query for conversations with contact information. The expected result is a cursor pointing + * to one row for each contact in a conversation. + * E.g.: + * ThreadId | ThreadName | ... | ContactName | ContactPrecence | ... | + * 1 | "Bowling" | ... | Hans | 1 | ... | + * 1 | "Bowling" | ... | Peter | 2 | ... | + * 2 | "" | ... | Peter | 2 | ... | + * 3 | "" | ... | Hans | 1 | ... | + * + * @param accountId the ID of the account + * @param threadId filter on a single threadId - null if no filtering is needed. + * @param read filter on a read status: + * null: no filtering on read is needed. + * true: return only threads that has NO unread messages. + * false: return only threads that has unread messages. + * @param periodEnd last_activity time stamp of the the newest thread to include in the + * result. + * @param periodBegin last_activity time stamp of the the oldest thread to include in the + * result. + * @param searchString if not null, include only threads that has contacts that matches the + * searchString as part of the contact name or nickName. + * @param projection A list of the columns that is needed in the result + * @param sortOrder the sort order + * @return a Cursor representing the query result. + */ + abstract protected Cursor queryConversation(String accountId, Long threadId, Boolean read, + Long periodEnd, Long periodBegin, String searchString, String[] projection, + String sortOrder); + + /** + * For the ConvoContact table the selection (where clause) can only include the + * following columns: + * _id: the ID of the contact only equals + * convo_id: id of conversation contact is part of + * name: partial name search + * x_bt_uid: the ID of the bt uid only equals + * chat_state: active, inactive, gone, composing, paused + * last_active: less than, greater than and equals + * presence_state: online, do_not_disturb, away, offline + * priority: level of priority 0 - 100 + * last_online: less than, greater than and equals + * @param accountId the ID of the account + * @param contactId the ID of the contact + * @param projection + * @param selection + * @param selectionArgs + * @param sortOrder + * @return a cursor to query result + */ + abstract protected Cursor queryConvoContact(String accountId, Long contactId, + 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. + * Conversations can be modified in the following cases: + * - the read status - changing between read, unread + * - the last activity - the time stamp of last message sent of received in the conversation + * ConvoContacts can be modified in the following cases: + * - the chat_state - chat status of the contact in conversation + * - the last_active - the time stamp of last action in the conversation + * - the presence_state - the time stamp of last time contact online + * - the status - the status text of the contact available in a conversation + * - the last_online - the time stamp of last time contact online + * 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); + if(accountId == null) { + throw new IllegalArgumentException("Account ID missing in update values!"); + } + 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(table.equals(BluetoothMapContract.TABLE_CONVERSATION)) { + return 0; // We do not support changing conversation + } else if(table.equals(BluetoothMapContract.TABLE_CONVOCONTACT)) { + return 0; // We do not support changing contacts + } 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, Integer 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); + + /** + * Utility function to Creates a ContentValues object based on a modified valuesSet. + * To be used after changing the keys and optionally values of a valueSet obtained + * from a ContentValues object received in update(). + * @param valueSet the values as received in the contentProvider + * @param keyMap the key map <btKey, emailKey> + * @return a new ContentValues object with the keys replaced as specified in the + * keyMap + */ + protected ContentValues createContentValues(Set<Entry<String,Object>> valueSet, + Map<String, String> keyMap) { + ContentValues values = new ContentValues(valueSet.size()); + for(Entry<String,Object> ent : valueSet) { + String key = keyMap.get(ent.getKey()); // Convert the key name + Object value = ent.getValue(); + if(value == null) { + values.putNull(key); + } else if(ent.getValue() instanceof Boolean) { + values.put(key, (Boolean) value); + } else if(ent.getValue() instanceof Byte) { + values.put(key, (Byte) value); + } else if(ent.getValue() instanceof byte[]) { + values.put(key, (byte[]) value); + } else if(ent.getValue() instanceof Double) { + values.put(key, (Double) value); + } else if(ent.getValue() instanceof Float) { + values.put(key, (Float) value); + } else if(ent.getValue() instanceof Integer) { + values.put(key, (Integer) value); + } else if(ent.getValue() instanceof Long) { + values.put(key, (Long) value); + } else if(ent.getValue() instanceof Short) { + values.put(key, (Short) value); + } else if(ent.getValue() instanceof String) { + values.put(key, (String) value); + } else { + throw new IllegalArgumentException("Unknown data type in content value"); + } + } + return values; + } + + @Override + public Bundle call(String method, String arg, Bundle extras) { + long callingId = Binder.clearCallingIdentity(); + if(D)Log.w(TAG, "call(): method=" + method + " arg=" + arg + "ThreadId: " + + Thread.currentThread().getId()); + int ret = -1; + 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; + } + ret = syncFolder(accountId, folderId); + } else if (method.equals(BluetoothMapContract.METHOD_SET_OWNER_STATUS)) { + int presenceState = extras.getInt(BluetoothMapContract.EXTRA_PRESENCE_STATE); + String presenceStatus = extras.getString( + BluetoothMapContract.EXTRA_PRESENCE_STATUS); + long lastActive = extras.getLong(BluetoothMapContract.EXTRA_LAST_ACTIVE); + int chatState = extras.getInt(BluetoothMapContract.EXTRA_CHAT_STATE); + String convoId = extras.getString(BluetoothMapContract.EXTRA_CONVERSATION_ID); + ret = setOwnerStatus(presenceState, presenceStatus, lastActive, chatState, convoId); + + } else if (method.equals(BluetoothMapContract.METHOD_SET_BLUETOOTH_STATE)) { + boolean bluetoothState = extras.getBoolean( + BluetoothMapContract.EXTRA_BLUETOOTH_STATE); + ret = setBluetoothStatus(bluetoothState); + } + } finally { + Binder.restoreCallingIdentity(callingId); + } + if(ret == 0) { + return new Bundle(); + } + 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); + + /** + * Set the properties that should change presence or chat state of owner + * e.g. when the owner is active on a BT client device but not on the BT server device + * where the IM application is installed, it should still be possible to show an active status. + * @param presenceState should follow the contract specified values + * @param presenceStatus string the owners current status + * @param lastActive time stamp of the owners last activity + * @param chatState should follow the contract specified values + * @param convoId ID to the conversation to change + * @return 0 at success + */ + abstract protected int setOwnerStatus(int presenceState, String presenceStatus, + long lastActive, int chatState, String convoId); + + /** + * Notify the application of the Bluetooth state + * @param bluetoothState 'on' of 'off' + * @return 0 at success + */ + abstract protected int setBluetoothStatus(boolean bluetoothState); + + + + /** + * 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); + } +} |