From 5a60e47497f21f64e6d79420dc4c56c1907df22a Mon Sep 17 00:00:00 2001 From: kschulz Date: Tue, 17 Mar 2015 11:47:46 +0100 Subject: 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 --- .../bluetooth/mapapi/BluetoothMapContract.java | 751 +++++++++++++++++++-- .../bluetooth/mapapi/BluetoothMapIMProvider.java | 689 +++++++++++++++++++ 2 files changed, 1395 insertions(+), 45 deletions(-) create mode 100644 lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapIMProvider.java (limited to 'lib/mapapi/com') 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: * * * 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: *
<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
@@ -97,6 +101,48 @@ public final class BluetoothMapContract {
     public static final String EXTRA_UPDATE_ACCOUNT_ID = "UpdateAccountId";
     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
@@ -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) {
@@ -176,12 +224,67 @@ public final class BluetoothMapContract {
                 .build();
     }
 
+    /**
+     * 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,14 +410,106 @@ 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.
          *
          * 

Type: INTEGER (boolean) hide = 0, show = 1

*/ 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. + *

Type: TEXT

+ * 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 + * + *

Type: TEXT

+ * 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. + *

Type: INTEGER (long)

+ * read-only + */ + public static final String _ID = "_id"; + // FIXME add message parts for IM attachments + /** + * is this a text part yes/no? + *

Type: TEXT

+ * 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 + * + *

Type: TEXT

+ * 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 + * + *

Type: TEXT

+ * 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. + * + *

Type: TEXT

+ * read-only + */ + + public static final String CONTENT_ID = "cid"; + + /** + * The raw data in either text format or binary format + * + *

Type: BLOB

+ * 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: @@ -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 + *

Type: TEXT

+ * read-only. + */ + public static final String BODY = "body"; + /** * Message subject. *

Type: TEXT

@@ -359,11 +573,18 @@ public final class BluetoothMapContract { /** * Reception state - the amount of the message that have been loaded from the server. - *

Type: INTEGER see RECEPTION_STATE_ constants below

+ *

Type: TEXT see RECEPTION_STATE_* constants below

* read-only */ public static final String RECEPTION_STATE = "reception_state"; + /** + * Delivery state - the amount of the message that have been loaded from the server. + *

Type: TEXT see DELIVERY_STATE_* constants below

+ * read-only + */ + public static final String DEVILERY_STATE = "delivery_state"; + /** To be able to filter messages with attachments, we need this flag. *

Type: INTEGER (boolean) no attachment = 0, attachment = 1

* 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. + *

Type: TEXT

+ * 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. @@ -405,6 +632,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. + *

Type: INTEGER (long)

+ * read/write + */ + public static final String FOLDER_ID = "folder_id"; + + /** + * The unique ID for a row in the account table which owns this message. + *

Type: INTEGER (long)

+ * 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" + *

Type: INTEGER (long)

+ * read-only + */ + public static final String THREAD_ID = "thread_id"; + + /** + * The Name of the thread/conversation a message belongs to. + *

Type: TEXT

+ * 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(); @@ -429,29 +690,18 @@ 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. - *

Type: INTEGER (long)

- * read/write - */ - public static final String FOLDER_ID = "folder_id"; - - /** - * The unique ID for a row in the account table which owns this message. - *

Type: INTEGER (long)

- * 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" - *

Type: INTEGER (long)

- * 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. + *

Type: INTEGER (long)

+ * read-only + */ +// Should not be needed anymore public static final String _ID = "_id"; + + /** + * The unique ID for a Thread. + *

Type: INTEGER (long)

+ * read-only + */ + public static final String THREAD_ID = "thread_id"; + + /** + * The unique ID for a row. + *

Type: INTEGER (long)

+ * 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} + *

Type: TEXT

+ * 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 + *

Type: TEXT

+ * 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) + *

Type: INTEGER (long)

+ * read-only + */ + public static final String LAST_THREAD_ACTIVITY = "last_thread_activity"; + + /** + * The status on the conversation, either 'read' or 'unread' + *

Type: INTEGER (boolean) unread = 0, read = 1

+ * 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 + *

Type: INTEGER (long)

+ * 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. + *

Type: TEXT

+ * 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 + *

Type: INTEGER (long)

+ * read-only + */ +// Should not be needed anymore public static final String _ID = "_id"; + + /** + * The ID of the conversation the contact is part of. + *

Type: INTEGER (long)

+ * read-only + */ + public static final String CONVO_ID = "convo_id"; + + /** + * The name of contact in instant message application + *

Type: TEXT

+ * read-only + */ + public static final String NAME = "name"; + + /** + * The nickname of contact in instant message group chat conversation. + *

Type: TEXT

+ * read-only + */ + public static final String NICKNAME = "nickname"; + + + /** + * The unique ID for all Bluetooth contacts available through PBAP. + *

Type: INTEGER (long)

+ * 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 + *

Type: TEXT

+ * 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. +// *

Type: TEXT

+// * read-only +// */ +// public static final String CONTACT_ID = "contact_id"; +// +// /** +// * The thread id for a conversation. +// *

Type: INTEGER (long)

+// * read-only +// */ +// public static final String CONVO_ID = "convo_id"; + + /** + * The chat state of contact in conversation, see {@link ChatState} + *

Type: INTERGER

+ * read-only + */ + public static final String CHAT_STATE = "chat_state"; + +// /** +// * The geo location of the contact +// *

Type: TEXT

+// * 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 + *

Type: INTEGER (long)

+ * 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. +// *

Type: TEXT

+// * read-only +// */ +// public static final String CONTACT_ID = "contact_id"; + + /** + * The presence state of contact, see {@link PresenceState} + *

Type: INTERGER

+ * read-only + */ + public static final String PRESENCE_STATE = "presence_state"; + + /** + * The priority of contact presence + *

Type: INTERGER

+ * read-only + */ +// TODO: IS THIS NEEDED - not in latest specification + public static final String PRIORITY = "priority"; + + /** + * The last status text from contact + *

Type: TEXT

+ * read-only + */ + public static final String STATUS_TEXT = "status_text"; + + /** + * The time stamp of the last time the contact was online + *

Type: INTEGER (long)

+ * 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 }; /** @@ -543,6 +1122,18 @@ public final class BluetoothMapContract { AccountColumns.FLAG_EXPOSE, }; + /** + * 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 */ @@ -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 + * @return the converted projection + */ + protected String[] convertProjection(String[] projection, Map 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 + * @return a new ContentValues object with the keys replaced as specified in the + * keyMap + */ + protected ContentValues createContentValues(Set> valueSet, + Map keyMap) { + ContentValues values = new ContentValues(valueSet.size()); + for(Entry 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 segments = uri.getPathSegments(); + if (segments.size() < 1) { + throw new IllegalArgumentException("No AccountId pressent in URI: " + uri); + } + return segments.get(0); + } +} -- cgit v1.2.3