summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk13
-rw-r--r--AndroidManifest.xml10
-rw-r--r--lib/mapapi/BluetoothMapContract.java557
-rw-r--r--lib/mapapi/BluetoothMapEmailProvider.java696
-rw-r--r--res/layout/bluetooth_map_email_settings.xml42
-rw-r--r--res/layout/bluetooth_map_email_settings_account_group.xml48
-rw-r--r--res/layout/bluetooth_map_email_settings_account_item.xml43
-rw-r--r--res/layout/bluetooth_transfer_item.xml1
-rw-r--r--res/values/strings.xml12
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapAppParams.java178
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapAuthenticator.java88
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapContent.java1590
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapContentObserver.java1577
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapEmailAppObserver.java287
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapEmailSettings.java56
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapEmailSettingsAdapter.java347
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapEmailSettingsDataHolder.java23
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapEmailSettingsItem.java172
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapEmailSettingsLoader.java202
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapFolderElement.java209
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapMasInstance.java386
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapMessageListing.java30
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapMessageListingElement.java237
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapObexServer.java608
-rwxr-xr-xsrc/com/android/bluetooth/map/BluetoothMapService.java894
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapSmsPdu.java162
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapUtils.java52
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapbMessage.java253
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapbMessageEmail.java79
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapbMessageMms.java (renamed from src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java)376
-rw-r--r--src/com/android/bluetooth/map/BluetoothMapbMessageSms.java38
-rw-r--r--src/com/android/bluetooth/map/BluetoothMnsObexClient.java133
-rwxr-xr-xtests/Android.mk3
-rwxr-xr-xtests/AndroidManifest.xml12
-rwxr-xr-xtests/src/com/android/bluetooth/tests/BluetoothMapbMessageTest.java18
35 files changed, 7246 insertions, 2186 deletions
diff --git a/Android.mk b/Android.mk
index 53b2810ff..2b8a2ad12 100644
--- a/Android.mk
+++ b/Android.mk
@@ -4,13 +4,24 @@ include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := \
+ $(call all-java-files-under, lib)
+
+LOCAL_MODULE := bluetooth.mapsapi
+
+include $(BUILD_JAVA_LIBRARY)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := \
$(call all-java-files-under, src)
LOCAL_PACKAGE_NAME := Bluetooth
LOCAL_CERTIFICATE := platform
LOCAL_JNI_SHARED_LIBRARIES := libbluetooth_jni
-LOCAL_JAVA_LIBRARIES := javax.obex telephony-common
+LOCAL_JAVA_LIBRARIES := javax.obex telephony-common bluetooth.mapsapi
LOCAL_STATIC_JAVA_LIBRARIES := com.android.vcard
LOCAL_REQUIRED_MODULES := bluetooth.default
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8f37a629f..e879a75d4 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
+ <uses-permission android:name="android.permission.BLUETOOTH_MAP" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
@@ -233,8 +234,17 @@
android:enabled="@bool/profile_supported_map" >
<intent-filter>
<action android:name="android.bluetooth.IBluetoothMap" />
+ <action android:name="android.btmap.intent.action.SHOW_MAPS_EMAIL_SETTINGS" />
+ <action android:name="com.android.bluetooth.map.USER_CONFIRM_TIMEOUT"/>
</intent-filter>
</service>
+ <activity android:name=".map.BluetoothMapEmailSettings"
+ android:process="@string/process"
+ android:label="@string/bluetooth_map_email_settings_title"
+ android:excludeFromRecents="true"
+ android:configChanges="orientation|keyboardHidden"
+ android:enabled="@bool/profile_supported_map">
+ </activity>
<service
android:process="@string/process"
android:name = ".gatt.GattService"
diff --git a/lib/mapapi/BluetoothMapContract.java b/lib/mapapi/BluetoothMapContract.java
new file mode 100644
index 000000000..32018ba95
--- /dev/null
+++ b/lib/mapapi/BluetoothMapContract.java
@@ -0,0 +1,557 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.mapapi;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+
+/**
+ * This class defines the minimum sets of data needed for an E-mail client to
+ * implement to claim support for the Bluetooth Message Access Profile.
+ * Access to three data sets are needed:
+ * <ul>
+ * <li>Message data set containing lists of messages.</li>
+ * <li>Account data set containing info on the existing accounts, and whether to expose
+ * these accounts. The content of the account data set is often sensitive information,
+ * hence care must be taken, not to reveal any personal information nor passwords.
+ * The accounts in this data base will be exposed in the settings menu, where the user
+ * is able to enable and disable the EXPOSE_FLAG, and thereby provide access to an
+ * account from another device, without any password protection the e-mail client
+ * might provide.</li>
+ * <li>Folder data set with the folder structure for the messages. Each message is linked to an
+ * entry in this data set.</li>
+ * </ul>
+ *
+ * To enable that the Bluetooth Message Access Server can detect the content provider implementing
+ * this interface, the {@code provider} tag for the bluetooth related content provider must
+ * have an intent-filter like the following in the manifest:
+ * <pre class="prettyprint">&lt;provider android:authorities="[PROVIDER AUTHORITY]"
+ android:exported="true"
+ android:enabled="true"
+ android:permission="android.permission.BLUETOOTH_MAP"&gt;
+ * ...
+ * &lt;intent-filter&gt;
+ &lt;action android:name="android.content.action.BLEUETOOT_MAP_PROVIDER" /&gt;
+ &lt;/intent-filter&gt;
+ * ...
+ * &lt;/provider&gt;
+ * [PROVIDER AUTHORITY] shall be the providers authority value which implements this
+ * contract. Only a single authority shall be used. The android.permission.BLUETOOTH_MAP
+ * permission is needed for the provider.
+ */
+public final class BluetoothMapContract {
+ /**
+ * Constructor - should not be used
+ */
+ private BluetoothMapContract(){
+ /* class should not be instantiated */
+ }
+
+ /**
+ * Provider interface that should be used as intent-filter action in the provider section
+ * of the manifest file.
+ */
+ public static final String PROVIDER_INTERFACE = "android.bluetooth.action.BLUETOOTH_MAP_PROVIDER";
+
+ /**
+ * The Bluetooth Message Access profile allows a remote BT-MAP client to trigger
+ * an update of a folder for a specific e-mail account, register for reception
+ * of new messages from the server.
+ *
+ * Additionally the Bluetooth Message Access profile allows a remote BT-MAP client
+ * to push a message to a folder - e.g. outbox or draft. The Bluetooth profile
+ * implementation will place a new message in one of these existing folders through
+ * the content provider.
+ *
+ * ContentProvider.call() is used for these purposes, and the METHOD_UPDATE_FOLDER
+ * method name shall trigger an update of the specified folder for a specified
+ * account.
+ *
+ * This shall be a non blocking call simply starting the update, and the update should
+ * both send and receive messages, depending on what makes sense for the specified
+ * folder.
+ * Bundle extra parameter will carry two INTEGER (long) values:
+ * EXTRA_UPDATE_ACCOUNT_ID containing the account_id
+ * EXTRA_UPDATE_FOLDER_ID containing the folder_id of the folder to update
+ *
+ * The status for send complete of messages shall be reported by updating the sent-flag
+ * and e.g. for outbox messages, move them to the sent folder in the message table of the
+ * content provider and trigger a change notification to any attached content observer.
+ */
+ public static final String METHOD_UPDATE_FOLDER = "UpdateFolder";
+ public static final String EXTRA_UPDATE_ACCOUNT_ID = "UpdateAccountId";
+ public static final String EXTRA_UPDATE_FOLDER_ID = "UpdateFolderId";
+
+ /**
+ * These column names are used as last path segment of the URI (getLastPathSegment()).
+ * Access to a specific row in the tables is done by using the where-clause, hence
+ * support for .../#id if not needed for the Email clients.
+ * The URI format for accessing the tables are as follows:
+ * content://ProviderAuthority/TABLE_ACCOUNT
+ * content://ProviderAuthority/account_id/TABLE_MESSAGE
+ * content://ProviderAuthority/account_id/TABLE_FOLDER
+ */
+
+ /**
+ * Build URI representing the given Accounts data-set in a
+ * bluetooth provider. When queried, the direct URI for the account
+ * with the given accountID is returned.
+ */
+ public static Uri buildAccountUri(String authority) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority).appendPath(TABLE_ACCOUNT).build();
+ }
+ /**
+ * Build URI representing the given Account data-set with specific Id in a
+ * Bluetooth provider. When queried, the direct URI for the account
+ * with the given accountID is returned.
+ */
+ public static Uri buildAccountUriwithId(String authority, String accountId) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority)
+ .appendPath(TABLE_ACCOUNT)
+ .appendPath(accountId)
+ .build();
+ }
+ /**
+ * Build URI representing the entire Message table in a
+ * bluetooth provider.
+ */
+ public static Uri buildMessageUri(String authority) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority)
+ .appendPath(TABLE_MESSAGE)
+ .build();
+ }
+ /**
+ * Build URI representing the given Message data-set in a
+ * bluetooth provider. When queried, the URI for the Messages
+ * with the given accountID is returned.
+ */
+ public static Uri buildMessageUri(String authority, String accountId) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority)
+ .appendPath(accountId)
+ .appendPath(TABLE_MESSAGE)
+ .build();
+ }
+ /**
+ * Build URI representing the given Message data-set with specific messageId in a
+ * bluetooth provider. When queried, the direct URI for the account
+ * with the given accountID is returned.
+ */
+ public static Uri buildMessageUriWithId(String authority, String accountId,String messageId) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority)
+ .appendPath(accountId)
+ .appendPath(TABLE_MESSAGE)
+ .appendPath(messageId)
+ .build();
+ }
+ /**
+ * Build URI representing the given Message data-set in a
+ * bluetooth provider. When queried, the direct URI for the account
+ * with the given accountID is returned.
+ */
+ public static Uri buildFolderUri(String authority, String accountId) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority)
+ .appendPath(accountId)
+ .appendPath(TABLE_FOLDER)
+ .build();
+ }
+
+ /**
+ * @hide
+ */
+ public static final String TABLE_ACCOUNT = "Account";
+ public static final String TABLE_MESSAGE = "Message";
+ public static final String TABLE_FOLDER = "Folder";
+
+ /**
+ * Mandatory folders for the Bluetooth message access profile.
+ * The email client shall at least implement the following root folders.
+ * E.g. as a mapping for them such that the naming will match the underlying
+ * matching folder ID's.
+ */
+ public static final String FOLDER_NAME_INBOX = "inbox";
+ public static final String FOLDER_NAME_OUTBOX = "outbox";
+ public static final String FOLDER_NAME_SENT = "sent";
+ public static final String FOLDER_NAME_DELETED = "deleted";
+ public static final String FOLDER_NAME_DRAFT = "draft";
+
+
+ /**
+ * To push RFC2822 encoded messages into a folder and read RFC2822 encoded messages from
+ * a folder, the openFile() interface will be used as follows:
+ * Open a file descriptor to a message.
+ * Two modes supported for read: With and without attachments.
+ * One mode exist for write and the actual content will be with or without
+ * attachments.
+ *
+ * mode will be "r" for read and "w" for write, never "rw".
+ *
+ * URI format:
+ * The URI scheme is as follows.
+ * For reading messages with attachments:
+ * content://ProviderAuthority/account_id/TABLE_MESSAGE/msgId
+ * Note: This shall be an offline operation, including only message parts and attachments
+ * already downloaded to the device.
+ *
+ * For reading messages without attachments:
+ * content://ProviderAuthority/account_id/TABLE_MESSAGE/msgId/FILE_MSG_NO_ATTACHMENTS
+ * Note: This shall be an offline operation, including only message parts already
+ * downloaded to the device.
+ *
+ * For downloading and reading messages with attachments:
+ * content://ProviderAuthority/account_id/TABLE_MESSAGE/msgId/FILE_MSG_DOWNLOAD
+ * Note: This shall download the message content and all attachments if possible,
+ * else throw an IOException.
+ *
+ * For downloading and reading messages without attachments:
+ * content://ProviderAuthority/account_id/TABLE_MESSAGE/msgId/FILE_MSG_DOWNLOAD_NO_ATTACHMENTS
+ * Note: This shall download the message content if possible, else throw an IOException.
+ *
+ * When reading from the file descriptor, the content provider shall return a stream
+ * of bytes containing a RFC2822 encoded message, as if the message was send to an email
+ * server.
+ *
+ * When a byte stream is written to the file descriptor, the content provider shall
+ * decode the RFC2822 encoded data and insert the message into the TABLE_MESSAGE at the ID
+ * supplied in URI - additionally the message content shall be stored in the underlying
+ * data base structure as if the message was received from an email server. The Message ID
+ * will be created using a insert on the TABLE_MESSAGE prior to calling openFile().
+ * Hence the procedure for inserting a message is:
+ * - uri/msgId = insert(uri, value: folderId=xxx)
+ * - fd = openFile(uri/msgId)
+ * - fd.write (RFC2822 encoded data)
+ *
+ * The Bluetooth Message Access Client might not know what to put into the From:
+ * header nor have a valid time stamp, hence the content provider shall check
+ * if the From: and Date: headers have been set by the message written, else
+ * it must fill in appropriate values.
+ */
+ public static final String FILE_MSG_NO_ATTACHMENTS = "NO_ATTACHMENTS";
+ public static final String FILE_MSG_DOWNLOAD = "DOWNLOAD";
+ public static final String FILE_MSG_DOWNLOAD_NO_ATTACHMENTS = "DOWNLOAD_NO_ATTACHMENTS";
+
+ /**
+ * Account Table
+ * The columns needed to supply account information.
+ * The e-mail client app may choose to expose all e-mails as being from the same account,
+ * but it is not recommended, as this would be a violation of the Bluetooth specification.
+ * The Bluetooth Message Access settings activity will provide the user the ability to
+ * change the FLAG_EXPOSE values for each account in this table.
+ * The Bluetooth Message Access service will read the values when Bluetooth is turned on,
+ * and again on every notified change through the content observer interface.
+ */
+ public interface AccountColumns {
+
+ /**
+ * The unique ID for a row.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String _ID = "_id";
+
+ /**
+ * The account name to display to the user on the device when selecting whether
+ * or not to share the account over Bluetooth.
+ *
+ * The account display name should not reveal any sensitive information e.g. email-
+ * address, as it will be added to the Bluetooth SDP record, which can be read by
+ * any Bluetooth enabled device. (Access to any account content is only provided to
+ * authenticated devices). It is recommended that if the email client uses the email
+ * address as account name, then the address should be obfuscated (i.e. replace "@"
+ * with ".")
+ * <P>Type: TEXT</P>
+ * read-only
+ */
+ public static final String ACCOUNT_DISPLAY_NAME = "account_display_name";
+
+ /**
+ * Expose this account to other authenticated Bluetooth devices. If the expose flag
+ * is set, this account will be listed as an available account to access from another
+ * Bluetooth device.
+ *
+ * This is a read/write flag, that can be set either from within the E-mail client
+ * UI or the Bluetooth settings menu.
+ *
+ * It is recommended to either ask the user whether to expose the account, or set this
+ * to "show" as default.
+ *
+ * This setting shall not be used to enforce whether or not an account should be shared
+ * or not if the account is bound by an administrative security policy. In this case
+ * the email app should not list the account at all if it is not to be shareable over BT.
+ *
+ * <P>Type: INTEGER (boolean) hide = 0, show = 1</P>
+ */
+ public static final String FLAG_EXPOSE = "flag_expose";
+
+ }
+
+ /**
+ * The actual message table containing all messages.
+ * Content that must support filtering using WHERE clauses:
+ * - To, From, Cc, Bcc, Date, ReadFlag, PriorityFlag, folder_id, account_id
+ * Additional content that must be supplied:
+ * - Subject, AttachmentFlag, LoadedState, MessageSize, AttachmentSize
+ * Content that must support update:
+ * - FLAG_READ and FOLDER_ID (FOLDER_ID is used to move a message to deleted)
+ * Additional insert of a new message with the following values shall be supported:
+ * - FOLDER_ID
+ *
+ * When doing an insert on this table, the actual content of the message (subject,
+ * date etc) written through file-i/o takes precedence over the inserted values and should
+ * overwrite them.
+ */
+ public interface MessageColumns {
+
+ /**
+ * The unique ID for a row.
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String _ID = "_id";
+
+ /**
+ * The date the message was received as a unix timestamp
+ * (miliseconds since 00:00:00 UTC 1/1-1970).
+ *
+ * <P>Type: INTEGER (long)</P>
+ * read-only
+ */
+ public static final String DATE = "date";
+
+ /**
+ * Message subject.
+ * <P>Type: TEXT</P>
+ * read-only.
+ */
+ public static final String SUBJECT = "subject";
+
+ /**
+ * Message Read flag
+ * <P>Type: INTEGER (boolean) unread = 0, read = 1</P>
+ * read/write
+ */
+ public static final String FLAG_READ = "flag_read";
+
+ /**
+ * Message Priority flag
+ * <P>Type: INTEGER (boolean) normal priority = 0, high priority = 1</P>
+ * read-only
+ */
+ public static final String FLAG_HIGH_PRIORITY = "high_priority";
+
+ /**
+ * Reception state - the amount of the message that have been loaded from the server.
+ * <P>Type: INTEGER see RECEPTION_STATE_ constants below </P>
+ * read-only
+ */
+ public static final String RECEPTION_STATE = "reception_state";
+
+ /** To be able to filter messages with attachments, we need this flag.
+ * <P>Type: INTEGER (boolean) no attachment = 0, attachment = 1 </P>
+ * read-only
+ */
+ public static final String FLAG_ATTACHMENT = "flag_attachment";
+
+ /** The overall size in bytes of the attachments of the message.
+ * <P>Type: INTEGER </P>
+ */
+ public static final String ATTACHMENT_SIZE = "attachment_size";
+
+ /** The overall size in bytes of the message including any attachments.
+ * This value is informative only and should be the size an email client
+ * would display as size for the message.
+ * <P>Type: INTEGER </P>
+ * read-only
+ */
+ public static final String MESSAGE_SIZE = "message_size";
+
+ /** Indicates that the message or a part of it is protected by a DRM scheme.
+ * <P>Type: INTEGER (boolean) no DRM = 0, DRM protected = 1 </P>
+ * read-only
+ */
+ public static final String FLAG_PROTECTED = "flag_protected";
+
+ /**
+ * A comma-delimited list of FROM addresses in RFC2822 format.
+ * The list must be compatible with Rfc822Tokenizer.tokenize();
+ * <P>Type: TEXT</P>
+ * read-only
+ */
+ public static final String FROM_LIST = "from_list";
+
+ /**
+ * A comma-delimited list of TO addresses in RFC2822 format.
+ * The list must be compatible with Rfc822Tokenizer.tokenize();
+ * <P>Type: TEXT</P>
+ * read-only
+ */
+ public static final String TO_LIST = "to_list";
+
+ /**
+ * A comma-delimited list of CC addresses in RFC2822 format.
+ * The list must be compatible with Rfc822Tokenizer.tokenize();
+ * <P>Type: TEXT</P>
+ * read-only
+ */
+ public static final String CC_LIST = "cc_list";
+
+ /**
+ * A comma-delimited list of BCC addresses in RFC2822 format.
+ * The list must be compatible with Rfc822Tokenizer.tokenize();
+ * <P>Type: TEXT</P>
+ * read-only
+ */
+ public static final String BCC_LIST = "bcc_list";
+
+ /**
+ * A comma-delimited list of REPLY-TO addresses in RFC2822 format.
+ * The list must be compatible with Rfc822Tokenizer.tokenize();
+ * <P>Type: TEXT</P>
+ * read-only
+ */
+ public static final String REPLY_TO_LIST = "reply_to_List";
+
+ /**
+ * The unique ID for a row in the folder table in which this message belongs.
+ * <P>Type: INTEGER (long)</P>
+ * read/write
+ */
+ public static final String FOLDER_ID = "folder_id";
+
+ /**
+ * The unique ID for a row in the account table which owns this message.
+ * <P>Type: INTEGER (long)</P>
+ * read-only
+ */
+ public static final String ACCOUNT_ID = "account_id";
+
+ /**
+ * The ID identify the thread a message belongs to. If no thread id is available,
+ * set value to "-1"
+ * <P>Type: INTEGER (long)</P>
+ * read-only
+ */
+ public static final String THREAD_ID = "thread_id";
+ }
+
+ /**
+ * Indicates that the message, including any attachments, has been received from the
+ * server to the device.
+ */
+ public static final String RECEPTION_STATE_COMPLETE = "complete";
+ /**
+ * Indicates the message is partially received from the email server.
+ */
+ public static final String RECEPTION_STATE_FRACTIONED = "fractioned";
+ /**
+ * Indicates that only a notification about the message have been received.
+ */
+ public static final String RECEPTION_STATE_NOTIFICATION = "notification";
+
+ /**
+ * Message folder structure
+ * MAP enforces use of a folder structure with mandatory folders:
+ * - inbox, outbox, sent, deleted, draft
+ * User defined folders are supported.
+ * The folder table must provide filtering (use of WHERE clauses) of the following entries:
+ * - account_id (linking the folder to an e-mail account)
+ * - parent_id (linking the folders individually)
+ * The folder table must have a folder name for each entry, and the mandatory folders
+ * MUST exist for each account_id. The folders may be empty.
+ * Use the FOLDER_NAME_xxx constants for the mandatory folders. Their names must
+ * not be translated into other languages, as the folder browsing is string based, and
+ * many Bluetooth Message Clients will use these strings to navigate to the folders.
+ */
+ public interface FolderColumns {
+
+ /**
+ * The unique ID for a row.
+ * <P>Type: INTEGER (long)</P>
+ * read-only
+ */
+ public static final String _ID = "_id";
+
+ /**
+ * The folder display name to present to the user.
+ * <P>Type: TEXT</P>
+ * read-only
+ */
+ public static final String NAME = "name";
+
+ /**
+ * The _id-key to the account this folder refers to.
+ * <P>Type: INTEGER (long)</P>
+ * read-only
+ */
+ public static final String ACCOUNT_ID = "account_id";
+
+ /**
+ * The _id-key to the parent folder. -1 for root folders.
+ * <P>Type: INTEGER (long)</P>
+ * read-only
+ */
+ public static final String PARENT_FOLDER_ID = "parent_id";
+ }
+ /**
+ * A projection of all the columns in the Message table
+ */
+ public static final String[] BT_MESSAGE_PROJECTION = new String[] {
+ MessageColumns._ID,
+ MessageColumns.DATE,
+ MessageColumns.SUBJECT,
+ MessageColumns.FLAG_READ,
+ MessageColumns.FLAG_ATTACHMENT,
+ MessageColumns.FOLDER_ID,
+ MessageColumns.ACCOUNT_ID,
+ MessageColumns.FROM_LIST,
+ MessageColumns.TO_LIST,
+ MessageColumns.CC_LIST,
+ MessageColumns.BCC_LIST,
+ MessageColumns.REPLY_TO_LIST,
+ MessageColumns.FLAG_PROTECTED,
+ MessageColumns.FLAG_HIGH_PRIORITY,
+ MessageColumns.MESSAGE_SIZE,
+ MessageColumns.ATTACHMENT_SIZE,
+ MessageColumns.RECEPTION_STATE,
+ MessageColumns.THREAD_ID
+ };
+
+ /**
+ * A projection of all the columns in the Account table
+ */
+ public static final String[] BT_ACCOUNT_PROJECTION = new String[] {
+ AccountColumns._ID,
+ AccountColumns.ACCOUNT_DISPLAY_NAME,
+ AccountColumns.FLAG_EXPOSE,
+ };
+
+ /**
+ * A projection of all the columns in the Folder table
+ */
+ public static final String[] BT_FOLDER_PROJECTION = new String[] {
+ FolderColumns._ID,
+ FolderColumns.NAME,
+ FolderColumns.ACCOUNT_ID,
+ FolderColumns.PARENT_FOLDER_ID
+ };
+
+
+}
diff --git a/lib/mapapi/BluetoothMapEmailProvider.java b/lib/mapapi/BluetoothMapEmailProvider.java
new file mode 100644
index 000000000..d95148f07
--- /dev/null
+++ b/lib/mapapi/BluetoothMapEmailProvider.java
@@ -0,0 +1,696 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.mapapi;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * A base implementation of the BluetoothMapEmailContract.
+ * A base class for a ContentProvider that allows access to Email messages from a Bluetooth
+ * device through the Message Access Profile.
+ */
+public abstract class BluetoothMapEmailProvider extends ContentProvider {
+
+ private static final String TAG = "BluetoothMapEmailProvider";
+ private static final boolean D = true;
+
+ private static final int MATCH_ACCOUNT = 1;
+ private static final int MATCH_MESSAGE = 2;
+ private static final int MATCH_FOLDER = 3;
+
+ protected ContentResolver mResolver;
+
+ private Uri CONTENT_URI = null;
+ private String mAuthority;
+ private UriMatcher mMatcher;
+
+
+ private PipeReader mPipeReader = new PipeReader();
+ private PipeWriter mPipeWriter = new PipeWriter();
+
+ /**
+ * Write the content of a message to a stream as MIME encoded RFC-2822 data.
+ * @param accountId the ID of the account to which the message belong
+ * @param messageId the ID of the message to write to the stream
+ * @param includeAttachment true if attachments should be included
+ * @param download true if any missing part of the message shall be downloaded
+ * before written to the stream. The download flag will determine
+ * whether or not attachments shall be downloaded or only the message content.
+ * @param out the FileOurputStream to write to.
+ * @throws IOException
+ */
+ abstract protected void WriteMessageToStream(long accountId, long messageId,
+ boolean includeAttachment, boolean download, FileOutputStream out)
+ throws IOException;
+
+ /**
+ * @return the CONTENT_URI exposed. This will be used to send out notifications.
+ */
+ abstract protected Uri getContentUri();
+
+ /**
+ * Implementation is provided by the parent class.
+ */
+ @Override
+ public void attachInfo(Context context, ProviderInfo info) {
+ mAuthority = info.authority;
+
+ mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ mMatcher.addURI(mAuthority, BluetoothMapContract.TABLE_ACCOUNT, MATCH_ACCOUNT);
+ mMatcher.addURI(mAuthority, "#/"+BluetoothMapContract.TABLE_FOLDER, MATCH_FOLDER);
+ mMatcher.addURI(mAuthority, "#/"+BluetoothMapContract.TABLE_MESSAGE, MATCH_MESSAGE);
+
+ // Sanity check our setup
+ if (!info.exported) {
+ throw new SecurityException("Provider must be exported");
+ }
+ // Enforce correct permissions are used
+ if (!android.Manifest.permission.BLUETOOTH_MAP.equals(info.writePermission)){
+ throw new SecurityException("Provider must be protected by " +
+ android.Manifest.permission.BLUETOOTH_MAP);
+ }
+ mResolver = context.getContentResolver();
+ super.attachInfo(context, info);
+ }
+
+
+ /**
+ * Interface to write a stream of data to a pipe. Use with
+ * {@link ContentProvider#openPipeHelper}.
+ */
+ public interface PipeDataReader<T> {
+ /**
+ * Called from a background thread to stream data from a pipe.
+ * Note that the pipe is blocking, so this thread can block on
+ * reads for an arbitrary amount of time if the client is slow
+ * at writing.
+ *
+ * @param input The pipe where data should be read. This will be
+ * closed for you upon returning from this function.
+ * @param uri The URI whose data is to be written.
+ * @param mimeType The desired type of data to be written.
+ * @param opts Options supplied by caller.
+ * @param args Your own custom arguments.
+ */
+ public void readDataFromPipe(ParcelFileDescriptor input, Uri uri, String mimeType,
+ Bundle opts, T args);
+ }
+
+ public class PipeReader implements PipeDataReader<Cursor> {
+ /**
+ * Read the data from the pipe and generate a message.
+ * Use the message to do an update of the message specified by the URI.
+ */
+ @Override
+ public void readDataFromPipe(ParcelFileDescriptor input, Uri uri,
+ String mimeType, Bundle opts, Cursor args) {
+ Log.v(TAG, "readDataFromPipe(): uri=" + uri.toString());
+ FileInputStream fIn = null;
+ try {
+ fIn = new FileInputStream(input.getFileDescriptor());
+ long messageId = Long.valueOf(uri.getLastPathSegment());
+ long accountId = Long.valueOf(getAccountId(uri));
+ UpdateMimeMessageFromStream(fIn, accountId, messageId);
+ } catch (IOException e) {
+ Log.w(TAG,"IOException: ", e);
+ /* TODO: How to signal the error to the calling entity? Had expected readDataFromPipe
+ * to throw IOException?
+ */
+ } finally {
+ try {
+ if(fIn != null)
+ fIn.close();
+ } catch (IOException e) {
+ Log.w(TAG,e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Read a MIME encoded RFC-2822 fileStream and update the message content.
+ * The Date and/or From headers may not be present in the MIME encoded
+ * message, and this function shall add appropriate values if the headers
+ * are missing. From should be set to the owner of the account.
+ *
+ * @param input the file stream to read data from
+ * @param accountId the accountId
+ * @param messageId ID of the message to update
+ */
+ abstract protected void UpdateMimeMessageFromStream(FileInputStream input, long accountId,
+ long messageId) throws IOException;
+
+ public class PipeWriter implements PipeDataWriter<Cursor> {
+ /**
+ * Generate a message based on the cursor, and write the encoded data to the stream.
+ */
+
+ public void writeDataToPipe(ParcelFileDescriptor output, Uri uri, String mimeType,
+ Bundle opts, Cursor c) {
+ if (D) Log.d(TAG, "writeDataToPipe(): uri=" + uri.toString() +
+ " - getLastPathSegment() = " + uri.getLastPathSegment());
+
+ FileOutputStream fout = null;
+
+ try {
+ fout = new FileOutputStream(output.getFileDescriptor());
+
+ boolean includeAttachments = true;
+ boolean download = false;
+ List<String> segments = uri.getPathSegments();
+ long messageId = Long.parseLong(segments.get(2));
+ long accountId = Long.parseLong(getAccountId(uri));
+ if(segments.size() >= 4) {
+ String format = segments.get(3);
+ if(format.equalsIgnoreCase(BluetoothMapContract.FILE_MSG_NO_ATTACHMENTS)) {
+ includeAttachments = false;
+ } else if(format.equalsIgnoreCase(BluetoothMapContract.FILE_MSG_DOWNLOAD_NO_ATTACHMENTS)) {
+ includeAttachments = false;
+ download = true;
+ } else if(format.equalsIgnoreCase(BluetoothMapContract.FILE_MSG_DOWNLOAD)) {
+ download = true;
+ }
+ }
+
+ WriteMessageToStream(accountId, messageId, includeAttachments, download, fout);
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ /* TODO: How to signal the error to the calling entity? Had expected writeDataToPipe
+ * to throw IOException?
+ */
+ } finally {
+ try {
+ fout.flush();
+ } catch (IOException e) {
+ Log.w(TAG, "IOException: ", e);
+ }
+ try {
+ fout.close();
+ } catch (IOException e) {
+ Log.w(TAG, "IOException: ", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * This function shall be called when any Account database content have changed
+ * to Notify any attached observers.
+ * @param accountId the ID of the account that changed. Null is a valid value,
+ * if accountId is unknown or multiple accounts changed.
+ */
+ protected void onAccountChanged(String accountId) {
+ Uri newUri = null;
+
+ if(mAuthority == null){
+ return;
+ }
+ if(accountId == null){
+ newUri = BluetoothMapContract.buildAccountUri(mAuthority);
+ } else {
+ newUri = BluetoothMapContract.buildAccountUriwithId(mAuthority, accountId);
+ }
+ if(D) Log.d(TAG,"onAccountChanged() accountId = " + accountId + " URI: " + newUri);
+ mResolver.notifyChange(newUri, null);
+ }
+
+ /**
+ * This function shall be called when any Message database content have changed
+ * to notify any attached observers.
+ * @param accountId Null is a valid value, if accountId is unknown, but
+ * recommended for increased performance.
+ * @param messageId Null is a valid value, if multiple messages changed or the
+ * messageId is unknown, but recommended for increased performance.
+ */
+ protected void onMessageChanged(String accountId, String messageId) {
+ Uri newUri = null;
+
+ if(mAuthority == null){
+ return;
+ }
+
+ if(accountId == null){
+ newUri = BluetoothMapContract.buildMessageUri(mAuthority);
+ } else {
+ if(messageId == null)
+ {
+ newUri = BluetoothMapContract.buildMessageUri(mAuthority,accountId);
+ } else {
+ newUri = BluetoothMapContract.buildMessageUriWithId(mAuthority,accountId, messageId);
+ }
+ }
+ if(D) Log.d(TAG,"onMessageChanged() accountId = " + accountId + " messageId = " + messageId + " URI: " + newUri);
+ mResolver.notifyChange(newUri, null);
+ }
+
+ /**
+ * Not used, this is just a dummy implementation.
+ */
+ @Override
+ public String getType(Uri uri) {
+ return "Email";
+ }
+
+ /**
+ * Open a file descriptor to a message.
+ * Two modes supported for read: With and without attachments.
+ * One mode exist for write and the actual content will be with or without
+ * attachments.
+ *
+ * Mode will be "r" or "w".
+ *
+ * URI format:
+ * The URI scheme is as follows.
+ * For messages with attachments:
+ * content://com.android.mail.bluetoothprovider/Messages/msgId#
+ *
+ * For messages without attachments:
+ * content://com.android.mail.bluetoothprovider/Messages/msgId#/NO_ATTACHMENTS
+ *
+ * UPDATE: For write.
+ * First create a message in the DB using insert into the message DB
+ * Then open a file handle to the #id
+ * write the data to a stream created from the fileHandle.
+ *
+ * @param uri the URI to open. ../Messages/#id
+ * @param mode the mode to use. The following modes exist: - UPDATE do not work - use URI
+ * - "read_with_attachments" - to read an e-mail including any attachments
+ * - "read_no_attachments" - to read an e-mail excluding any attachments
+ * - "write" - to add a mime encoded message to the database. This write
+ * should not trigger the message to be send.
+ * @return the ParcelFileDescriptor
+ * @throws FileNotFoundException
+ */
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+ long callingId = Binder.clearCallingIdentity();
+ if(D)Log.d(TAG, "openFile(): uri=" + uri.toString() + " - getLastPathSegment() = " +
+ uri.getLastPathSegment());
+ try {
+ /* To be able to do abstraction of the file IO, we simply ignore the URI at this
+ * point and let the read/write function implementations parse the URI. */
+ if(mode.equals("w")) {
+ return openInversePipeHelper(uri, null, null, null, mPipeReader);
+ } else {
+ return openPipeHelper (uri, null, null, null, mPipeWriter);
+ }
+ } catch (IOException e) {
+ Log.w(TAG,e);
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ return null;
+ }
+
+ /**
+ * A helper function for implementing {@link #openFile}, for
+ * creating a data pipe and background thread allowing you to stream
+ * data back from the client. This function returns a new
+ * ParcelFileDescriptor that should be returned to the caller (the caller
+ * is responsible for closing it).
+ *
+ * @param uri The URI whose data is to be written.
+ * @param mimeType The desired type of data to be written.
+ * @param opts Options supplied by caller.
+ * @param args Your own custom arguments.
+ * @param func Interface implementing the function that will actually
+ * stream the data.
+ * @return Returns a new ParcelFileDescriptor holding the read side of
+ * the pipe. This should be returned to the caller for reading; the caller
+ * is responsible for closing it when done.
+ */
+ private <T> ParcelFileDescriptor openInversePipeHelper(final Uri uri, final String mimeType,
+ final Bundle opts, final T args, final PipeDataReader<T> func)
+ throws FileNotFoundException {
+ try {
+ final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
+
+ AsyncTask<Object, Object, Object> task = new AsyncTask<Object, Object, Object>() {
+ @Override
+ protected Object doInBackground(Object... params) {
+ func.readDataFromPipe(fds[0], uri, mimeType, opts, args);
+ try {
+ fds[0].close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failure closing pipe", e);
+ }
+ return null;
+ }
+ };
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Object[])null);
+
+ return fds[1];
+ } catch (IOException e) {
+ throw new FileNotFoundException("failure making pipe");
+ }
+ }
+
+ /**
+ * The MAP specification states that a delete request from MAP client is a folder shift to the
+ * 'deleted' folder.
+ * Only use case of delete() is when transparency is requested for push messages, then
+ * message should not remain in sent folder and therefore must be deleted
+ */
+ @Override
+ public int delete(Uri uri, String where, String[] selectionArgs) {
+ if (D) Log.d(TAG, "delete(): uri=" + uri.toString() );
+ int result = 0;
+
+ String table = uri.getPathSegments().get(1);
+ if(table == null)
+ throw new IllegalArgumentException("Table missing in URI");
+ // the id of the entry to be deleted from the database
+ String messageId = uri.getLastPathSegment();
+ if (messageId == null)
+ throw new IllegalArgumentException("Message ID missing in update values!");
+
+
+ String accountId = getAccountId(uri);
+ if (accountId == null)
+ throw new IllegalArgumentException("Account ID missing in update values!");
+
+ long callingId = Binder.clearCallingIdentity();
+ try {
+ if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) {
+ return deleteMessage(accountId, messageId);
+ } else {
+ if (D) Log.w(TAG, "Unknown table name: " + table);
+ return result;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ }
+
+ /**
+ * This function deletes a message.
+ * @param accountId the ID of the Account
+ * @param messageId the ID of the message to delete.
+ * @return the number of messages deleted - 0 if the message was not found.
+ */
+ abstract protected int deleteMessage(String accountId, String messageId);
+
+ /**
+ * Insert is used to add new messages to the data base.
+ * Insert message approach:
+ * - Insert an empty message to get an _id with only a folder_id
+ * - Open the _id for write
+ * - Write the message content
+ * (When the writer completes, this provider should do an update of the message)
+ */
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ String table = uri.getLastPathSegment();
+ if(table == null){
+ throw new IllegalArgumentException("Table missing in URI");
+ }
+ String accountId = getAccountId(uri);
+ Long folderId = values.getAsLong(BluetoothMapContract.MessageColumns.FOLDER_ID);
+ if(folderId == null) {
+ throw new IllegalArgumentException("FolderId missing in ContentValues");
+ }
+
+ String id; // the id of the entry inserted into the database
+ long callingId = Binder.clearCallingIdentity();
+ Log.d(TAG, "insert(): uri=" + uri.toString() + " - getLastPathSegment() = " +
+ uri.getLastPathSegment());
+ try {
+ if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) {
+ id = insertMessage(accountId, folderId.toString());
+ if(D) Log.i(TAG, "insert() ID: " + id);
+ return Uri.parse(uri.toString() + "/" + id);
+ } else {
+ Log.w(TAG, "Unknown table name: " + table);
+ return null;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ }
+
+
+ /**
+ * Inserts an empty message into the Message data base in the specified folder.
+ * This is done before the actual message content is written by fileIO.
+ * @param accountId the ID of the account
+ * @param folderId the ID of the folder to create a new message in.
+ * @return the message id as a string
+ */
+ abstract protected String insertMessage(String accountId, String folderId);
+
+ /**
+ * Utility function to build a projection based on a projectionMap.
+ *
+ * "btColumnName" -> "emailColumnName as btColumnName" for each entry.
+ *
+ * This supports SQL statements in the emailColumnName entry.
+ * @param projection
+ * @param projectionMap <btColumnName, emailColumnName>
+ * @return the converted projection
+ */
+ protected String[] convertProjection(String[] projection, Map<String,String> projectionMap) {
+ String[] newProjection = new String[projection.length];
+ for(int i = 0; i < projection.length; i++) {
+ newProjection[i] = projectionMap.get(projection[i]) + " as " + projection[i];
+ }
+ return newProjection;
+ }
+
+ /**
+ * This query needs to map from the data used in the e-mail client to BluetoothMapContract type of data.
+ */
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ long callingId = Binder.clearCallingIdentity();
+ try {
+ String accountId = null;
+ switch (mMatcher.match(uri)) {
+ case MATCH_ACCOUNT:
+ return queryAccount(projection, selection, selectionArgs, sortOrder);
+ case MATCH_FOLDER:
+ accountId = getAccountId(uri);
+ return queryFolder(accountId, projection, selection, selectionArgs, sortOrder);
+ case MATCH_MESSAGE:
+ accountId = getAccountId(uri);
+ return queryMessage(accountId, projection, selection, selectionArgs, sortOrder);
+ default:
+ throw new UnsupportedOperationException("Unsupported Uri " + uri);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ }
+
+ /**
+ * Query account information.
+ * This function shall return only exposable e-mail accounts. Hence shall not
+ * return accounts that has policies suggesting not to be shared them.
+ * @param projection
+ * @param selection
+ * @param selectionArgs
+ * @param sortOrder
+ * @return a cursor to the accounts that are subject to exposure over BT.
+ */
+ abstract protected Cursor queryAccount(String[] projection, String selection, String[] selectionArgs,
+ String sortOrder);
+
+ /**
+ * Filter out the non usable folders and ensure to name the mandatory folders
+ * inbox, outbox, sent, deleted and draft.
+ * @param accountId
+ * @param projection
+ * @param selection
+ * @param selectionArgs
+ * @param sortOrder
+ * @return
+ */
+ abstract protected Cursor queryFolder(String accountId, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder);
+ /**
+ * For the message table the selection (where clause) can only include the following columns:
+ * date: less than, greater than and equals
+ * flagRead: = 1 or = 0
+ * flagPriority: = 1 or = 0
+ * folder_id: the ID of the folder only equals
+ * toList: partial name/address search
+ * ccList: partial name/address search
+ * bccList: partial name/address search
+ * fromList: partial name/address search
+ * Additionally the COUNT and OFFSET shall be supported.
+ * @param accountId the ID of the account
+ * @param projection
+ * @param selection
+ * @param selectionArgs
+ * @param sortOrder
+ * @return a cursor to query result
+ */
+ abstract protected Cursor queryMessage(String accountId, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder);
+
+ /**
+ * update()
+ * Messages can be modified in the following cases:
+ * - the folder_key of a message - hence the message can be moved to a new folder,
+ * but the content cannot be modified.
+ * - the FLAG_READ state can be changed.
+ * The selection statement will always be selection of a message ID, when updating a message,
+ * hence this function will be called multiple times if multiple messages must be updated
+ * due to the nature of the Bluetooth Message Access profile.
+ */
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+
+ String table = uri.getLastPathSegment();
+ if(table == null){
+ throw new IllegalArgumentException("Table missing in URI");
+ }
+ if(selection != null) {
+ throw new IllegalArgumentException("selection shall not be used, ContentValues shall contain the data");
+ }
+
+ long callingId = Binder.clearCallingIdentity();
+ if(D)Log.w(TAG, "update(): uri=" + uri.toString() + " - getLastPathSegment() = " +
+ uri.getLastPathSegment());
+ try {
+ if(table.equals(BluetoothMapContract.TABLE_ACCOUNT)) {
+ String accountId = values.getAsString(BluetoothMapContract.AccountColumns._ID);
+ if(accountId == null) {
+ throw new IllegalArgumentException("Account ID missing in update values!");
+ }
+ Integer exposeFlag = values.getAsInteger(BluetoothMapContract.AccountColumns.FLAG_EXPOSE);
+ if(exposeFlag == null){
+ throw new IllegalArgumentException("Expose flag missing in update values!");
+ }
+ return updateAccount(accountId, exposeFlag);
+ } else if(table.equals(BluetoothMapContract.TABLE_FOLDER)) {
+ return 0; // We do not support changing folders
+ } else if(table.equals(BluetoothMapContract.TABLE_MESSAGE)) {
+ String accountId = getAccountId(uri);
+ Long messageId = values.getAsLong(BluetoothMapContract.MessageColumns._ID);
+ if(messageId == null) {
+ throw new IllegalArgumentException("Message ID missing in update values!");
+ }
+ Long folderId = values.getAsLong(BluetoothMapContract.MessageColumns.FOLDER_ID);
+ Boolean flagRead = values.getAsBoolean(BluetoothMapContract.MessageColumns.FLAG_READ);
+ return updateMessage(accountId, messageId, folderId, flagRead);
+ } else {
+ if(D)Log.w(TAG, "Unknown table name: " + table);
+ return 0;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ }
+
+ /**
+ * Update an entry in the account table. Only the expose flag will be
+ * changed through this interface.
+ * @param accountId the ID of the account to change.
+ * @param flagExpose the updated value.
+ * @return the number of entries changed - 0 if account not found or value cannot be changed.
+ */
+ abstract protected int updateAccount(String accountId, int flagExpose);
+
+ /**
+ * Update an entry in the message table.
+ * @param accountId ID of the account to which the messageId relates
+ * @param messageId the ID of the message to update
+ * @param folderId the new folder ID value to set - ignore if null.
+ * @param flagRead the new flagRead value to set - ignore if null.
+ * @return
+ */
+ abstract protected int updateMessage(String accountId, Long messageId, Long folderId, Boolean flagRead);
+
+
+ @Override
+ public Bundle call(String method, String arg, Bundle extras) {
+ long callingId = Binder.clearCallingIdentity();
+ if(D)Log.d(TAG, "call(): method=" + method + " arg=" + arg + "ThreadId: " + Thread.currentThread().getId());
+
+ try {
+ if(method.equals(BluetoothMapContract.METHOD_UPDATE_FOLDER)) {
+ long accountId = extras.getLong(BluetoothMapContract.EXTRA_UPDATE_ACCOUNT_ID, -1);
+ if(accountId == -1) {
+ Log.w(TAG, "No account ID in CALL");
+ return null;
+ }
+ long folderId = extras.getLong(BluetoothMapContract.EXTRA_UPDATE_FOLDER_ID, -1);
+ if(folderId == -1) {
+ Log.w(TAG, "No folder ID in CALL");
+ return null;
+ }
+ int ret = syncFolder(accountId, folderId);
+ if(ret == 0) {
+ return new Bundle();
+ }
+ return null;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ return null;
+ }
+
+ /**
+ * Trigger a sync of the specified folder.
+ * @param accountId the ID of the account that owns the folder
+ * @param folderId the ID of the folder.
+ * @return 0 at success
+ */
+ abstract protected int syncFolder(long accountId, long folderId);
+
+ /**
+ * Need this to suppress warning in unit tests.
+ */
+ @Override
+ public void shutdown() {
+ // Don't call super.shutdown(), which emits a warning...
+ }
+
+ /**
+ * Extract the BluetoothMapContract.AccountColumns._ID from the given URI.
+ */
+ public static String getAccountId(Uri uri) {
+ final List<String> segments = uri.getPathSegments();
+ if (segments.size() < 1) {
+ throw new IllegalArgumentException("No AccountId pressent in URI: " + uri);
+ }
+ return segments.get(0);
+ }
+}
diff --git a/res/layout/bluetooth_map_email_settings.xml b/res/layout/bluetooth_map_email_settings.xml
new file mode 100644
index 000000000..02acdd8cb
--- /dev/null
+++ b/res/layout/bluetooth_map_email_settings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright (C) 2014 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.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/bluetooth_map_email_settings_liniar_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context=".BluetoothMapEmailSettings" >
+
+ <TextView
+ android:id="@+id/bluetooth_map_email_settings_description_text_view"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+
+ android:text="@string/bluetooth_map_email_settings_intro" />
+
+ <ExpandableListView
+ android:id="@+id/bluetooth_map_email_settings_list_view"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="3"
+ android:orientation="vertical" >
+
+ </ExpandableListView>
+
+</LinearLayout>
diff --git a/res/layout/bluetooth_map_email_settings_account_group.xml b/res/layout/bluetooth_map_email_settings_account_group.xml
new file mode 100644
index 000000000..abb8de801
--- /dev/null
+++ b/res/layout/bluetooth_map_email_settings_account_group.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright (C) 2014 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.
+*/
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+ <ImageView
+ android:id="@+id/bluetooth_map_email_settings_group_icon"
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:maxWidth="64dp"
+ android:maxHeight="64dp"
+ android:layout_marginLeft="30dp"
+ />
+ <TextView
+ android:id ="@+id/bluetooth_map_email_settings_group_text_view"
+ android:layout_width ="wrap_content"
+ android:layout_height ="wrap_content"
+ android:layout_toRightOf="@id/bluetooth_map_email_settings_group_icon"/>
+ <CheckBox
+ android:id ="@+id/bluetooth_map_email_settings_group_checkbox"
+ android:layout_width ="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:focusable="false"/>
+
+
+
+</RelativeLayout> \ No newline at end of file
diff --git a/res/layout/bluetooth_map_email_settings_account_item.xml b/res/layout/bluetooth_map_email_settings_account_item.xml
new file mode 100644
index 000000000..f0469d5b3
--- /dev/null
+++ b/res/layout/bluetooth_map_email_settings_account_item.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright (C) 2014 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.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="51dp"
+ android:clickable="true"
+ android:gravity="fill_vertical"
+ android:orientation="horizontal"
+ android:paddingLeft="51dp"
+ android:paddingRight="25dp"
+ tools:context=".BluetoothMapEmailSettings" android:baselineAligned="false">
+
+ <TextView
+ android:id="@+id/bluetooth_map_email_settings_item_text_view"
+ android:layout_width="match_parent"
+ android:layout_height="51dp"
+ android:layout_weight="1"
+ android:gravity="left|center_vertical"/>
+
+ <CheckBox
+ android:id="@+id/bluetooth_map_email_settings_item_check"
+ android:layout_width="110dp"
+ android:layout_height="51dp"
+ android:layout_gravity="right"
+ android:layout_weight="2"
+ android:gravity="right|center_vertical" />
+</LinearLayout>
diff --git a/res/layout/bluetooth_transfer_item.xml b/res/layout/bluetooth_transfer_item.xml
index 626d931f1..ffc105260 100644
--- a/res/layout/bluetooth_transfer_item.xml
+++ b/res/layout/bluetooth_transfer_item.xml
@@ -27,6 +27,7 @@
android:layout_height="@android:dimen/app_icon_size"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
+ android:contentDescription ="@string/bluetooth_map_email_settings_app_icon"
android:scaleType="center"
/>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3ab888491..623a09223 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -96,8 +96,8 @@
<!-- label for the notification item of sending file -->
<string name="notification_sending">Bluetooth share: Sending <xliff:g id="file">%1$s</xliff:g></string>
<!-- label for the notification item of sent file -->
- <string name="notification_sent">Bluetooth share: Sent <xliff:g id="file">%1$s</xliff:g></string>
- <!-- label for the notification item of sent file -status -->
+ <string name="notification_sent">Bluetooth share: Sent <xliff:g id="file">%1$s</xliff:g></string>
+ <!-- label for the notification item of sent file -status -->
<string name="notification_sent_complete">100% complete</string>
<!-- label for the notification item of failed sending file -->
<string name="notification_sent_fail">Bluetooth share: File <xliff:g id="file">%1$s</xliff:g> not sent</string>
@@ -224,4 +224,12 @@
<!-- Do not translate. android:process of this application. -->
<string name="process" translate="false"><xliff:g id="x" /></string>
+
+ <string name="bluetooth_map_email_settings_save">Save</string>
+ <string name="bluetooth_map_email_settings_cancel">Cancel</string>
+ <string name="bluetooth_map_email_settings_intro">Select the email accounts you want to share through Bluetooth. You still have to accept any acceess to the accounts when connecting.</string>
+ <string name="bluetooth_map_email_settings_count">Slots left:</string>
+ <string name="bluetooth_map_email_settings_app_icon">Application Icon</string>
+ <string name="bluetooth_map_email_settings_title">Bluetooth Message Sharing Settings</string>
+ <string name="bluetooth_map_email_settings_no_account_slots_left">Cannot select account. 0 slots left</string>
</resources>
diff --git a/src/com/android/bluetooth/map/BluetoothMapAppParams.java b/src/com/android/bluetooth/map/BluetoothMapAppParams.java
index cb607c704..df0561278 100644
--- a/src/com/android/bluetooth/map/BluetoothMapAppParams.java
+++ b/src/com/android/bluetooth/map/BluetoothMapAppParams.java
@@ -86,35 +86,45 @@ public class BluetoothMapAppParams {
public static final int STATUS_VALUE_NO = 0;
public static final int CHARSET_NATIVE = 0;
public static final int CHARSET_UTF8 = 1;
+ public static final int FRACTION_REQUEST_FIRST = 0;
+ public static final int FRACTION_REQUEST_NEXT = 1;
+ public static final int FRACTION_DELIVER_MORE = 0;
+ public static final int FRACTION_DELIVER_LAST = 1;
+
+
+ public static final int FILTER_NO_SMS_GSM = 0x01;
+ public static final int FILTER_NO_SMS_CDMA = 0x02;
+ public static final int FILTER_NO_EMAIL = 0x04;
+ public static final int FILTER_NO_MMS = 0x08;
/* Default values for omitted application parameters */
public static final long PARAMETER_MASK_ALL_ENABLED = 0xFFFF; // TODO: Update when bit 16-31 will be used.
- private int maxListCount = INVALID_VALUE_PARAMETER;
- private int startOffset = INVALID_VALUE_PARAMETER;
- private int filterMessageType = INVALID_VALUE_PARAMETER;
- private long filterPeriodBegin = INVALID_VALUE_PARAMETER;
- private long filterPeriodEnd = INVALID_VALUE_PARAMETER;
- private int filterReadStatus = INVALID_VALUE_PARAMETER;
- private String filterRecipient = null;
- private String filterOriginator = null;
- private int filterPriority = INVALID_VALUE_PARAMETER;
- private int attachment = INVALID_VALUE_PARAMETER;
- private int transparent = INVALID_VALUE_PARAMETER;
- private int retry = INVALID_VALUE_PARAMETER;
- private int newMessage = INVALID_VALUE_PARAMETER;
- private int notificationStatus = INVALID_VALUE_PARAMETER;
- private int masInstanceId = INVALID_VALUE_PARAMETER;
- private long parameterMask = INVALID_VALUE_PARAMETER;
- private int folderListingSize = INVALID_VALUE_PARAMETER;
- private int messageListingSize = INVALID_VALUE_PARAMETER;
- private int subjectLength = INVALID_VALUE_PARAMETER;
- private int charset = INVALID_VALUE_PARAMETER;
- private int fractionRequest = INVALID_VALUE_PARAMETER;
- private int fractionDeliver = INVALID_VALUE_PARAMETER;
- private int statusIndicator = INVALID_VALUE_PARAMETER;
- private int statusValue = INVALID_VALUE_PARAMETER;
- private long mseTime = INVALID_VALUE_PARAMETER;
+ private int mMaxListCount = INVALID_VALUE_PARAMETER;
+ private int mStartOffset = INVALID_VALUE_PARAMETER;
+ private int mFilterMessageType = INVALID_VALUE_PARAMETER;
+ private long mFilterPeriodBegin = INVALID_VALUE_PARAMETER;
+ private long mFilterPeriodEnd = INVALID_VALUE_PARAMETER;
+ private int mFilterReadStatus = INVALID_VALUE_PARAMETER;
+ private String mFilterRecipient = null;
+ private String mFilterOriginator = null;
+ private int mFilterPriority = INVALID_VALUE_PARAMETER;
+ private int mAttachment = INVALID_VALUE_PARAMETER;
+ private int mTransparent = INVALID_VALUE_PARAMETER;
+ private int mRetry = INVALID_VALUE_PARAMETER;
+ private int mNewMessage = INVALID_VALUE_PARAMETER;
+ private int mNotificationStatus = INVALID_VALUE_PARAMETER;
+ private int mMasInstanceId = INVALID_VALUE_PARAMETER;
+ private long mParameterMask = INVALID_VALUE_PARAMETER;
+ private int mFolderListingSize = INVALID_VALUE_PARAMETER;
+ private int mMessageListingSize = INVALID_VALUE_PARAMETER;
+ private int mSubjectLength = INVALID_VALUE_PARAMETER;
+ private int mCharset = INVALID_VALUE_PARAMETER;
+ private int mFractionRequest = INVALID_VALUE_PARAMETER;
+ private int mFractionDeliver = INVALID_VALUE_PARAMETER;
+ private int mStatusIndicator = INVALID_VALUE_PARAMETER;
+ private int mStatusValue = INVALID_VALUE_PARAMETER;
+ private long mMseTime = INVALID_VALUE_PARAMETER;
/**
* Default constructor, used to build an application parameter object to be
@@ -212,10 +222,14 @@ public class BluetoothMapAppParams {
setFilterReadStatus(appParams[i] & 0x03); // Lower two bits
break;
case FILTER_RECIPIENT:
- setFilterRecipient(new String(appParams, i, tagLength));
+ if(tagLength != 0) {
+ setFilterRecipient(new String(appParams, i, tagLength));
+ }
break;
case FILTER_ORIGINATOR:
- setFilterOriginator(new String(appParams, i, tagLength));
+ if(tagLength != 0) {
+ setFilterOriginator(new String(appParams, i, tagLength));
+ }
break;
case FILTER_PRIORITY:
if (tagLength != FILTER_PRIORITY_LEN) {
@@ -524,263 +538,263 @@ public class BluetoothMapAppParams {
}
public int getMaxListCount() {
- return maxListCount;
+ return mMaxListCount;
}
public void setMaxListCount(int maxListCount) throws IllegalArgumentException {
if (maxListCount < 0 || maxListCount > 0xFFFF)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0xFFFF");
- this.maxListCount = maxListCount;
+ this.mMaxListCount = maxListCount;
}
public int getStartOffset() {
- return startOffset;
+ return mStartOffset;
}
public void setStartOffset(int startOffset) throws IllegalArgumentException {
if (startOffset < 0 || startOffset > 0xFFFF)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0xFFFF");
- this.startOffset = startOffset;
+ this.mStartOffset = startOffset;
}
public int getFilterMessageType() {
- return filterMessageType;
+ return mFilterMessageType;
}
public void setFilterMessageType(int filterMessageType) throws IllegalArgumentException {
if (filterMessageType < 0 || filterMessageType > 0x000F)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x000F");
- this.filterMessageType = filterMessageType;
+ this.mFilterMessageType = filterMessageType;
}
public long getFilterPeriodBegin() {
- return filterPeriodBegin;
+ return mFilterPeriodBegin;
}
public String getFilterPeriodBeginString() {
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
- Date date = new Date(filterPeriodBegin);
+ Date date = new Date(mFilterPeriodBegin);
return format.format(date); // Format to YYYYMMDDTHHMMSS local time
}
public void setFilterPeriodBegin(long filterPeriodBegin) {
- this.filterPeriodBegin = filterPeriodBegin;
+ this.mFilterPeriodBegin = filterPeriodBegin;
}
public void setFilterPeriodBegin(String filterPeriodBegin) throws ParseException {
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
Date date = format.parse(filterPeriodBegin);
- this.filterPeriodBegin = date.getTime();
+ this.mFilterPeriodBegin = date.getTime();
}
public long getFilterPeriodEnd() {
- return filterPeriodEnd;
+ return mFilterPeriodEnd;
}
public String getFilterPeriodEndString() {
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
- Date date = new Date(filterPeriodEnd);
+ Date date = new Date(mFilterPeriodEnd);
return format.format(date); // Format to YYYYMMDDTHHMMSS local time
}
public void setFilterPeriodEnd(long filterPeriodEnd) {
- this.filterPeriodEnd = filterPeriodEnd;
+ this.mFilterPeriodEnd = filterPeriodEnd;
}
public void setFilterPeriodEnd(String filterPeriodEnd) throws ParseException {
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
Date date = format.parse(filterPeriodEnd);
- this.filterPeriodEnd = date.getTime();
+ this.mFilterPeriodEnd = date.getTime();
}
public int getFilterReadStatus() {
- return filterReadStatus;
+ return mFilterReadStatus;
}
public void setFilterReadStatus(int filterReadStatus) throws IllegalArgumentException {
if (filterReadStatus < 0 || filterReadStatus > 0x0002)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0002");
- this.filterReadStatus = filterReadStatus;
+ this.mFilterReadStatus = filterReadStatus;
}
public String getFilterRecipient() {
- return filterRecipient;
+ return mFilterRecipient;
}
public void setFilterRecipient(String filterRecipient) {
- this.filterRecipient = filterRecipient;
+ this.mFilterRecipient = filterRecipient;
}
public String getFilterOriginator() {
- return filterOriginator;
+ return mFilterOriginator;
}
public void setFilterOriginator(String filterOriginator) {
- this.filterOriginator = filterOriginator;
+ this.mFilterOriginator = filterOriginator;
}
public int getFilterPriority() {
- return filterPriority;
+ return mFilterPriority;
}
public void setFilterPriority(int filterPriority) throws IllegalArgumentException {
if (filterPriority < 0 || filterPriority > 0x0002)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0002");
- this.filterPriority = filterPriority;
+ this.mFilterPriority = filterPriority;
}
public int getAttachment() {
- return attachment;
+ return mAttachment;
}
public void setAttachment(int attachment) throws IllegalArgumentException {
if (attachment < 0 || attachment > 0x0001)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
- this.attachment = attachment;
+ this.mAttachment = attachment;
}
public int getTransparent() {
- return transparent;
+ return mTransparent;
}
public void setTransparent(int transparent) throws IllegalArgumentException {
if (transparent < 0 || transparent > 0x0001)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
- this.transparent = transparent;
+ this.mTransparent = transparent;
}
public int getRetry() {
- return retry;
+ return mRetry;
}
public void setRetry(int retry) throws IllegalArgumentException {
if (retry < 0 || retry > 0x0001)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
- this.retry = retry;
+ this.mRetry = retry;
}
public int getNewMessage() {
- return newMessage;
+ return mNewMessage;
}
public void setNewMessage(int newMessage) throws IllegalArgumentException {
if (newMessage < 0 || newMessage > 0x0001)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
- this.newMessage = newMessage;
+ this.mNewMessage = newMessage;
}
public int getNotificationStatus() {
- return notificationStatus;
+ return mNotificationStatus;
}
public void setNotificationStatus(int notificationStatus) throws IllegalArgumentException {
if (notificationStatus < 0 || notificationStatus > 0x0001)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
- this.notificationStatus = notificationStatus;
+ this.mNotificationStatus = notificationStatus;
}
public int getMasInstanceId() {
- return masInstanceId;
+ return mMasInstanceId;
}
public void setMasInstanceId(int masInstanceId) {
if (masInstanceId < 0 || masInstanceId > 0x00FF)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x00FF");
- this.masInstanceId = masInstanceId;
+ this.mMasInstanceId = masInstanceId;
}
public long getParameterMask() {
- return parameterMask;
+ return mParameterMask;
}
public void setParameterMask(long parameterMask) {
if (parameterMask < 0 || parameterMask > 0xFFFFFFFFL)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0xFFFFFFFF");
- this.parameterMask = parameterMask;
+ this.mParameterMask = parameterMask;
}
public int getFolderListingSize() {
- return folderListingSize;
+ return mFolderListingSize;
}
public void setFolderListingSize(int folderListingSize) {
if (folderListingSize < 0 || folderListingSize > 0xFFFF)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0xFFFF");
- this.folderListingSize = folderListingSize;
+ this.mFolderListingSize = folderListingSize;
}
public int getMessageListingSize() {
- return messageListingSize;
+ return mMessageListingSize;
}
public void setMessageListingSize(int messageListingSize) {
if (messageListingSize < 0 || messageListingSize > 0xFFFF)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0xFFFF");
- this.messageListingSize = messageListingSize;
+ this.mMessageListingSize = messageListingSize;
}
public int getSubjectLength() {
- return subjectLength;
+ return mSubjectLength;
}
public void setSubjectLength(int subjectLength) {
if (subjectLength < 0 || subjectLength > 0xFF)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x00FF");
- this.subjectLength = subjectLength;
+ this.mSubjectLength = subjectLength;
}
public int getCharset() {
- return charset;
+ return mCharset;
}
public void setCharset(int charset) {
if (charset < 0 || charset > 0x1)
throw new IllegalArgumentException("Out of range: " + charset + ", valid range is 0x0000 to 0x0001");
- this.charset = charset;
+ this.mCharset = charset;
}
public int getFractionRequest() {
- return fractionRequest;
+ return mFractionRequest;
}
public void setFractionRequest(int fractionRequest) {
if (fractionRequest < 0 || fractionRequest > 0x1)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
- this.fractionRequest = fractionRequest;
+ this.mFractionRequest = fractionRequest;
}
public int getFractionDeliver() {
- return fractionDeliver;
+ return mFractionDeliver;
}
public void setFractionDeliver(int fractionDeliver) {
if (fractionDeliver < 0 || fractionDeliver > 0x1)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
- this.fractionDeliver = fractionDeliver;
+ this.mFractionDeliver = fractionDeliver;
}
public int getStatusIndicator() {
- return statusIndicator;
+ return mStatusIndicator;
}
public void setStatusIndicator(int statusIndicator) {
if (statusIndicator < 0 || statusIndicator > 0x1)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
- this.statusIndicator = statusIndicator;
+ this.mStatusIndicator = statusIndicator;
}
public int getStatusValue() {
- return statusValue;
+ return mStatusValue;
}
public void setStatusValue(int statusValue) {
if (statusValue < 0 || statusValue > 0x1)
throw new IllegalArgumentException("Out of range, valid range is 0x0000 to 0x0001");
- this.statusValue = statusValue;
+ this.mStatusValue = statusValue;
}
public long getMseTime() {
- return mseTime;
+ return mMseTime;
}
public String getMseTimeString() {
@@ -790,12 +804,12 @@ public class BluetoothMapAppParams {
}
public void setMseTime(long mseTime) {
- this.mseTime = mseTime;
+ this.mMseTime = mseTime;
}
public void setMseTime(String mseTime) throws ParseException {
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmssZ");
Date date = format.parse(mseTime);
- this.mseTime = date.getTime();
+ this.mMseTime = date.getTime();
}
}
diff --git a/src/com/android/bluetooth/map/BluetoothMapAuthenticator.java b/src/com/android/bluetooth/map/BluetoothMapAuthenticator.java
deleted file mode 100644
index 2d345a133..000000000
--- a/src/com/android/bluetooth/map/BluetoothMapAuthenticator.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
-* Copyright (C) 2013 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.map;
-
-import android.os.Handler;
-import android.os.Message;
-import android.util.Log;
-
-import javax.obex.Authenticator;
-import javax.obex.PasswordAuthentication;
-
-/**
- * BluetoothMapAuthenticator is a used by BluetoothObexServer for obex
- * authentication procedure.
- */
-public class BluetoothMapAuthenticator implements Authenticator {
- private static final String TAG = "BluetoothMapAuthenticator";
-
- private boolean mChallenged;
-
- private boolean mAuthCancelled;
-
- private String mSessionKey;
-
- private Handler mCallback;
-
- public BluetoothMapAuthenticator(final Handler callback) {
- mCallback = callback;
- mChallenged = false;
- mAuthCancelled = false;
- mSessionKey = null;
- }
-
- public final synchronized void setChallenged(final boolean bool) {
- mChallenged = bool;
- }
-
- public final synchronized void setCancelled(final boolean bool) {
- mAuthCancelled = bool;
- }
-
- public final synchronized void setSessionKey(final String string) {
- mSessionKey = string;
- }
-
- private void waitUserConfirmation() {
- Message msg = Message.obtain(mCallback);
- msg.what = BluetoothMapService.MSG_OBEX_AUTH_CHALL;
- msg.sendToTarget();
- synchronized (this) {
- while (!mChallenged && !mAuthCancelled) {
- try {
- wait();
- } catch (InterruptedException e) {
- Log.e(TAG, "Interrupted while waiting on isChallenged");
- }
- }
- }
- }
-
- public PasswordAuthentication onAuthenticationChallenge(final String description,
- final boolean isUserIdRequired, final boolean isFullAccess) {
- waitUserConfirmation();
- if (mSessionKey.trim().length() != 0) {
- PasswordAuthentication pa = new PasswordAuthentication(null, mSessionKey.getBytes());
- return pa;
- }
- return null;
- }
-
- // TODO: Reserved for future use only, in case MSE challenge MCE
- public byte[] onAuthenticationResponse(final byte[] userName) {
- byte[] b = null;
- return b;
- }
-}
diff --git a/src/com/android/bluetooth/map/BluetoothMapContent.java b/src/com/android/bluetooth/map/BluetoothMapContent.java
index 81cd2fdf3..58bfc5289 100644
--- a/src/com/android/bluetooth/map/BluetoothMapContent.java
+++ b/src/com/android/bluetooth/map/BluetoothMapContent.java
@@ -1,5 +1,5 @@
/*
-* Copyright (C) 2013 Samsung System LSI
+* Copyright (C) 2014 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
@@ -14,12 +14,6 @@
*/
package com.android.bluetooth.map;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.text.ParseException;
-
import org.apache.http.util.ByteArrayBuffer;
import android.content.ContentResolver;
@@ -27,24 +21,46 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Debug;
+import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
+import com.android.bluetooth.mapapi.BluetoothMapContract;
+import com.android.bluetooth.mapapi.BluetoothMapContract.MessageColumns;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.Telephony.Mms;
import android.provider.Telephony.Sms;
+import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
import android.util.Log;
+import com.android.bluetooth.map.BluetoothMapSmsPdu.SmsPdu;
import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
import com.google.android.mms.pdu.CharacterSets;
import com.google.android.mms.pdu.PduHeaders;
+import com.android.bluetooth.map.BluetoothMapAppParams;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
public class BluetoothMapContent {
private static final String TAG = "BluetoothMapContent";
- private static final boolean D = false;
- private static final boolean V = false;
+ private static final boolean D = BluetoothMapService.DEBUG;
+ private static final boolean V = BluetoothMapService.VERBOSE;
private static final int MASK_SUBJECT = 0x1;
private static final int MASK_DATETIME = 0x2;
@@ -73,8 +89,11 @@ public class BluetoothMapContent {
public static final int MMS_BCC = 0x81;
public static final int MMS_CC = 0x82;
+ public static final String INSERT_ADDRES_TOKEN = "insert-address-token";
+
private Context mContext;
private ContentResolver mResolver;
+ private String mBaseEmailUri = null;
static final String[] SMS_PROJECTION = new String[] {
BaseColumns._ID,
@@ -86,7 +105,7 @@ public class BluetoothMapContent {
Sms.TYPE,
Sms.STATUS,
Sms.LOCKED,
- Sms.ERROR_CODE,
+ Sms.ERROR_CODE
};
static final String[] MMS_PROJECTION = new String[] {
@@ -102,86 +121,121 @@ public class BluetoothMapContent {
Mms.READ,
Mms.MESSAGE_BOX,
Mms.STATUS,
- Mms.PRIORITY,
+ Mms.PRIORITY
};
private class FilterInfo {
public static final int TYPE_SMS = 0;
public static final int TYPE_MMS = 1;
+ public static final int TYPE_EMAIL = 2;
+
+ int mMsgType = TYPE_SMS;
+ int mPhoneType = 0;
+ String mPhoneNum = null;
+ String mPhoneAlphaTag = null;
+ /*column indices used to optimize queries */
+ public int mEmailColThreadId = -1;
+ public int mEmailColProtected = -1;
+ public int mEmailColFolder = -1;
+ public int mMmsColFolder = -1;
+ public int mSmsColFolder = -1;
+ public int mEmailColRead = -1;
+ public int mSmsColRead = -1;
+ public int mMmsColRead = -1;
+ public int mEmailColPriority = -1;
+ public int mMmsColAttachmentSize = -1;
+ public int mEmailColAttachment = -1;
+ public int mEmailColAttachementSize = -1;
+ public int mMmsColTextOnly = -1;
+ public int mMmsColId = -1;
+ public int mSmsColId = -1;
+ public int mEmailColSize = -1;
+ public int mSmsColSubject = -1;
+ public int mMmsColSize = -1;
+ public int mEmailColToAddress = -1;
+ public int mEmailColCcAddress = -1;
+ public int mEmailColBccAddress = -1;
+ public int mSmsColAddress = -1;
+ public int mSmsColDate = -1;
+ public int mMmsColDate = -1;
+ public int mEmailColDate = -1;
+ public int mMmsColSubject = -1;
+ public int mEmailColSubject = -1;
+ public int mSmsColType = -1;
+ public int mEmailColFromAddress = -1;
+ public int mEmailColId = -1;
+
+
+ public void setEmailColumns(Cursor c) {
+ mEmailColThreadId = c.getColumnIndex(BluetoothMapContract.MessageColumns.THREAD_ID);
+ mEmailColProtected = c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_PROTECTED);
+ mEmailColFolder = c.getColumnIndex(BluetoothMapContract.MessageColumns.FOLDER_ID);
+ mEmailColRead = c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_READ);
+ mEmailColPriority = c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY);
+ mEmailColAttachment = c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_ATTACHMENT);
+ mEmailColAttachementSize = c.getColumnIndex(BluetoothMapContract.MessageColumns.ATTACHMENT_SIZE);
+ mEmailColSize = c.getColumnIndex(BluetoothMapContract.MessageColumns.MESSAGE_SIZE);
+ mEmailColToAddress = c.getColumnIndex(BluetoothMapContract.MessageColumns.TO_LIST);
+ mEmailColCcAddress = c.getColumnIndex(BluetoothMapContract.MessageColumns.CC_LIST);
+ mEmailColBccAddress = c.getColumnIndex(BluetoothMapContract.MessageColumns.BCC_LIST);
+ mEmailColDate = c.getColumnIndex(BluetoothMapContract.MessageColumns.DATE);
+ mEmailColSubject = c.getColumnIndex(BluetoothMapContract.MessageColumns.SUBJECT);
+ mEmailColFromAddress = c.getColumnIndex(BluetoothMapContract.MessageColumns.FROM_LIST);
+ mEmailColId = c.getColumnIndex(BluetoothMapContract.MessageColumns._ID);
+ }
+
+ public void setSmsColumns(Cursor c) {
+ mSmsColId = c.getColumnIndex(BaseColumns._ID);
+ mSmsColFolder = c.getColumnIndex(Sms.TYPE);
+ mSmsColRead = c.getColumnIndex(Sms.READ);
+ mSmsColSubject = c.getColumnIndex(Sms.BODY);
+ mSmsColAddress = c.getColumnIndex(Sms.ADDRESS);
+ mSmsColDate = c.getColumnIndex(Sms.DATE);
+ mSmsColType = c.getColumnIndex(Sms.TYPE);
+ }
+
+ public void setMmsColumns(Cursor c) {
+ mMmsColId = c.getColumnIndex(BaseColumns._ID);
+ mMmsColFolder = c.getColumnIndex(Mms.MESSAGE_BOX);
+ mMmsColRead = c.getColumnIndex(Mms.READ);
+ mMmsColAttachmentSize = c.getColumnIndex(Mms.MESSAGE_SIZE);
+ mMmsColTextOnly = c.getColumnIndex(Mms.TEXT_ONLY);
+ mMmsColSize = c.getColumnIndex(Mms.MESSAGE_SIZE);
+ mMmsColDate = c.getColumnIndex(Mms.DATE);
+ mMmsColSubject = c.getColumnIndex(Mms.SUBJECT);
- int msgType = TYPE_SMS;
- int phoneType = 0;
- String phoneNum = null;
- String phoneAlphaTag = null;
+ }
}
- public BluetoothMapContent(final Context context) {
+ public BluetoothMapContent(final Context context, String emailBaseUri) {
mContext = context;
mResolver = mContext.getContentResolver();
if (mResolver == null) {
- Log.d(TAG, "getContentResolver failed");
+ if (D) Log.d(TAG, "getContentResolver failed");
}
+ mBaseEmailUri = emailBaseUri;
}
- private void addSmsEntry() {
- if (D) Log.d(TAG, "*** Adding dummy sms ***");
- ContentValues mVal = new ContentValues();
- mVal.put(Sms.ADDRESS, "1234");
- mVal.put(Sms.BODY, "Hello!!!");
- mVal.put(Sms.DATE, System.currentTimeMillis());
- mVal.put(Sms.READ, "0");
-
- Uri mUri = mResolver.insert(Sms.CONTENT_URI, mVal);
- }
-
- private BluetoothMapAppParams buildAppParams() {
- BluetoothMapAppParams ap = new BluetoothMapAppParams();
- try {
- int paramMask = (MASK_SUBJECT
- | MASK_DATETIME
- | MASK_SENDER_NAME
- | MASK_SENDER_ADDRESSING
- | MASK_RECIPIENT_NAME
- | MASK_RECIPIENT_ADDRESSING
- | MASK_TYPE
- | MASK_SIZE
- | MASK_RECEPTION_STATUS
- | MASK_TEXT
- | MASK_ATTACHMENT_SIZE
- | MASK_PRIORITY
- | MASK_READ
- | MASK_SENT
- | MASK_PROTECTED
- );
- ap.setMaxListCount(5);
- ap.setStartOffset(0);
- ap.setFilterMessageType(0);
- ap.setFilterPeriodBegin("20130101T000000");
- ap.setFilterPeriodEnd("20131230T000000");
- ap.setFilterReadStatus(0);
- ap.setParameterMask(paramMask);
- ap.setSubjectLength(10);
- /* ap.setFilterOriginator("Sms*"); */
- /* ap.setFilterRecipient("41*"); */
- } catch (ParseException e) {
- return null;
- }
- return ap;
- }
private void printSms(Cursor c) {
String body = c.getString(c.getColumnIndex(Sms.BODY));
- if (D) Log.d(TAG, "printSms " + BaseColumns._ID + ": " + c.getLong(c.getColumnIndex(BaseColumns._ID)) +
- " " + Sms.THREAD_ID + " : " + c.getLong(c.getColumnIndex(Sms.THREAD_ID)) +
- " " + Sms.ADDRESS + " : " + c.getString(c.getColumnIndex(Sms.ADDRESS)) +
- " " + Sms.BODY + " : " + body.substring(0, Math.min(body.length(), 8)) +
- " " + Sms.DATE + " : " + c.getLong(c.getColumnIndex(Sms.DATE)) +
- " " + Sms.TYPE + " : " + c.getInt(c.getColumnIndex(Sms.TYPE)));
+ if (V) Log.v(TAG, "printSms " + BaseColumns._ID + ": " + c.getLong(c.getColumnIndex(BaseColumns._ID)) +
+ "\n " + Sms.THREAD_ID + " : " + c.getLong(c.getColumnIndex(Sms.THREAD_ID)) +
+ "\n " + Sms.ADDRESS + " : " + c.getString(c.getColumnIndex(Sms.ADDRESS)) +
+ "\n " + Sms.BODY + " : " + body.substring(0, Math.min(body.length(), 8)) +
+ "\n " + Sms.DATE + " : " + c.getLong(c.getColumnIndex(Sms.DATE)) +
+ "\n " + Sms.READ + " : " + c.getLong(c.getColumnIndex(Sms.READ)) +
+ "\n " + Sms.TYPE + " : " + c.getInt(c.getColumnIndex(Sms.TYPE)) +
+ "\n " + Sms.STATUS + " : " + c.getInt(c.getColumnIndex(Sms.STATUS)) +
+ "\n " + Sms.LOCKED + " : " + c.getInt(c.getColumnIndex(Sms.LOCKED)) +
+ "\n " + Sms.ERROR_CODE + " : " + c.getInt(c.getColumnIndex(Sms.ERROR_CODE)));
+
+
}
private void printMms(Cursor c) {
- if (D) Log.d(TAG, "printMms " + BaseColumns._ID + ": " + c.getLong(c.getColumnIndex(BaseColumns._ID)) +
+ if (V) Log.v(TAG, "printMms " + BaseColumns._ID + ": " + c.getLong(c.getColumnIndex(BaseColumns._ID)) +
"\n " + Mms.THREAD_ID + " : " + c.getLong(c.getColumnIndex(Mms.THREAD_ID)) +
"\n " + Mms.MESSAGE_ID + " : " + c.getString(c.getColumnIndex(Mms.MESSAGE_ID)) +
"\n " + Mms.SUBJECT + " : " + c.getString(c.getColumnIndex(Mms.SUBJECT)) +
@@ -191,19 +245,41 @@ public class BluetoothMapContent {
"\n " + Mms.DATE_SENT + " : " + c.getLong(c.getColumnIndex(Mms.DATE_SENT)) +
"\n " + Mms.READ + " : " + c.getInt(c.getColumnIndex(Mms.READ)) +
"\n " + Mms.MESSAGE_BOX + " : " + c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)) +
- "\n " + Mms.STATUS + " : " + c.getInt(c.getColumnIndex(Mms.STATUS)));
+ "\n " + Mms.STATUS + " : " + c.getInt(c.getColumnIndex(Mms.STATUS)) +
+ "\n " + Mms.PRIORITY + " : " + c.getInt(c.getColumnIndex(Mms.PRIORITY)) +
+ "\n " + Mms.MESSAGE_SIZE + " : " + c.getInt(c.getColumnIndex(Mms.MESSAGE_SIZE)));
+ }
+
+ private String getDateTimeString(long timestamp) {
+ SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
+ Date date = new Date(timestamp);
+ return format.format(date); // Format to YYYYMMDDTHHMMSS local time
+ }
+ private void printEmail(Cursor c) {
+ if (V) Log.v(TAG, "printEmail " + BaseColumns._ID + ": " + c.getLong(c.getColumnIndex(BaseColumns._ID)) +
+ "\n " + BluetoothMapContract.MessageColumns.DATE + " : " + getDateTimeString(c.getLong(c.getColumnIndex(BluetoothMapContract.MessageColumns.DATE))) +
+ "\n " + BluetoothMapContract.MessageColumns.SUBJECT + " : " + c.getString(c.getColumnIndex(BluetoothMapContract.MessageColumns.SUBJECT)) +
+ "\n " + BluetoothMapContract.MessageColumns.FLAG_READ + " : " + c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_READ)) +
+ "\n " + BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY + " : " + c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_HIGH_PRIORITY)) +
+ "\n " + BluetoothMapContract.MessageColumns.RECEPTION_STATE + " : " + c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.RECEPTION_STATE)) +
+ "\n " + BluetoothMapContract.MessageColumns.FLAG_ATTACHMENT + " : " + c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_ATTACHMENT)) +
+ "\n " + BluetoothMapContract.MessageColumns.MESSAGE_SIZE + " : " + c.getLong(c.getColumnIndex(BluetoothMapContract.MessageColumns.MESSAGE_SIZE)) +
+ "\n " + BluetoothMapContract.MessageColumns.FLAG_PROTECTED + " : " + c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_PROTECTED)) +
+ "\n " + BluetoothMapContract.MessageColumns.FROM_LIST + " : " + c.getString(c.getColumnIndex(BluetoothMapContract.MessageColumns.FROM_LIST)) +
+ "\n " + BluetoothMapContract.MessageColumns.TO_LIST + " : " + c.getString(c.getColumnIndex(BluetoothMapContract.MessageColumns.TO_LIST)) +
+ "\n " + BluetoothMapContract.MessageColumns.CC_LIST + " : " + c.getString(c.getColumnIndex(BluetoothMapContract.MessageColumns.CC_LIST)) +
+ "\n " + BluetoothMapContract.MessageColumns.BCC_LIST + " : " + c.getString(c.getColumnIndex(BluetoothMapContract.MessageColumns.BCC_LIST)) +
+ "\n " + BluetoothMapContract.MessageColumns.REPLY_TO_LIST + " : " + c.getString(c.getColumnIndex(BluetoothMapContract.MessageColumns.REPLY_TO_LIST)) +
+ "\n " + BluetoothMapContract.MessageColumns.FOLDER_ID + " : " + c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.FOLDER_ID)) +
+ "\n " + BluetoothMapContract.MessageColumns.ACCOUNT_ID + " : " + c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.ACCOUNT_ID)) );
}
private void printMmsAddr(long id) {
final String[] projection = null;
String selection = new String("msg_id=" + id);
- String uriStr = String.format("content://mms/%d/addr", id);
+ String uriStr = new String(Mms.CONTENT_URI + "/" + id + "/addr");
Uri uriAddress = Uri.parse(uriStr);
- Cursor c = mResolver.query(
- uriAddress,
- projection,
- selection,
- null, null);
+ Cursor c = mResolver.query(uriAddress, projection, selection, null, null);
if (c.moveToFirst()) {
do {
@@ -222,7 +298,7 @@ public class BluetoothMapContent {
}
private void printMmsPartImage(long partid) {
- String uriStr = String.format("content://mms/part/%d", partid);
+ String uriStr = new String(Mms.CONTENT_URI + "/part/" + partid);
Uri uriAddress = Uri.parse(uriStr);
int ch;
StringBuffer sb = new StringBuffer("");
@@ -245,13 +321,9 @@ public class BluetoothMapContent {
private void printMmsParts(long id) {
final String[] projection = null;
String selection = new String("mid=" + id);
- String uriStr = String.format("content://mms/%d/part", id);
+ String uriStr = new String(Mms.CONTENT_URI + "/" + id + "/part");
Uri uriAddress = Uri.parse(uriStr);
- Cursor c = mResolver.query(
- uriAddress,
- projection,
- selection,
- null, null);
+ Cursor c = mResolver.query(uriAddress, projection, selection, null, null);
if (D) Log.d(TAG, " parts:");
if (c.moveToFirst()) {
@@ -285,80 +357,44 @@ public class BluetoothMapContent {
}
}
- public void dumpMmsTable() {
- if (D) Log.d(TAG, "**** Dump of mms table ****");
- Cursor c = mResolver.query(Mms.CONTENT_URI,
- MMS_PROJECTION, null, null, "_id DESC");
- if (c != null) {
- if (D) Log.d(TAG, "c.getCount() = " + c.getCount());
- c.moveToPosition(-1);
- while (c.moveToNext()) {
- printMms(c);
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
- printMmsAddr(id);
- printMmsParts(id);
- }
- } else {
- Log.d(TAG, "query failed");
- c.close();
- }
- }
-
- public void dumpSmsTable() {
- addSmsEntry();
- if (D) Log.d(TAG, "**** Dump of sms table ****");
- Cursor c = mResolver.query(Sms.CONTENT_URI,
- SMS_PROJECTION, null, null, "_id DESC");
- if (c != null) {
- if (D) Log.d(TAG, "c.getCount() = " + c.getCount());
- c.moveToPosition(-1);
- while (c.moveToNext()) {
- printSms(c);
- }
- } else {
- Log.d(TAG, "query failed");
- c.close();
- }
-
- }
-
- public void dumpMessages() {
- dumpSmsTable();
- dumpMmsTable();
-
- BluetoothMapAppParams ap = buildAppParams();
- if (D) Log.d(TAG, "message listing size = " + msgListingSize("inbox", ap));
- BluetoothMapMessageListing mList = msgListing("inbox", ap);
- try {
- mList.encode();
- } catch (UnsupportedEncodingException ex) {
- /* do nothing */
- }
- mList = msgListing("sent", ap);
- try {
- mList.encode();
- } catch (UnsupportedEncodingException ex) {
- /* do nothing */
- }
- }
private void setProtected(BluetoothMapMessageListingElement e, Cursor c,
- FilterInfo fi, BluetoothMapAppParams ap) {
+ FilterInfo fi, BluetoothMapAppParams ap) {
if ((ap.getParameterMask() & MASK_PROTECTED) != 0) {
String protect = "no";
- if (D) Log.d(TAG, "setProtected: " + protect);
+ if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ int flagProtected = c.getInt(fi.mEmailColProtected);
+ if (flagProtected == 1) {
+ protect = "yes";
+ }
+ }
+ if (V) Log.d(TAG, "setProtected: " + protect + "\n");
e.setProtect(protect);
}
}
+ /**
+ * Email only
+ */
+ private void setThreadId(BluetoothMapMessageListingElement e, Cursor c,
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ long threadId = c.getLong(fi.mEmailColThreadId);
+ e.setThreadId(threadId);
+ if (V) Log.d(TAG, "setThreadId: " + threadId + "\n");
+ }
+ }
+
private void setSent(BluetoothMapMessageListingElement e, Cursor c,
- FilterInfo fi, BluetoothMapAppParams ap) {
+ FilterInfo fi, BluetoothMapAppParams ap) {
if ((ap.getParameterMask() & MASK_SENT) != 0) {
int msgType = 0;
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- msgType = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ msgType = c.getInt(fi.mSmsColFolder);
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ msgType = c.getInt(fi.mMmsColFolder);
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ msgType = c.getInt(fi.mEmailColFolder);
}
String sent = null;
if (msgType == 2) {
@@ -366,41 +402,45 @@ public class BluetoothMapContent {
} else {
sent = "no";
}
- if (D) Log.d(TAG, "setSent: " + sent);
+ if (V) Log.d(TAG, "setSent: " + sent);
e.setSent(sent);
}
}
private void setRead(BluetoothMapMessageListingElement e, Cursor c,
- FilterInfo fi, BluetoothMapAppParams ap) {
+ FilterInfo fi, BluetoothMapAppParams ap) {
int read = 0;
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- read = c.getInt(c.getColumnIndex(Sms.READ));
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- read = c.getInt(c.getColumnIndex(Mms.READ));
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ read = c.getInt(fi.mSmsColRead);
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ read = c.getInt(fi.mMmsColRead);
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ read = c.getInt(fi.mEmailColRead);
}
String setread = null;
- if (read == 1) {
- setread = "yes";
- } else {
- setread = "no";
- }
- if (D) Log.d(TAG, "setRead: " + setread);
- e.setRead(setread, ((ap.getParameterMask() & MASK_READ) != 0));
+
+ if (V) Log.d(TAG, "setRead: " + setread);
+ e.setRead((read==1?true:false), ((ap.getParameterMask() & MASK_READ) != 0));
}
private void setPriority(BluetoothMapMessageListingElement e, Cursor c,
- FilterInfo fi, BluetoothMapAppParams ap) {
- String priority = "no";
+ FilterInfo fi, BluetoothMapAppParams ap) {
if ((ap.getParameterMask() & MASK_PRIORITY) != 0) {
+ String priority = "no";
+ if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ int highPriority = c.getInt(fi.mEmailColPriority);
+ if (highPriority == 1) {
+ priority = "yes";
+ }
+ }
int pri = 0;
- if (fi.msgType == FilterInfo.TYPE_MMS) {
+ if (fi.mMsgType == FilterInfo.TYPE_MMS) {
pri = c.getInt(c.getColumnIndex(Mms.PRIORITY));
}
if (pri == PduHeaders.PRIORITY_HIGH) {
priority = "yes";
}
- if (D) Log.d(TAG, "setPriority: " + priority);
+ if (V) Log.d(TAG, "setPriority: " + priority);
e.setPriority(priority);
}
}
@@ -413,29 +453,39 @@ public class BluetoothMapContent {
* extract the length (in bytes) of the text parts.
*/
private void setAttachmentSize(BluetoothMapMessageListingElement e, Cursor c,
- FilterInfo fi, BluetoothMapAppParams ap) {
+ FilterInfo fi, BluetoothMapAppParams ap) {
if ((ap.getParameterMask() & MASK_ATTACHMENT_SIZE) != 0) {
int size = 0;
- if (fi.msgType == FilterInfo.TYPE_MMS) {
- size = c.getInt(c.getColumnIndex(Mms.MESSAGE_SIZE));
+ if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ if(c.getInt(fi.mMmsColTextOnly) == 0) {
+ size = c.getInt(fi.mMmsColAttachmentSize);
+ }
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ int attachment = c.getInt(fi.mEmailColAttachment);
+ size = c.getInt(fi.mEmailColAttachementSize);
+ if(attachment == 1 && size == 0) {
+ size = 1; /* Ensure we indicate we have attachments in the size, it the
+ message has attachments, in case the e-mail client do not
+ report a size */
+ }
}
- if (D) Log.d(TAG, "setAttachmentSize: " + size);
+ if (V) Log.d(TAG, "setAttachmentSize: " + size);
e.setAttachmentSize(size);
}
}
private void setText(BluetoothMapMessageListingElement e, Cursor c,
- FilterInfo fi, BluetoothMapAppParams ap) {
+ FilterInfo fi, BluetoothMapAppParams ap) {
if ((ap.getParameterMask() & MASK_TEXT) != 0) {
String hasText = "";
- if (fi.msgType == FilterInfo.TYPE_SMS) {
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
hasText = "yes";
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- int textOnly = c.getInt(c.getColumnIndex(Mms.TEXT_ONLY));
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ int textOnly = c.getInt(fi.mMmsColTextOnly);
if (textOnly == 1) {
hasText = "yes";
} else {
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ long id = c.getLong(fi.mMmsColId);
String text = getTextPartsMms(id);
if (text != null && text.length() > 0) {
hasText = "yes";
@@ -443,8 +493,10 @@ public class BluetoothMapContent {
hasText = "no";
}
}
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ hasText = "yes";
}
- if (D) Log.d(TAG, "setText: " + hasText);
+ if (V) Log.d(TAG, "setText: " + hasText);
e.setText(hasText);
}
}
@@ -453,7 +505,7 @@ public class BluetoothMapContent {
FilterInfo fi, BluetoothMapAppParams ap) {
if ((ap.getParameterMask() & MASK_RECEPTION_STATUS) != 0) {
String status = "complete";
- if (D) Log.d(TAG, "setReceptionStatus: " + status);
+ if (V) Log.d(TAG, "setReceptionStatus: " + status);
e.setReceptionStatus(status);
}
}
@@ -462,13 +514,15 @@ public class BluetoothMapContent {
FilterInfo fi, BluetoothMapAppParams ap) {
if ((ap.getParameterMask() & MASK_SIZE) != 0) {
int size = 0;
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- String subject = c.getString(c.getColumnIndex(Sms.BODY));
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ String subject = c.getString(fi.mSmsColSubject);
size = subject.length();
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- size = c.getInt(c.getColumnIndex(Mms.MESSAGE_SIZE));
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ size = c.getInt(fi.mMmsColSize);
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ size = c.getInt(fi.mEmailColSize);
}
- if (D) Log.d(TAG, "setSize: " + size);
+ if (V) Log.d(TAG, "setSize: " + size);
e.setSize(size);
}
}
@@ -477,36 +531,68 @@ public class BluetoothMapContent {
FilterInfo fi, BluetoothMapAppParams ap) {
if ((ap.getParameterMask() & MASK_TYPE) != 0) {
TYPE type = null;
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- if (fi.phoneType == TelephonyManager.PHONE_TYPE_GSM) {
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ if (fi.mPhoneType == TelephonyManager.PHONE_TYPE_GSM) {
type = TYPE.SMS_GSM;
- } else if (fi.phoneType == TelephonyManager.PHONE_TYPE_CDMA) {
+ } else if (fi.mPhoneType == TelephonyManager.PHONE_TYPE_CDMA) {
type = TYPE.SMS_CDMA;
}
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
type = TYPE.MMS;
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ type = TYPE.EMAIL;
}
- if (D) Log.d(TAG, "setType: " + type);
+ if (V) Log.d(TAG, "setType: " + type);
e.setType(type);
}
}
+ private String setRecipientAddressingEmail(BluetoothMapMessageListingElement e, Cursor c, FilterInfo fi) {
+ String toAddress, ccAddress, bccAddress;
+ toAddress = c.getString(fi.mEmailColToAddress);
+ ccAddress = c.getString(fi.mEmailColCcAddress);
+ bccAddress = c.getString(fi.mEmailColBccAddress);
+
+ String address = "";
+ if (toAddress != null) {
+ address += toAddress;
+ if (ccAddress != null) {
+ address += ",";
+ }
+ }
+ if (ccAddress != null) {
+ address += ccAddress;
+ if (bccAddress != null) {
+ address += ",";
+ }
+ }
+ if (bccAddress != null) {
+ address += bccAddress;
+ }
+ return address;
+ }
+
private void setRecipientAddressing(BluetoothMapMessageListingElement e, Cursor c,
FilterInfo fi, BluetoothMapAppParams ap) {
if ((ap.getParameterMask() & MASK_RECIPIENT_ADDRESSING) != 0) {
- String address = "";
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
+ String address = null;
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ int msgType = c.getInt(fi.mSmsColType);
if (msgType == 1) {
- address = fi.phoneNum;
+ address = fi.mPhoneNum;
} else {
address = c.getString(c.getColumnIndex(Sms.ADDRESS));
}
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
address = getAddressMms(mResolver, id, MMS_TO);
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ /* Might be another way to handle addresses */
+ address = setRecipientAddressingEmail(e, c,fi);
}
- if (D) Log.d(TAG, "setRecipientAddressing: " + address);
+ if (V) Log.v(TAG, "setRecipientAddressing: " + address);
+ if(address == null)
+ address = "";
e.setRecipientAddressing(address);
}
}
@@ -514,93 +600,145 @@ public class BluetoothMapContent {
private void setRecipientName(BluetoothMapMessageListingElement e, Cursor c,
FilterInfo fi, BluetoothMapAppParams ap) {
if ((ap.getParameterMask() & MASK_RECIPIENT_NAME) != 0) {
- String name = "";
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
+ String name = null;
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ int msgType = c.getInt(fi.mSmsColType);
if (msgType != 1) {
- String phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
- name = getContactNameFromPhone(phone);
+ String phone = c.getString(fi.mSmsColAddress);
+ if (phone != null && !phone.isEmpty())
+ name = getContactNameFromPhone(phone);
} else {
- name = fi.phoneAlphaTag;
+ name = fi.mPhoneAlphaTag;
}
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
- String phone = getAddressMms(mResolver, id, MMS_TO);
- name = getContactNameFromPhone(phone);
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ long id = c.getLong(fi.mMmsColId);
+ String phone;
+ if(e.getRecipientAddressing() != null){
+ phone = getAddressMms(mResolver, id, MMS_TO);
+ } else {
+ phone = e.getRecipientAddressing();
+ }
+ if (phone != null && !phone.isEmpty())
+ name = getContactNameFromPhone(phone);
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ /* Might be another way to handle address and names */
+ name = setRecipientAddressingEmail(e,c,fi);
}
- if (D) Log.d(TAG, "setRecipientName: " + name);
+ if (V) Log.v(TAG, "setRecipientName: " + name);
+ if(name == null)
+ name = "";
e.setRecipientName(name);
}
}
private void setSenderAddressing(BluetoothMapMessageListingElement e, Cursor c,
- FilterInfo fi, BluetoothMapAppParams ap) {
+ FilterInfo fi, BluetoothMapAppParams ap) {
if ((ap.getParameterMask() & MASK_SENDER_ADDRESSING) != 0) {
String address = null;
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
- if (msgType == 1) {
- address = c.getString(c.getColumnIndex(Sms.ADDRESS));
+ String tempAddress;
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ int msgType = c.getInt(fi.mSmsColType);
+ if (msgType == 1) { // INBOX
+ tempAddress = c.getString(fi.mSmsColAddress);
} else {
- address = fi.phoneNum;
+ tempAddress = fi.mPhoneNum;
}
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
- address = getAddressMms(mResolver, id, MMS_FROM);
+ if(tempAddress == null) {
+ /* This can only happen on devices with no SIM -
+ hence will typically not have any SMS messages. */
+ } else {
+ address = PhoneNumberUtils.extractNetworkPortion(tempAddress);
+ /* extractNetworkPortion can return N if the number is a service "number" = a string
+ * with the a name in (i.e. "Some-Tele-company" would return N because of the N in compaNy)
+ * Hence we need to check if the number is actually a string with alpha chars.
+ * */
+ Boolean alpha = PhoneNumberUtils.stripSeparators(tempAddress).matches("[0-9]*[a-zA-Z]+[0-9]*");
+
+ if(address == null || address.length() < 2 || alpha) {
+ address = tempAddress; // if the number is a service acsii text just use it
+ }
+ }
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ long id = c.getLong(fi.mMmsColId);
+ tempAddress = getAddressMms(mResolver, id, MMS_FROM);
+ address = PhoneNumberUtils.extractNetworkPortion(tempAddress);
+ if(address == null || address.length() < 1){
+ address = tempAddress; // if the number is a service acsii text just use it
+ }
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ address = c.getString(fi.mEmailColFromAddress);
}
- if (D) Log.d(TAG, "setSenderAddressing: " + address);
+ if (V) Log.v(TAG, "setSenderAddressing: " + address);
+ if(address == null)
+ address = "";
e.setSenderAddressing(address);
}
}
private void setSenderName(BluetoothMapMessageListingElement e, Cursor c,
- FilterInfo fi, BluetoothMapAppParams ap) {
+ FilterInfo fi, BluetoothMapAppParams ap) {
if ((ap.getParameterMask() & MASK_SENDER_NAME) != 0) {
- String name = "";
- if (fi.msgType == FilterInfo.TYPE_SMS) {
+ String name = null;
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
if (msgType == 1) {
- String phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
- name = getContactNameFromPhone(phone);
+ String phone = c.getString(fi.mSmsColAddress);
+ if (phone != null && !phone.isEmpty())
+ name = getContactNameFromPhone(phone);
} else {
- name = fi.phoneAlphaTag;
+ name = fi.mPhoneAlphaTag;
}
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
- String phone = getAddressMms(mResolver, id, MMS_FROM);
- name = getContactNameFromPhone(phone);
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ long id = c.getLong(fi.mMmsColId);
+ String phone;
+ if(e.getSenderAddressing() != null){
+ phone = getAddressMms(mResolver, id, MMS_FROM);
+ } else {
+ phone = e.getSenderAddressing();
+ }
+ if (phone != null && !phone.isEmpty() )
+ name = getContactNameFromPhone(phone);
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ name = c.getString(fi.mEmailColFromAddress);
}
- if (D) Log.d(TAG, "setSenderName: " + name);
+ if (V) Log.v(TAG, "setSenderName: " + name);
+ if(name == null)
+ name = "";
e.setSenderName(name);
}
}
private void setDateTime(BluetoothMapMessageListingElement e, Cursor c,
- FilterInfo fi, BluetoothMapAppParams ap) {
- long date = 0;
-
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- date = c.getLong(c.getColumnIndex(Sms.DATE));
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- /* Use Mms.DATE for all messages. Although contract class states */
- /* Mms.DATE_SENT are for outgoing messages. But that is not working. */
- date = c.getLong(c.getColumnIndex(Mms.DATE)) * 1000L;
-
- /* int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); */
- /* if (msgBox == Mms.MESSAGE_BOX_INBOX) { */
- /* date = c.getLong(c.getColumnIndex(Mms.DATE)) * 1000L; */
- /* } else { */
- /* date = c.getLong(c.getColumnIndex(Mms.DATE_SENT)) * 1000L; */
- /* } */
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ if ((ap.getParameterMask() & MASK_DATETIME) != 0) {
+ long date = 0;
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ date = c.getLong(fi.mSmsColDate);
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ /* Use Mms.DATE for all messages. Although contract class states */
+ /* Mms.DATE_SENT are for outgoing messages. But that is not working. */
+ date = c.getLong(fi.mMmsColDate) * 1000L;
+
+ /* int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); */
+ /* if (msgBox == Mms.MESSAGE_BOX_INBOX) { */
+ /* date = c.getLong(c.getColumnIndex(Mms.DATE)) * 1000L; */
+ /* } else { */
+ /* date = c.getLong(c.getColumnIndex(Mms.DATE_SENT)) * 1000L; */
+ /* } */
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ date = c.getLong(fi.mEmailColDate);
+ }
+ e.setDateTime(date);
+ if (V) Log.v(TAG, "setDateTime: " + e.getDateTimeString());
}
- e.setDateTime(date);
}
private String getTextPartsMms(long id) {
String text = "";
String selection = new String("mid=" + id);
- String uriStr = String.format("content://mms/%d/part", id);
+ String uriStr = new String(Mms.CONTENT_URI + "/" + id + "/part");
Uri uriAddress = Uri.parse(uriStr);
+ // TODO: maybe use a projection with only "ct" and "text"
Cursor c = mResolver.query(uriAddress, null, selection,
null, null);
@@ -608,7 +746,10 @@ public class BluetoothMapContent {
do {
String ct = c.getString(c.getColumnIndex("ct"));
if (ct.equals("text/plain")) {
- text += c.getString(c.getColumnIndex("text"));
+ String part = c.getString(c.getColumnIndex("text"));
+ if(part != null) {
+ text += part;
+ }
}
} while(c.moveToNext());
}
@@ -619,74 +760,63 @@ public class BluetoothMapContent {
}
private void setSubject(BluetoothMapMessageListingElement e, Cursor c,
- FilterInfo fi, BluetoothMapAppParams ap) {
+ FilterInfo fi, BluetoothMapAppParams ap) {
String subject = "";
int subLength = ap.getSubjectLength();
if(subLength == BluetoothMapAppParams.INVALID_VALUE_PARAMETER)
subLength = 256;
if ((ap.getParameterMask() & MASK_SUBJECT) != 0) {
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- subject = c.getString(c.getColumnIndex(Sms.BODY));
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- subject = c.getString(c.getColumnIndex(Mms.SUBJECT));
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ subject = c.getString(fi.mSmsColSubject);
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ subject = c.getString(fi.mMmsColSubject);
if (subject == null || subject.length() == 0) {
/* Get subject from mms text body parts - if any exists */
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ long id = c.getLong(fi.mMmsColId);
subject = getTextPartsMms(id);
}
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ subject = c.getString(fi.mEmailColSubject);
}
- if (subject != null) {
- subject = subject.substring(0, Math.min(subject.length(),
- subLength));
+ if (subject != null && subject.length() > subLength) {
+ subject = subject.substring(0, subLength);
}
- if (D) Log.d(TAG, "setSubject: " + subject);
+ if (V) Log.d(TAG, "setSubject: " + subject);
e.setSubject(subject);
}
}
private void setHandle(BluetoothMapMessageListingElement e, Cursor c,
- FilterInfo fi, BluetoothMapAppParams ap) {
- long handle = c.getLong(c.getColumnIndex(BaseColumns._ID));
- TYPE type = null;
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- if (fi.phoneType == TelephonyManager.PHONE_TYPE_GSM) {
- type = TYPE.SMS_GSM;
- } else if (fi.phoneType == TelephonyManager.PHONE_TYPE_CDMA) {
- type = TYPE.SMS_CDMA;
- }
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- type = TYPE.MMS;
- }
- if (D) Log.d(TAG, "setHandle: " + handle + " - Type: " + type.name());
- e.setHandle(handle, type);
+ FilterInfo fi, BluetoothMapAppParams ap) {
+ long handle = -1;
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ handle = c.getLong(fi.mSmsColId);
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ handle = c.getLong(fi.mMmsColId);
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ handle = c.getLong(fi.mEmailColId);
+ }
+ if (V) Log.d(TAG, "setHandle: " + handle );
+ e.setHandle(handle);
}
private BluetoothMapMessageListingElement element(Cursor c, FilterInfo fi,
- BluetoothMapAppParams ap) {
+ BluetoothMapAppParams ap) {
BluetoothMapMessageListingElement e = new BluetoothMapMessageListingElement();
-
setHandle(e, c, fi, ap);
- setSubject(e, c, fi, ap);
setDateTime(e, c, fi, ap);
- setSenderName(e, c, fi, ap);
- setSenderAddressing(e, c, fi, ap);
- setRecipientName(e, c, fi, ap);
- setRecipientAddressing(e, c, fi, ap);
setType(e, c, fi, ap);
- setSize(e, c, fi, ap);
- setReceptionStatus(e, c, fi, ap);
- setText(e, c, fi, ap);
- setAttachmentSize(e, c, fi, ap);
- setPriority(e, c, fi, ap);
setRead(e, c, fi, ap);
- setSent(e, c, fi, ap);
- setProtected(e, c, fi, ap);
+ // we set number and name for sender/recipient later
+ // they require lookup on contacts so no need to
+ // do it for all elements unless they are to be used.
+ e.setCursorIndex(c.getPosition());
return e;
}
private String getContactNameFromPhone(String phone) {
- String name = "";
+ String name = null;
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
Uri.encode(phone));
@@ -708,13 +838,15 @@ public class BluetoothMapContent {
static public String getAddressMms(ContentResolver r, long id, int type) {
String selection = new String("msg_id=" + id + " AND type=" + type);
- String uriStr = String.format("content://mms/%d/addr", id);
+ String uriStr = new String(Mms.CONTENT_URI + "/" + id + "/addr");
Uri uriAddress = Uri.parse(uriStr);
String addr = null;
Cursor c = r.query(uriAddress, null, selection, null, null);
if (c != null && c.moveToFirst()) {
- addr = c.getString(c.getColumnIndex("address"));
+ addr = c.getString(c.getColumnIndex(Mms.Addr.ADDRESS));
+ if(addr.equals(INSERT_ADDRES_TOKEN))
+ addr = "";
}
if (c != null) {
@@ -723,18 +855,22 @@ public class BluetoothMapContent {
return addr;
}
+ /**
+ * Matching functions for originator and recipient for MMS
+ * @return true if found a match
+ */
private boolean matchRecipientMms(Cursor c, FilterInfo fi, String recip) {
boolean res;
long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
String phone = getAddressMms(mResolver, id, MMS_TO);
if (phone != null && phone.length() > 0) {
if (phone.matches(recip)) {
- if (D) Log.d(TAG, "match recipient phone = " + phone);
+ if (V) Log.v(TAG, "matchRecipientMms: match recipient phone = " + phone);
res = true;
} else {
String name = getContactNameFromPhone(phone);
if (name != null && name.length() > 0 && name.matches(recip)) {
- if (D) Log.d(TAG, "match recipient name = " + name);
+ if (V) Log.v(TAG, "matchRecipientMms: match recipient name = " + name);
res = true;
} else {
res = false;
@@ -750,28 +886,27 @@ public class BluetoothMapContent {
boolean res;
int msgType = c.getInt(c.getColumnIndex(Sms.TYPE));
if (msgType == 1) {
- String phone = fi.phoneNum;
- String name = fi.phoneAlphaTag;
+ String phone = fi.mPhoneNum;
+ String name = fi.mPhoneAlphaTag;
if (phone != null && phone.length() > 0 && phone.matches(recip)) {
- if (D) Log.d(TAG, "match recipient phone = " + phone);
+ if (V) Log.v(TAG, "matchRecipientSms: match recipient phone = " + phone);
res = true;
} else if (name != null && name.length() > 0 && name.matches(recip)) {
- if (D) Log.d(TAG, "match recipient name = " + name);
+ if (V) Log.v(TAG, "matchRecipientSms: match recipient name = " + name);
res = true;
} else {
res = false;
}
- }
- else {
+ } else {
String phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
if (phone != null && phone.length() > 0) {
if (phone.matches(recip)) {
- if (D) Log.d(TAG, "match recipient phone = " + phone);
+ if (V) Log.v(TAG, "matchRecipientSms: match recipient phone = " + phone);
res = true;
} else {
String name = getContactNameFromPhone(phone);
if (name != null && name.length() > 0 && name.matches(recip)) {
- if (D) Log.d(TAG, "match recipient name = " + name);
+ if (V) Log.v(TAG, "matchRecipientSms: match recipient name = " + name);
res = true;
} else {
res = false;
@@ -790,12 +925,12 @@ public class BluetoothMapContent {
if (recip != null && recip.length() > 0) {
recip = recip.replace("*", ".*");
recip = ".*" + recip + ".*";
- if (fi.msgType == FilterInfo.TYPE_SMS) {
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
res = matchRecipientSms(c, fi, recip);
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
res = matchRecipientMms(c, fi, recip);
} else {
- if (D) Log.d(TAG, "Unknown msg type: " + fi.msgType);
+ if (D) Log.d(TAG, "matchRecipient: Unknown msg type: " + fi.mMsgType);
res = false;
}
} else {
@@ -810,12 +945,12 @@ public class BluetoothMapContent {
String phone = getAddressMms(mResolver, id, MMS_FROM);
if (phone != null && phone.length() > 0) {
if (phone.matches(orig)) {
- if (D) Log.d(TAG, "match originator phone = " + phone);
+ if (V) Log.v(TAG, "matchOriginatorMms: match originator phone = " + phone);
res = true;
} else {
String name = getContactNameFromPhone(phone);
if (name != null && name.length() > 0 && name.matches(orig)) {
- if (D) Log.d(TAG, "match originator name = " + name);
+ if (V) Log.v(TAG, "matchOriginatorMms: match originator name = " + name);
res = true;
} else {
res = false;
@@ -834,12 +969,12 @@ public class BluetoothMapContent {
String phone = c.getString(c.getColumnIndex(Sms.ADDRESS));
if (phone !=null && phone.length() > 0) {
if (phone.matches(orig)) {
- if (D) Log.d(TAG, "match originator phone = " + phone);
+ if (V) Log.v(TAG, "matchOriginatorSms: match originator phone = " + phone);
res = true;
} else {
String name = getContactNameFromPhone(phone);
if (name != null && name.length() > 0 && name.matches(orig)) {
- if (D) Log.d(TAG, "match originator name = " + name);
+ if (V) Log.v(TAG, "matchOriginatorSms: match originator name = " + name);
res = true;
} else {
res = false;
@@ -848,15 +983,14 @@ public class BluetoothMapContent {
} else {
res = false;
}
- }
- else {
- String phone = fi.phoneNum;
- String name = fi.phoneAlphaTag;
+ } else {
+ String phone = fi.mPhoneNum;
+ String name = fi.mPhoneAlphaTag;
if (phone != null && phone.length() > 0 && phone.matches(orig)) {
- if (D) Log.d(TAG, "match originator phone = " + phone);
+ if (V) Log.v(TAG, "matchOriginatorSms: match originator phone = " + phone);
res = true;
} else if (name != null && name.length() > 0 && name.matches(orig)) {
- if (D) Log.d(TAG, "match originator name = " + name);
+ if (V) Log.v(TAG, "matchOriginatorSms: match originator name = " + name);
res = true;
} else {
res = false;
@@ -871,12 +1005,12 @@ public class BluetoothMapContent {
if (orig != null && orig.length() > 0) {
orig = orig.replace("*", ".*");
orig = ".*" + orig + ".*";
- if (fi.msgType == FilterInfo.TYPE_SMS) {
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
res = matchOriginatorSms(c, fi, orig);
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
res = matchOriginatorMms(c, fi, orig);
} else {
- Log.d(TAG, "Unknown msg type: " + fi.msgType);
+ if(D) Log.d(TAG, "matchOriginator: Unknown msg type: " + fi.mMsgType);
res = false;
}
} else {
@@ -893,22 +1027,22 @@ public class BluetoothMapContent {
}
}
+ /*
+ * Where filter functions
+ * */
private String setWhereFilterFolderTypeSms(String folder) {
String where = "";
- if ("inbox".equalsIgnoreCase(folder)) {
- where = "type = 1 AND thread_id <> -1";
- }
- else if ("outbox".equalsIgnoreCase(folder)) {
- where = "(type = 4 OR type = 5 OR type = 6) AND thread_id <> -1";
- }
- else if ("sent".equalsIgnoreCase(folder)) {
- where = "type = 2 AND thread_id <> -1";
- }
- else if ("draft".equalsIgnoreCase(folder)) {
- where = "type = 3 AND thread_id <> -1";
- }
- else if ("deleted".equalsIgnoreCase(folder)) {
- where = "thread_id = -1";
+ if (BluetoothMapContract.FOLDER_NAME_INBOX.equalsIgnoreCase(folder)) {
+ where = Sms.TYPE + " = 1 AND " + Sms.THREAD_ID + " <> -1";
+ } else if (BluetoothMapContract.FOLDER_NAME_OUTBOX.equalsIgnoreCase(folder)) {
+ where = "(" + Sms.TYPE + " = 4 OR " + Sms.TYPE + " = 5 OR "
+ + Sms.TYPE + " = 6) AND " + Sms.THREAD_ID + " <> -1";
+ } else if (BluetoothMapContract.FOLDER_NAME_SENT.equalsIgnoreCase(folder)) {
+ where = Sms.TYPE + " = 2 AND " + Sms.THREAD_ID + " <> -1";
+ } else if (BluetoothMapContract.FOLDER_NAME_DRAFT.equalsIgnoreCase(folder)) {
+ where = Sms.TYPE + " = 3 AND " + Sms.THREAD_ID + " <> -1";
+ } else if (BluetoothMapContract.FOLDER_NAME_DELETED.equalsIgnoreCase(folder)) {
+ where = Sms.THREAD_ID + " = -1";
}
return where;
@@ -916,69 +1050,99 @@ public class BluetoothMapContent {
private String setWhereFilterFolderTypeMms(String folder) {
String where = "";
- if ("inbox".equalsIgnoreCase(folder)) {
- where = "msg_box = 1 AND thread_id <> -1";
- }
- else if ("outbox".equalsIgnoreCase(folder)) {
- where = "msg_box = 4 AND thread_id <> -1";
- }
- else if ("sent".equalsIgnoreCase(folder)) {
- where = "msg_box = 2 AND thread_id <> -1";
- }
- else if ("draft".equalsIgnoreCase(folder)) {
- where = "msg_box = 3 AND thread_id <> -1";
- }
- else if ("deleted".equalsIgnoreCase(folder)) {
- where = "thread_id = -1";
+ if (BluetoothMapContract.FOLDER_NAME_INBOX.equalsIgnoreCase(folder)) {
+ where = Mms.MESSAGE_BOX + " = 1 AND " + Mms.THREAD_ID + " <> -1";
+ } else if (BluetoothMapContract.FOLDER_NAME_OUTBOX.equalsIgnoreCase(folder)) {
+ where = Mms.MESSAGE_BOX + " = 4 AND " + Mms.THREAD_ID + " <> -1";
+ } else if (BluetoothMapContract.FOLDER_NAME_SENT.equalsIgnoreCase(folder)) {
+ where = Mms.MESSAGE_BOX + " = 2 AND " + Mms.THREAD_ID + " <> -1";
+ } else if (BluetoothMapContract.FOLDER_NAME_DRAFT.equalsIgnoreCase(folder)) {
+ where = Mms.MESSAGE_BOX + " = 3 AND " + Mms.THREAD_ID + " <> -1";
+ } else if (BluetoothMapContract.FOLDER_NAME_DELETED.equalsIgnoreCase(folder)) {
+ where = Mms.THREAD_ID + " = -1";
}
return where;
}
- private String setWhereFilterFolderType(String folder, FilterInfo fi) {
+ private String setWhereFilterFolderTypeEmail(long folderId) {
String where = "";
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- where = setWhereFilterFolderTypeSms(folder);
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- where = setWhereFilterFolderTypeMms(folder);
+ if (folderId >= 0) {
+ where = BluetoothMapContract.MessageColumns.FOLDER_ID + " = " + folderId;
+ } else {
+ Log.e(TAG, "setWhereFilterFolderTypeEmail: not valid!" );
+ throw new IllegalArgumentException("Invalid folder ID");
}
+ return where;
+ }
+ private String setWhereFilterFolderType(BluetoothMapFolderElement folderElement, FilterInfo fi) {
+ String where = "";
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ where = setWhereFilterFolderTypeSms(folderElement.getName());
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ where = setWhereFilterFolderTypeMms(folderElement.getName());
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ where = setWhereFilterFolderTypeEmail(folderElement.getEmailFolderId());
+ }
return where;
}
- private String setWhereFilterReadStatus(BluetoothMapAppParams ap) {
+ private String setWhereFilterReadStatus(BluetoothMapAppParams ap, FilterInfo fi) {
String where = "";
if (ap.getFilterReadStatus() != -1) {
- if ((ap.getFilterReadStatus() & 0x01) != 0) {
- where = " AND read=0 ";
- }
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ if ((ap.getFilterReadStatus() & 0x01) != 0) {
+ where = " AND " + Sms.READ + "= 0";
+ }
- if ((ap.getFilterReadStatus() & 0x02) != 0) {
- where = " AND read=1 ";
+ if ((ap.getFilterReadStatus() & 0x02) != 0) {
+ where = " AND " + Sms.READ + "= 1";
+ }
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ if ((ap.getFilterReadStatus() & 0x01) != 0) {
+ where = " AND " + Mms.READ + "= 0";
+ }
+
+ if ((ap.getFilterReadStatus() & 0x02) != 0) {
+ where = " AND " + Mms.READ + "= 1";
+ }
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ if ((ap.getFilterReadStatus() & 0x01) != 0) {
+ where = " AND " + BluetoothMapContract.MessageColumns.FLAG_READ + "= 0";
+ }
+
+ if ((ap.getFilterReadStatus() & 0x02) != 0) {
+ where = " AND " + BluetoothMapContract.MessageColumns.FLAG_READ + "= 1";
+ }
}
}
-
return where;
}
private String setWhereFilterPeriod(BluetoothMapAppParams ap, FilterInfo fi) {
String where = "";
if ((ap.getFilterPeriodBegin() != -1)) {
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- where = " AND date >= " + ap.getFilterPeriodBegin();
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- where = " AND date >= " + (ap.getFilterPeriodBegin() / 1000L);
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ where = " AND " + Sms.DATE + " >= " + ap.getFilterPeriodBegin();
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ where = " AND " + Mms.DATE + " >= " + (ap.getFilterPeriodBegin() / 1000L);
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ where = " AND " + BluetoothMapContract.MessageColumns.DATE + " >= " + (ap.getFilterPeriodBegin());
}
}
if ((ap.getFilterPeriodEnd() != -1)) {
- if (fi.msgType == FilterInfo.TYPE_SMS) {
- where += " AND date < " + ap.getFilterPeriodEnd();
- } else if (fi.msgType == FilterInfo.TYPE_MMS) {
- where += " AND date < " + (ap.getFilterPeriodEnd() / 1000L);
+ if (fi.mMsgType == FilterInfo.TYPE_SMS) {
+ where += " AND " + Sms.DATE + " < " + ap.getFilterPeriodEnd();
+ } else if (fi.mMsgType == FilterInfo.TYPE_MMS) {
+ where += " AND " + Mms.DATE + " < " + (ap.getFilterPeriodEnd() / 1000L);
+ } else if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ where += " AND " + BluetoothMapContract.MessageColumns.DATE + " < " + (ap.getFilterPeriodEnd());
}
}
+
return where;
}
@@ -1025,46 +1189,22 @@ public class BluetoothMapContent {
return where;
}
- private String setWhereFilterOriginator(BluetoothMapAppParams ap,
- FilterInfo fi) {
+ private String setWhereFilterOriginatorEmail(BluetoothMapAppParams ap) {
String where = "";
String orig = ap.getFilterOriginator();
+ /* Be aware of wild cards in the beginning of string, may not be valid? */
if (orig != null && orig.length() > 0) {
- String phones = setWhereFilterPhones(orig);
-
- if (phones.length() > 0) {
- where = " AND ((type <> 1) OR ( " + phones + " ))";
- } else {
- where = " AND (type <> 1)";
- }
-
- orig = orig.replace("*", ".*");
- orig = ".*" + orig + ".*";
-
- boolean localPhoneMatchOrig = false;
- if (fi.phoneNum != null && fi.phoneNum.length() > 0
- && fi.phoneNum.matches(orig)) {
- localPhoneMatchOrig = true;
- }
-
- if (fi.phoneAlphaTag != null && fi.phoneAlphaTag.length() > 0
- && fi.phoneAlphaTag.matches(orig)) {
- localPhoneMatchOrig = true;
- }
-
- if (!localPhoneMatchOrig) {
- where += " AND (type = 1)";
- }
+ orig = orig.replace("*", "%");
+ where = " AND " + BluetoothMapContract.MessageColumns.FROM_LIST + " LIKE '%" + orig + "%'";
}
-
return where;
}
private String setWhereFilterPriority(BluetoothMapAppParams ap, FilterInfo fi) {
String where = "";
int pri = ap.getFilterPriority();
/*only MMS have priority info */
- if(fi.msgType == FilterInfo.TYPE_MMS)
+ if(fi.mMsgType == FilterInfo.TYPE_MMS)
{
if(pri == 0x0002)
{
@@ -1078,63 +1218,57 @@ public class BluetoothMapContent {
return where;
}
- private String setWhereFilterRecipient(BluetoothMapAppParams ap,
- FilterInfo fi) {
+ private String setWhereFilterRecipientEmail(BluetoothMapAppParams ap) {
String where = "";
String recip = ap.getFilterRecipient();
+ /* Be aware of wild cards in the beginning of string, may not be valid? */
if (recip != null && recip.length() > 0) {
- String phones = setWhereFilterPhones(recip);
-
- if (phones.length() > 0) {
- where = " AND ((type = 1) OR ( " + phones + " ))";
- } else {
- where = " AND (type = 1)";
- }
-
- recip = recip.replace("*", ".*");
- recip = ".*" + recip + ".*";
-
- boolean localPhoneMatchOrig = false;
- if (fi.phoneNum != null && fi.phoneNum.length() > 0
- && fi.phoneNum.matches(recip)) {
- localPhoneMatchOrig = true;
- }
-
- if (fi.phoneAlphaTag != null && fi.phoneAlphaTag.length() > 0
- && fi.phoneAlphaTag.matches(recip)) {
- localPhoneMatchOrig = true;
- }
-
- if (!localPhoneMatchOrig) {
- where += " AND (type <> 1)";
- }
+ recip = recip.replace("*", "%");
+ where = " AND ("
+ + BluetoothMapContract.MessageColumns.TO_LIST + " LIKE '%" + recip + "%' OR "
+ + BluetoothMapContract.MessageColumns.CC_LIST + " LIKE '%" + recip + "%' OR "
+ + BluetoothMapContract.MessageColumns.BCC_LIST + " LIKE '%" + recip + "%' )";
}
-
return where;
}
- private String setWhereFilter(String folder, FilterInfo fi, BluetoothMapAppParams ap) {
+ private String setWhereFilter(BluetoothMapFolderElement folderElement,
+ FilterInfo fi, BluetoothMapAppParams ap) {
String where = "";
- where += setWhereFilterFolderType(folder, fi);
- where += setWhereFilterReadStatus(ap);
- where += setWhereFilterPeriod(ap, fi);
- where += setWhereFilterPriority(ap,fi);
- /* where += setWhereFilterOriginator(ap, fi); */
- /* where += setWhereFilterRecipient(ap, fi); */
+ where += setWhereFilterFolderType(folderElement, fi);
+ if(!where.isEmpty()) {
+ where += setWhereFilterReadStatus(ap, fi);
+ where += setWhereFilterPeriod(ap, fi);
+ where += setWhereFilterPriority(ap,fi);
+
+ if (fi.mMsgType == FilterInfo.TYPE_EMAIL) {
+ where += setWhereFilterOriginatorEmail(ap);
+ where += setWhereFilterRecipientEmail(ap);
+ }
+ }
- if (D) Log.d(TAG, "where: " + where);
return where;
}
+ /**
+ * Determine from application parameter if sms should be included.
+ * The filter mask is set for message types not selected
+ * @param fi
+ * @param ap
+ * @return boolean true if sms is selected, false if not
+ */
private boolean smsSelected(FilterInfo fi, BluetoothMapAppParams ap) {
int msgType = ap.getFilterMessageType();
- int phoneType = fi.phoneType;
+ int phoneType = fi.mPhoneType;
+
+ if (D) Log.d(TAG, "smsSelected msgType: " + msgType);
if (msgType == -1)
return true;
+
if ((msgType & 0x03) == 0)
return true;
@@ -1147,9 +1281,18 @@ public class BluetoothMapContent {
return false;
}
+ /**
+ * Determine from application parameter if mms should be included.
+ * The filter mask is set for message types not selected
+ * @param fi
+ * @param ap
+ * @return boolean true if sms is selected, false if not
+ */
private boolean mmsSelected(FilterInfo fi, BluetoothMapAppParams ap) {
int msgType = ap.getFilterMessageType();
+ if (D) Log.d(TAG, "mmsSelected msgType: " + msgType);
+
if (msgType == -1)
return true;
@@ -1159,98 +1302,238 @@ public class BluetoothMapContent {
return false;
}
+ /**
+ * Determine from application parameter if email should be included.
+ * The filter mask is set for message types not selected
+ * @param fi
+ * @param ap
+ * @return boolean true if sms is selected, false if not
+ */
+ private boolean emailSelected(FilterInfo fi, BluetoothMapAppParams ap) {
+ int msgType = ap.getFilterMessageType();
+
+ if (D) Log.d(TAG, "emailSelected msgType: " + msgType);
+
+ if (msgType == -1)
+ return true;
+
+ if ((msgType & 0x04) == 0)
+ return true;
+
+ return false;
+ }
+
private void setFilterInfo(FilterInfo fi) {
TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
if (tm != null) {
- fi.phoneType = tm.getPhoneType();
- fi.phoneNum = tm.getLine1Number();
- fi.phoneAlphaTag = tm.getLine1AlphaTag();
- if (D) Log.d(TAG, "phone type = " + fi.phoneType +
- " phone num = " + fi.phoneNum +
- " phone alpha tag = " + fi.phoneAlphaTag);
+ fi.mPhoneType = tm.getPhoneType();
+ fi.mPhoneNum = tm.getLine1Number();
+ fi.mPhoneAlphaTag = tm.getLine1AlphaTag();
+ if (D) Log.d(TAG, "phone type = " + fi.mPhoneType +
+ " phone num = " + fi.mPhoneNum +
+ " phone alpha tag = " + fi.mPhoneAlphaTag);
}
}
- public BluetoothMapMessageListing msgListing(String folder, BluetoothMapAppParams ap) {
- Log.d(TAG, "msgListing: folder = " + folder);
+ /**
+ * Get a listing of message in folder after applying filter.
+ * @param folder Must contain a valid folder string != null
+ * @param ap Parameters specifying message content and filters
+ * @return Listing object containing requested messages
+ */
+ public BluetoothMapMessageListing msgListing(BluetoothMapFolderElement folderElement,
+ BluetoothMapAppParams ap) {
+ if (D) Log.d(TAG, "msgListing: folderName = " + folderElement.getName()
+ + " folderId = " + folderElement.getEmailFolderId()
+ + " messageType = " + ap.getFilterMessageType() );
BluetoothMapMessageListing bmList = new BluetoothMapMessageListing();
- BluetoothMapMessageListingElement e = null;
+
/* We overwrite the parameter mask here if it is 0 or not present, as this
* should cause all parameters to be included in the message list. */
if(ap.getParameterMask() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER ||
ap.getParameterMask() == 0) {
ap.setParameterMask(BluetoothMapAppParams.PARAMETER_MASK_ALL_ENABLED);
- if (V) Log.w(TAG, "msgListing(): appParameterMask is zero or not present, " +
+ if (V) Log.v(TAG, "msgListing(): appParameterMask is zero or not present, " +
"changing to: " + ap.getParameterMask());
}
/* Cache some info used throughout filtering */
FilterInfo fi = new FilterInfo();
setFilterInfo(fi);
-
- if (smsSelected(fi, ap)) {
- fi.msgType = FilterInfo.TYPE_SMS;
+ Cursor smsCursor = null;
+ Cursor mmsCursor = null;
+ Cursor emailCursor = null;
+ String limit = "";
+ int countNum = ap.getMaxListCount();
+ int offsetNum = ap.getStartOffset();
+ if(ap.getMaxListCount()>0){
+ limit=" LIMIT "+ (ap.getMaxListCount()+ap.getStartOffset());
+ }
+
+ if (smsSelected(fi, ap) && folderElement.hasSmsMmsContent()) {
+ if(ap.getFilterMessageType() == (BluetoothMapAppParams.FILTER_NO_EMAIL|
+ BluetoothMapAppParams.FILTER_NO_MMS|
+ BluetoothMapAppParams.FILTER_NO_SMS_GSM)||
+ ap.getFilterMessageType() == (BluetoothMapAppParams.FILTER_NO_EMAIL|
+ BluetoothMapAppParams.FILTER_NO_MMS|
+ BluetoothMapAppParams.FILTER_NO_SMS_CDMA)){
+ //set real limit and offset if only this type is used (only if offset/limit is used
+ limit = " LIMIT " + ap.getMaxListCount()+" OFFSET "+ ap.getStartOffset();
+ if(D) Log.d(TAG, "SMS Limit => "+limit);
+ offsetNum = 0;
+ }
+ fi.mMsgType = FilterInfo.TYPE_SMS;
if(ap.getFilterPriority() != 1){ /*SMS cannot have high priority*/
- String where = setWhereFilter(folder, fi, ap);
-
- Cursor c = mResolver.query(Sms.CONTENT_URI,
- SMS_PROJECTION, where, null, "date DESC");
+ String where = setWhereFilter(folderElement, fi, ap);
+ if(!where.isEmpty()) {
+ if (D) Log.d(TAG, "msgType: " + fi.mMsgType);
+ smsCursor = mResolver.query(Sms.CONTENT_URI,
+ SMS_PROJECTION, where, null, Sms.DATE + " DESC" + limit);
+ if (smsCursor != null) {
+ BluetoothMapMessageListingElement e = null;
+ // store column index so we dont have to look them up anymore (optimization)
+ if(D) Log.d(TAG, "Found " + smsCursor.getCount() + " sms messages.");
+ fi.setSmsColumns(smsCursor);
+ while (smsCursor.moveToNext()) {
+ if (matchAddresses(smsCursor, fi, ap)) {
+ if(V) printSms(smsCursor);
+ e = element(smsCursor, fi, ap);
+ bmList.add(e);
+ }
+ }
+ }
+ }
+ }
+ }
- if (c != null) {
- while (c.moveToNext()) {
- if (matchAddresses(c, fi, ap)) {
- printSms(c);
- e = element(c, fi, ap);
+ if (mmsSelected(fi, ap) && folderElement.hasSmsMmsContent()) {
+ if(ap.getFilterMessageType() == (BluetoothMapAppParams.FILTER_NO_EMAIL|
+ BluetoothMapAppParams.FILTER_NO_SMS_CDMA|
+ BluetoothMapAppParams.FILTER_NO_SMS_GSM)){
+ //set real limit and offset if only this type is used (only if offset/limit is used
+ limit = " LIMIT " + ap.getMaxListCount()+" OFFSET "+ ap.getStartOffset();
+ if(D) Log.d(TAG, "MMS Limit => "+limit);
+ offsetNum = 0;
+ }
+ fi.mMsgType = FilterInfo.TYPE_MMS;
+ String where = setWhereFilter(folderElement, fi, ap);
+ if(!where.isEmpty()) {
+ if (D) Log.d(TAG, "msgType: " + fi.mMsgType);
+ mmsCursor = mResolver.query(Mms.CONTENT_URI,
+ MMS_PROJECTION, where, null, Mms.DATE + " DESC" + limit);
+ if (mmsCursor != null) {
+ BluetoothMapMessageListingElement e = null;
+ // store column index so we dont have to look them up anymore (optimization)
+ fi.setMmsColumns(mmsCursor);
+ int cnt = 0;
+ if(D) Log.d(TAG, "Found " + mmsCursor.getCount() + " mms messages.");
+ while (mmsCursor.moveToNext()) {
+ if (matchAddresses(mmsCursor, fi, ap)) {
+ if(V) printMms(mmsCursor);
+ e = element(mmsCursor, fi, ap);
bmList.add(e);
}
}
- c.close();
}
}
}
- if (mmsSelected(fi, ap)) {
- fi.msgType = FilterInfo.TYPE_MMS;
-
- String where = setWhereFilter(folder, fi, ap);
-
- Cursor c = mResolver.query(Mms.CONTENT_URI,
- MMS_PROJECTION, where, null, "date DESC");
-
- if (c != null) {
- int cnt = 0;
- while (c.moveToNext()) {
- if (matchAddresses(c, fi, ap)) {
- printMms(c);
- e = element(c, fi, ap);
+ if (emailSelected(fi, ap) && folderElement.getEmailFolderId() != -1) {
+ if(ap.getFilterMessageType() == (BluetoothMapAppParams.FILTER_NO_MMS|
+ BluetoothMapAppParams.FILTER_NO_SMS_CDMA|
+ BluetoothMapAppParams.FILTER_NO_SMS_GSM)){
+ //set real limit and offset if only this type is used (only if offset/limit is used
+ limit = " LIMIT " + ap.getMaxListCount()+" OFFSET "+ ap.getStartOffset();
+ if(D) Log.d(TAG, "Email Limit => "+limit);
+ offsetNum = 0;
+ }
+ fi.mMsgType = FilterInfo.TYPE_EMAIL;
+ String where = setWhereFilter(folderElement, fi, ap);
+
+ if(!where.isEmpty()) {
+ if (D) Log.d(TAG, "msgType: " + fi.mMsgType);
+ Uri contentUri = Uri.parse(mBaseEmailUri + BluetoothMapContract.TABLE_MESSAGE);
+ emailCursor = mResolver.query(contentUri, BluetoothMapContract.BT_MESSAGE_PROJECTION,
+ where, null, BluetoothMapContract.MessageColumns.DATE + " DESC" + limit);
+ if (emailCursor != null) {
+ BluetoothMapMessageListingElement e = null;
+ // store column index so we dont have to look them up anymore (optimization)
+ fi.setEmailColumns(emailCursor);
+ int cnt = 0;
+ while (emailCursor.moveToNext()) {
+ if(V) printEmail(emailCursor);
+ if(D) Log.d(TAG, "Found " + emailCursor.getCount() + " email messages.");
+ e = element(emailCursor, fi, ap);
bmList.add(e);
}
+ // emailCursor.close();
}
- c.close();
}
}
/* Enable this if post sorting and segmenting needed */
bmList.sort();
- bmList.segment(ap.getMaxListCount(), ap.getStartOffset());
-
+ bmList.segment(ap.getMaxListCount(), offsetNum);
+ List<BluetoothMapMessageListingElement> list = bmList.getList();
+ int listSize = list.size();
+ Cursor tmpCursor = null;
+ for(int x=0;x<listSize;x++){
+ BluetoothMapMessageListingElement ele = list.get(x);
+ if((ele.getType().equals(TYPE.SMS_GSM)||ele.getType().equals(TYPE.SMS_CDMA)) && smsCursor != null){
+ tmpCursor = smsCursor;
+ fi.mMsgType = FilterInfo.TYPE_SMS;
+ }else if(ele.getType().equals(TYPE.MMS) && mmsCursor != null){
+ tmpCursor = mmsCursor;
+ fi.mMsgType = FilterInfo.TYPE_MMS;
+ }else if(ele.getType().equals(TYPE.EMAIL) && emailCursor != null){
+ tmpCursor = emailCursor;
+ fi.mMsgType = FilterInfo.TYPE_EMAIL;
+ }
+ if(tmpCursor != null){
+ tmpCursor.moveToPosition(ele.getCursorIndex());
+ setSenderAddressing(ele, tmpCursor, fi, ap);
+ setSenderName(ele, tmpCursor, fi, ap);
+ setRecipientAddressing(ele, tmpCursor, fi, ap);
+ setRecipientName(ele, tmpCursor, fi, ap);
+ setSubject(ele, tmpCursor, fi, ap);
+ setSize(ele, tmpCursor, fi, ap);
+ setReceptionStatus(ele, tmpCursor, fi, ap);
+ setText(ele, tmpCursor, fi, ap);
+ setAttachmentSize(ele, tmpCursor, fi, ap);
+ setPriority(ele, tmpCursor, fi, ap);
+ setSent(ele, tmpCursor, fi, ap);
+ setProtected(ele, tmpCursor, fi, ap);
+ setThreadId(ele, tmpCursor, fi, ap);
+ }
+ }
+ if(emailCursor != null)emailCursor.close();
+ if(smsCursor != null)smsCursor.close();
+ if(mmsCursor != null)mmsCursor.close();
+ if(D)Log.d(TAG, "messagelisting end");
return bmList;
}
- public int msgListingSize(String folder, BluetoothMapAppParams ap) {
- if (D) Log.d(TAG, "msgListingSize: folder = " + folder);
+ /**
+ * Get the size of the message listing
+ * @param folder Must contain a valid folder string != null
+ * @param ap Parameters specifying message content and filters
+ * @return Integer equal to message listing size
+ */
+ public int msgListingSize(BluetoothMapFolderElement folderElement,
+ BluetoothMapAppParams ap) {
+ if (D) Log.d(TAG, "msgListingSize: folder = " + folderElement.getName());
int cnt = 0;
/* Cache some info used throughout filtering */
FilterInfo fi = new FilterInfo();
setFilterInfo(fi);
- if (smsSelected(fi, ap)) {
- fi.msgType = FilterInfo.TYPE_SMS;
- String where = setWhereFilter(folder, fi, ap);
+ if (smsSelected(fi, ap) && folderElement.hasSmsMmsContent()) {
+ fi.mMsgType = FilterInfo.TYPE_SMS;
+ String where = setWhereFilter(folderElement, fi, ap);
Cursor c = mResolver.query(Sms.CONTENT_URI,
- SMS_PROJECTION, where, null, "date DESC");
+ SMS_PROJECTION, where, null, Sms.DATE + " DESC");
if (c != null) {
cnt = c.getCount();
@@ -1258,42 +1541,57 @@ public class BluetoothMapContent {
}
}
- if (mmsSelected(fi, ap)) {
- fi.msgType = FilterInfo.TYPE_MMS;
- String where = setWhereFilter(folder, fi, ap);
+ if (mmsSelected(fi, ap) && folderElement.hasSmsMmsContent()) {
+ fi.mMsgType = FilterInfo.TYPE_MMS;
+ String where = setWhereFilter(folderElement, fi, ap);
Cursor c = mResolver.query(Mms.CONTENT_URI,
- MMS_PROJECTION, where, null, "date DESC");
-
+ MMS_PROJECTION, where, null, Mms.DATE + " DESC");
if (c != null) {
cnt += c.getCount();
c.close();
}
}
+ if (emailSelected(fi, ap) && folderElement.getEmailFolderId() != -1) {
+ fi.mMsgType = FilterInfo.TYPE_EMAIL;
+ String where = setWhereFilter(folderElement, fi, ap);
+ if(!where.isEmpty()) {
+ Uri contentUri = Uri.parse(mBaseEmailUri + BluetoothMapContract.TABLE_MESSAGE);
+ Cursor c = mResolver.query(contentUri, BluetoothMapContract.BT_MESSAGE_PROJECTION,
+ where, null, BluetoothMapContract.MessageColumns.DATE + " DESC");
+ if (c != null) {
+ cnt += c.getCount();
+ c.close();
+ }
+ }
+ }
+
if (D) Log.d(TAG, "msgListingSize: size = " + cnt);
return cnt;
}
+
/**
* Return true if there are unread messages in the requested list of messages
* @param folder folder where the message listing should come from
* @param ap application parameter object
* @return true if unread messages are in the list, else false
*/
- public boolean msgListingHasUnread(String folder, BluetoothMapAppParams ap) {
- if (D) Log.d(TAG, "msgListingHasUnread: folder = " + folder);
+ public boolean msgListingHasUnread(BluetoothMapFolderElement folderElement,
+ BluetoothMapAppParams ap) {
+ if (D) Log.d(TAG, "msgListingHasUnread: folder = " + folderElement.getName());
int cnt = 0;
/* Cache some info used throughout filtering */
FilterInfo fi = new FilterInfo();
setFilterInfo(fi);
- if (smsSelected(fi, ap)) {
- fi.msgType = FilterInfo.TYPE_SMS;
- String where = setWhereFilterFolderType(folder, fi);
- where += " AND read=0 ";
+ if (smsSelected(fi, ap) && folderElement.hasSmsMmsContent()) {
+ fi.mMsgType = FilterInfo.TYPE_SMS;
+ String where = setWhereFilterFolderType(folderElement, fi);
+ where += " AND " + Sms.READ + "=0 ";
where += setWhereFilterPeriod(ap, fi);
Cursor c = mResolver.query(Sms.CONTENT_URI,
- SMS_PROJECTION, where, null, "date DESC");
+ SMS_PROJECTION, where, null, Sms.DATE + " DESC");
if (c != null) {
cnt = c.getCount();
@@ -1301,13 +1599,13 @@ public class BluetoothMapContent {
}
}
- if (mmsSelected(fi, ap)) {
- fi.msgType = FilterInfo.TYPE_MMS;
- String where = setWhereFilterFolderType(folder, fi);
- where += " AND read=0 ";
+ if (mmsSelected(fi, ap) && folderElement.hasSmsMmsContent()) {
+ fi.mMsgType = FilterInfo.TYPE_MMS;
+ String where = setWhereFilterFolderType(folderElement, fi);
+ where += " AND " + Mms.READ + "=0 ";
where += setWhereFilterPeriod(ap, fi);
Cursor c = mResolver.query(Mms.CONTENT_URI,
- MMS_PROJECTION, where, null, "date DESC");
+ MMS_PROJECTION, where, null, Sms.DATE + " DESC");
if (c != null) {
cnt += c.getCount();
@@ -1315,6 +1613,23 @@ public class BluetoothMapContent {
}
}
+
+ if (emailSelected(fi, ap) && folderElement.getEmailFolderId() != -1) {
+ fi.mMsgType = FilterInfo.TYPE_EMAIL;
+ String where = setWhereFilterFolderType(folderElement, fi);
+ if(!where.isEmpty()) {
+ where += " AND " + BluetoothMapContract.MessageColumns.FLAG_READ + "=0 ";
+ where += setWhereFilterPeriod(ap, fi);
+ Uri contentUri = Uri.parse(mBaseEmailUri + BluetoothMapContract.TABLE_MESSAGE);
+ Cursor c = mResolver.query(contentUri, BluetoothMapContract.BT_MESSAGE_PROJECTION,
+ where, null, BluetoothMapContract.MessageColumns.DATE + " DESC");
+ if (c != null) {
+ cnt += c.getCount();
+ c.close();
+ }
+ }
+ }
+
if (D) Log.d(TAG, "msgListingHasUnread: numUnread = " + cnt);
return (cnt>0)?true:false;
}
@@ -1327,26 +1642,31 @@ public class BluetoothMapContent {
private String getFolderName(int type, int threadId) {
if(threadId == -1)
- return "deleted";
+ return BluetoothMapContract.FOLDER_NAME_DELETED;
switch(type) {
case 1:
- return "inbox";
+ return BluetoothMapContract.FOLDER_NAME_INBOX;
case 2:
- return "sent";
+ return BluetoothMapContract.FOLDER_NAME_SENT;
case 3:
- return "draft";
+ return BluetoothMapContract.FOLDER_NAME_DRAFT;
case 4: // Just name outbox, failed and queued "outbox"
case 5:
case 6:
- return "outbox";
+ return BluetoothMapContract.FOLDER_NAME_OUTBOX;
}
return "";
}
- public byte[] getMessage(String handle, BluetoothMapAppParams appParams) throws UnsupportedEncodingException{
+ public byte[] getMessage(String handle, BluetoothMapAppParams appParams,
+ BluetoothMapFolderElement folderElement) throws UnsupportedEncodingException{
TYPE type = BluetoothMapUtils.getMsgTypeFromHandle(handle);
long id = BluetoothMapUtils.getCpHandle(handle);
+ if(appParams.getFractionRequest() == BluetoothMapAppParams.FRACTION_REQUEST_NEXT) {
+ throw new IllegalArgumentException("FRACTION_REQUEST_NEXT does not make sence as" +
+ " we always return the full message.");
+ }
switch(type) {
case SMS_GSM:
case SMS_CDMA:
@@ -1354,12 +1674,12 @@ public class BluetoothMapContent {
case MMS:
return getMmsMessage(id, appParams);
case EMAIL:
- throw new IllegalArgumentException("Email not implemented - invalid message handle.");
+ return getEmailMessage(id, appParams, folderElement);
}
throw new IllegalArgumentException("Invalid message handle.");
}
- private void setVCardFromPhoneNumber(BluetoothMapbMessage message, String phone, boolean incoming) {
+ private String setVCardFromPhoneNumber(BluetoothMapbMessage message, String phone, boolean incoming) {
String contactId = null, contactName = null;
String[] phoneNumbers = null;
String[] emailAddresses = null;
@@ -1385,23 +1705,10 @@ public class BluetoothMapContent {
if(contactId == null) {
phoneNumbers = new String[1];
phoneNumbers[0] = phone;
- }
- else {
- // Fetch all contact phone numbers
- p = mResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,
- ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
- new String[]{contactId},
- null);
- if(p != null) {
- int i = 0;
- phoneNumbers = new String[p.getCount()];
- while (p != null && p.moveToNext()) {
- String number = p.getString(
- p.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
- phoneNumbers[i++] = number;
- }
- p.close();
- }
+ } else {
+ // use only actual phone number
+ phoneNumbers = new String[1];
+ phoneNumbers[0] = phone;
// Fetch contact e-mail addresses
p = mResolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, null,
@@ -1419,10 +1726,14 @@ public class BluetoothMapContent {
p.close();
}
}
- if(incoming == true)
+ if(incoming == true) {
+ if(V) Log.d(TAG, "Adding originator for phone:" + phone);
message.addOriginator(contactName, contactName, phoneNumbers, emailAddresses); // Use version 3.0 as we only have a formatted name
- else
+ } else {
+ if(V) Log.d(TAG, "Adding recipient for phone:" + phone);
message.addRecipient(contactName, contactName, phoneNumbers, emailAddresses); // Use version 3.0 as we only have a formatted name
+ }
+ return contactName;
}
public static final int MAP_MESSAGE_CHARSET_NATIVE = 0;
@@ -1438,8 +1749,7 @@ public class BluetoothMapContent {
if(c != null && c.moveToFirst())
{
-
- if(V) Log.d(TAG,"c.count: " + c.getCount());
+ if(V) Log.v(TAG,"c.count: " + c.getCount());
if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) {
message.setType(TYPE.SMS_GSM);
@@ -1483,37 +1793,36 @@ public class BluetoothMapContent {
throw new IllegalArgumentException("SMS handle not found");
}
- private void extractMmsAddresses(long id, BluetoothMapbMessageMmsEmail message) {
+ private void extractMmsAddresses(long id, BluetoothMapbMessageMms message) {
final String[] projection = null;
- String selection = new String("msg_id=" + id);
- String uriStr = String.format("content://mms/%d/addr", id);
+ String selection = new String(Mms.Addr.MSG_ID + "=" + id);
+ String uriStr = new String(Mms.CONTENT_URI + "/" + id + "/addr");
Uri uriAddress = Uri.parse(uriStr);
- Cursor c = mResolver.query(
- uriAddress,
- projection,
- selection,
- null, null);
- /* TODO: Change the setVCard...() to return the vCard, and use the name in message.addXxx() */
+ String contactName = null;
+ Cursor c = mResolver.query( uriAddress, projection, selection, null, null);
if (c.moveToFirst()) {
do {
- String address = c.getString(c.getColumnIndex("address"));
- Integer type = c.getInt(c.getColumnIndex("type"));
+ String address = c.getString(c.getColumnIndex(Mms.Addr.ADDRESS));
+ if(address.equals(INSERT_ADDRES_TOKEN))
+ continue;
+ Integer type = c.getInt(c.getColumnIndex(Mms.Addr.TYPE));
switch(type) {
case MMS_FROM:
- setVCardFromPhoneNumber(message, address, true);
- message.addFrom(null, address);
+ contactName = setVCardFromPhoneNumber(message, address, true);
+ message.addFrom(contactName, address);
break;
case MMS_TO:
- setVCardFromPhoneNumber(message, address, false);
- message.addTo(null, address);
+ contactName = setVCardFromPhoneNumber(message, address, false);
+ message.addTo(contactName, address);
break;
case MMS_CC:
- setVCardFromPhoneNumber(message, address, false);
- message.addCc(null, address);
+ contactName = setVCardFromPhoneNumber(message, address, false);
+ message.addCc(contactName, address);
break;
case MMS_BCC:
- setVCardFromPhoneNumber(message, address, false);
- message.addBcc(null, address);
+ contactName = setVCardFromPhoneNumber(message, address, false);
+ message.addBcc(contactName, address);
+ break;
default:
break;
}
@@ -1527,7 +1836,7 @@ public class BluetoothMapContent {
* @return
*/
private byte[] readMmsDataPart(long partid) {
- String uriStr = String.format("content://mms/part/%d", partid);
+ String uriStr = new String(Mms.CONTENT_URI + "/part/" + partid);
Uri uriAddress = Uri.parse(uriStr);
InputStream is = null;
ByteArrayOutputStream os = new ByteArrayOutputStream();
@@ -1560,15 +1869,15 @@ public class BluetoothMapContent {
* @param id the content provider ID of the message
* @param message the bMessage object to add the information to
*/
- private void extractMmsParts(long id, BluetoothMapbMessageMmsEmail message)
+ private void extractMmsParts(long id, BluetoothMapbMessageMms message)
{
- /* TODO: If the attachment appParam is set to "no", only add the text parts.
- * (content type contains "text" - case insensitive) */
+ /* Handling of filtering out non-text parts for exclude
+ * attachments is handled within the bMessage object. */
final String[] projection = null;
- String selection = new String("mid=" + id);
- String uriStr = String.format("content://mms/%d/part", id);
+ String selection = new String(Mms.Part.MSG_ID + "=" + id);
+ String uriStr = new String(Mms.CONTENT_URI + "/"+ id + "/part");
Uri uriAddress = Uri.parse(uriStr);
- BluetoothMapbMessageMmsEmail.MimePart part;
+ BluetoothMapbMessageMms.MimePart part;
Cursor c = mResolver.query(
uriAddress,
projection,
@@ -1578,17 +1887,17 @@ public class BluetoothMapContent {
if (c.moveToFirst()) {
do {
Long partId = c.getLong(c.getColumnIndex(BaseColumns._ID));
- String contentType = c.getString(c.getColumnIndex("ct"));
- String name = c.getString(c.getColumnIndex("name"));
- String charset = c.getString(c.getColumnIndex("chset"));
- String filename = c.getString(c.getColumnIndex("fn"));
- String text = c.getString(c.getColumnIndex("text"));
- Integer fd = c.getInt(c.getColumnIndex("_data"));
- String cid = c.getString(c.getColumnIndex("cid"));
- String cl = c.getString(c.getColumnIndex("cl"));
- String cdisp = c.getString(c.getColumnIndex("cd"));
-
- if(D) Log.d(TAG, " _id : " + partId +
+ String contentType = c.getString(c.getColumnIndex(Mms.Part.CONTENT_TYPE));
+ String name = c.getString(c.getColumnIndex(Mms.Part.NAME));
+ String charset = c.getString(c.getColumnIndex(Mms.Part.CHARSET));
+ String filename = c.getString(c.getColumnIndex(Mms.Part.FILENAME));
+ String text = c.getString(c.getColumnIndex(Mms.Part.TEXT));
+ Integer fd = c.getInt(c.getColumnIndex(Mms.Part._DATA));
+ String cid = c.getString(c.getColumnIndex(Mms.Part.CONTENT_ID));
+ String cl = c.getString(c.getColumnIndex(Mms.Part.CONTENT_LOCATION));
+ String cdisp = c.getString(c.getColumnIndex(Mms.Part.CONTENT_DISPOSITION));
+
+ if(V) Log.d(TAG, " _id : " + partId +
"\n ct : " + contentType +
"\n partname : " + name +
"\n charset : " + charset +
@@ -1600,33 +1909,32 @@ public class BluetoothMapContent {
"\n cdisp : " + cdisp);
part = message.addMimePart();
- part.contentType = contentType;
- part.partName = name;
- part.contentId = cid;
- part.contentLocation = cl;
- part.contentDisposition = cdisp;
+ part.mContentType = contentType;
+ part.mPartName = name;
+ part.mContentId = cid;
+ part.mContentLocation = cl;
+ part.mContentDisposition = cdisp;
try {
if(text != null) {
- part.data = text.getBytes("UTF-8");
- part.charsetName = "utf-8";
- }
- else {
- part.data = readMmsDataPart(partId);
+ part.mData = text.getBytes("UTF-8");
+ part.mCharsetName = "utf-8";
+ } else {
+ part.mData = readMmsDataPart(partId);
if(charset != null)
- part.charsetName = CharacterSets.getMimeName(Integer.parseInt(charset));
+ part.mCharsetName = CharacterSets.getMimeName(Integer.parseInt(charset));
}
} catch (NumberFormatException e) {
Log.d(TAG,"extractMmsParts",e);
- part.data = null;
- part.charsetName = null;
+ part.mData = null;
+ part.mCharsetName = null;
} catch (UnsupportedEncodingException e) {
Log.d(TAG,"extractMmsParts",e);
- part.data = null;
- part.charsetName = null;
+ part.mData = null;
+ part.mCharsetName = null;
} finally {
}
- part.fileName = filename;
+ part.mFileName = filename;
} while(c.moveToNext());
}
message.updateCharset();
@@ -1642,7 +1950,10 @@ public class BluetoothMapContent {
*/
public byte[] getMmsMessage(long id, BluetoothMapAppParams appParams) throws UnsupportedEncodingException {
int msgBox, threadId;
- BluetoothMapbMessageMmsEmail message = new BluetoothMapbMessageMmsEmail();
+ if (appParams.getCharset() == MAP_MESSAGE_CHARSET_NATIVE)
+ throw new IllegalArgumentException("MMS charset native not allowed for MMS - must be utf-8");
+
+ BluetoothMapbMessageMms message = new BluetoothMapbMessageMms();
Cursor c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION, "_ID = " + id, null, null);
if(c != null && c.moveToFirst())
{
@@ -1658,12 +1969,11 @@ public class BluetoothMapContent {
msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
message.setFolder(getFolderName(msgBox, threadId));
-
message.setSubject(c.getString(c.getColumnIndex(Mms.SUBJECT)));
message.setMessageId(c.getString(c.getColumnIndex(Mms.MESSAGE_ID)));
message.setContentType(c.getString(c.getColumnIndex(Mms.CONTENT_TYPE)));
message.setDate(c.getLong(c.getColumnIndex(Mms.DATE)) * 1000L);
- message.setTextOnly(c.getInt(c.getColumnIndex(Mms.TEXT_ONLY)) == 0 ? false : true); // - TODO: Do we need this - yes, if we have only text, we should not make this a multipart message
+ message.setTextOnly(c.getInt(c.getColumnIndex(Mms.TEXT_ONLY)) == 0 ? false : true);
message.setIncludeAttachments(appParams.getAttachment() == 0 ? false : true);
// c.getLong(c.getColumnIndex(Mms.DATE_SENT)); - this is never used
// c.getInt(c.getColumnIndex(Mms.STATUS)); - don't know what this is
@@ -1677,12 +1987,148 @@ public class BluetoothMapContent {
c.close();
return message.encode();
- }
- else if(c != null) {
+ } else if(c != null) {
c.close();
}
throw new IllegalArgumentException("MMS handle not found");
}
+ /**
+ *
+ * @param id the content provider id for the message to fetch.
+ * @param appParams The application parameter object received from the client.
+ * @return a byte[] containing the utf-8 encoded bMessage to send to the client.
+ * @throws UnsupportedEncodingException if UTF-8 is not supported,
+ * which is guaranteed to be supported on an android device
+ */
+ public byte[] getEmailMessage(long id, BluetoothMapAppParams appParams,
+ BluetoothMapFolderElement currentFolder) throws UnsupportedEncodingException {
+ // Log print out of application parameters set
+ if(D && appParams != null) {
+ Log.d(TAG,"TYPE_MESSAGE (GET): Attachment = " + appParams.getAttachment() +
+ ", Charset = " + appParams.getCharset() +
+ ", FractionRequest = " + appParams.getFractionRequest());
+ }
+
+ // Throw exception if requester NATIVE charset for Email
+ // Exception is caught by MapObexServer sendGetMessageResp
+ if (appParams.getCharset() == MAP_MESSAGE_CHARSET_NATIVE)
+ throw new IllegalArgumentException("EMAIL charset not UTF-8");
+
+ BluetoothMapbMessageEmail message = new BluetoothMapbMessageEmail();
+ Uri contentUri = Uri.parse(mBaseEmailUri + BluetoothMapContract.TABLE_MESSAGE);
+ Cursor c = mResolver.query(contentUri, BluetoothMapContract.BT_MESSAGE_PROJECTION, "_ID = " + id, null, null);
+ if(c != null && c.moveToFirst())
+ {
+ BluetoothMapFolderElement folderElement;
+ FileInputStream is = null;
+ ParcelFileDescriptor fd = null;
+
+ // Handle fraction requests
+ int fractionRequest = appParams.getFractionRequest();
+ if (fractionRequest != BluetoothMapAppParams.INVALID_VALUE_PARAMETER) {
+ // Fraction requested
+ if(V) {
+ String fractionStr = (fractionRequest == 0) ? "FIRST" : "NEXT";
+ Log.v(TAG, "getEmailMessage - FractionRequest " + fractionStr
+ + " - send compete message" );
+ }
+ // Check if message is complete and if not - request message from server
+ if (c.getString(c.getColumnIndex(
+ BluetoothMapContract.MessageColumns.RECEPTION_STATE)).equalsIgnoreCase(
+ BluetoothMapContract.RECEPTION_STATE_COMPLETE) == false) {
+ // TODO: request message from server
+ Log.w(TAG, "getEmailMessage - receptionState not COMPLETE - Not Implemented!" );
+ }
+ }
+ // Set read status:
+ String read = c.getString(c.getColumnIndex(BluetoothMapContract.MessageColumns.FLAG_READ));
+ if (read != null && read.equalsIgnoreCase("1"))
+ message.setStatus(true);
+ else
+ message.setStatus(false);
+
+ // Set message type:
+ message.setType(TYPE.EMAIL);
+
+ // Set folder:
+ long folderId = c.getLong(c.getColumnIndex(BluetoothMapContract.MessageColumns.FOLDER_ID));
+ folderElement = currentFolder.getEmailFolderById(folderId);
+ message.setCompleteFolder(folderElement.getFullPath());
+
+ // Set recipient:
+ String nameEmail = c.getString(c.getColumnIndex(BluetoothMapContract.MessageColumns.TO_LIST));
+ Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(nameEmail);
+ if (tokens.length != 0) {
+ if(D) Log.d(TAG, "Recipient count= " + tokens.length);
+ int i = 0;
+ while (i < tokens.length) {
+ if(V) Log.d(TAG, "Recipient = " + tokens[i].toString());
+ String[] emails = new String[1];
+ emails[0] = tokens[i].getAddress();
+ String name = tokens[i].getName();
+ message.addRecipient(name, name, null, emails);
+ i++;
+ }
+ }
+
+ // Set originator:
+ nameEmail = c.getString(c.getColumnIndex(BluetoothMapContract.MessageColumns.FROM_LIST));
+ tokens = Rfc822Tokenizer.tokenize(nameEmail);
+ if (tokens.length != 0) {
+ if(D) Log.d(TAG, "Originator count= " + tokens.length);
+ int i = 0;
+ while (i < tokens.length) {
+ if(V) Log.d(TAG, "Originator = " + tokens[i].toString());
+ String[] emails = new String[1];
+ emails[0] = tokens[i].getAddress();
+ String name = tokens[i].getName();
+ message.addOriginator(name, name, null, emails);
+ i++;
+ }
+ }
+ c.close();
+
+ // Find out if we get attachments
+ String attStr = (appParams.getAttachment() == 0) ? "/" + BluetoothMapContract.FILE_MSG_NO_ATTACHMENTS : "";
+ Uri uri = Uri.parse(contentUri + "/" + id + attStr);
+
+ // Get email message body content
+ int count = 0;
+ try {
+ fd = mResolver.openFileDescriptor(uri, "r");
+ is = new FileInputStream(fd.getFileDescriptor());
+ StringBuilder email = new StringBuilder("");
+ byte[] buffer = new byte[1024];
+ while((count = is.read(buffer)) != -1) {
+ // TODO: Handle breaks within a UTF8 character
+ email.append(new String(buffer,0,count));
+ if(V) Log.d(TAG, "Email part = " + new String(buffer,0,count) + " count=" + count);
+ }
+ // Set email message body:
+ message.setEmailBody(email.toString());
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, e);
+ } catch (NullPointerException e) {
+ Log.w(TAG, e);
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ }
+ finally {
+ try {
+ if(is != null)
+ is.close();
+ } catch (IOException e) {}
+ try {
+ if(fd != null)
+ fd.close();
+ } catch (IOException e) {}
+ }
+ return message.encode();
+ } else if(c != null) {
+ c.close();
+ }
+ throw new IllegalArgumentException("EMAIL handle not found");
+ }
}
diff --git a/src/com/android/bluetooth/map/BluetoothMapContentObserver.java b/src/com/android/bluetooth/map/BluetoothMapContentObserver.java
index 204bb0239..7101f220b 100644
--- a/src/com/android/bluetooth/map/BluetoothMapContentObserver.java
+++ b/src/com/android/bluetooth/map/BluetoothMapContentObserver.java
@@ -1,5 +1,5 @@
/*
-* Copyright (C) 2013 Samsung System LSI
+* Copyright (C) 2014 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
@@ -14,36 +14,47 @@
*/
package com.android.bluetooth.map;
-import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
+import javax.obex.ResponseCodes;
+
import org.xmlpull.v1.XmlSerializer;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
+import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.IntentFilter.MalformedMimeTypeException;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Build;
import android.os.Handler;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
import android.provider.BaseColumns;
+import com.android.bluetooth.mapapi.BluetoothMapContract;
+import com.android.bluetooth.mapapi.BluetoothMapContract.MessageColumns;
import android.provider.Telephony;
import android.provider.Telephony.Mms;
import android.provider.Telephony.MmsSms;
@@ -54,34 +65,59 @@ import android.telephony.ServiceState;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.telephony.TelephonyManager;
+import android.text.format.DateUtils;
import android.util.Log;
import android.util.Xml;
import android.os.Looper;
import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
-import com.android.bluetooth.map.BluetoothMapbMessageMmsEmail.MimePart;
+import com.android.bluetooth.map.BluetoothMapbMessageMms.MimePart;
import com.google.android.mms.pdu.PduHeaders;
public class BluetoothMapContentObserver {
private static final String TAG = "BluetoothMapContentObserver";
- private static final boolean D = false;
- private static final boolean V = false;
+ private static final boolean D = BluetoothMapService.DEBUG;
+ private static final boolean V = BluetoothMapService.VERBOSE;
+
+ private static final String EVENT_TYPE_DELETE = "MessageDeleted";
+ private static final String EVENT_TYPE_SHIFT = "MessageShift";
+ private static final String EVENT_TYPE_NEW = "NewMessage";
+ private static final String EVENT_TYPE_DELEVERY_SUCCESS = "DeliverySuccess";
+ private static final String EVENT_TYPE_SENDING_SUCCESS = "SendingSuccess";
+ private static final String EVENT_TYPE_SENDING_FAILURE = "SendingFailure";
+ private static final String EVENT_TYPE_DELIVERY_FAILURE = "DeliveryFailure";
+
+
+ private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
private Context mContext;
private ContentResolver mResolver;
+ private ContentProviderClient mProviderClient = null;
private BluetoothMnsObexClient mMnsClient;
+ private BluetoothMapMasInstance mMasInstance = null;
private int mMasId;
+ private boolean mEnableSmsMms = false;
+ private boolean mObserverRegistered = false;
+ private BluetoothMapEmailSettingsItem mAccount;
+ private String mAuthority = null;
+
+ private BluetoothMapFolderElement mFolders =
+ new BluetoothMapFolderElement("DUMMY", null); // Will be set by the MAS when generated.
+ private Uri mMessageUri = null;
public static final int DELETED_THREAD_ID = -1;
- /* X-Mms-Message-Type field types. These are from PduHeaders.java */
+ // X-Mms-Message-Type field types. These are from PduHeaders.java
public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84;
+ // Text only MMS converted to SMS if sms parts less than or equal to defined count
+ private static final int CONVERT_MMS_TO_SMS_PART_COUNT = 10;
+
private TYPE mSmsType;
static final String[] SMS_PROJECTION = new String[] {
- BaseColumns._ID,
+ Sms._ID,
Sms.THREAD_ID,
Sms.ADDRESS,
Sms.BODY,
@@ -90,30 +126,60 @@ public class BluetoothMapContentObserver {
Sms.TYPE,
Sms.STATUS,
Sms.LOCKED,
- Sms.ERROR_CODE,
+ Sms.ERROR_CODE
};
- static final String[] MMS_PROJECTION = new String[] {
- BaseColumns._ID,
+ static final String[] SMS_PROJECTION_SHORT = new String[] {
+ Sms._ID,
+ Sms.THREAD_ID,
+ Sms.TYPE
+ };
+
+ static final String[] MMS_PROJECTION_SHORT = new String[] {
+ Mms._ID,
Mms.THREAD_ID,
- Mms.MESSAGE_ID,
- Mms.MESSAGE_SIZE,
- Mms.SUBJECT,
- Mms.CONTENT_TYPE,
- Mms.TEXT_ONLY,
- Mms.DATE,
- Mms.DATE_SENT,
- Mms.READ,
- Mms.MESSAGE_BOX,
Mms.MESSAGE_TYPE,
- Mms.STATUS,
+ Mms.MESSAGE_BOX
};
- public BluetoothMapContentObserver(final Context context) {
+ static final String[] EMAIL_PROJECTION_SHORT = new String[] {
+ BluetoothMapContract.MessageColumns._ID,
+ BluetoothMapContract.MessageColumns.FOLDER_ID,
+ BluetoothMapContract.MessageColumns.FLAG_READ
+ };
+
+
+ public BluetoothMapContentObserver(final Context context,
+ BluetoothMnsObexClient mnsClient,
+ BluetoothMapMasInstance masInstance,
+ BluetoothMapEmailSettingsItem account,
+ boolean enableSmsMms) throws RemoteException {
mContext = context;
mResolver = mContext.getContentResolver();
+ mAccount = account;
+ mMasInstance = masInstance;
+ mMasId = mMasInstance.getMasId();
+ if(account != null) {
+ mAuthority = Uri.parse(account.mBase_uri).getAuthority();
+ mMessageUri = Uri.parse(account.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE);
+ mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority);
+ if (mProviderClient == null) {
+ throw new RemoteException("Failed to acquire provider for " + mAuthority);
+ }
+ mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
+ }
+ mEnableSmsMms = enableSmsMms;
mSmsType = getSmsType();
+ mMnsClient = mnsClient;
+ }
+
+ /**
+ * Set the folder structure to be used for this instance.
+ * @param folderStructure
+ */
+ public void setFolderStructure(BluetoothMapFolderElement folderStructure) {
+ this.mFolders = folderStructure;
}
private TYPE getSmsType() {
@@ -140,28 +206,28 @@ public class BluetoothMapContentObserver {
if (V) Log.d(TAG, "onChange on thread: " + Thread.currentThread().getId()
+ " Uri: " + uri.toString() + " selfchange: " + selfChange);
- handleMsgListChanges();
+ handleMsgListChanges(uri);
}
};
private static final String folderSms[] = {
"",
- "inbox",
- "sent",
- "draft",
- "outbox",
- "outbox",
- "outbox",
- "inbox",
- "inbox",
+ BluetoothMapContract.FOLDER_NAME_INBOX,
+ BluetoothMapContract.FOLDER_NAME_SENT,
+ BluetoothMapContract.FOLDER_NAME_DRAFT,
+ BluetoothMapContract.FOLDER_NAME_OUTBOX,
+ BluetoothMapContract.FOLDER_NAME_OUTBOX,
+ BluetoothMapContract.FOLDER_NAME_OUTBOX,
+ BluetoothMapContract.FOLDER_NAME_INBOX,
+ BluetoothMapContract.FOLDER_NAME_INBOX,
};
private static final String folderMms[] = {
"",
- "inbox",
- "sent",
- "draft",
- "outbox",
+ BluetoothMapContract.FOLDER_NAME_INBOX,
+ BluetoothMapContract.FOLDER_NAME_SENT,
+ BluetoothMapContract.FOLDER_NAME_DRAFT,
+ BluetoothMapContract.FOLDER_NAME_OUTBOX,
};
private class Event {
@@ -171,18 +237,28 @@ public class BluetoothMapContentObserver {
String oldFolder;
TYPE msgType;
+ final static String PATH = "telecom/msg/";
+
public Event(String eventType, long handle, String folder,
String oldFolder, TYPE msgType) {
- String PATH = "telecom/msg/";
+
this.eventType = eventType;
this.handle = handle;
if (folder != null) {
- this.folder = PATH + folder;
+ if(msgType == TYPE.EMAIL) {
+ this.folder = folder;
+ } else {
+ this.folder = PATH + folder;
+ }
} else {
this.folder = null;
}
if (oldFolder != null) {
- this.oldFolder = PATH + oldFolder;
+ if(msgType == TYPE.EMAIL) {
+ this.oldFolder = oldFolder;
+ } else {
+ this.oldFolder = PATH + oldFolder;
+ }
} else {
this.oldFolder = null;
}
@@ -195,7 +271,7 @@ public class BluetoothMapContentObserver {
try {
xmlEvtReport.setOutput(sw);
xmlEvtReport.startDocument(null, null);
- xmlEvtReport.text("\n");
+ xmlEvtReport.text("\r\n");
xmlEvtReport.startTag("", "MAP-event-report");
xmlEvtReport.attribute("", "version", "1.0");
@@ -214,14 +290,14 @@ public class BluetoothMapContentObserver {
xmlEvtReport.endTag("", "MAP-event-report");
xmlEvtReport.endDocument();
} catch (IllegalArgumentException e) {
- e.printStackTrace();
+ if(D) Log.w(TAG,e);
} catch (IllegalStateException e) {
- e.printStackTrace();
+ if(D) Log.w(TAG,e);
} catch (IOException e) {
- e.printStackTrace();
+ if(D) Log.w(TAG,e);
}
- if (V) System.out.println(sw.toString());
+ if (V) Log.d(TAG, sw.toString());
return sw.toString().getBytes("UTF-8");
}
@@ -229,33 +305,125 @@ public class BluetoothMapContentObserver {
private class Msg {
long id;
- int type;
-
- public Msg(long id, int type) {
+ int type; // Used as folder for SMS/MMS
+ int threadId; // Used for SMS/MMS at delete
+ long folderId = -1; // Email folder ID
+ long oldFolderId = -1; // Used for email undelete
+ boolean localInitiatedSend = false; // Used for MMS to filter out events
+ boolean transparent = false; // Used for EMAIL to delete message sent with transparency
+
+ public Msg(long id, int type, int threadId) {
this.id = id;
this.type = type;
+ this.threadId = threadId;
+ }
+ public Msg(long id, long folderId) {
+ this.id = id;
+ this.folderId = folderId;
+ }
+
+ /* Eclipse generated hashCode() and equals() to make
+ * hashMap lookup work independent of whether the obj
+ * is used for email or SMS/MMS and whether or not the
+ * oldFolder is set. */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (int) (id ^ (id >>> 32));
+ return result;
}
- }
- private Map<Long, Msg> mMsgListSms =
- Collections.synchronizedMap(new HashMap<Long, Msg>());
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Msg other = (Msg) obj;
+ if (id != other.id)
+ return false;
+ return true;
+ }
+ }
- private Map<Long, Msg> mMsgListMms =
- Collections.synchronizedMap(new HashMap<Long, Msg>());
+ private Map<Long, Msg> mMsgListSms = new HashMap<Long, Msg>();
+
+ private Map<Long, Msg> mMsgListMms = new HashMap<Long, Msg>();
+
+ private Map<Long, Msg> mMsgListEmail = new HashMap<Long, Msg>();
+
+ public int setNotificationRegistration(int notificationStatus) throws RemoteException {
+ // Forward the request to the MNS thread as a message - including the MAS instance ID.
+ if(D) Log.d(TAG,"setNotificationRegistration() enter");
+ Handler mns = mMnsClient.getMessageHandler();
+ if(mns != null) {
+ Message msg = mns.obtainMessage();
+ msg.what = BluetoothMnsObexClient.MSG_MNS_NOTIFICATION_REGISTRATION;
+ msg.arg1 = mMasId;
+ msg.arg2 = notificationStatus;
+ mns.sendMessageDelayed(msg, 10); // Send message without forcing a context switch
+ /* Some devices - e.g. PTS needs to get the unregister confirm before we actually
+ * disconnect the MNS. */
+ if(D) Log.d(TAG,"setNotificationRegistration() MSG_MNS_NOTIFICATION_REGISTRATION send to MNS");
+ } else {
+ // This should not happen except at shutdown.
+ if(D) Log.d(TAG,"setNotificationRegistration() Unable to send registration request");
+ return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
+ }
+ if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) {
+ registerObserver();
+ } else {
+ unregisterObserver();
+ }
+ return ResponseCodes.OBEX_HTTP_OK;
+ }
- public void registerObserver(BluetoothMnsObexClient mns, int masId) {
+ public void registerObserver() throws RemoteException{
if (V) Log.d(TAG, "registerObserver");
+
+ if (mObserverRegistered)
+ return;
+
/* Use MmsSms Uri since the Sms Uri is not notified on deletes */
- mMasId = masId;
- mMnsClient = mns;
- mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver);
+ if(mEnableSmsMms){
+ //this is sms/mms
+ mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver);
+ mObserverRegistered = true;
+ }
+ if(mAccount != null) {
+
+ mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority);
+ if (mProviderClient == null) {
+ throw new RemoteException("Failed to acquire provider for " + mAuthority);
+ }
+ mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
+
+ /* For URI's without account ID */
+ Uri uri = Uri.parse(mAccount.mBase_uri_no_account + "/" + BluetoothMapContract.TABLE_MESSAGE);
+ if(D) Log.d(TAG, "Registering observer for: " + uri);
+ mResolver.registerContentObserver(uri, true, mObserver);
+
+ /* For URI's with account ID - is handled the same way as without ID, but is
+ * only triggered for MAS instances with matching account ID. */
+ uri = Uri.parse(mAccount.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE);
+ if(D) Log.d(TAG, "Registering observer for: " + uri);
+ mResolver.registerContentObserver(uri, true, mObserver);
+ mObserverRegistered = true;
+ }
initMsgList();
}
public void unregisterObserver() {
if (V) Log.d(TAG, "unregisterObserver");
mResolver.unregisterContentObserver(mObserver);
- mMnsClient = null;
+ mObserverRegistered = false;
+ if(mProviderClient != null){
+ mProviderClient.release();
+ mProviderClient = null;
+ }
}
private void sendEvent(Event evt) {
@@ -274,47 +442,73 @@ public class BluetoothMapContentObserver {
}
}
- private void initMsgList() {
+ private void initMsgList() throws RemoteException {
if (V) Log.d(TAG, "initMsgList");
- mMsgListSms.clear();
- mMsgListMms.clear();
-
- HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();
+ if(mEnableSmsMms) {
- Cursor c = mResolver.query(Sms.CONTENT_URI,
- SMS_PROJECTION, null, null, null);
+ HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();
- if (c != null && c.moveToFirst()) {
- do {
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
- int type = c.getInt(c.getColumnIndex(Sms.TYPE));
+ Cursor c = mResolver.query(Sms.CONTENT_URI,
+ SMS_PROJECTION_SHORT, null, null, null);
- Msg msg = new Msg(id, type);
- msgListSms.put(id, msg);
- } while (c.moveToNext());
- c.close();
- }
+ if (c != null && c.moveToFirst()) {
+ do {
+ long id = c.getLong(c.getColumnIndex(Sms._ID));
+ int type = c.getInt(c.getColumnIndex(Sms.TYPE));
+ int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
- mMsgListSms = msgListSms;
+ Msg msg = new Msg(id, type, threadId);
+ msgListSms.put(id, msg);
+ } while (c.moveToNext());
+ c.close();
+ }
+ synchronized(mMsgListSms) {
+ mMsgListSms.clear();
+ mMsgListSms = msgListSms;
+ }
- HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();
+ HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();
- c = mResolver.query(Mms.CONTENT_URI,
- MMS_PROJECTION, null, null, null);
+ c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT, null, null, null);
- if (c != null && c.moveToFirst()) {
- do {
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
- int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
+ if (c != null && c.moveToFirst()) {
+ do {
+ long id = c.getLong(c.getColumnIndex(Mms._ID));
+ int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
+ int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
- Msg msg = new Msg(id, type);
- msgListMms.put(id, msg);
- } while (c.moveToNext());
- c.close();
+ Msg msg = new Msg(id, type, threadId);
+ msgListMms.put(id, msg);
+ } while (c.moveToNext());
+ c.close();
+ }
+ synchronized(mMsgListMms) {
+ mMsgListMms.clear();
+ mMsgListMms = msgListMms;
+ }
}
- mMsgListMms = msgListMms;
+ if(mAccount != null) {
+ HashMap<Long, Msg> msgListEmail = new HashMap<Long, Msg>();
+ Uri uri = mMessageUri;
+ Cursor c = mProviderClient.query(uri, EMAIL_PROJECTION_SHORT, null, null, null);
+
+ if (c != null && c.moveToFirst()) {
+ do {
+ long id = c.getLong(c.getColumnIndex(MessageColumns._ID));
+ long folderId = c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.FOLDER_ID));
+
+ Msg msg = new Msg(id, folderId);
+ msgListEmail.put(id, msg);
+ } while (c.moveToNext());
+ c.close();
+ }
+ synchronized(mMsgListEmail) {
+ mMsgListEmail.clear();
+ mMsgListEmail = msgListEmail;
+ }
+ }
}
private void handleMsgListChangesSms() {
@@ -323,34 +517,56 @@ public class BluetoothMapContentObserver {
HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>();
Cursor c = mResolver.query(Sms.CONTENT_URI,
- SMS_PROJECTION, null, null, null);
+ SMS_PROJECTION_SHORT, null, null, null);
synchronized(mMsgListSms) {
if (c != null && c.moveToFirst()) {
do {
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ long id = c.getLong(c.getColumnIndex(Sms._ID));
int type = c.getInt(c.getColumnIndex(Sms.TYPE));
+ int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
Msg msg = mMsgListSms.remove(id);
+ /* We must filter out any actions made by the MCE, hence do not send e.g. a message
+ * deleted and/or MessageShift for messages deleted by the MCE. */
+
if (msg == null) {
/* New message */
- msg = new Msg(id, type);
+ msg = new Msg(id, type, threadId);
msgListSms.put(id, msg);
- if (folderSms[type].equals("inbox")) {
- Event evt = new Event("NewMessage", id, folderSms[type],
- null, mSmsType);
- sendEvent(evt);
- }
+ /* Incoming message from the network */
+ Event evt = new Event(EVENT_TYPE_NEW, id, folderSms[type],
+ null, mSmsType);
+ sendEvent(evt);
} else {
/* Existing message */
if (type != msg.type) {
Log.d(TAG, "new type: " + type + " old type: " + msg.type);
- Event evt = new Event("MessageShift", id, folderSms[type],
- folderSms[msg.type], mSmsType);
- sendEvent(evt);
+ String oldFolder = folderSms[msg.type];
+ String newFolder = folderSms[type];
+ // Filter out the intermediate outbox steps
+ if(!oldFolder.equals(newFolder)) {
+ Event evt = new Event(EVENT_TYPE_SHIFT, id, folderSms[type],
+ oldFolder, mSmsType);
+ sendEvent(evt);
+ }
msg.type = type;
+ } else if(threadId != msg.threadId) {
+ Log.d(TAG, "Message delete change: type: " + type + " old type: " + msg.type
+ + "\n threadId: " + threadId + " old threadId: " + msg.threadId);
+ if(threadId == DELETED_THREAD_ID) { // Message deleted
+ Event evt = new Event(EVENT_TYPE_DELETE, id, BluetoothMapContract.FOLDER_NAME_DELETED,
+ folderSms[msg.type], mSmsType);
+ sendEvent(evt);
+ msg.threadId = threadId;
+ } else { // Undelete
+ Event evt = new Event(EVENT_TYPE_SHIFT, id, folderSms[msg.type],
+ BluetoothMapContract.FOLDER_NAME_DELETED, mSmsType);
+ sendEvent(evt);
+ msg.threadId = threadId;
+ }
}
msgListSms.put(id, msg);
}
@@ -359,8 +575,9 @@ public class BluetoothMapContentObserver {
}
for (Msg msg : mMsgListSms.values()) {
- Event evt = new Event("MessageDeleted", msg.id, "deleted",
- folderSms[msg.type], mSmsType);
+ Event evt = new Event(EVENT_TYPE_DELETE, msg.id,
+ BluetoothMapContract.FOLDER_NAME_DELETED,
+ folderSms[msg.type], mSmsType);
sendEvent(evt);
}
@@ -374,46 +591,69 @@ public class BluetoothMapContentObserver {
HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>();
Cursor c = mResolver.query(Mms.CONTENT_URI,
- MMS_PROJECTION, null, null, null);
+ MMS_PROJECTION_SHORT, null, null, null);
synchronized(mMsgListMms) {
if (c != null && c.moveToFirst()) {
do {
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ long id = c.getLong(c.getColumnIndex(Mms._ID));
int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE));
+ int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
Msg msg = mMsgListMms.remove(id);
+ /* We must filter out any actions made by the MCE, hence do not send e.g. a message
+ * deleted and/or MessageShift for messages deleted by the MCE. */
+
if (msg == null) {
/* New message - only notify on retrieve conf */
- if (folderMms[type].equals("inbox") &&
+ if (folderMms[type].equals(BluetoothMapContract.FOLDER_NAME_INBOX) &&
mtype != MESSAGE_TYPE_RETRIEVE_CONF) {
continue;
}
- msg = new Msg(id, type);
+ msg = new Msg(id, type, threadId);
msgListMms.put(id, msg);
- if (folderMms[type].equals("inbox")) {
- Event evt = new Event("NewMessage", id, folderMms[type],
+ /* Incoming message from the network */
+ Event evt = new Event(EVENT_TYPE_NEW, id, folderMms[type],
null, TYPE.MMS);
- sendEvent(evt);
- }
+ sendEvent(evt);
} else {
/* Existing message */
if (type != msg.type) {
Log.d(TAG, "new type: " + type + " old type: " + msg.type);
- Event evt = new Event("MessageShift", id, folderMms[type],
- folderMms[msg.type], TYPE.MMS);
- sendEvent(evt);
+ Event evt;
+ if(msg.localInitiatedSend == false) {
+ // Only send events about local initiated changes
+ evt = new Event(EVENT_TYPE_SHIFT, id, folderMms[type],
+ folderMms[msg.type], TYPE.MMS);
+ sendEvent(evt);
+ }
msg.type = type;
- if (folderMms[type].equals("sent")) {
- evt = new Event("SendingSuccess", id,
+ if (folderMms[type].equals(BluetoothMapContract.FOLDER_NAME_SENT)
+ && msg.localInitiatedSend == true) {
+ msg.localInitiatedSend = false; // Stop tracking changes for this message
+ evt = new Event(EVENT_TYPE_SENDING_SUCCESS, id,
folderSms[type], null, TYPE.MMS);
sendEvent(evt);
}
+ } else if(threadId != msg.threadId) {
+ Log.d(TAG, "Message delete change: type: " + type + " old type: " + msg.type
+ + "\n threadId: " + threadId + " old threadId: " + msg.threadId);
+ if(threadId == DELETED_THREAD_ID) { // Message deleted
+ Event evt = new Event(EVENT_TYPE_DELETE, id, BluetoothMapContract.FOLDER_NAME_DELETED,
+ folderMms[msg.type], TYPE.MMS);
+ sendEvent(evt);
+ msg.threadId = threadId;
+ } else { // Undelete
+ Event evt = new Event(EVENT_TYPE_SHIFT, id, folderMms[msg.type],
+ BluetoothMapContract.FOLDER_NAME_DELETED, TYPE.MMS);
+ sendEvent(evt);
+ msg.threadId = threadId;
+ }
}
msgListMms.put(id, msg);
}
@@ -422,18 +662,231 @@ public class BluetoothMapContentObserver {
}
for (Msg msg : mMsgListMms.values()) {
- Event evt = new Event("MessageDeleted", msg.id, "deleted",
- folderMms[msg.type], TYPE.MMS);
+ Event evt = new Event(EVENT_TYPE_DELETE, msg.id,
+ BluetoothMapContract.FOLDER_NAME_DELETED,
+ folderMms[msg.type], TYPE.MMS);
sendEvent(evt);
}
-
mMsgListMms = msgListMms;
}
}
- private void handleMsgListChanges() {
- handleMsgListChangesSms();
- handleMsgListChangesMms();
+ private void handleMsgListChangesEmail(Uri uri) throws RemoteException{
+ if (V) Log.v(TAG, "handleMsgListChangesEmail uri: " + uri.toString());
+
+ // TODO: Change observer to handle accountId and message ID if present
+
+ HashMap<Long, Msg> msgListEmail = new HashMap<Long, Msg>();
+
+ Cursor c = mProviderClient.query(mMessageUri, EMAIL_PROJECTION_SHORT, null, null, null);
+
+ synchronized(mMsgListEmail) {
+ if (c != null && c.moveToFirst()) {
+ do {
+ long id = c.getLong(c.getColumnIndex(BluetoothMapContract.MessageColumns._ID));
+ int folderId = c.getInt(c.getColumnIndex(
+ BluetoothMapContract.MessageColumns.FOLDER_ID));
+ Msg msg = mMsgListEmail.remove(id);
+ BluetoothMapFolderElement folderElement = mFolders.getEmailFolderById(folderId);
+ String newFolder;
+ if(folderElement != null) {
+ newFolder = folderElement.getFullPath();
+ } else {
+ newFolder = "unknown"; // This can happen if a new folder is created while connected
+ }
+
+ /* We must filter out any actions made by the MCE, hence do not send e.g. a message
+ * deleted and/or MessageShift for messages deleted by the MCE. */
+
+ if (msg == null) {
+ /* New message */
+ msg = new Msg(id, folderId);
+ msgListEmail.put(id, msg);
+ Event evt = new Event(EVENT_TYPE_NEW, id, newFolder,
+ null, TYPE.EMAIL);
+ sendEvent(evt);
+ } else {
+ /* Existing message */
+ if (folderId != msg.folderId) {
+ if (D) Log.d(TAG, "new folderId: " + folderId + " old folderId: " + msg.folderId);
+ BluetoothMapFolderElement oldFolderElement = mFolders.getEmailFolderById(msg.folderId);
+ String oldFolder;
+ if(oldFolderElement != null) {
+ oldFolder = oldFolderElement.getFullPath();
+ } else {
+ // This can happen if a new folder is created while connected
+ oldFolder = "unknown";
+ }
+ BluetoothMapFolderElement deletedFolder =
+ mFolders.getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED);
+ BluetoothMapFolderElement sentFolder =
+ mFolders.getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_SENT);
+ /*
+ * If the folder is now 'deleted', send a deleted-event in stead of a shift
+ * or if message is sent initiated by MAP Client, then send sending-success
+ * otherwise send folderShift
+ */
+ if(deletedFolder != null && deletedFolder.getEmailFolderId() == folderId) {
+ Event evt = new Event(EVENT_TYPE_DELETE, msg.id, newFolder,
+ oldFolder, TYPE.EMAIL);
+ sendEvent(evt);
+ } else if(sentFolder != null
+ && sentFolder.getEmailFolderId() == folderId
+ && msg.localInitiatedSend == true) {
+ if(msg.transparent) {
+ mResolver.delete(ContentUris.withAppendedId(mMessageUri, id), null, null);
+ } else {
+ msg.localInitiatedSend = false;
+ Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id,
+ oldFolder, null, TYPE.EMAIL);
+ sendEvent(evt);
+ }
+ } else {
+ Event evt = new Event(EVENT_TYPE_SHIFT, id, newFolder,
+ oldFolder, TYPE.EMAIL);
+ sendEvent(evt);
+ }
+ msg.folderId = folderId;
+ }
+ msgListEmail.put(id, msg);
+ }
+ } while (c.moveToNext());
+ c.close();
+ }
+
+ // For all messages no longer in the database send a delete notification
+ for (Msg msg : mMsgListEmail.values()) {
+ BluetoothMapFolderElement oldFolderElement = mFolders.getEmailFolderById(msg.folderId);
+ String oldFolder;
+ if(oldFolderElement != null) {
+ oldFolder = oldFolderElement.getFullPath();
+ } else {
+ oldFolder = "unknown";
+ }
+ /* Some e-mail clients delete the message after sending, and creates a new message in sent.
+ * We cannot track the message anymore, hence send both a send success and delete message.
+ */
+ if(msg.localInitiatedSend == true) {
+ msg.localInitiatedSend = false;
+ // If message is send with transparency don't set folder as message is deleted
+ if (msg.transparent)
+ oldFolder = null;
+ Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, oldFolder, null, TYPE.EMAIL);
+ sendEvent(evt);
+ }
+ /* As this message deleted is only send on a real delete - don't set folder.
+ * - only send delete event if message is not sent with transparency
+ */
+ if (!msg.transparent) {
+
+ Event evt = new Event(EVENT_TYPE_DELETE, msg.id, null, oldFolder, TYPE.EMAIL);
+ sendEvent(evt);
+ }
+ }
+ mMsgListEmail = msgListEmail;
+ }
+ }
+
+ private void handleMsgListChanges(Uri uri) {
+ if(uri.getAuthority().equals(mAuthority)) {
+ try {
+ handleMsgListChangesEmail(uri);
+ }catch(RemoteException e){
+ mMasInstance.restartObexServerSession();
+ Log.w(TAG, "Problems contacting the ContentProvider in mas Instance "+mMasId+" restaring ObexServerSession");
+ }
+
+ } else {
+ handleMsgListChangesSms();
+ handleMsgListChangesMms();
+ }
+ }
+
+ private boolean setEmailMessageStatusDelete(BluetoothMapFolderElement mCurrentFolder,
+ String uriStr, long handle, int status) {
+ boolean res = false;
+ Uri uri = Uri.parse(uriStr + BluetoothMapContract.TABLE_MESSAGE);
+
+ int updateCount = 0;
+ ContentValues contentValues = new ContentValues();
+ BluetoothMapFolderElement deleteFolder = mFolders.
+ getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED);
+ contentValues.put(BluetoothMapContract.MessageColumns._ID, handle);
+ synchronized(mMsgListEmail) {
+ Msg msg = mMsgListEmail.get(handle);
+ if (status == BluetoothMapAppParams.STATUS_VALUE_YES) {
+ /* Set deleted folder id */
+ long folderId = -1;
+ if(deleteFolder != null) {
+ folderId = deleteFolder.getEmailFolderId();
+ }
+ contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID,folderId);
+ updateCount = mResolver.update(uri, contentValues, null, null);
+ /* The race between updating the value in our cached values and the database
+ * is handled by the synchronized statement. */
+ if(updateCount > 0) {
+ res = true;
+ if (msg != null) {
+ msg.oldFolderId = msg.folderId;
+ // Update the folder ID to avoid triggering an event for MCE initiated actions.
+ msg.folderId = folderId;
+ }
+ if(D) Log.d(TAG, "Deleted MSG: " + handle + " from folderId: " + folderId);
+ } else {
+ Log.w(TAG, "Msg: " + handle + " - Set delete status " + status
+ + " failed for folderId " + folderId);
+ }
+ } else if (status == BluetoothMapAppParams.STATUS_VALUE_NO) {
+ /* Undelete message. move to old folder if we know it,
+ * else move to inbox - as dictated by the spec. */
+ if(msg != null && deleteFolder != null &&
+ msg.folderId == deleteFolder.getEmailFolderId()) {
+ /* Only modify messages in the 'Deleted' folder */
+ long folderId = -1;
+ if (msg != null && msg.oldFolderId != -1) {
+ folderId = msg.oldFolderId;
+ } else {
+ BluetoothMapFolderElement inboxFolder = mCurrentFolder.
+ getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_INBOX);
+ if(inboxFolder != null) {
+ folderId = inboxFolder.getEmailFolderId();
+ }
+ if(D)Log.d(TAG,"We did not delete the message, hence the old folder is unknown. Moving to inbox.");
+ }
+ contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId);
+ updateCount = mResolver.update(uri, contentValues, null, null);
+ if(updateCount > 0) {
+ res = true;
+ // Update the folder ID to avoid triggering an event for MCE initiated actions.
+ msg.folderId = folderId;
+ } else {
+ if(D)Log.d(TAG,"We did not delete the message, hence the old folder is unknown. Moving to inbox.");
+ }
+ }
+ }
+ if(V) {
+ BluetoothMapFolderElement folderElement;
+ String folderName = "unknown";
+ if (msg != null) {
+ folderElement = mCurrentFolder.getEmailFolderById(msg.folderId);
+ if(folderElement != null) {
+ folderName = folderElement.getName();
+ }
+ }
+ Log.d(TAG,"setEmailMessageStatusDelete: " + handle + " from " + folderName
+ + " status: " + status);
+ }
+ }
+ if(res == false) {
+ Log.w(TAG, "Set delete status " + status + " failed.");
+ }
+ return res;
+ }
+
+ private void updateThreadId(Uri uri, String valueString, long threadId) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(valueString, threadId);
+ mResolver.update(uri, contentValues, null, null);
}
private boolean deleteMessageMms(long handle) {
@@ -445,12 +898,18 @@ public class BluetoothMapContentObserver {
int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
if (threadId != DELETED_THREAD_ID) {
/* Set deleted thread id */
- ContentValues contentValues = new ContentValues();
- contentValues.put(Mms.THREAD_ID, DELETED_THREAD_ID);
- mResolver.update(uri, contentValues, null, null);
+ synchronized(mMsgListMms) {
+ Msg msg = mMsgListMms.get(handle);
+ if(msg != null) { // This will always be the case
+ msg.threadId = DELETED_THREAD_ID;
+ }
+ }
+ updateThreadId(uri, Mms.THREAD_ID, DELETED_THREAD_ID);
} else {
/* Delete from observer message list to avoid delete notifications */
- mMsgListMms.remove(handle);
+ synchronized(mMsgListMms) {
+ mMsgListMms.remove(handle);
+ }
/* Delete message */
mResolver.delete(uri, null, null);
}
@@ -462,12 +921,6 @@ public class BluetoothMapContentObserver {
return res;
}
- private void updateThreadIdMms(Uri uri, long threadId) {
- ContentValues contentValues = new ContentValues();
- contentValues.put(Mms.THREAD_ID, threadId);
- mResolver.update(uri, contentValues, null, null);
- }
-
private boolean unDeleteMessageMms(long handle) {
boolean res = false;
Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
@@ -479,7 +932,7 @@ public class BluetoothMapContentObserver {
/* Restore thread id from address, or if no thread for address
* create new thread by insert and remove of fake message */
String address;
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ long id = c.getLong(c.getColumnIndex(Mms._ID));
int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
if (msgBox == Mms.MESSAGE_BOX_INBOX) {
address = BluetoothMapContent.getAddressMms(mResolver, id,
@@ -490,7 +943,14 @@ public class BluetoothMapContentObserver {
}
Set<String> recipients = new HashSet<String>();
recipients.addAll(Arrays.asList(address));
- updateThreadIdMms(uri, Telephony.Threads.getOrCreateThreadId(mContext, recipients));
+ Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients);
+ synchronized(mMsgListMms) {
+ Msg msg = mMsgListMms.get(handle);
+ if(msg != null) { // This will always be the case
+ msg.threadId = oldThreadId.intValue();
+ }
+ }
+ updateThreadId(uri, Mms.THREAD_ID, oldThreadId);
} else {
Log.d(TAG, "Message not in deleted folder: handle " + handle
+ " threadId " + threadId);
@@ -512,13 +972,19 @@ public class BluetoothMapContentObserver {
/* Move to deleted folder, or delete if already in deleted folder */
int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
if (threadId != DELETED_THREAD_ID) {
+ synchronized(mMsgListSms) {
+ Msg msg = mMsgListSms.get(handle);
+ if(msg != null) { // This will always be the case
+ msg.threadId = DELETED_THREAD_ID;
+ }
+ }
/* Set deleted thread id */
- ContentValues contentValues = new ContentValues();
- contentValues.put(Sms.THREAD_ID, DELETED_THREAD_ID);
- mResolver.update(uri, contentValues, null, null);
+ updateThreadId(uri, Sms.THREAD_ID, DELETED_THREAD_ID);
} else {
/* Delete from observer message list to avoid delete notifications */
- mMsgListSms.remove(handle);
+ synchronized(mMsgListSms) {
+ mMsgListSms.remove(handle);
+ }
/* Delete message */
mResolver.delete(uri, null, null);
}
@@ -530,12 +996,6 @@ public class BluetoothMapContentObserver {
return res;
}
- private void updateThreadIdSms(Uri uri, long threadId) {
- ContentValues contentValues = new ContentValues();
- contentValues.put(Sms.THREAD_ID, threadId);
- mResolver.update(uri, contentValues, null, null);
- }
-
private boolean unDeleteMessageSms(long handle) {
boolean res = false;
Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
@@ -547,7 +1007,14 @@ public class BluetoothMapContentObserver {
String address = c.getString(c.getColumnIndex(Sms.ADDRESS));
Set<String> recipients = new HashSet<String>();
recipients.addAll(Arrays.asList(address));
- updateThreadIdSms(uri, Telephony.Threads.getOrCreateThreadId(mContext, recipients));
+ Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients);
+ synchronized(mMsgListSms) {
+ Msg msg = mMsgListSms.get(handle);
+ if(msg != null) { // This will always be the case
+ msg.threadId = oldThreadId.intValue(); // The threadId is specified as an int, so it is safe to truncate
+ }
+ }
+ updateThreadId(uri, Sms.THREAD_ID, oldThreadId);
} else {
Log.d(TAG, "Message not in deleted folder: handle " + handle
+ " threadId " + threadId);
@@ -560,29 +1027,43 @@ public class BluetoothMapContentObserver {
return res;
}
- public boolean setMessageStatusDeleted(long handle, TYPE type, int statusValue) {
+ public boolean setMessageStatusDeleted(long handle, TYPE type,
+ BluetoothMapFolderElement mCurrentFolder, String uriStr, int statusValue) {
boolean res = false;
if (D) Log.d(TAG, "setMessageStatusDeleted: handle " + handle
+ " type " + type + " value " + statusValue);
- if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) {
- if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
- res = deleteMessageSms(handle);
- } else if (type == TYPE.MMS) {
- res = deleteMessageMms(handle);
- }
- } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) {
- if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
- res = unDeleteMessageSms(handle);
- } else if (type == TYPE.MMS) {
- res = unDeleteMessageMms(handle);
+ if (type == TYPE.EMAIL) {
+ res = setEmailMessageStatusDelete(mCurrentFolder, uriStr, handle, statusValue);
+ } else {
+ if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) {
+ if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
+ res = deleteMessageSms(handle);
+ } else if (type == TYPE.MMS) {
+ res = deleteMessageMms(handle);
+ }
+ } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) {
+ if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
+ res = unDeleteMessageSms(handle);
+ } else if (type == TYPE.MMS) {
+ res = unDeleteMessageMms(handle);
+ }
}
}
+
return res;
}
- public boolean setMessageStatusRead(long handle, TYPE type, int statusValue) {
- boolean res = true;
+ /**
+ *
+ * @param handle
+ * @param type
+ * @param uriStr
+ * @param statusValue
+ * @return true at success
+ */
+ public boolean setMessageStatusRead(long handle, TYPE type, String uriStr, int statusValue) throws RemoteException{
+ int count = 0;
if (D) Log.d(TAG, "setMessageStatusRead: handle " + handle
+ " type " + type + " value " + statusValue);
@@ -591,22 +1072,36 @@ public class BluetoothMapContentObserver {
/* by the MCE shall change the MSE read status. */
if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) {
- Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
+ Uri uri = Sms.Inbox.CONTENT_URI;//ContentUris.withAppendedId(Sms.CONTENT_URI, handle);
Cursor c = mResolver.query(uri, null, null, null, null);
-
ContentValues contentValues = new ContentValues();
contentValues.put(Sms.READ, statusValue);
- mResolver.update(uri, contentValues, null, null);
+ contentValues.put(Sms.SEEN, statusValue);
+ String where = Sms._ID+"="+handle;
+ String values = contentValues.toString();
+ if (D) Log.d(TAG, " -> SMS Uri: " + uri.toString() + " Where " + where + " values " + values);
+ count = mResolver.update(uri, contentValues, where, null);
+ if (D) Log.d(TAG, " -> "+count +" rows updated!");
} else if (type == TYPE.MMS) {
Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle);
Cursor c = mResolver.query(uri, null, null, null, null);
-
+ if (D) Log.d(TAG, " -> MMS Uri: " + uri.toString());
ContentValues contentValues = new ContentValues();
contentValues.put(Mms.READ, statusValue);
- mResolver.update(uri, contentValues, null, null);
- }
+ count = mResolver.update(uri, contentValues, null, null);
- return res;
+ if (D) Log.d(TAG, " -> "+count +" rows updated!");
+ } if (type == TYPE.EMAIL) {
+ Uri uri = mMessageUri;
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(BluetoothMapContract.MessageColumns.FLAG_READ, statusValue);
+ contentValues.put(BluetoothMapContract.MessageColumns._ID, handle);
+ count = mProviderClient.update(uri, contentValues, null, null);
+ }
+ if(count < 1) {
+ return false;
+ }
+ return true;
}
private class PushMsgInfo {
@@ -615,10 +1110,14 @@ public class BluetoothMapContentObserver {
int retry;
String phone;
Uri uri;
+ long timestamp;
int parts;
int partsSent;
int partsDelivered;
boolean resend;
+ boolean sendInProgress;
+ boolean failedSent; // Set to true if a single part sent fail is received.
+ int statusDelivered; // Set to != 0 if a single part deliver fail is received.
public PushMsgInfo(long id, int transparent,
int retry, String phone, Uri uri) {
@@ -628,14 +1127,19 @@ public class BluetoothMapContentObserver {
this.phone = phone;
this.uri = uri;
this.resend = false;
+ this.sendInProgress = false;
+ this.failedSent = false;
+ this.statusDelivered = 0; /* Assume success */
+ this.timestamp = 0;
};
}
private Map<Long, PushMsgInfo> mPushMsgList =
Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>());
- public long pushMessage(BluetoothMapbMessage msg, String folder,
- BluetoothMapAppParams ap) throws IllegalArgumentException {
+ public long pushMessage(BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement,
+ BluetoothMapAppParams ap, String emailBaseUri)
+ throws IllegalArgumentException, RemoteException, IOException {
if (D) Log.d(TAG, "pushMessage");
ArrayList<BluetoothMapbMessage.vCard> recipientList = msg.getRecipients();
int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ?
@@ -643,61 +1147,158 @@ public class BluetoothMapContentObserver {
int retry = ap.getRetry();
int charset = ap.getCharset();
long handle = -1;
+ long folderId = -1;
if (recipientList == null) {
- Log.d(TAG, "empty recipient list");
+ if (D) Log.d(TAG, "empty recipient list");
return -1;
}
- for (BluetoothMapbMessage.vCard recipient : recipientList) {
- if(recipient.getEnvLevel() == 0) // Only send the message to the top level recipient
- {
- /* Only send to first address */
- String phone = recipient.getFirstPhoneNumber();
- boolean read = false;
- boolean deliveryReport = true;
-
- switch(msg.getType()){
- case MMS:
- {
- /* Send message if folder is outbox */
- /* to do, support MMS in the future */
- /*
- handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMmsEmail)msg);
- */
- break;
+ if ( msg.getType().equals(TYPE.EMAIL) ) {
+ /* Write the message to the database */
+ String msgBody = ((BluetoothMapbMessageEmail) msg).getEmailBody();
+ if (V) {
+ int length = msgBody.length();
+ Log.v(TAG, "pushMessage: message string length = " + length);
+ String messages[] = msgBody.split("\r\n");
+ Log.v(TAG, "pushMessage: messages count=" + messages.length);
+ for(int i = 0; i < messages.length; i++) {
+ Log.v(TAG, "part " + i + ":" + messages[i]);
+ }
+ }
+ FileOutputStream os = null;
+ ParcelFileDescriptor fdOut = null;
+ Uri uriInsert = Uri.parse(emailBaseUri + BluetoothMapContract.TABLE_MESSAGE);
+ if (D) Log.d(TAG, "pushMessage - uriInsert= " + uriInsert.toString() +
+ ", intoFolder id=" + folderElement.getEmailFolderId());
+
+ synchronized(mMsgListEmail) {
+ // Now insert the empty message into folder
+ ContentValues values = new ContentValues();
+ folderId = folderElement.getEmailFolderId();
+ values.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId);
+ Uri uriNew = mProviderClient.insert(uriInsert, values);
+ if (D) Log.d(TAG, "pushMessage - uriNew= " + uriNew.toString());
+ handle = Long.parseLong(uriNew.getLastPathSegment());
+
+ try {
+ fdOut = mProviderClient.openFile(uriNew, "w");
+ os = new FileOutputStream(fdOut.getFileDescriptor());
+ // Write Email to DB
+ os.write(msgBody.getBytes(), 0, msgBody.getBytes().length);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, e);
+ throw(new IOException("Unable to open file stream"));
+ } catch (NullPointerException e) {
+ Log.w(TAG, e);
+ throw(new IllegalArgumentException("Unable to parse message."));
+ } finally {
+ try {
+ if(os != null)
+ os.close();
+ } catch (IOException e) {Log.w(TAG, e);}
+ try {
+ if(fdOut != null)
+ fdOut.close();
+ } catch (IOException e) {Log.w(TAG, e);}
+ }
+
+ /* Extract the data for the inserted message, and store in local mirror, to
+ * avoid sending a NewMessage Event. */
+ Msg newMsg = new Msg(handle, folderId);
+ newMsg.transparent = (transparent == 1) ? true : false;
+ if ( folderId == folderElement.getEmailFolderByName(
+ BluetoothMapContract.FOLDER_NAME_OUTBOX).getEmailFolderId() ) {
+ newMsg.localInitiatedSend = true;
+ }
+ mMsgListEmail.put(handle, newMsg);
+ }
+ } else { // type SMS_* of MMS
+ for (BluetoothMapbMessage.vCard recipient : recipientList) {
+ if(recipient.getEnvLevel() == 0) // Only send the message to the top level recipient
+ {
+ /* Only send to first address */
+ String phone = recipient.getFirstPhoneNumber();
+ String email = recipient.getFirstEmail();
+ String folder = folderElement.getName();
+ boolean read = false;
+ boolean deliveryReport = true;
+ String msgBody = null;
+
+ /* If MMS contains text only and the size is less than ten SMS's
+ * then convert the MMS to type SMS and then proceed
+ */
+ if (msg.getType().equals(TYPE.MMS) &&
+ (((BluetoothMapbMessageMms) msg).getTextOnly() == true)) {
+ msgBody = ((BluetoothMapbMessageMms) msg).getMessageAsText();
+ SmsManager smsMng = SmsManager.getDefault();
+ ArrayList<String> parts = smsMng.divideMessage(msgBody);
+ int smsParts = parts.size();
+ if (smsParts <= CONVERT_MMS_TO_SMS_PART_COUNT ) {
+ if (D) Log.d(TAG, "pushMessage - converting MMS to SMS, sms parts=" + smsParts );
+ msg.setType(mSmsType);
+ } else {
+ if (D) Log.d(TAG, "pushMessage - MMS text only but to big to convert to SMS");
+ msgBody = null;
+ }
+
}
- case SMS_GSM: //fall-through
- case SMS_CDMA:
- {
+
+ if (msg.getType().equals(TYPE.MMS)) {
+ /* Send message if folder is outbox else just store in draft*/
+ handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMms)msg);
+ } else if (msg.getType().equals(TYPE.SMS_GSM) ||
+ msg.getType().equals(TYPE.SMS_CDMA) ) {
/* Add the message to the database */
- String msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody();
- Uri contentUri = Uri.parse("content://sms/" + folder);
- Uri uri = Sms.addMessageToUri(mResolver, contentUri, phone, msgBody,
- "", System.currentTimeMillis(), read, deliveryReport);
-
- if (uri == null) {
- Log.d(TAG, "pushMessage - failure on add to uri " + contentUri);
- return -1;
- }
+ if(msgBody == null)
+ msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody();
+
+ /* We need to lock the SMS list while updating the database, to avoid sending
+ * events on MCE initiated operation. */
+ Uri contentUri = Uri.parse(Sms.CONTENT_URI+ "/" + folder);
+ Uri uri;
+ synchronized(mMsgListSms) {
+ uri = Sms.addMessageToUri(mResolver, contentUri, phone, msgBody,
+ "", System.currentTimeMillis(), read, deliveryReport);
+
+ if(V) Log.v(TAG, "Sms.addMessageToUri() returned: " + uri);
+ if (uri == null) {
+ if (D) Log.d(TAG, "pushMessage - failure on add to uri " + contentUri);
+ return -1;
+ }
+ Cursor c = mResolver.query(uri, SMS_PROJECTION_SHORT, null, null, null);
+
+ /* Extract the data for the inserted message, and store in local mirror, to
+ * avoid sending a NewMessage Event. */
+ if (c != null && c.moveToFirst()) {
+ long id = c.getLong(c.getColumnIndex(Sms._ID));
+ int type = c.getInt(c.getColumnIndex(Sms.TYPE));
+ int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID));
+ Msg newMsg = new Msg(id, type, threadId);
+ mMsgListSms.put(id, newMsg);
+ c.close();
+ } else {
+ return -1; // This can only happen, if the message is deleted just as it is added
+ }
- handle = Long.parseLong(uri.getLastPathSegment());
+ handle = Long.parseLong(uri.getLastPathSegment());
- /* Send message if folder is outbox */
- if (folder.equals("outbox")) {
- PushMsgInfo msgInfo = new PushMsgInfo(handle, transparent,
- retry, phone, uri);
- mPushMsgList.put(handle, msgInfo);
- sendMessage(msgInfo, msgBody);
+ /* Send message if folder is outbox */
+ if (folder.equals(BluetoothMapContract.FOLDER_NAME_OUTBOX)) {
+ PushMsgInfo msgInfo = new PushMsgInfo(handle, transparent,
+ retry, phone, uri);
+ mPushMsgList.put(handle, msgInfo);
+ sendMessage(msgInfo, msgBody);
+ if(V) Log.v(TAG, "sendMessage returned...");
+ }
+ /* sendMessage causes the message to be deleted and reinserted, hence we need to lock
+ * the list while this is happening. */
}
- break;
- }
- case EMAIL:
- {
- break;
+ } else {
+ if (D) Log.d(TAG, "pushMessage - failure on type " );
+ return -1;
}
}
-
}
}
@@ -705,9 +1306,7 @@ public class BluetoothMapContentObserver {
return handle;
}
-
-
- public long sendMmsMessage(String folder,String to_address, BluetoothMapbMessageMmsEmail msg) {
+ public long sendMmsMessage(String folder, String to_address, BluetoothMapbMessageMms msg) {
/*
*strategy:
*1) parse message into parts
@@ -719,42 +1318,46 @@ public class BluetoothMapContentObserver {
*else if folder !outbox:
*1) push message to folder
* */
- long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, to_address, msg);
- /* if invalid handle (-1) then just return the handle - else continue sending (if folder is outbox) */
- if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase("outbox")) {
- moveDraftToOutbox(handle);
-
- Intent sendIntent = new Intent("android.intent.action.MMS_SEND_OUTBOX_MSG");
- Log.d(TAG, "broadcasting intent: "+sendIntent.toString());
- mContext.sendBroadcast(sendIntent);
+ if (folder != null && (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)
+ || folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT))) {
+ long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, to_address, msg);
+ /* if invalid handle (-1) then just return the handle - else continue sending (if folder is outbox) */
+ if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)) {
+ moveDraftToOutbox(handle);
+ Intent sendIntent = new Intent("android.intent.action.MMS_SEND_OUTBOX_MSG");
+ if (D) Log.d(TAG, "broadcasting intent: "+sendIntent.toString());
+ mContext.sendBroadcast(sendIntent);
+ }
+ return handle;
+ } else {
+ /* not allowed to push mms to anything but outbox/draft */
+ throw new IllegalArgumentException("Cannot push message to other folders than outbox/draft");
}
- return handle;
}
private void moveDraftToOutbox(long handle) {
- ContentResolver contentResolver = mContext.getContentResolver();
/*Move message by changing the msg_box value in the content provider database */
if (handle != -1) {
String whereClause = " _id= " + handle;
- Uri uri = Uri.parse("content://mms");
- Cursor queryResult = contentResolver.query(uri, null, whereClause, null, null);
+ Uri uri = Mms.CONTENT_URI;
+ Cursor queryResult = mResolver.query(uri, null, whereClause, null, null);
if (queryResult != null) {
if (queryResult.getCount() > 0) {
queryResult.moveToFirst();
ContentValues data = new ContentValues();
/* set folder to be outbox */
- data.put("msg_box", Mms.MESSAGE_BOX_OUTBOX);
- contentResolver.update(uri, data, whereClause, null);
- Log.d(TAG, "moved draft MMS to outbox");
+ data.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_OUTBOX);
+ mResolver.update(uri, data, whereClause, null);
+ if (D) Log.d(TAG, "moved draft MMS to outbox");
}
queryResult.close();
}else {
- Log.d(TAG, "Could not move draft to outbox ");
+ if (D) Log.d(TAG, "Could not move draft to outbox ");
}
}
}
- private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMmsEmail msg) {
+ private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMms msg) {
/**
* strategy:
* 1) parse msg into parts + header
@@ -764,101 +1367,153 @@ public class BluetoothMapContentObserver {
*/
ContentValues values = new ContentValues();
- values.put("msg_box", folder);
-
- values.put("read", 0);
- values.put("seen", 0);
- values.put("sub", msg.getSubject());
- values.put("sub_cs", 106);
- values.put("ct_t", "application/vnd.wap.multipart.related");
- values.put("exp", 604800);
- values.put("m_cls", PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
- values.put("m_type", PduHeaders.MESSAGE_TYPE_SEND_REQ);
- values.put("v", PduHeaders.CURRENT_MMS_VERSION);
- values.put("pri", PduHeaders.PRIORITY_NORMAL);
- values.put("rr", PduHeaders.VALUE_NO);
- values.put("tr_id", "T"+ Long.toHexString(System.currentTimeMillis()));
- values.put("d_rpt", PduHeaders.VALUE_NO);
- values.put("locked", 0);
- if(msg.getTextOnly() == true)
- values.put("text_only", true);
+ values.put(Mms.MESSAGE_BOX, folder);
+ values.put(Mms.READ, 0);
+ values.put(Mms.SEEN, 0);
+ if(msg.getSubject() != null) {
+ values.put(Mms.SUBJECT, msg.getSubject());
+ } else {
+ values.put(Mms.SUBJECT, "");
+ }
- values.put("m_size", msg.getSize());
+ if(msg.getSubject() != null && msg.getSubject().length() > 0) {
+ values.put(Mms.SUBJECT_CHARSET, 106);
+ }
+ values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related");
+ values.put(Mms.EXPIRY, 604800);
+ values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
+ values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
+ values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION);
+ values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL);
+ values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO);
+ values.put(Mms.TRANSACTION_ID, "T"+ Long.toHexString(System.currentTimeMillis()));
+ values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO);
+ values.put(Mms.LOCKED, 0);
+ if(msg.getTextOnly() == true)
+ values.put(Mms.TEXT_ONLY, true);
+ values.put(Mms.MESSAGE_SIZE, msg.getSize());
- // Get thread id
+ // Get thread id
Set<String> recipients = new HashSet<String>();
recipients.addAll(Arrays.asList(to_address));
- values.put("thread_id", Telephony.Threads.getOrCreateThreadId(mContext, recipients));
- Uri uri = Uri.parse("content://mms");
+ values.put(Mms.THREAD_ID, Telephony.Threads.getOrCreateThreadId(mContext, recipients));
+ Uri uri = Mms.CONTENT_URI;
- ContentResolver cr = mContext.getContentResolver();
- uri = cr.insert(uri, values);
+ synchronized (mMsgListMms) {
- if (uri == null) {
- // unable to insert MMS
- Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri);
- return -1;
- }
+ uri = mResolver.insert(uri, values);
+
+ if (uri == null) {
+ // unable to insert MMS
+ Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri);
+ return -1;
+ }
+ /* As we already have all the values we need, we could skip the query, but
+ doing the query ensures we get any changes made by the content provider
+ at insert. */
+ Cursor c = mResolver.query(uri, MMS_PROJECTION_SHORT, null, null, null);
+
+ if (c != null && c.moveToFirst()) {
+ long id = c.getLong(c.getColumnIndex(Mms._ID));
+ int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX));
+ int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID));
+
+ /* We must filter out any actions made by the MCE. Add the new message to
+ * the list of known messages. */
+
+ Msg newMsg = new Msg(id, type, threadId);
+ newMsg.localInitiatedSend = true;
+ mMsgListMms.put(id, newMsg);
+ c.close();
+ }
+ } // Done adding changes, unlock access to mMsgListMms to allow sending MMS events again
long handle = Long.parseLong(uri.getLastPathSegment());
- if (V){
- Log.v(TAG, " NEW URI " + uri.toString());
- }
+ if (V) Log.v(TAG, " NEW URI " + uri.toString());
+
try {
- if(V) Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base.");
- for(MimePart part : msg.getMimeParts()) {
- int count = 0;
- count++;
- values.clear();
- if(part.contentType != null && part.contentType.toUpperCase().contains("TEXT")) {
- values.put("ct", "text/plain");
- values.put("chset", 106);
- if(part.partName != null) {
- values.put("fn", part.partName);
- values.put("name", part.partName);
- } else if(part.contentId == null && part.contentLocation == null) {
- /* We must set at least one part identifier */
- values.put("fn", "text_" + count +".txt");
- values.put("name", "text_" + count +".txt");
- }
- if(part.contentId != null) {
- values.put("cid", part.contentId);
+ if(msg.getMimeParts() == null) {
+ /* Perhaps this message have been deleted, and no longer have any content, but only headers */
+ Log.w(TAG, "No MMS parts present...");
+ } else {
+ if(V) Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base.");
+ for(MimePart part : msg.getMimeParts()) {
+ int count = 0;
+ count++;
+ values.clear();
+ if(part.mContentType != null && part.mContentType.toUpperCase().contains("TEXT")) {
+ values.put(Mms.Part.CONTENT_TYPE, "text/plain");
+ values.put(Mms.Part.CHARSET, 106);
+ if(part.mPartName != null) {
+ values.put(Mms.Part.FILENAME, part.mPartName);
+ values.put(Mms.Part.NAME, part.mPartName);
+ } else {
+ values.put(Mms.Part.FILENAME, "text_" + count +".txt");
+ values.put(Mms.Part.NAME, "text_" + count +".txt");
+ }
+ // Ensure we have "ci" set
+ if(part.mContentId != null) {
+ values.put(Mms.Part.CONTENT_ID, part.mContentId);
+ } else {
+ if(part.mPartName != null) {
+ values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">");
+ } else {
+ values.put(Mms.Part.CONTENT_ID, "<text_" + count + ">");
+ }
+ }
+ // Ensure we have "cl" set
+ if(part.mContentLocation != null) {
+ values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
+ } else {
+ if(part.mPartName != null) {
+ values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".txt");
+ } else {
+ values.put(Mms.Part.CONTENT_LOCATION, "text_" + count + ".txt");
+ }
+ }
+
+ if(part.mContentDisposition != null) {
+ values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
+ }
+ values.put(Mms.Part.TEXT, part.getDataAsString());
+ uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part");
+ uri = mResolver.insert(uri, values);
+ if(V) Log.v(TAG, "Added TEXT part");
+
+ } else if (part.mContentType != null && part.mContentType.toUpperCase().contains("SMIL")){
+
+ values.put(Mms.Part.SEQ, -1);
+ values.put(Mms.Part.CONTENT_TYPE, "application/smil");
+ if(part.mContentId != null) {
+ values.put(Mms.Part.CONTENT_ID, part.mContentId);
+ } else {
+ values.put(Mms.Part.CONTENT_ID, "<smil_" + count + ">");
+ }
+ if(part.mContentLocation != null) {
+ values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
+ } else {
+ values.put(Mms.Part.CONTENT_LOCATION, "smil_" + count + ".xml");
+ }
+
+ if(part.mContentDisposition != null)
+ values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
+ values.put(Mms.Part.FILENAME, "smil.xml");
+ values.put(Mms.Part.NAME, "smil.xml");
+ values.put(Mms.Part.TEXT, new String(part.mData, "UTF-8"));
+
+ uri = Uri.parse(Mms.CONTENT_URI+ "/" + handle + "/part");
+ uri = mResolver.insert(uri, values);
+ if (V) Log.v(TAG, "Added SMIL part");
+
+ }else /*VIDEO/AUDIO/IMAGE*/ {
+ writeMmsDataPart(handle, part, count);
+ if (V) Log.v(TAG, "Added OTHER part");
+ }
+ if (uri != null){
+ if (V) Log.v(TAG, "Added part with content-type: "+ part.mContentType + " to Uri: " + uri.toString());
+ }
}
- if(part.contentLocation != null)
- values.put("cl", part.contentLocation);
- if(part.contentDisposition != null)
- values.put("cd", part.contentDisposition);
- values.put("text", new String(part.data, "UTF-8"));
- uri = Uri.parse("content://mms/" + handle + "/part");
- uri = cr.insert(uri, values);
- if(V) Log.v(TAG, "Added TEXT part");
-
- } else if (part.contentType != null && part.contentType.toUpperCase().contains("SMIL")){
-
- values.put("seq", -1);
- values.put("ct", "application/smil");
- if(part.contentId != null)
- values.put("cid", part.contentId);
- if(part.contentLocation != null)
- values.put("cl", part.contentLocation);
- if(part.contentDisposition != null)
- values.put("cd", part.contentDisposition);
- values.put("fn", "smil.xml");
- values.put("name", "smil.xml");
- values.put("text", new String(part.data, "UTF-8"));
-
- uri = Uri.parse("content://mms/" + handle + "/part");
- uri = cr.insert(uri, values);
- if(V) Log.v(TAG, "Added SMIL part");
-
- }else /*VIDEO/AUDIO/IMAGE*/ {
- writeMmsDataPart(handle, part, count);
- if(V) Log.v(TAG, "Added OTHER part");
- }
- if (uri != null && V){
- Log.v(TAG, "Added part with content-type: "+ part.contentType + " to Uri: " + uri.toString());
}
- }
} catch (UnsupportedEncodingException e) {
Log.w(TAG, e);
} catch (IOException e) {
@@ -866,25 +1521,25 @@ public class BluetoothMapContentObserver {
}
values.clear();
- values.put("contact_id", "null");
- values.put("address", "insert-address-token");
- values.put("type", BluetoothMapContent.MMS_FROM);
- values.put("charset", 106);
+ values.put(Mms.Addr.CONTACT_ID, "null");
+ values.put(Mms.Addr.ADDRESS, "insert-address-token");
+ values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_FROM);
+ values.put(Mms.Addr.CHARSET, 106);
- uri = Uri.parse("content://mms/" + handle + "/addr");
- uri = cr.insert(uri, values);
+ uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr");
+ uri = mResolver.insert(uri, values);
if (uri != null && V){
Log.v(TAG, " NEW URI " + uri.toString());
}
values.clear();
- values.put("contact_id", "null");
- values.put("address", to_address);
- values.put("type", BluetoothMapContent.MMS_TO);
- values.put("charset", 106);
+ values.put(Mms.Addr.CONTACT_ID, "null");
+ values.put(Mms.Addr.ADDRESS, to_address);
+ values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_TO);
+ values.put(Mms.Addr.CHARSET, 106);
- uri = Uri.parse("content://mms/" + handle + "/addr");
- uri = cr.insert(uri, values);
+ uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr");
+ uri = mResolver.insert(uri, values);
if (uri != null && V){
Log.v(TAG, " NEW URI " + uri.toString());
}
@@ -894,34 +1549,47 @@ public class BluetoothMapContentObserver {
private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException{
ContentValues values = new ContentValues();
- values.put("mid", handle);
- if(part.contentType != null){
- //Remove last char if ';' from contentType
- if(part.contentType.charAt(part.contentType.length() - 1) == ';') {
- part.contentType = part.contentType.substring(0,part.contentType.length() -1);
+ values.put(Mms.Part.MSG_ID, handle);
+ if(part.mContentType != null) {
+ values.put(Mms.Part.CONTENT_TYPE, part.mContentType);
+ } else {
+ Log.w(TAG, "MMS has no CONTENT_TYPE for part " + count);
+ }
+ if(part.mContentId != null) {
+ values.put(Mms.Part.CONTENT_ID, part.mContentId);
+ } else {
+ if(part.mPartName != null) {
+ values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">");
+ } else {
+ values.put(Mms.Part.CONTENT_ID, "<part_" + count + ">");
}
- values.put("ct", part.contentType);
}
- if(part.contentId != null)
- values.put("cid", part.contentId);
- if(part.contentLocation != null)
- values.put("cl", part.contentLocation);
- if(part.contentDisposition != null)
- values.put("cd", part.contentDisposition);
- if(part.partName != null) {
- values.put("fn", part.partName);
- values.put("name", part.partName);
- } else if(part.contentId == null && part.contentLocation == null) {
+
+ if(part.mContentLocation != null) {
+ values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation);
+ } else {
+ if(part.mPartName != null) {
+ values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".dat");
+ } else {
+ values.put(Mms.Part.CONTENT_LOCATION, "part_" + count + ".dat");
+ }
+ }
+ if(part.mContentDisposition != null)
+ values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition);
+ if(part.mPartName != null) {
+ values.put(Mms.Part.FILENAME, part.mPartName);
+ values.put(Mms.Part.NAME, part.mPartName);
+ } else {
/* We must set at least one part identifier */
- values.put("fn", "part_" + count + ".dat");
- values.put("name", "part_" + count + ".dat");
+ values.put(Mms.Part.FILENAME, "part_" + count + ".dat");
+ values.put(Mms.Part.NAME, "part_" + count + ".dat");
}
- Uri partUri = Uri.parse("content://mms/" + handle + "/part");
+ Uri partUri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part");
Uri res = mResolver.insert(partUri, values);
// Add data to part
OutputStream os = mResolver.openOutputStream(res);
- os.write(part.data);
+ os.write(part.mData);
os.close();
}
@@ -931,21 +1599,52 @@ public class BluetoothMapContentObserver {
SmsManager smsMng = SmsManager.getDefault();
ArrayList<String> parts = smsMng.divideMessage(msgBody);
msgInfo.parts = parts.size();
+ // We add a time stamp to differentiate delivery reports from each other for resent messages
+ msgInfo.timestamp = Calendar.getInstance().getTime().getTime();
+ msgInfo.partsDelivered = 0;
+ msgInfo.partsSent = 0;
ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts);
ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts);
+ /* We handle the SENT intent in the MAP service, as this object
+ * is destroyed at disconnect, hence if a disconnect occur while sending
+ * a message, there is no intent handler to move the message from outbox
+ * to the correct folder.
+ * The correct solution would be to create a service that will start based on
+ * the intent, if BT is turned off. */
+
for (int i = 0; i < msgInfo.parts; i++) {
- Intent intent;
- intent = new Intent(ACTION_MESSAGE_DELIVERY, null);
- intent.putExtra("HANDLE", msgInfo.id);
- deliveryIntents.add(PendingIntent.getBroadcast(mContext, 0, intent,
- PendingIntent.FLAG_UPDATE_CURRENT));
-
- intent = new Intent(ACTION_MESSAGE_SENT, null);
- intent.putExtra("HANDLE", msgInfo.id);
- sentIntents.add(PendingIntent.getBroadcast(mContext, 0, intent,
- PendingIntent.FLAG_UPDATE_CURRENT));
+ Intent intentDelivery, intentSent;
+
+ intentDelivery = new Intent(ACTION_MESSAGE_DELIVERY, null);
+ /* Add msgId and part number to ensure the intents are different, and we
+ * thereby get an intent for each msg part.
+ * setType is needed to create different intents for each message id/ time stamp,
+ * as the extras are not used when comparing. */
+ intentDelivery.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i);
+ intentDelivery.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id);
+ intentDelivery.putExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, msgInfo.timestamp);
+ PendingIntent pendingIntentDelivery = PendingIntent.getBroadcast(mContext, 0,
+ intentDelivery, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ intentSent = new Intent(ACTION_MESSAGE_SENT, null);
+ /* Add msgId and part number to ensure the intents are different, and we
+ * thereby get an intent for each msg part.
+ * setType is needed to create different intents for each message id/ time stamp,
+ * as the extras are not used when comparing. */
+ intentSent.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i);
+ intentSent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id);
+ intentSent.putExtra(EXTRA_MESSAGE_SENT_URI, msgInfo.uri.toString());
+ intentSent.putExtra(EXTRA_MESSAGE_SENT_RETRY, msgInfo.retry);
+ intentSent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, msgInfo.transparent);
+
+ PendingIntent pendingIntentSent = PendingIntent.getBroadcast(mContext, 0,
+ intentSent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ // We use the same pending intent for all parts, but do not set the one shot flag.
+ deliveryIntents.add(pendingIntentDelivery);
+ sentIntents.add(pendingIntentSent);
}
Log.d(TAG, "sendMessage to " + msgInfo.phone);
@@ -956,21 +1655,38 @@ public class BluetoothMapContentObserver {
private static final String ACTION_MESSAGE_DELIVERY =
"com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY";
- private static final String ACTION_MESSAGE_SENT =
+ public static final String ACTION_MESSAGE_SENT =
"com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT";
+ public static final String EXTRA_MESSAGE_SENT_HANDLE = "HANDLE";
+ public static final String EXTRA_MESSAGE_SENT_RESULT = "result";
+ public static final String EXTRA_MESSAGE_SENT_URI = "uri";
+ public static final String EXTRA_MESSAGE_SENT_RETRY = "retry";
+ public static final String EXTRA_MESSAGE_SENT_TRANSPARENT = "transparent";
+ public static final String EXTRA_MESSAGE_SENT_TIMESTAMP = "timestamp";
+
private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver();
+ private boolean mInitialized = false;
+
private class SmsBroadcastReceiver extends BroadcastReceiver {
private final String[] ID_PROJECTION = new String[] { Sms._ID };
- private final Uri UPDATE_STATUS_URI = Uri.parse("content://sms/status");
+ private final Uri UPDATE_STATUS_URI = Uri.withAppendedPath(Sms.CONTENT_URI, "/status");
public void register() {
Handler handler = new Handler(Looper.getMainLooper());
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_MESSAGE_DELIVERY);
- intentFilter.addAction(ACTION_MESSAGE_SENT);
+ /* The reception of ACTION_MESSAGE_SENT have been moved to the MAP
+ * service, to be able to handle message sent events after a disconnect. */
+ //intentFilter.addAction(ACTION_MESSAGE_SENT);
+ try{
+ intentFilter.addDataType("message/*");
+ } catch (MalformedMimeTypeException e) {
+ Log.e(TAG, "Wrong mime type!!!", e);
+ }
+
mContext.registerReceiver(this, intentFilter, null, handler);
}
@@ -985,7 +1701,7 @@ public class BluetoothMapContentObserver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
- long handle = intent.getLongExtra("HANDLE", -1);
+ long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1);
PushMsgInfo msgInfo = mPushMsgList.get(handle);
Log.d(TAG, "onReceive: action" + action);
@@ -996,12 +1712,36 @@ public class BluetoothMapContentObserver {
}
if (action.equals(ACTION_MESSAGE_SENT)) {
+ int result = intent.getIntExtra(EXTRA_MESSAGE_SENT_RESULT, Activity.RESULT_CANCELED);
msgInfo.partsSent++;
+ if(result != Activity.RESULT_OK) {
+ // If just one of the parts in the message fails, we need to send the entire message again
+ msgInfo.failedSent = true;
+ }
+ if(D) Log.d(TAG, "onReceive: msgInfo.partsSent = " + msgInfo.partsSent
+ + ", msgInfo.parts = " + msgInfo.parts + " result = " + result);
+
if (msgInfo.partsSent == msgInfo.parts) {
actionMessageSent(context, intent, msgInfo);
}
} else if (action.equals(ACTION_MESSAGE_DELIVERY)) {
- msgInfo.partsDelivered++;
+ long timestamp = intent.getLongExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, 0);
+ int status = -1;
+ if(msgInfo.timestamp == timestamp) {
+ msgInfo.partsDelivered++;
+ byte[] pdu = intent.getByteArrayExtra("pdu");
+ String format = intent.getStringExtra("format");
+
+ SmsMessage message = SmsMessage.createFromPdu(pdu, format);
+ if (message == null) {
+ Log.d(TAG, "actionMessageDelivery: Can't get message from pdu");
+ return;
+ }
+ status = message.getStatus();
+ if(status != 0/*0 is success*/) {
+ msgInfo.statusDelivered = status;
+ }
+ }
if (msgInfo.partsDelivered == msgInfo.parts) {
actionMessageDelivery(context, intent, msgInfo);
}
@@ -1010,23 +1750,28 @@ public class BluetoothMapContentObserver {
}
}
- private void actionMessageSent(Context context, Intent intent,
- PushMsgInfo msgInfo) {
- int result = getResultCode();
+ private void actionMessageSent(Context context, Intent intent, PushMsgInfo msgInfo) {
+ /* As the MESSAGE_SENT intent is forwarded from the MAP service, we use the intent
+ * to carry the result, as getResult() will not return the correct value.
+ */
boolean delete = false;
- if (result == Activity.RESULT_OK) {
- Log.d(TAG, "actionMessageSent: result OK");
+ if(D) Log.d(TAG,"actionMessageSent(): msgInfo.failedSent = " + msgInfo.failedSent);
+
+ msgInfo.sendInProgress = false;
+
+ if (msgInfo.failedSent == false) {
+ if(D) Log.d(TAG, "actionMessageSent: result OK");
if (msgInfo.transparent == 0) {
if (!Sms.moveMessageToFolder(context, msgInfo.uri,
Sms.MESSAGE_TYPE_SENT, 0)) {
- Log.d(TAG, "Failed to move " + msgInfo.uri + " to SENT");
+ Log.w(TAG, "Failed to move " + msgInfo.uri + " to SENT");
}
} else {
delete = true;
}
- Event evt = new Event("SendingSuccess", msgInfo.id,
+ Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msgInfo.id,
folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
sendEvent(evt);
@@ -1034,20 +1779,22 @@ public class BluetoothMapContentObserver {
if (msgInfo.retry == 1) {
/* Notify failure, but keep message in outbox for resending */
msgInfo.resend = true;
- Event evt = new Event("SendingFailure", msgInfo.id,
+ msgInfo.partsSent = 0; // Reset counter for the retry
+ msgInfo.failedSent = false;
+ Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id,
folderSms[Sms.MESSAGE_TYPE_OUTBOX], null, mSmsType);
sendEvent(evt);
} else {
if (msgInfo.transparent == 0) {
if (!Sms.moveMessageToFolder(context, msgInfo.uri,
Sms.MESSAGE_TYPE_FAILED, 0)) {
- Log.d(TAG, "Failed to move " + msgInfo.uri + " to FAILED");
+ Log.w(TAG, "Failed to move " + msgInfo.uri + " to FAILED");
}
} else {
delete = true;
}
- Event evt = new Event("SendingFailure", msgInfo.id,
+ Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id,
folderSms[Sms.MESSAGE_TYPE_FAILED], null, mSmsType);
sendEvent(evt);
}
@@ -1055,25 +1802,18 @@ public class BluetoothMapContentObserver {
if (delete == true) {
/* Delete from Observer message list to avoid delete notifications */
- mMsgListSms.remove(msgInfo.id);
+ synchronized(mMsgListSms) {
+ mMsgListSms.remove(msgInfo.id);
+ }
/* Delete from DB */
mResolver.delete(msgInfo.uri, null, null);
}
}
- private void actionMessageDelivery(Context context, Intent intent,
- PushMsgInfo msgInfo) {
+ private void actionMessageDelivery(Context context, Intent intent, PushMsgInfo msgInfo) {
Uri messageUri = intent.getData();
- byte[] pdu = intent.getByteArrayExtra("pdu");
- String format = intent.getStringExtra("format");
-
- SmsMessage message = SmsMessage.createFromPdu(pdu, format);
- if (message == null) {
- Log.d(TAG, "actionMessageDelivery: Can't get message from pdu");
- return;
- }
- int status = message.getStatus();
+ msgInfo.sendInProgress = false;
Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null);
@@ -1082,14 +1822,12 @@ public class BluetoothMapContentObserver {
int messageId = cursor.getInt(0);
Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId);
- boolean isStatusReport = message.isStatusReportMessage();
- Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + status +
- ", isStatusReport=" + isStatusReport);
+ if(D) Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + msgInfo.statusDelivered);
ContentValues contentValues = new ContentValues(2);
- contentValues.put(Sms.STATUS, status);
+ contentValues.put(Sms.STATUS, msgInfo.statusDelivered);
contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis());
mResolver.update(updateUri, contentValues, null, null);
} else {
@@ -1099,12 +1837,12 @@ public class BluetoothMapContentObserver {
cursor.close();
}
- if (status == 0) {
- Event evt = new Event("DeliverySuccess", msgInfo.id,
+ if (msgInfo.statusDelivered == 0) {
+ Event evt = new Event(EVENT_TYPE_DELEVERY_SUCCESS, msgInfo.id,
folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
sendEvent(evt);
} else {
- Event evt = new Event("DeliveryFailure", msgInfo.id,
+ Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id,
folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType);
sendEvent(evt);
}
@@ -1113,6 +1851,54 @@ public class BluetoothMapContentObserver {
}
}
+ static public void actionMessageSentDisconnected(Context context, Intent intent, int result) {
+ boolean delete = false;
+ //int retry = intent.getIntExtra(EXTRA_MESSAGE_SENT_RETRY, 0);
+ int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0);
+ String uriString = intent.getStringExtra(EXTRA_MESSAGE_SENT_URI);
+ if(uriString == null) {
+ // Nothing we can do about it, just bail out
+ return;
+ }
+ Uri uri = Uri.parse(uriString);
+
+ if (result == Activity.RESULT_OK) {
+ Log.d(TAG, "actionMessageSentDisconnected: result OK");
+ if (transparent == 0) {
+ if (!Sms.moveMessageToFolder(context, uri,
+ Sms.MESSAGE_TYPE_SENT, 0)) {
+ Log.d(TAG, "Failed to move " + uri + " to SENT");
+ }
+ } else {
+ delete = true;
+ }
+ } else {
+ /*if (retry == 1) {
+ The retry feature only works while connected, else we fail the send,
+ * and move the message to failed, to let the user/app resend manually later.
+ } else */{
+ if (transparent == 0) {
+ if (!Sms.moveMessageToFolder(context, uri,
+ Sms.MESSAGE_TYPE_FAILED, 0)) {
+ Log.d(TAG, "Failed to move " + uri + " to FAILED");
+ }
+ } else {
+ delete = true;
+ }
+ }
+ }
+
+ if (delete == true) {
+ /* Delete from DB */
+ ContentResolver resolver = context.getContentResolver();
+ if(resolver != null) {
+ resolver.delete(uri, null, null);
+ } else {
+ Log.w(TAG, "Unable to get resolver");
+ }
+ }
+ }
+
private void registerPhoneServiceStateListener() {
TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE);
@@ -1131,12 +1917,13 @@ public class BluetoothMapContentObserver {
if (c != null && c.moveToFirst()) {
do {
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ long id = c.getLong(c.getColumnIndex(Sms._ID));
String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
PushMsgInfo msgInfo = mPushMsgList.get(id);
- if (msgInfo == null || msgInfo.resend == false) {
+ if (msgInfo == null || msgInfo.resend == false || msgInfo.sendInProgress == true) {
continue;
}
+ msgInfo.sendInProgress = true;
sendMessage(msgInfo, msgBody);
} while (c.moveToNext());
c.close();
@@ -1151,7 +1938,7 @@ public class BluetoothMapContentObserver {
if (c != null && c.moveToFirst()) {
do {
- long id = c.getLong(c.getColumnIndex(BaseColumns._ID));
+ long id = c.getLong(c.getColumnIndex(Sms._ID));
String msgBody = c.getString(c.getColumnIndex(Sms.BODY));
PushMsgInfo msgInfo = mPushMsgList.get(id);
if (msgInfo == null || msgInfo.resend == false) {
@@ -1166,7 +1953,7 @@ public class BluetoothMapContentObserver {
private void removeDeletedMessages() {
/* Remove messages from virtual "deleted" folder (thread_id -1) */
- mResolver.delete(Uri.parse("content://sms/"),
+ mResolver.delete(Sms.CONTENT_URI,
"thread_id = " + DELETED_THREAD_ID, null);
}
@@ -1183,12 +1970,24 @@ public class BluetoothMapContentObserver {
public void init() {
mSmsBroadcastReceiver.register();
registerPhoneServiceStateListener();
+ mInitialized = true;
}
public void deinit() {
+ mInitialized = false;
+ unregisterObserver();
mSmsBroadcastReceiver.unregister();
unRegisterPhoneServiceStateListener();
failPendingMessages();
removeDeletedMessages();
}
+
+ public boolean handleSmsSendIntent(Context context, Intent intent){
+ if(mInitialized) {
+ mSmsBroadcastReceiver.onReceive(context, intent);
+ return true;
+ }
+ return false;
+ }
+
}
diff --git a/src/com/android/bluetooth/map/BluetoothMapEmailAppObserver.java b/src/com/android/bluetooth/map/BluetoothMapEmailAppObserver.java
new file mode 100644
index 000000000..38d0444fe
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapEmailAppObserver.java
@@ -0,0 +1,287 @@
+/*
+* Copyright (C) 2014 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.map;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.Set;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import com.android.bluetooth.mapapi.BluetoothMapContract;
+import android.util.Log;
+
+/**
+ * Class to construct content observers for for email applications on the system.
+ *
+ *
+ */
+
+public class BluetoothMapEmailAppObserver{
+
+ private static final String TAG = "BluetoothMapEmailAppObserver";
+
+ private static final boolean D = BluetoothMapService.DEBUG;
+ private static final boolean V = BluetoothMapService.VERBOSE;
+ /* */
+ private LinkedHashMap<BluetoothMapEmailSettingsItem, ArrayList<BluetoothMapEmailSettingsItem>> mFullList;
+ private LinkedHashMap<String,ContentObserver> mObserverMap = new LinkedHashMap<String,ContentObserver>();
+ private ContentResolver mResolver;
+ private Context mContext;
+ private BroadcastReceiver mReceiver;
+ private PackageManager mPackageManager = null;
+ BluetoothMapEmailSettingsLoader mLoader;
+ BluetoothMapService mMapService = null;
+
+ public BluetoothMapEmailAppObserver(final Context context, BluetoothMapService mapService) {
+ mContext = context;
+ mMapService = mapService;
+ mResolver = context.getContentResolver();
+ mLoader = new BluetoothMapEmailSettingsLoader(mContext);
+ mFullList = mLoader.parsePackages(false); /* Get the current list of apps */
+ createReceiver();
+ initObservers();
+ }
+
+
+ private BluetoothMapEmailSettingsItem getApp(String packageName) {
+ if(V) Log.d(TAG, "getApp(): Looking for " + packageName);
+ for(BluetoothMapEmailSettingsItem app:mFullList.keySet()){
+ if(V) Log.d(TAG, " Comparing: " + app.getPackageName());
+ if(app.getPackageName().equals(packageName)) {
+ if(V) Log.d(TAG, " found " + app.mBase_uri_no_account);
+ return app;
+ }
+ }
+ if(V) Log.d(TAG, " NOT FOUND!");
+ return null;
+ }
+
+ private void handleAccountChanges(String packageNameWithProvider) {
+
+ if(D)Log.d(TAG,"handleAccountChanges (packageNameWithProvider: "+packageNameWithProvider+"\n");
+ String packageName = packageNameWithProvider.replaceFirst("\\.[^\\.]+$", "");
+ BluetoothMapEmailSettingsItem app = getApp(packageName);
+ if(app != null) {
+ ArrayList<BluetoothMapEmailSettingsItem> newAccountList = mLoader.parseAccounts(app);
+ ArrayList<BluetoothMapEmailSettingsItem> oldAccountList = mFullList.get(app);
+ ArrayList<BluetoothMapEmailSettingsItem> addedAccountList =
+ (ArrayList<BluetoothMapEmailSettingsItem>)newAccountList.clone();
+ ArrayList<BluetoothMapEmailSettingsItem> removedAccountList = mFullList.get(app); // Same as oldAccountList.clone
+
+ mFullList.put(app, newAccountList);
+ for(BluetoothMapEmailSettingsItem newAcc: newAccountList){
+ for(BluetoothMapEmailSettingsItem oldAcc: oldAccountList){
+ if(newAcc.getId() == oldAcc.getId()){
+ // For each match remove from both removed and added lists
+ removedAccountList.remove(oldAcc);
+ addedAccountList.remove(newAcc);
+ if(!newAcc.getName().equals(oldAcc.getName()) && newAcc.mIsChecked){
+ // Name Changed and the acc is visible - Change Name in SDP record
+ mMapService.updateMasInstances(BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_RENAMED);
+ if(V)Log.d(TAG, " UPDATE_MAS_INSTANCES_ACCOUNT_RENAMED");
+ }
+ if(newAcc.mIsChecked != oldAcc.mIsChecked) {
+ // Visibility changed
+ if(newAcc.mIsChecked){
+ // account added - create SDP record
+ mMapService.updateMasInstances(BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_ADDED);
+ if(V)Log.d(TAG, " UPDATE_MAS_INSTANCES_ACCOUNT_ADDED isChecked changed");
+ } else {
+ // account removed - remove SDP record
+ mMapService.updateMasInstances(BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED);
+ if(V)Log.d(TAG, " UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED isChecked changed");
+ }
+ }
+ break;
+ }
+ }
+ }
+ // Notify on any removed accounts
+ for(BluetoothMapEmailSettingsItem removedAcc: removedAccountList){
+ mMapService.updateMasInstances(BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED);
+ if(V)Log.d(TAG, " UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED " + removedAcc);
+ }
+ // Notify on any new accounts
+ for(BluetoothMapEmailSettingsItem addedAcc: addedAccountList){
+ mMapService.updateMasInstances(BluetoothMapService.UPDATE_MAS_INSTANCES_ACCOUNT_ADDED);
+ if(V)Log.d(TAG, " UPDATE_MAS_INSTANCES_ACCOUNT_ADDED " + addedAcc);
+ }
+
+ } else {
+ Log.e(TAG, "Received change notification on package not registered for notifications!");
+
+ }
+ }
+
+ /**
+ * Adds a new content observer to the list of content observers.
+ * The key for the observer is the uri as string
+ * @param uri uri for the package that supports MAP email
+ */
+
+ public void registerObserver(BluetoothMapEmailSettingsItem app) {
+ Uri uri = BluetoothMapContract.buildAccountUri(app.getProviderAuthority());
+ if (V) Log.d(TAG, "registerObserver for URI "+uri.toString()+"\n");
+ ContentObserver observer = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ onChange(selfChange, null);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ if (V) Log.d(TAG, "onChange on thread: " + Thread.currentThread().getId()
+ + " Uri: " + uri + " selfchange: " + selfChange);
+ if(uri != null) {
+ handleAccountChanges(uri.getHost());
+ } else {
+ Log.e(TAG, "Unable to handle change as the URI is NULL!");
+ }
+
+ }
+ };
+ mObserverMap.put(uri.toString(), observer);
+ mResolver.registerContentObserver(uri, true, observer);
+ }
+
+ public void unregisterObserver(BluetoothMapEmailSettingsItem app) {
+ Uri uri = BluetoothMapContract.buildAccountUri(app.getProviderAuthority());
+ if (V) Log.d(TAG, "unregisterObserver("+uri.toString()+")\n");
+ mResolver.unregisterContentObserver(mObserverMap.get(uri.toString()));
+ mObserverMap.remove(uri.toString());
+ }
+
+ private void initObservers(){
+ if(D)Log.d(TAG,"initObservers()");
+ for(BluetoothMapEmailSettingsItem app: mFullList.keySet()){
+ registerObserver(app);
+ }
+ }
+
+ private void deinitObservers(){
+ if(D)Log.d(TAG,"deinitObservers()");
+ for(BluetoothMapEmailSettingsItem app: mFullList.keySet()){
+ unregisterObserver(app);
+ }
+ }
+
+ private void createReceiver(){
+ if(D)Log.d(TAG,"createReceiver()\n");
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ intentFilter.addDataScheme("package");
+ mReceiver= new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if(D)Log.d(TAG,"onReceive\n");
+ String action = intent.getAction();
+ if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+ Uri data = intent.getData();
+ String packageName = data.getEncodedSchemeSpecificPart();
+ if(D)Log.d(TAG,"The installed package is: "+ packageName);
+ // PackageInfo pInfo = getPackageInfo(packageName);
+ ResolveInfo rInfo = mPackageManager.resolveActivity(intent, 0);
+ BluetoothMapEmailSettingsItem app = mLoader.createAppItem(rInfo, false);
+ if(app != null) {
+ registerObserver(app);
+ // Add all accounts to mFullList
+ ArrayList<BluetoothMapEmailSettingsItem> newAccountList = mLoader.parseAccounts(app);
+ mFullList.put(app, newAccountList);
+ }
+ }
+ else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+
+ Uri data = intent.getData();
+ String packageName = data.getEncodedSchemeSpecificPart();
+ if(D)Log.d(TAG,"The removed package is: "+ packageName);
+ BluetoothMapEmailSettingsItem app = getApp(packageName);
+ /* Find the object and remove from fullList */
+ if(app != null) {
+ unregisterObserver(app);
+ mFullList.remove(app);
+ }
+ }
+ }
+ };
+ mContext.registerReceiver(mReceiver,new IntentFilter(Intent.ACTION_PACKAGE_ADDED));
+ }
+ private void removeReceiver(){
+ if(D)Log.d(TAG,"removeReceiver()\n");
+ mContext.unregisterReceiver(mReceiver);
+ }
+ private PackageInfo getPackageInfo(String packageName){
+ mPackageManager = mContext.getPackageManager();
+ try {
+ return mPackageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA|PackageManager.GET_SERVICES);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG,"Error getting package metadata", e);
+ }
+ return null;
+ }
+
+ /**
+ * Method to get a list of the accounts (across all apps) that are set to be shared
+ * through MAP.
+ * @return Arraylist<BluetoothMapEmailSettingsItem> containing all enabled accounts
+ */
+ public ArrayList<BluetoothMapEmailSettingsItem> getEnabledAccountItems(){
+ if(D)Log.d(TAG,"getEnabledAccountItems()\n");
+ ArrayList<BluetoothMapEmailSettingsItem> list = new ArrayList<BluetoothMapEmailSettingsItem>();
+ for(BluetoothMapEmailSettingsItem app:mFullList.keySet()){
+ ArrayList<BluetoothMapEmailSettingsItem> accountList = mFullList.get(app);
+ for(BluetoothMapEmailSettingsItem acc: accountList){
+ if(acc.mIsChecked) {
+ list.add(acc);
+ }
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Method to get a list of the accounts (across all apps).
+ * @return Arraylist<BluetoothMapEmailSettingsItem> containing all accounts
+ */
+ public ArrayList<BluetoothMapEmailSettingsItem> getAllAccountItems(){
+ if(D)Log.d(TAG,"getAllAccountItems()\n");
+ ArrayList<BluetoothMapEmailSettingsItem> list = new ArrayList<BluetoothMapEmailSettingsItem>();
+ for(BluetoothMapEmailSettingsItem app:mFullList.keySet()){
+ ArrayList<BluetoothMapEmailSettingsItem> accountList = mFullList.get(app);
+ list.addAll(accountList);
+ }
+ return list;
+ }
+
+
+ /**
+ * Cleanup all resources - must be called to avoid leaks.
+ */
+ public void shutdown() {
+ deinitObservers();
+ removeReceiver();
+ }
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapEmailSettings.java b/src/com/android/bluetooth/map/BluetoothMapEmailSettings.java
new file mode 100644
index 000000000..ae9cc327a
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapEmailSettings.java
@@ -0,0 +1,56 @@
+/*
+* Copyright (C) 2014 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.map;
+import com.android.bluetooth.R;
+import com.android.bluetooth.map.BluetoothMapEmailSettingsItem;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.ExpandableListView;
+
+
+public class BluetoothMapEmailSettings extends Activity {
+
+ private static final String TAG = "BluetoothMapEmailSettings";
+ private static final boolean D = BluetoothMapService.DEBUG;
+ private static final boolean V = BluetoothMapService.VERBOSE;
+
+
+
+ BluetoothMapEmailSettingsLoader mLoader = new BluetoothMapEmailSettingsLoader(this);
+ LinkedHashMap<BluetoothMapEmailSettingsItem,ArrayList<BluetoothMapEmailSettingsItem>> mGroups;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ /* set UI */
+ setContentView(R.layout.bluetooth_map_email_settings);
+ /* create structure for list of groups + items*/
+ mGroups = mLoader.parsePackages(true);
+
+
+ /* update expandable listview with correct items */
+ ExpandableListView listView = (ExpandableListView) findViewById(R.id.bluetooth_map_email_settings_list_view);
+
+ BluetoothMapEmailSettingsAdapter adapter = new BluetoothMapEmailSettingsAdapter(this,listView, mGroups, mLoader.getAccountsEnabledCount());
+ listView.setAdapter(adapter);
+ }
+
+
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapEmailSettingsAdapter.java b/src/com/android/bluetooth/map/BluetoothMapEmailSettingsAdapter.java
new file mode 100644
index 000000000..feca9c0c6
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapEmailSettingsAdapter.java
@@ -0,0 +1,347 @@
+/*
+* Copyright (C) 2014 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.map;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.android.bluetooth.R;
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import com.android.bluetooth.mapapi.BluetoothMapContract;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.CheckBox;
+import android.widget.CheckedTextView;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.OnGroupExpandListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.CompoundButton;
+import com.android.bluetooth.map.BluetoothMapEmailSettingsItem;
+import com.android.bluetooth.map.BluetoothMapEmailSettingsLoader;
+public class BluetoothMapEmailSettingsAdapter extends BaseExpandableListAdapter {
+ private static final boolean D = BluetoothMapService.DEBUG;
+ private static final boolean V = BluetoothMapService.VERBOSE;
+ private static final String TAG = "BluetoothMapEmailSettingsAdapter";
+ private boolean mCheckAll = true;
+ public LayoutInflater mInflater;
+ public Activity mActivity;
+ /*needed to prevent random checkbox toggles due to item reuse */
+ ArrayList<Boolean> mPositionArray;
+ private LinkedHashMap<BluetoothMapEmailSettingsItem,
+ ArrayList<BluetoothMapEmailSettingsItem>> mProupList;
+ private ArrayList<BluetoothMapEmailSettingsItem> mMainGroup;
+ private int[] mGroupStatus;
+ /* number of accounts possible to share */
+ private int mSlotsLeft = 10;
+
+
+ public BluetoothMapEmailSettingsAdapter(Activity act,
+ ExpandableListView listView,
+ LinkedHashMap<BluetoothMapEmailSettingsItem,
+ ArrayList<BluetoothMapEmailSettingsItem>> groupsList,
+ int enabledAccountsCounts) {
+ mActivity = act;
+ this.mProupList = groupsList;
+ mInflater = act.getLayoutInflater();
+ mGroupStatus = new int[groupsList.size()];
+ mSlotsLeft = mSlotsLeft-enabledAccountsCounts;
+
+ listView.setOnGroupExpandListener(new OnGroupExpandListener() {
+
+ public void onGroupExpand(int groupPosition) {
+ BluetoothMapEmailSettingsItem group = mMainGroup.get(groupPosition);
+ if (mProupList.get(group).size() > 0)
+ mGroupStatus[groupPosition] = 1;
+
+ }
+ });
+ mMainGroup = new ArrayList<BluetoothMapEmailSettingsItem>();
+ for (Map.Entry<BluetoothMapEmailSettingsItem, ArrayList<BluetoothMapEmailSettingsItem>> mapEntry : mProupList.entrySet()) {
+ mMainGroup.add(mapEntry.getKey());
+ }
+ }
+
+ @Override
+ public BluetoothMapEmailSettingsItem getChild(int groupPosition, int childPosition) {
+ BluetoothMapEmailSettingsItem item = mMainGroup.get(groupPosition);
+ return mProupList.get(item).get(childPosition);
+ }
+ private ArrayList<BluetoothMapEmailSettingsItem> getChild(BluetoothMapEmailSettingsItem group) {
+ return mProupList.get(group);
+ }
+
+ @Override
+ public long getChildId(int groupPosition, int childPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getChildView(final int groupPosition, final int childPosition,
+ boolean isLastChild, View convertView, ViewGroup parent) {
+
+
+ final ChildHolder holder;
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.bluetooth_map_email_settings_account_item, null);
+ holder = new ChildHolder();
+ holder.cb = (CheckBox) convertView.findViewById(R.id.bluetooth_map_email_settings_item_check);
+ holder.title = (TextView) convertView.findViewById(R.id.bluetooth_map_email_settings_item_text_view);
+ convertView.setTag(holder);
+ } else {
+ holder = (ChildHolder) convertView.getTag();
+ }
+ final BluetoothMapEmailSettingsItem child = getChild(groupPosition, childPosition);
+ holder.cb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ BluetoothMapEmailSettingsItem parentGroup = (BluetoothMapEmailSettingsItem)getGroup(groupPosition);
+ boolean oldIsChecked = child.mIsChecked; // needed to prevent updates on UI redraw
+ child.mIsChecked = isChecked;
+ if (isChecked) {
+ ArrayList<BluetoothMapEmailSettingsItem> childList = getChild(parentGroup);
+ int childIndex = childList.indexOf(child);
+ boolean isAllChildClicked = true;
+ if(mSlotsLeft-childList.size() >=0){
+
+ for (int i = 0; i < childList.size(); i++) {
+ if (i != childIndex) {
+ BluetoothMapEmailSettingsItem siblings = childList.get(i);
+ if (!siblings.mIsChecked) {
+ isAllChildClicked = false;
+ BluetoothMapEmailSettingsDataHolder.mCheckedChilds.put(child.getName(),
+ parentGroup.getName());
+ break;
+
+ }
+ }
+ }
+ }else {
+ showWarning(mActivity.getString(R.string.bluetooth_map_email_settings_no_account_slots_left));
+ isAllChildClicked = false;
+ child.mIsChecked = false;
+ }
+ if (isAllChildClicked) {
+ parentGroup.mIsChecked = true;
+ if(!(BluetoothMapEmailSettingsDataHolder.mCheckedChilds.containsKey(child.getName())==true)){
+ BluetoothMapEmailSettingsDataHolder.mCheckedChilds.put(child.getName(),
+ parentGroup.getName());
+ }
+ mCheckAll = false;
+ }
+
+
+ } else {
+ if (parentGroup.mIsChecked) {
+ parentGroup.mIsChecked = false;
+ mCheckAll = false;
+ BluetoothMapEmailSettingsDataHolder.mCheckedChilds.remove(child.getName());
+ } else {
+ mCheckAll = true;
+ BluetoothMapEmailSettingsDataHolder.mCheckedChilds.remove(child.getName());
+ }
+ // child.isChecked =false;
+ }
+ notifyDataSetChanged();
+ if(child.mIsChecked != oldIsChecked){
+ updateAccount(child);
+ }
+
+ }
+
+ });
+
+ holder.cb.setChecked(child.mIsChecked);
+ holder.title.setText(child.getName());
+ if(D)Log.i("childs are", BluetoothMapEmailSettingsDataHolder.mCheckedChilds.toString());
+ return convertView;
+
+ }
+
+
+
+ @Override
+ public int getChildrenCount(int groupPosition) {
+ BluetoothMapEmailSettingsItem item = mMainGroup.get(groupPosition);
+ return mProupList.get(item).size();
+ }
+
+ @Override
+ public BluetoothMapEmailSettingsItem getGroup(int groupPosition) {
+ return mMainGroup.get(groupPosition);
+ }
+
+ @Override
+ public int getGroupCount() {
+ return mMainGroup.size();
+ }
+
+ @Override
+ public void onGroupCollapsed(int groupPosition) {
+ super.onGroupCollapsed(groupPosition);
+ }
+
+ @Override
+ public void onGroupExpanded(int groupPosition) {
+ super.onGroupExpanded(groupPosition);
+ }
+
+ @Override
+ public long getGroupId(int groupPosition) {
+ return 0;
+ }
+
+ @Override
+ public View getGroupView(int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+
+ final GroupHolder holder;
+
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.bluetooth_map_email_settings_account_group, null);
+ holder = new GroupHolder();
+ holder.cb = (CheckBox) convertView.findViewById(R.id.bluetooth_map_email_settings_group_checkbox);
+ holder.imageView = (ImageView) convertView
+ .findViewById(R.id.bluetooth_map_email_settings_group_icon);
+ holder.title = (TextView) convertView.findViewById(R.id.bluetooth_map_email_settings_group_text_view);
+ convertView.setTag(holder);
+ } else {
+ holder = (GroupHolder) convertView.getTag();
+ }
+
+ final BluetoothMapEmailSettingsItem groupItem = getGroup(groupPosition);
+ holder.imageView.setImageDrawable(groupItem.getIcon());
+
+
+ holder.title.setText(groupItem.getName());
+
+ holder.cb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ if (mCheckAll) {
+ ArrayList<BluetoothMapEmailSettingsItem> childItem = getChild(groupItem);
+ for (BluetoothMapEmailSettingsItem children : childItem)
+ {
+ boolean oldIsChecked = children.mIsChecked;
+ if(mSlotsLeft >0){
+ children.mIsChecked = isChecked;
+ if(oldIsChecked != children.mIsChecked){
+ updateAccount(children);
+ }
+ }else {
+ showWarning(mActivity.getString(R.string.bluetooth_map_email_settings_no_account_slots_left));
+ isChecked = false;
+ }
+ }
+ }
+ groupItem.mIsChecked = isChecked;
+ notifyDataSetChanged();
+ new Handler().postDelayed(new Runnable() {
+
+ public void run() {
+ if (!mCheckAll)
+ mCheckAll = true;
+ }
+ }, 50);
+
+ }
+
+ });
+ holder.cb.setChecked(groupItem.mIsChecked);
+ return convertView;
+
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ private class GroupHolder {
+ public ImageView imageView;
+ public CheckBox cb;
+ public TextView title;
+
+ }
+
+ private class ChildHolder {
+ public TextView title;
+ public CheckBox cb;
+ }
+ public void updateAccount(BluetoothMapEmailSettingsItem account) {
+ updateSlotCounter(account.mIsChecked);
+ if(D)Log.d(TAG,"Updating account settings for "+account.getName() +". Value is:"+account.mIsChecked);
+ ContentResolver mResolver = mActivity.getContentResolver();
+ Uri uri = Uri.parse(account.mBase_uri_no_account+"/"+BluetoothMapContract.TABLE_ACCOUNT);
+ ContentValues values = new ContentValues();
+ values.put(BluetoothMapContract.AccountColumns.FLAG_EXPOSE, ((account.mIsChecked)?1:0)); // get title
+ values.put(BluetoothMapContract.AccountColumns._ID, account.getId()); // get title
+ mResolver.update(uri, values, null ,null);
+
+ }
+ private void updateSlotCounter(boolean isChecked){
+ if(isChecked)
+ {
+ mSlotsLeft--;
+ }else {
+ mSlotsLeft++;
+ }
+ CharSequence text;
+
+ if (mSlotsLeft <=0)
+ {
+ text = mActivity.getString(R.string.bluetooth_map_email_settings_no_account_slots_left);
+ }else {
+ text= mActivity.getString(R.string.bluetooth_map_email_settings_count) + " "+ String.valueOf(mSlotsLeft);
+ }
+
+ int duration = Toast.LENGTH_SHORT;
+
+ Toast toast = Toast.makeText(mActivity, text, duration);
+ toast.show();
+ }
+ private void showWarning(String text){
+ int duration = Toast.LENGTH_SHORT;
+
+ Toast toast = Toast.makeText(mActivity, text, duration);
+ toast.show();
+
+ }
+
+
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapEmailSettingsDataHolder.java b/src/com/android/bluetooth/map/BluetoothMapEmailSettingsDataHolder.java
new file mode 100644
index 000000000..104e2af8b
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapEmailSettingsDataHolder.java
@@ -0,0 +1,23 @@
+/*
+* Copyright (C) 2014 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.map;
+
+import java.util.HashMap;
+
+public class BluetoothMapEmailSettingsDataHolder {
+ public static HashMap<String, String> mCheckedChilds = new HashMap<String, String>();
+}
+
diff --git a/src/com/android/bluetooth/map/BluetoothMapEmailSettingsItem.java b/src/com/android/bluetooth/map/BluetoothMapEmailSettingsItem.java
new file mode 100644
index 000000000..ec1e14863
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapEmailSettingsItem.java
@@ -0,0 +1,172 @@
+/*
+* Copyright (C) 2014 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.map;
+
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+/**
+ * Class to contain all the info about the items of the Map Email Settings Menu.
+ * It can be used for both Email Apps (group Parent item) and Accounts (Group child Item).
+ *
+ */
+public class BluetoothMapEmailSettingsItem implements Comparable<BluetoothMapEmailSettingsItem>{
+ private static final String TAG = "BluetoothMapEmailSettingsItem";
+
+ private static final boolean D = BluetoothMapService.DEBUG;
+ private static final boolean V = BluetoothMapService.VERBOSE;
+
+ protected boolean mIsChecked;
+ private String mName;
+ private String mPackageName;
+ private String mId;
+ private String mProviderAuthority;
+ private Drawable mIcon;
+ public String mBase_uri;
+ public String mBase_uri_no_account;
+ public BluetoothMapEmailSettingsItem(String id, String name, String packageName, String authority, Drawable icon) {
+ this.mName = name;
+ this.mIcon = icon;
+ this.mPackageName = packageName;
+ this.mId = id;
+ this.mProviderAuthority = authority;
+ this.mBase_uri_no_account = "content://" + authority;
+ this.mBase_uri = mBase_uri_no_account + "/"+id;
+ }
+
+ public long getAccountId() {
+ if(mId != null) {
+ return Long.parseLong(mId);
+ }
+ return -1;
+ }
+
+ @Override
+ public int compareTo(BluetoothMapEmailSettingsItem other) {
+
+ if(!other.mId.equals(this.mId)){
+ if(V) Log.d(TAG, "Wrong id : " + this.mId + " vs " + other.mId);
+ return -1;
+ }
+ if(!other.mName.equals(this.mName)){
+ if(V) Log.d(TAG, "Wrong name : " + this.mName + " vs " + other.mName);
+ return -1;
+ }
+ if(!other.mPackageName.equals(this.mPackageName)){
+ if(V) Log.d(TAG, "Wrong packageName : " + this.mPackageName + " vs " + other.mPackageName);
+ return -1;
+ }
+ if(!other.mProviderAuthority.equals(this.mProviderAuthority)){
+ if(V) Log.d(TAG, "Wrong providerName : " + this.mProviderAuthority + " vs " + other.mProviderAuthority);
+ return -1;
+ }
+ if(other.mIsChecked != this.mIsChecked){
+ if(V) Log.d(TAG, "Wrong isChecked : " + this.mIsChecked + " vs " + other.mIsChecked);
+ return -1;
+ }
+ return 0;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((mId == null) ? 0 : mId.hashCode());
+ result = prime * result + ((mName == null) ? 0 : mName.hashCode());
+ result = prime * result
+ + ((mPackageName == null) ? 0 : mPackageName.hashCode());
+ result = prime * result
+ + ((mProviderAuthority == null) ? 0 : mProviderAuthority.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ BluetoothMapEmailSettingsItem other = (BluetoothMapEmailSettingsItem) obj;
+ if (mId == null) {
+ if (other.mId != null)
+ return false;
+ } else if (!mId.equals(other.mId))
+ return false;
+ if (mName == null) {
+ if (other.mName != null)
+ return false;
+ } else if (!mName.equals(other.mName))
+ return false;
+ if (mPackageName == null) {
+ if (other.mPackageName != null)
+ return false;
+ } else if (!mPackageName.equals(other.mPackageName))
+ return false;
+ if (mProviderAuthority == null) {
+ if (other.mProviderAuthority != null)
+ return false;
+ } else if (!mProviderAuthority.equals(other.mProviderAuthority))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return mName + " (" + mBase_uri + ")";
+ }
+
+ public Drawable getIcon() {
+ return mIcon;
+ }
+
+ public void setIcon(Drawable icon) {
+ this.mIcon = icon;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void setName(String name) {
+ this.mName = name;
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ public void setId(String id) {
+ this.mId = id;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public void setPackageName(String packageName) {
+ this.mPackageName = packageName;
+ }
+
+ public String getProviderAuthority() {
+ return mProviderAuthority;
+ }
+
+ public void setProviderAuthority(String providerAuthority) {
+ this.mProviderAuthority = providerAuthority;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/bluetooth/map/BluetoothMapEmailSettingsLoader.java b/src/com/android/bluetooth/map/BluetoothMapEmailSettingsLoader.java
new file mode 100644
index 000000000..d42a8088b
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapEmailSettingsLoader.java
@@ -0,0 +1,202 @@
+/*
+* Copyright (C) 2014 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.map;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import com.android.bluetooth.map.BluetoothMapEmailSettingsItem;
+
+
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import com.android.bluetooth.mapapi.BluetoothMapContract;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+
+public class BluetoothMapEmailSettingsLoader {
+ private static final String TAG = "BluetoothMapEmailSettingsLoader";
+ private static final boolean D = BluetoothMapService.DEBUG;
+ private static final boolean V = BluetoothMapService.VERBOSE;
+ private Context mContext = null;
+ private PackageManager mPackageManager = null;
+ private ContentResolver mResolver;
+ private int mAccountsEnabledCount = 0;
+ private ContentProviderClient mProviderClient = null;
+ private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
+
+ public BluetoothMapEmailSettingsLoader(Context ctx)
+ {
+ mContext = ctx;
+ }
+
+ /**
+ * Method to look through all installed packages system-wide and find those that contain the
+ * BTMAP meta-tag in their manifest file. For each app the list of accounts are fetched using
+ * the method parseAccounts().
+ * @return LinkedHashMap with the packages as keys(BluetoothMapEmailSettingsItem) and
+ * values as ArrayLists of BluetoothMapEmailSettingsItems.
+ */
+ public LinkedHashMap<BluetoothMapEmailSettingsItem,
+ ArrayList<BluetoothMapEmailSettingsItem>> parsePackages(boolean includeIcon) {
+
+ LinkedHashMap<BluetoothMapEmailSettingsItem, ArrayList<BluetoothMapEmailSettingsItem>> groups =
+ new LinkedHashMap<BluetoothMapEmailSettingsItem,ArrayList<BluetoothMapEmailSettingsItem>>();
+ Intent searchIntent = new Intent(BluetoothMapContract.PROVIDER_INTERFACE);
+ // reset the counter every time this method is called.
+ mAccountsEnabledCount=0;
+ // find all installed packages and filter out those that do not support Map Email.
+ // this is done by looking for a apps with content providers containing the intent-filter for
+ // android.content.action.BTMAP_SHARE in the manifest file.
+ mPackageManager = mContext.getPackageManager();
+ List<ResolveInfo> resInfos = mPackageManager
+ .queryIntentContentProviders(searchIntent, 0);
+
+ if (resInfos != null) {
+ if(D) Log.d(TAG,"Found " + resInfos.size() + " applications");
+ for (ResolveInfo rInfo : resInfos) {
+ // We cannot rely on apps that have been force-stopped in the application settings menu.
+ if ((rInfo.providerInfo.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) == 0) {
+ BluetoothMapEmailSettingsItem app = createAppItem(rInfo, includeIcon);
+ if (app != null){
+ ArrayList<BluetoothMapEmailSettingsItem> accounts = parseAccounts(app);
+ // we do not want to list apps without accounts
+ if(accounts.size() >0)
+ {
+ // we need to make sure that the "select all" checkbox is checked if all accounts in the list are checked
+ app.mIsChecked = true;
+ for (BluetoothMapEmailSettingsItem acc: accounts)
+ {
+ if(!acc.mIsChecked)
+ {
+ app.mIsChecked = false;
+ break;
+ }
+ }
+ groups.put(app, accounts);
+ }
+ }
+ } else {
+ if(D)Log.d(TAG,"Ignoring force-stopped authority "+ rInfo.providerInfo.authority +"\n");
+ }
+ }
+ }
+ else {
+ if(D) Log.d(TAG,"Found no applications");
+ }
+ return groups;
+ }
+
+ public BluetoothMapEmailSettingsItem createAppItem(ResolveInfo rInfo,
+ boolean includeIcon) {
+ String provider = rInfo.providerInfo.authority;
+ if(provider != null) {
+ String name = rInfo.loadLabel(mPackageManager).toString();
+ if(D)Log.d(TAG,rInfo.providerInfo.packageName + " - " + name + " - meta-data(provider = " + provider+")\n");
+ BluetoothMapEmailSettingsItem app = new BluetoothMapEmailSettingsItem(
+ "0",
+ name,
+ rInfo.providerInfo.packageName,
+ provider,
+ (includeIcon == false)? null : rInfo.loadIcon(mPackageManager));
+ return app;
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Method for getting the accounts under a given contentprovider from a package.
+ * This
+ * @param app The parent app object
+ * @return An ArrayList of BluetoothMapEmailSettingsItems containing all the accounts from the app
+ */
+ public ArrayList<BluetoothMapEmailSettingsItem> parseAccounts(BluetoothMapEmailSettingsItem app) {
+ Cursor c = null;
+ if(D) Log.d(TAG,"Adding app "+app.getPackageName());
+ ArrayList<BluetoothMapEmailSettingsItem> children = new ArrayList<BluetoothMapEmailSettingsItem>();
+ // Get the list of accounts from the email apps content resolver (if possible
+ mResolver = mContext.getContentResolver();
+ try{
+ mProviderClient = mResolver.acquireUnstableContentProviderClient(Uri.parse(app.mBase_uri_no_account));
+ if (mProviderClient == null) {
+ throw new RemoteException("Failed to acquire provider for " + app.getPackageName());
+ }
+ mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
+
+
+ Uri uri = Uri.parse(app.mBase_uri_no_account + "/" + BluetoothMapContract.TABLE_ACCOUNT);
+
+ c = mProviderClient.query(uri, BluetoothMapContract.BT_ACCOUNT_PROJECTION, null, null, BluetoothMapContract.AccountColumns._ID+" DESC");
+ } catch (RemoteException e){
+ if(D)Log.d(TAG,"Could not establish ContentProviderClient for "+app.getPackageName()+
+ " - returning empty account list" );
+ return children;
+ }
+
+ if (c != null) {
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ if(D)Log.d(TAG,"Adding account " +c.getString(c.getColumnIndex(BluetoothMapContract.AccountColumns.ACCOUNT_DISPLAY_NAME))+
+ " with ID "+String.valueOf((c.getInt(c.getColumnIndex(BluetoothMapContract.AccountColumns._ID)))));
+
+ BluetoothMapEmailSettingsItem child = new BluetoothMapEmailSettingsItem(
+ /*id*/ String.valueOf((c.getInt(c.getColumnIndex(BluetoothMapContract.AccountColumns._ID)))),
+ /*name*/ c.getString(c.getColumnIndex(BluetoothMapContract.AccountColumns.ACCOUNT_DISPLAY_NAME)) ,
+ /*package name*/ app.getPackageName(),
+ /*providerMeta*/ app.getProviderAuthority(),
+ /*icon*/ null);
+
+ child.mIsChecked = (c.getInt(c.getColumnIndex(BluetoothMapContract.AccountColumns.FLAG_EXPOSE))!=0);
+ /*update the account counter so we can make sure that not to many accounts are checked. */
+ if(child.mIsChecked)
+ {
+ mAccountsEnabledCount++;
+ }
+ children.add(child);
+ }
+ c.close();
+ } else {
+ if(D)Log.d(TAG, "query failed");
+ }
+ return children;
+ }
+ /**
+ * Gets the number of enabled accounts in total across all supported apps.
+ * NOTE that this method should not be called before the parsePackages method
+ * has been successfully called.
+ * @return number of enabled accounts
+ */
+ public int getAccountsEnabledCount() {
+ if(D)Log.d(TAG,"Enabled Accounts count:"+ mAccountsEnabledCount);
+ return mAccountsEnabledCount;
+ }
+
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapFolderElement.java b/src/com/android/bluetooth/map/BluetoothMapFolderElement.java
index d3909ddad..42728b1ff 100644
--- a/src/com/android/bluetooth/map/BluetoothMapFolderElement.java
+++ b/src/com/android/bluetooth/map/BluetoothMapFolderElement.java
@@ -1,5 +1,5 @@
/*
-* Copyright (C) 2013 Samsung System LSI
+* Copyright (C) 2014 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
@@ -14,32 +14,62 @@
*/
package com.android.bluetooth.map;
+
+import android.util.Log;
+import org.xmlpull.v1.XmlSerializer;
+
+import com.android.internal.util.FastXmlSerializer;
+
import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map.Entry;
-import org.xmlpull.v1.XmlSerializer;
-
-import android.util.Xml;
/**
- * @author cbonde
+ * Class to contain a single folder element representation.
*
*/
public class BluetoothMapFolderElement {
- private String name;
- private BluetoothMapFolderElement parent = null;
- private ArrayList<BluetoothMapFolderElement> subFolders;
+ private String mName;
+ private BluetoothMapFolderElement mParent = null;
+ private boolean mHasSmsMmsContent = false;
+ private long mEmailFolderId = -1;
+ private HashMap<String, BluetoothMapFolderElement> mSubFolders;
+
+ private static final boolean D = BluetoothMapService.DEBUG;
+ private static final boolean V = BluetoothMapService.VERBOSE;
+
+ private final static String TAG = "BluetoothMapFolderElement";
- public BluetoothMapFolderElement( String name, BluetoothMapFolderElement parrent ){
- this.name = name;
- this.parent = parrent;
- subFolders = new ArrayList<BluetoothMapFolderElement>();
+ public BluetoothMapFolderElement( String name, BluetoothMapFolderElement parrent){
+ this.mName = name;
+ this.mParent = parrent;
+ mSubFolders = new HashMap<String, BluetoothMapFolderElement>();
}
public String getName() {
- return name;
+ return mName;
+ }
+
+ public boolean hasSmsMmsContent(){
+ return mHasSmsMmsContent;
+ }
+
+ public long getEmailFolderId(){
+ return mEmailFolderId;
+ }
+
+ public void setEmailFolderId(long emailFolderId) {
+ this.mEmailFolderId = emailFolderId;
+ }
+
+ public void setHasSmsMmsContent(boolean hasSmsMmsContent) {
+ this.mHasSmsMmsContent = hasSmsMmsContent;
}
/**
@@ -47,10 +77,68 @@ public class BluetoothMapFolderElement {
* @return the parent folder or null if we are at the root folder.
*/
public BluetoothMapFolderElement getParent() {
- return parent;
+ return mParent;
}
/**
+ * Build the full path to this folder
+ * @return a string representing the full path.
+ */
+ public String getFullPath() {
+ StringBuilder sb = new StringBuilder(mName);
+ BluetoothMapFolderElement current = mParent;
+ while(current != null) {
+ if(current.getParent() != null) {
+ sb.insert(0, current.mName + "/");
+ }
+ current = current.getParent();
+ }
+ //sb.insert(0, "/"); Should this be included? The MAP spec. do not include it in examples.
+ return sb.toString();
+ }
+
+
+ public BluetoothMapFolderElement getEmailFolderByName(String name) {
+ BluetoothMapFolderElement folderElement = this.getRoot();
+ folderElement = folderElement.getSubFolder("telecom");
+ folderElement = folderElement.getSubFolder("msg");
+ folderElement = folderElement.getSubFolder(name);
+ if (folderElement != null && folderElement.getEmailFolderId() == -1 )
+ folderElement = null;
+ return folderElement;
+ }
+
+ public BluetoothMapFolderElement getEmailFolderById(long id) {
+ return getEmailFolderById(id, this);
+ }
+
+ public static BluetoothMapFolderElement getEmailFolderById(long id,
+ BluetoothMapFolderElement folderStructure) {
+ if(folderStructure == null) {
+ return null;
+ }
+ return findEmailFolderById(id, folderStructure.getRoot());
+ }
+
+ private static BluetoothMapFolderElement findEmailFolderById(long id,
+ BluetoothMapFolderElement folder) {
+ if(folder.getEmailFolderId() == id) {
+ return folder;
+ }
+ /* Else */
+ for(BluetoothMapFolderElement subFolder : folder.mSubFolders.values().toArray(
+ new BluetoothMapFolderElement[folder.mSubFolders.size()]))
+ {
+ BluetoothMapFolderElement ret = findEmailFolderById(id, subFolder);
+ if(ret != null) {
+ return ret;
+ }
+ }
+ return null;
+ }
+
+
+ /**
* Fetch the root folder.
* @return the parent folder or null if we are at the root folder.
*/
@@ -62,13 +150,53 @@ public class BluetoothMapFolderElement {
}
/**
- * Add a folder.
+ * Add a virtual folder.
* @param name the name of the folder to add.
* @return the added folder element.
*/
public BluetoothMapFolderElement addFolder(String name){
- BluetoothMapFolderElement newFolder = new BluetoothMapFolderElement(name, this);
- subFolders.add(newFolder);
+ name = name.toLowerCase(Locale.US);
+ BluetoothMapFolderElement newFolder = mSubFolders.get(name);
+ if(D) Log.i(TAG,"addFolder():" + name);
+ if(newFolder == null) {
+ newFolder = new BluetoothMapFolderElement(name, this);
+ mSubFolders.put(name, newFolder);
+ }
+ return newFolder;
+ }
+
+ /**
+ * Add a sms/mms folder.
+ * @param name the name of the folder to add.
+ * @return the added folder element.
+ */
+ public BluetoothMapFolderElement addSmsMmsFolder(String name){
+ name = name.toLowerCase(Locale.US);
+ BluetoothMapFolderElement newFolder = mSubFolders.get(name);
+ if(D) Log.i(TAG,"addSmsMmsFolder():" + name);
+ if(newFolder == null) {
+ newFolder = new BluetoothMapFolderElement(name, this);
+ mSubFolders.put(name, newFolder);
+ }
+ newFolder.setHasSmsMmsContent(true);
+ return newFolder;
+ }
+
+ /**
+ * Add an Email folder.
+ * @param name the name of the folder to add.
+ * @return the added folder element.
+ */
+ public BluetoothMapFolderElement addEmailFolder(String name, long emailFolderId){
+ name = name.toLowerCase();
+ BluetoothMapFolderElement newFolder = mSubFolders.get(name);
+ if(V) Log.v(TAG,"addEmailFolder(): name = " + name
+ + "id = " + emailFolderId);
+ if(newFolder == null) {
+ newFolder = new BluetoothMapFolderElement(name, this);
+ mSubFolders.put(name, newFolder);
+ }
+ newFolder.setEmailFolderId(emailFolderId);
return newFolder;
}
@@ -77,7 +205,7 @@ public class BluetoothMapFolderElement {
* @return returns the number of sub folders.
*/
public int getSubFolderCount(){
- return subFolders.size();
+ return mSubFolders.size();
}
/**
@@ -86,47 +214,46 @@ public class BluetoothMapFolderElement {
* @return the subFolder element if found {@code null} otherwise.
*/
public BluetoothMapFolderElement getSubFolder(String folderName){
- for(BluetoothMapFolderElement subFolder : subFolders){
- if(subFolder.getName().equals(folderName))
- return subFolder;
- }
- return null;
+ return mSubFolders.get(folderName.toLowerCase());
}
public byte[] encode(int offset, int count) throws UnsupportedEncodingException {
StringWriter sw = new StringWriter();
- XmlSerializer xmlMsgElement = Xml.newSerializer();
+ XmlSerializer xmlMsgElement = new FastXmlSerializer();
int i, stopIndex;
- if(offset > subFolders.size())
+ // We need index based access to the subFolders
+ BluetoothMapFolderElement[] folders = mSubFolders.values().toArray(new BluetoothMapFolderElement[mSubFolders.size()]);
+
+ if(offset > mSubFolders.size())
throw new IllegalArgumentException("FolderListingEncode: offset > subFolders.size()");
stopIndex = offset + count;
- if(stopIndex > subFolders.size())
- stopIndex = subFolders.size();
+ if(stopIndex > mSubFolders.size())
+ stopIndex = mSubFolders.size();
try {
xmlMsgElement.setOutput(sw);
- xmlMsgElement.startDocument(null, null);
- xmlMsgElement.text("\n");
- xmlMsgElement.startTag("", "folder-listing");
- xmlMsgElement.attribute("", "version", "1.0");
+ xmlMsgElement.startDocument("UTF-8", true);
+ xmlMsgElement.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+ xmlMsgElement.startTag(null, "folder-listing");
+ xmlMsgElement.attribute(null, "version", "1.0");
for(i = offset; i<stopIndex; i++)
{
- xmlMsgElement.startTag("", "folder");
- xmlMsgElement.attribute("", "name", subFolders.get(i).getName());
- xmlMsgElement.endTag("", "folder");
+ xmlMsgElement.startTag(null, "folder");
+ xmlMsgElement.attribute(null, "name", folders[i].getName());
+ xmlMsgElement.endTag(null, "folder");
}
- xmlMsgElement.endTag("", "folder-listing");
+ xmlMsgElement.endTag(null, "folder-listing");
xmlMsgElement.endDocument();
} catch (IllegalArgumentException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
+ if(D) Log.w(TAG,e);
+ throw new IllegalArgumentException("error encoding folderElement");
} catch (IllegalStateException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
+ if(D) Log.w(TAG,e);
+ throw new IllegalArgumentException("error encoding folderElement");
} catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
+ if(D) Log.w(TAG,e);
+ throw new IllegalArgumentException("error encoding folderElement");
}
return sw.toString().getBytes("UTF-8");
}
diff --git a/src/com/android/bluetooth/map/BluetoothMapMasInstance.java b/src/com/android/bluetooth/map/BluetoothMapMasInstance.java
new file mode 100644
index 000000000..64caba49c
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapMasInstance.java
@@ -0,0 +1,386 @@
+/*
+* Copyright (C) 2014 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.map;
+
+import java.io.IOException;
+
+import javax.obex.ServerSession;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothServerSocket;
+import android.bluetooth.BluetoothSocket;
+import android.bluetooth.BluetoothUuid;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.Log;
+
+public class BluetoothMapMasInstance {
+ private static final String TAG = "BluetoothMapMasInstance";
+
+ private static final boolean D = BluetoothMapService.DEBUG;
+ private static final boolean V = BluetoothMapService.VERBOSE;
+
+ private static final int SDP_MAP_MSG_TYPE_EMAIL = 0x01;
+ private static final int SDP_MAP_MSG_TYPE_SMS_GSM = 0x02;
+ private static final int SDP_MAP_MSG_TYPE_SMS_CDMA = 0x04;
+ private static final int SDP_MAP_MSG_TYPE_MMS = 0x08;
+
+ private SocketAcceptThread mAcceptThread = null;
+
+ private ServerSession mServerSession = null;
+
+ // The handle to the socket registration with SDP
+ private BluetoothServerSocket mServerSocket = null;
+
+ // The actual incoming connection handle
+ private BluetoothSocket mConnSocket = null;
+
+ private BluetoothDevice mRemoteDevice = null; // The remote connected device
+
+ private BluetoothAdapter mAdapter;
+
+ private volatile boolean mInterrupted; // Used to interrupt socket accept thread
+
+ private Handler mServiceHandler = null; // MAP service message handler
+ private BluetoothMapService mMapService = null; // Handle to the outer MAP service
+ private Context mContext = null; // MAP service context
+ private BluetoothMnsObexClient mMnsClient = null; // Shared MAP MNS client
+ private BluetoothMapEmailSettingsItem mAccount = null; //
+ private String mBaseEmailUri = null; // Email client base URI for this instance
+ private int mMasInstanceId = -1;
+ private boolean mEnableSmsMms = false;
+ BluetoothMapContentObserver mObserver;
+
+ /**
+ * Create a e-mail MAS instance
+ * @param callback
+ * @param context
+ * @param mns
+ * @param emailBaseUri - use null to create a SMS/MMS MAS instance
+ */
+ public BluetoothMapMasInstance (BluetoothMapService mapService,
+ Context context,
+ BluetoothMapEmailSettingsItem account,
+ int masId,
+ boolean enableSmsMms) {
+ mMapService = mapService;
+ mServiceHandler = mapService.getHandler();
+ mContext = context;
+ mAccount = account;
+ if(account != null) {
+ mBaseEmailUri = account.mBase_uri;
+ }
+ mMasInstanceId = masId;
+ mEnableSmsMms = enableSmsMms;
+ init();
+ }
+
+ @Override
+ public String toString() {
+ return "MasId: " + mMasInstanceId + " Uri:" + mBaseEmailUri + " SMS/MMS:" + mEnableSmsMms;
+ }
+
+ private void init() {
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ }
+
+ public int getMasId() {
+ return mMasInstanceId;
+ }
+
+ /**
+ * A thread that runs in the background waiting for remote rfcomm
+ * connect. Once a remote socket connected, this thread shall be
+ * shutdown. When the remote disconnect, this thread shall run again
+ * waiting for next request.
+ */
+ private class SocketAcceptThread extends Thread {
+
+ private boolean stopped = false;
+
+ @Override
+ public void run() {
+ BluetoothServerSocket serverSocket;
+ if (mServerSocket == null) {
+ if (!initSocket()) {
+ return;
+ }
+ }
+
+ while (!stopped) {
+ try {
+ if (D) Log.d(TAG, "Accepting socket connection...");
+ serverSocket = mServerSocket;
+ if(serverSocket == null) {
+ Log.w(TAG, "mServerSocket is null");
+ break;
+ }
+ mConnSocket = serverSocket.accept();
+ if (D) Log.d(TAG, "Accepted socket connection...");
+
+ synchronized (BluetoothMapMasInstance.this) {
+ if (mConnSocket == null) {
+ Log.w(TAG, "mConnSocket is null");
+ break;
+ }
+ mRemoteDevice = mConnSocket.getRemoteDevice();
+ }
+
+ if (mRemoteDevice == null) {
+ Log.i(TAG, "getRemoteDevice() = null");
+ break;
+ }
+
+ /* Signal to the service that we have received an incoming connection.
+ */
+ boolean isValid = mMapService.onConnect(mRemoteDevice, BluetoothMapMasInstance.this);
+
+ if(isValid == false) {
+ // Close connection if we already have a connection with another device
+ Log.i(TAG, "RemoteDevice is invalid - closing.");
+ mConnSocket.close();
+ mConnSocket = null;
+ // now wait for a new connect
+ } else {
+ stopped = true; // job done ,close this thread;
+ }
+ } catch (IOException ex) {
+ stopped=true;
+ if (D) Log.v(TAG, "Accept exception: (expected at shutdown)", ex);
+ }
+ }
+ }
+
+ void shutdown() {
+ stopped = true;
+ if(mServerSocket != null) {
+ try {
+ mServerSocket.close();
+ } catch (IOException e) {
+ if(D) Log.d(TAG, "Exception while thread shurdown:", e);
+ } finally {
+ mServerSocket = null;
+ }
+ }
+ interrupt();
+ }
+ }
+
+ public void startRfcommSocketListener() {
+ if (D) Log.d(TAG, "Map Service startRfcommSocketListener");
+ mInterrupted = false; /* For this to work all calls to this function
+ and shutdown() must be from same thread. */
+ if (mAcceptThread == null) {
+ mAcceptThread = new SocketAcceptThread();
+ mAcceptThread.setName("BluetoothMapAcceptThread masId=" + mMasInstanceId);
+ mAcceptThread.start();
+ }
+ }
+
+ private final boolean initSocket() {
+ if (D) Log.d(TAG, "MAS initSocket()");
+
+ boolean initSocketOK = false;
+ final int CREATE_RETRY_TIME = 10;
+
+ // It's possible that create will fail in some cases. retry for 10 times
+ for (int i = 0; (i < CREATE_RETRY_TIME) && !mInterrupted; i++) {
+ initSocketOK = true;
+ try {
+ // It is mandatory for MSE to support initiation of bonding and
+ // encryption.
+ String masId = String.format("%02x", mMasInstanceId & 0xff);
+ String masName = "";
+ int messageTypeFlags = 0;
+ if(mEnableSmsMms) {
+ masName = "SMS/MMS";
+ messageTypeFlags |= SDP_MAP_MSG_TYPE_SMS_GSM |
+ SDP_MAP_MSG_TYPE_SMS_CDMA|
+ SDP_MAP_MSG_TYPE_MMS;
+ }
+ if(mBaseEmailUri != null) {
+ if(mEnableSmsMms) {
+ masName += "/EMAIL";
+ } else {
+ masName = mAccount.getName();
+ }
+ messageTypeFlags |= SDP_MAP_MSG_TYPE_EMAIL;
+ }
+ String msgTypes = String.format("%02x", messageTypeFlags & 0xff);
+ String sdpString = masId + msgTypes + masName;
+ if(V) Log.d(TAG, " masId = " + masId +
+ "\n msgTypes = " + msgTypes +
+ "\n masName = " + masName +
+ "\n SDP string = " + sdpString);
+ mServerSocket = mAdapter.listenUsingRfcommWithServiceRecord
+ (sdpString, BluetoothUuid.MAS.getUuid());
+
+ } catch (IOException e) {
+ Log.e(TAG, "Error create RfcommServerSocket " + e.toString());
+ initSocketOK = false;
+ }
+ if (!initSocketOK) {
+ // Need to break out of this loop if BT is being turned off.
+ if (mAdapter == null) break;
+ int state = mAdapter.getState();
+ if ((state != BluetoothAdapter.STATE_TURNING_ON) &&
+ (state != BluetoothAdapter.STATE_ON)) {
+ Log.w(TAG, "initServerSocket failed as BT is (being) turned off");
+ break;
+ }
+ try {
+ if (V) Log.v(TAG, "waiting 300 ms...");
+ Thread.sleep(300);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "socketAcceptThread thread was interrupted (3)");
+ }
+ } else {
+ break;
+ }
+ }
+ if (mInterrupted) {
+ initSocketOK = false;
+ // close server socket to avoid resource leakage
+ closeServerSocket();
+ }
+
+ if (initSocketOK) {
+ if (V) Log.v(TAG, "Succeed to create listening socket ");
+
+ } else {
+ Log.e(TAG, "Error to create listening socket after " + CREATE_RETRY_TIME + " try");
+ }
+ return initSocketOK;
+ }
+
+ /* Called for all MAS instances for each instance when auth. is completed, hence
+ * must check if it has a valid connection before creating a session.
+ * Returns true at success. */
+ public boolean startObexServerSession(BluetoothMnsObexClient mnsClient) throws IOException, RemoteException {
+ if (D) Log.d(TAG, "Map Service startObexServerSession masid = " + mMasInstanceId);
+
+ if (mConnSocket != null) {
+ if(mServerSession != null) {
+ // Already connected, just return true
+ return true;
+ }
+ mMnsClient = mnsClient;
+ BluetoothMapObexServer mapServer;
+ mObserver = new BluetoothMapContentObserver(mContext,
+ mMnsClient,
+ this,
+ mAccount,
+ mEnableSmsMms);
+ mObserver.init();
+ mapServer = new BluetoothMapObexServer(mServiceHandler,
+ mContext,
+ mObserver,
+ mMasInstanceId,
+ mAccount,
+ mEnableSmsMms);
+ // setup RFCOMM transport
+ BluetoothMapRfcommTransport transport = new BluetoothMapRfcommTransport(mConnSocket);
+ mServerSession = new ServerSession(transport, mapServer, null);
+ if (D) Log.d(TAG, " ServerSession started.");
+
+ return true;
+ }
+ if (D) Log.d(TAG, " No connection for this instance");
+ return false;
+ }
+
+ public boolean handleSmsSendIntent(Context context, Intent intent){
+ if(mObserver != null) {
+ return mObserver.handleSmsSendIntent(context, intent);
+ }
+ return false;
+ }
+
+ /**
+ * Check if this instance is started.
+ * @return true if started
+ */
+ public boolean isStarted() {
+ return (mConnSocket != null);
+ }
+
+ public void shutdown() {
+ if (D) Log.d(TAG, "MAP Service shutdown");
+
+ if (mServerSession != null) {
+ mServerSession.close();
+ mServerSession = null;
+ }
+ if (mObserver != null) {
+ mObserver.deinit();
+ mObserver = null;
+ }
+ mInterrupted = true;
+ if(mAcceptThread != null) {
+ mAcceptThread.shutdown();
+ try {
+ mAcceptThread.join();
+ } catch (InterruptedException e) {/* Not much we can do about this*/}
+ mAcceptThread = null;
+ }
+
+ closeConnectionSocket();
+ }
+
+ /**
+ * Stop a running server session or cleanup, and start a new
+ * RFComm socket listener thread.
+ */
+ public void restartObexServerSession() {
+ if (D) Log.d(TAG, "MAP Service stopObexServerSession");
+
+ shutdown();
+
+ // Last obex transaction is finished, we start to listen for incoming
+ // connection again -
+ startRfcommSocketListener();
+ }
+
+
+ private final synchronized void closeServerSocket() {
+ // exit SocketAcceptThread early
+ if (mServerSocket != null) {
+ try {
+ // this will cause mServerSocket.accept() return early with IOException
+ mServerSocket.close();
+ } catch (IOException ex) {
+ Log.e(TAG, "Close Server Socket error: " + ex);
+ } finally {
+ mServerSocket = null;
+ }
+ }
+ }
+
+ private final synchronized void closeConnectionSocket() {
+ if (mConnSocket != null) {
+ try {
+ mConnSocket.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Close Connection Socket error: " + e.toString());
+ } finally {
+ mConnSocket = null;
+ }
+ }
+ }
+
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapMessageListing.java b/src/com/android/bluetooth/map/BluetoothMapMessageListing.java
index eeda447ba..6a486c613 100644
--- a/src/com/android/bluetooth/map/BluetoothMapMessageListing.java
+++ b/src/com/android/bluetooth/map/BluetoothMapMessageListing.java
@@ -30,6 +30,8 @@ import android.util.Xml;
public class BluetoothMapMessageListing {
private boolean hasUnread = false;
private static final String TAG = "BluetoothMapMessageListing";
+ private static final boolean D = BluetoothMapService.DEBUG;
+
private List<BluetoothMapMessageListingElement> list;
public BluetoothMapMessageListing(){
@@ -38,7 +40,7 @@ public class BluetoothMapMessageListing {
public void add(BluetoothMapMessageListingElement element) {
list.add(element);
/* update info regarding whether the list contains unread messages */
- if (element.getRead().equalsIgnoreCase("no"))
+ if (element.getReadBool())
{
hasUnread = true;
}
@@ -65,6 +67,15 @@ public class BluetoothMapMessageListing {
return hasUnread;
}
+
+ /**
+ * returns the entire list as a list
+ * @return list
+ */
+ public List<BluetoothMapMessageListingElement> getList(){
+ return list;
+ }
+
/**
* Encode the list of BluetoothMapMessageListingElement(s) into a UTF-8
* formatted XML-string in a trimmed byte array
@@ -73,7 +84,7 @@ public class BluetoothMapMessageListing {
* @throws UnsupportedEncodingException
* if UTF-8 encoding is unsupported on the platform.
*/
- public byte[] encode() throws UnsupportedEncodingException {
+ public byte[] encode(boolean includeThreadId) throws UnsupportedEncodingException {
StringWriter sw = new StringWriter();
XmlSerializer xmlMsgElement = new FastXmlSerializer();
try {
@@ -84,16 +95,16 @@ public class BluetoothMapMessageListing {
xmlMsgElement.attribute(null, "version", "1.0");
// Do the XML encoding of list
for (BluetoothMapMessageListingElement element : list) {
- element.encode(xmlMsgElement); // Append the list element
+ element.encode(xmlMsgElement, includeThreadId); // Append the list element
}
xmlMsgElement.endTag(null, "MAP-msg-listing");
xmlMsgElement.endDocument();
} catch (IllegalArgumentException e) {
- Log.w(TAG, e.toString());
+ Log.w(TAG, e);
} catch (IllegalStateException e) {
- Log.w(TAG, e.toString());
+ Log.w(TAG, e);
} catch (IOException e) {
- Log.w(TAG, e.toString());
+ Log.w(TAG, e);
}
return sw.toString().getBytes("UTF-8");
}
@@ -103,9 +114,12 @@ public class BluetoothMapMessageListing {
}
public void segment(int count, int offset) {
- count = Math.min(count, list.size());
- if (offset + count <= list.size()) {
+ count = Math.min(count, list.size() - offset);
+ if (count > 0) {
list = list.subList(offset, offset + count);
+ if(list == null) {
+ list = new ArrayList<BluetoothMapMessageListingElement>(); // Return an empty list
+ }
} else {
if(offset > list.size()) {
list = new ArrayList<BluetoothMapMessageListingElement>();
diff --git a/src/com/android/bluetooth/map/BluetoothMapMessageListingElement.java b/src/com/android/bluetooth/map/BluetoothMapMessageListingElement.java
index 03b20009b..04788f2b1 100644
--- a/src/com/android/bluetooth/map/BluetoothMapMessageListingElement.java
+++ b/src/com/android/bluetooth/map/BluetoothMapMessageListingElement.java
@@ -35,225 +35,264 @@ public class BluetoothMapMessageListingElement
private static final boolean D = false;
private static final boolean V = false;
- private long cpHandle = 0; /* The content provider handle - without type information */
- private String mapHandle = null; /* The map hex-string handle with type information */
- private String subject = null;
- private long dateTime = 0;
- private String senderName = null;
- private String senderAddressing = null;
- private String replytoAddressing = null;
- private String recipientName = null;
- private String recipientAddressing = null;
- private TYPE type = null;
- private int size = -1;
- private String text = null;
- private String receptionStatus = null;
- private int attachmentSize = -1;
- private String priority = null;
- private String read = null;
- private String sent = null;
- private String protect = null;
- private boolean reportRead;
+ private long mCpHandle = 0; /* The content provider handle - without type information */
+ private String mSubject = null;
+ private long mDateTime = 0;
+ private String mSenderName = null;
+ private String mSenderAddressing = null;
+ private String mReplytoAddressing = null;
+ private String mRecipientName = null;
+ private String mRecipientAddressing = null;
+ private TYPE mType = null;
+ private int mSize = -1;
+ private String mText = null;
+ private String mReceptionStatus = null;
+ private int mAttachmentSize = -1;
+ private String mPriority = null;
+ private boolean mRead = false;
+ private String mSent = null;
+ private String mProtect = null;
+ private String mThreadId = null;
+ private boolean mReportRead = false;
+ private int mCursorIndex = 0;
+
+ public int getCursorIndex() {
+ return mCursorIndex;
+ }
+
+ public void setCursorIndex(int cursorIndex) {
+ this.mCursorIndex = cursorIndex;
+ }
+
public long getHandle() {
- return cpHandle;
+ return mCpHandle;
}
- public void setHandle(long handle, TYPE type) {
- this.cpHandle = handle;
- this.mapHandle = BluetoothMapUtils.getMapHandle(cpHandle, type);
+ public void setHandle(long handle) {
+ this.mCpHandle = handle;
}
public long getDateTime() {
- return dateTime;
+ return mDateTime;
}
public String getDateTimeString() {
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
- Date date = new Date(dateTime);
+ Date date = new Date(mDateTime);
return format.format(date); // Format to YYYYMMDDTHHMMSS local time
}
public void setDateTime(long dateTime) {
- this.dateTime = dateTime;
+ this.mDateTime = dateTime;
}
public String getSubject() {
- return subject;
+ return mSubject;
}
public void setSubject(String subject) {
- this.subject = subject;
+ this.mSubject = subject;
}
public String getSenderName() {
- return senderName;
+ return mSenderName;
}
public void setSenderName(String senderName) {
- this.senderName = senderName;
+ this.mSenderName = senderName;
}
public String getSenderAddressing() {
- return senderAddressing;
+ return mSenderAddressing;
}
public void setSenderAddressing(String senderAddressing) {
- /* TODO: This should depend on the type - for email, the addressing is an email address
- * Consider removing this again - to allow strings.
- */
- this.senderAddressing = PhoneNumberUtils.extractNetworkPortion(senderAddressing);
- if(this.senderAddressing == null || this.senderAddressing.length() < 2){
- this.senderAddressing = "11"; // Ensure we have at least two digits to
- }
+ this.mSenderAddressing = senderAddressing;
}
public String getReplyToAddressing() {
- return replytoAddressing;
+ return mReplytoAddressing;
}
public void setReplytoAddressing(String replytoAddressing) {
- this.replytoAddressing = replytoAddressing;
+ this.mReplytoAddressing = replytoAddressing;
}
public String getRecipientName() {
- return recipientName;
+ return mRecipientName;
}
public void setRecipientName(String recipientName) {
- this.recipientName = recipientName;
+ this.mRecipientName = recipientName;
}
public String getRecipientAddressing() {
- return recipientAddressing;
+ return mRecipientAddressing;
}
public void setRecipientAddressing(String recipientAddressing) {
- this.recipientAddressing = recipientAddressing;
+ this.mRecipientAddressing = recipientAddressing;
}
public TYPE getType() {
- return type;
+ return mType;
}
public void setType(TYPE type) {
- this.type = type;
+ this.mType = type;
}
public int getSize() {
- return size;
+ return mSize;
}
public void setSize(int size) {
- this.size = size;
+ this.mSize = size;
}
public String getText() {
- return text;
+ return mText;
}
public void setText(String text) {
- this.text = text;
+ this.mText = text;
}
public String getReceptionStatus() {
- return receptionStatus;
+ return mReceptionStatus;
}
public void setReceptionStatus(String receptionStatus) {
- this.receptionStatus = receptionStatus;
+ this.mReceptionStatus = receptionStatus;
}
public int getAttachmentSize() {
- return attachmentSize;
+ return mAttachmentSize;
}
public void setAttachmentSize(int attachmentSize) {
- this.attachmentSize = attachmentSize;
+ this.mAttachmentSize = attachmentSize;
}
public String getPriority() {
- return priority;
+ return mPriority;
}
public void setPriority(String priority) {
- this.priority = priority;
+ this.mPriority = priority;
}
public String getRead() {
- return read;
+ return (mRead?"yes":"no");
+ }
+ public boolean getReadBool() {
+ return mRead;
}
- public void setRead(String read, boolean reportRead) {
- this.read = read;
- this.reportRead = reportRead;
+ public void setRead(boolean read, boolean reportRead) {
+ this.mRead = read;
+ this.mReportRead = reportRead;
}
public String getSent() {
- return sent;
+ return mSent;
}
public void setSent(String sent) {
- this.sent = sent;
+ this.mSent = sent;
}
public String getProtect() {
- return protect;
+ return mProtect;
}
public void setProtect(String protect) {
- this.protect = protect;
+ this.mProtect = protect;
+ }
+
+ public void setThreadId(long threadId) {
+ if(threadId != -1) {
+ this.mThreadId = BluetoothMapUtils.getLongAsString(threadId);
+ }
}
public int compareTo(BluetoothMapMessageListingElement e) {
- if (this.dateTime < e.dateTime) {
+ if (this.mDateTime < e.mDateTime) {
return 1;
- } else if (this.dateTime > e.dateTime) {
+ } else if (this.mDateTime > e.mDateTime) {
return -1;
} else {
return 0;
}
}
+ /**
+ * Strip away any illegal XML characters, that would otherwise cause the
+ * xml serializer to throw an exception.
+ * Examples of such characters are the emojis used on Android.
+ * @param text The string to validate
+ * @return the same string if valid, otherwise a new String stripped for
+ * any illegal characters
+ */
+ private static String stripInvalidChars(String text) {
+ char out[] = new char[text.length()];
+ int i, o, l;
+ for(i=0, o=0, l=text.length(); i<l; i++){
+ char c = text.charAt(i);
+ if((c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd)) {
+ out[o++] = c;
+ } // Else we skip the character
+ }
+
+ if(i==o) {
+ return text;
+ } else { // We removed some characters, create the new string
+ return new String(out,0,o);
+ }
+ }
+
/* Encode the MapMessageListingElement into the StringBuilder reference.
* */
- public void encode(XmlSerializer xmlMsgElement) throws IllegalArgumentException, IllegalStateException, IOException
+ public void encode(XmlSerializer xmlMsgElement, boolean includeThreadId) throws IllegalArgumentException, IllegalStateException, IOException
{
// contruct the XML tag for a single msg in the msglisting
xmlMsgElement.startTag(null, "msg");
- xmlMsgElement.attribute(null, "handle", mapHandle);
- if(subject != null)
- xmlMsgElement.attribute(null, "subject", subject);
- if(dateTime != 0)
+ xmlMsgElement.attribute(null, "handle", BluetoothMapUtils.getMapHandle(mCpHandle, mType));
+ if(mSubject != null)
+ xmlMsgElement.attribute(null, "subject", stripInvalidChars(mSubject));
+ if(mDateTime != 0)
xmlMsgElement.attribute(null, "datetime", this.getDateTimeString());
- if(senderName != null)
- xmlMsgElement.attribute(null, "sender_name", senderName);
- if(senderAddressing != null)
- xmlMsgElement.attribute(null, "sender_addressing", senderAddressing);
- if(replytoAddressing != null)
- xmlMsgElement.attribute(null, "replyto_addressing",replytoAddressing);
- if(recipientName != null)
- xmlMsgElement.attribute(null, "recipient_name",recipientName);
- if(recipientAddressing != null)
- xmlMsgElement.attribute(null, "recipient_addressing", recipientAddressing);
- if(type != null)
- xmlMsgElement.attribute(null, "type", type.name());
- if(size != -1)
- xmlMsgElement.attribute(null, "size", Integer.toString(size));
- if(text != null)
- xmlMsgElement.attribute(null, "text", text);
- if(receptionStatus != null)
- xmlMsgElement.attribute(null, "reception_status", receptionStatus);
- if(attachmentSize != -1)
- xmlMsgElement.attribute(null, "attachment_size", Integer.toString(attachmentSize));
- if(priority != null)
- xmlMsgElement.attribute(null, "priority", priority);
- if(read != null && reportRead)
- xmlMsgElement.attribute(null, "read", read);
- if(sent != null)
- xmlMsgElement.attribute(null, "sent", sent);
- if(protect != null)
- xmlMsgElement.attribute(null, "protected", protect);
+ if(mSenderName != null)
+ xmlMsgElement.attribute(null, "sender_name", stripInvalidChars(mSenderName));
+ if(mSenderAddressing != null)
+ xmlMsgElement.attribute(null, "sender_addressing", mSenderAddressing);
+ if(mReplytoAddressing != null)
+ xmlMsgElement.attribute(null, "replyto_addressing",mReplytoAddressing);
+ if(mRecipientName != null)
+ xmlMsgElement.attribute(null, "recipient_name", stripInvalidChars(mRecipientName));
+ if(mRecipientAddressing != null)
+ xmlMsgElement.attribute(null, "recipient_addressing", mRecipientAddressing);
+ if(mType != null)
+ xmlMsgElement.attribute(null, "type", mType.name());
+ if(mSize != -1)
+ xmlMsgElement.attribute(null, "size", Integer.toString(mSize));
+ if(mText != null)
+ xmlMsgElement.attribute(null, "text", mText);
+ if(mReceptionStatus != null)
+ xmlMsgElement.attribute(null, "reception_status", mReceptionStatus);
+ if(mAttachmentSize != -1)
+ xmlMsgElement.attribute(null, "attachment_size", Integer.toString(mAttachmentSize));
+ if(mPriority != null)
+ xmlMsgElement.attribute(null, "priority", mPriority);
+ if(mReportRead)
+ xmlMsgElement.attribute(null, "read", getRead());
+ if(mSent != null)
+ xmlMsgElement.attribute(null, "sent", mSent);
+ if(mProtect != null)
+ xmlMsgElement.attribute(null, "protected", mProtect);
+ if(mThreadId != null && includeThreadId == true)
+ xmlMsgElement.attribute(null, "thread_id", mThreadId);
xmlMsgElement.endTag(null, "msg");
}
diff --git a/src/com/android/bluetooth/map/BluetoothMapObexServer.java b/src/com/android/bluetooth/map/BluetoothMapObexServer.java
index 37ccf4375..4b5a74e36 100644
--- a/src/com/android/bluetooth/map/BluetoothMapObexServer.java
+++ b/src/com/android/bluetooth/map/BluetoothMapObexServer.java
@@ -1,5 +1,5 @@
/*
-* Copyright (C) 2013 Samsung System LSI
+* Copyright (C) 2014 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
@@ -14,9 +14,26 @@
*/
package com.android.bluetooth.map;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import com.android.bluetooth.mapapi.BluetoothMapContract;
+import android.text.format.DateUtils;
+import android.util.Log;
+
+import com.android.bluetooth.map.BluetoothMapUtils;
+import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.text.ParseException;
import java.util.Arrays;
import java.util.Calendar;
@@ -25,13 +42,6 @@ import javax.obex.Operation;
import javax.obex.ResponseCodes;
import javax.obex.ServerRequestHandler;
-import com.android.bluetooth.map.BluetoothMapUtils;
-import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
-
-import android.content.Context;
-import android.os.Handler;
-import android.os.Message;
-import android.util.Log;
public class BluetoothMapObexServer extends ServerRequestHandler {
@@ -42,6 +52,12 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
private static final int UUID_LENGTH = 16;
+ private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
+
+ /* OBEX header and value used to detect clients that support threadId in the message listing. */
+ private static final int THREADED_MAIL_HEADER_ID = 0xFA;
+ private static final long THREAD_MAIL_KEY = 0x534c5349;
+
// 128 bit UUID for MAP
private static final byte[] MAP_TARGET = new byte[] {
(byte)0xBB, (byte)0x58, (byte)0x2B, (byte)0x40,
@@ -60,49 +76,166 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
private BluetoothMapFolderElement mCurrentFolder;
- private BluetoothMnsObexClient mMnsClient;
+ private BluetoothMapContentObserver mObserver = null;
private Handler mCallback = null;
private Context mContext;
- public static boolean sIsAborted = false;
+ private boolean mIsAborted = false;
BluetoothMapContent mOutContent;
- public BluetoothMapObexServer(Handler callback, Context context,
- BluetoothMnsObexClient mns) {
+ private String mBaseEmailUriString = null;
+ private long mAccountId = 0;
+ private BluetoothMapEmailSettingsItem mAccount = null;
+ private Uri mEmailFolderUri = null;
+
+ private int mMasId = 0;
+
+ private boolean mEnableSmsMms = false;
+ private boolean mThreadIdSupport = false; // true if peer supports threadId in msg listing
+ private String mAuthority;
+ private ContentResolver mResolver;
+ private ContentProviderClient mProviderClient = null;
+
+ public BluetoothMapObexServer(Handler callback,
+ Context context,
+ BluetoothMapContentObserver observer,
+ int masId,
+ BluetoothMapEmailSettingsItem account,
+ boolean enableSmsMms) throws RemoteException {
super();
mCallback = callback;
mContext = context;
- mOutContent = new BluetoothMapContent(mContext);
- mMnsClient = mns;
+ mObserver = observer;
+ mEnableSmsMms = enableSmsMms;
+ mAccount = account;
+ mMasId = masId;
+
+ if(account != null && account.getProviderAuthority() != null) {
+ mAccountId = account.getAccountId();
+ mAuthority = account.getProviderAuthority();
+ mResolver = mContext.getContentResolver();
+ if (D) Log.d(TAG, "BluetoothMapObexServer(): accountId=" + mAccountId);
+ mBaseEmailUriString = account.mBase_uri + "/";
+ if (D) Log.d(TAG, "BluetoothMapObexServer(): emailBaseUri=" + mBaseEmailUriString);
+ mEmailFolderUri = BluetoothMapContract.buildFolderUri(mAuthority,
+ Long.toString(mAccountId));
+ if (D) Log.d(TAG, "BluetoothMapObexServer(): mEmailFolderUri=" + mEmailFolderUri);
+ mProviderClient = acquireUnstableContentProviderOrThrow();
+ }
+
buildFolderStructure(); /* Build the default folder structure, and set
mCurrentFolder to root folder */
+ mObserver.setFolderStructure(mCurrentFolder.getRoot());
+
+ mOutContent = new BluetoothMapContent(mContext, mBaseEmailUriString);
+
+ }
+
+ /**
+ *
+ */
+ private ContentProviderClient acquireUnstableContentProviderOrThrow() throws RemoteException{
+ ContentProviderClient providerClient = mResolver.acquireUnstableContentProviderClient(mAuthority);
+ if (providerClient == null) {
+ throw new RemoteException("Failed to acquire provider for " + mAuthority);
+ }
+ providerClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
+ return providerClient;
}
/**
* Build the default minimal folder structure, as defined in the MAP specification.
*/
- private void buildFolderStructure(){
+ private void buildFolderStructure() throws RemoteException{
mCurrentFolder = new BluetoothMapFolderElement("root", null); // This will be the root element
BluetoothMapFolderElement tmpFolder;
tmpFolder = mCurrentFolder.addFolder("telecom"); // root/telecom
tmpFolder = tmpFolder.addFolder("msg"); // root/telecom/msg
- tmpFolder.addFolder("inbox"); // root/telecom/msg/inbox
- tmpFolder.addFolder("outbox");
- tmpFolder.addFolder("sent");
- tmpFolder.addFolder("deleted");
- tmpFolder.addFolder("draft");
+
+ addBaseFolders(tmpFolder); // Add the mandatory folders
+
+ if(mEnableSmsMms) {
+ addSmsMmsFolders(tmpFolder);
+ }
+ if(mEmailFolderUri != null) {
+ if (D) Log.d(TAG, "buildFolderStructure(): " + mEmailFolderUri.toString());
+ addEmailFolders(tmpFolder);
+ }
+ }
+
+ /**
+ * Add
+ * @param root
+ */
+ private void addBaseFolders(BluetoothMapFolderElement root) {
+ root.addFolder(BluetoothMapContract.FOLDER_NAME_INBOX); // root/telecom/msg/inbox
+ root.addFolder(BluetoothMapContract.FOLDER_NAME_OUTBOX);
+ root.addFolder(BluetoothMapContract.FOLDER_NAME_SENT);
+ root.addFolder(BluetoothMapContract.FOLDER_NAME_DELETED);
+ }
+
+
+ /**
+ * Add
+ * @param root
+ */
+ private void addSmsMmsFolders(BluetoothMapFolderElement root) {
+ root.addSmsMmsFolder(BluetoothMapContract.FOLDER_NAME_INBOX); // root/telecom/msg/inbox
+ root.addSmsMmsFolder(BluetoothMapContract.FOLDER_NAME_OUTBOX);
+ root.addSmsMmsFolder(BluetoothMapContract.FOLDER_NAME_SENT);
+ root.addSmsMmsFolder(BluetoothMapContract.FOLDER_NAME_DELETED);
+ root.addSmsMmsFolder(BluetoothMapContract.FOLDER_NAME_DRAFT);
+ }
+
+
+ /**
+ * Recursively adds folders based on the folders in the email content provider.
+ * Add a content observer? - to refresh the folder list if any change occurs.
+ * Consider simply deleting the entire table, and then rebuild using buildFolderStructure()
+ * WARNING: there is no way to notify the client about these changes - hence
+ * we need to either keep the folder structure constant, disconnect or fail anything
+ * referring to currentFolder.
+ * It is unclear what to set as current folder to be able to go one level up...
+ * The best solution would be to keep the folder structure constant during a connection.
+ * @param folder the parent folder to which subFolders needs to be added. The
+ * folder.getEmailFolderId() will be used to query sub-folders.
+ * Use a parentFolder with id -1 to get all folders from root.
+ */
+ private void addEmailFolders(BluetoothMapFolderElement parentFolder) throws RemoteException {
+ // Select all parent folders
+ BluetoothMapFolderElement newFolder;
+
+ String where = BluetoothMapContract.FolderColumns.PARENT_FOLDER_ID +
+ " = " + parentFolder.getEmailFolderId();
+ Cursor c = mProviderClient.query(mEmailFolderUri,
+ BluetoothMapContract.BT_FOLDER_PROJECTION, where, null, null);
+ if (c != null) {
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ String name = c.getString(c.getColumnIndex(BluetoothMapContract.FolderColumns.NAME));
+ long id = c.getLong(c.getColumnIndex(BluetoothMapContract.FolderColumns._ID));
+ newFolder = parentFolder.addEmailFolder(name, id);
+ addEmailFolders(newFolder); // Use recursion to add any sub folders
+ }
+ c.close();
+ } else {
+ if (D) Log.d(TAG, "addEmailFolders(): no elements found");
+ }
}
@Override
public int onConnect(final HeaderSet request, HeaderSet reply) {
if (D) Log.d(TAG, "onConnect():");
if (V) logHeader(request);
+ mThreadIdSupport = false; // Always assume not supported at new connect.
notifyUpdateWakeLock();
+ Long threadedMailKey = null;
try {
byte[] uuid = (byte[])request.getHeader(HeaderSet.TARGET);
+ threadedMailKey = (Long)request.getHeader(THREADED_MAIL_HEADER_ID);
if (uuid == null) {
return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
}
@@ -120,7 +253,7 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
}
reply.setHeader(HeaderSet.WHO, uuid);
} catch (IOException e) {
- Log.e(TAG, e.toString());
+ Log.e(TAG,"Exception during onConnect:", e);
return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
}
@@ -130,18 +263,28 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
if (D) Log.d(TAG, "onConnect(): remote=" + Arrays.toString(remote));
reply.setHeader(HeaderSet.TARGET, remote);
}
+ if(threadedMailKey != null && threadedMailKey.longValue() == THREAD_MAIL_KEY)
+ {
+ /* If the client provides the correct key we enable threaded e-mail support
+ * and reply to the client that we support the requested feature.
+ * This is currently an Android only feature. */
+ mThreadIdSupport = true;
+ reply.setHeader(THREADED_MAIL_HEADER_ID, THREAD_MAIL_KEY);
+ }
} catch (IOException e) {
- Log.e(TAG, e.toString());
+ Log.e(TAG,"Exception during onConnect:", e);
+ mThreadIdSupport = false;
return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
}
if (V) Log.v(TAG, "onConnect(): uuid is ok, will send out " +
"MSG_SESSION_ESTABLISHED msg.");
-
- Message msg = Message.obtain(mCallback);
- msg.what = BluetoothMapService.MSG_SESSION_ESTABLISHED;
- msg.sendToTarget();
+ if(mCallback != null) {
+ Message msg = Message.obtain(mCallback);
+ msg.what = BluetoothMapService.MSG_SESSION_ESTABLISHED;
+ msg.sendToTarget();
+ }
return ResponseCodes.OBEX_HTTP_OK;
}
@@ -164,13 +307,14 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
public int onAbort(HeaderSet request, HeaderSet reply) {
if (D) Log.d(TAG, "onAbort(): enter.");
notifyUpdateWakeLock();
- sIsAborted = true;
+ mIsAborted = true;
return ResponseCodes.OBEX_HTTP_OK;
}
@Override
public int onPut(final Operation op) {
if (D) Log.d(TAG, "onPut(): enter");
+ mIsAborted = false;
notifyUpdateWakeLock();
HeaderSet request = null;
String type, name;
@@ -180,90 +324,175 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
try {
request = op.getReceivedHeader();
type = (String)request.getHeader(HeaderSet.TYPE);
+
name = (String)request.getHeader(HeaderSet.NAME);
appParamRaw = (byte[])request.getHeader(HeaderSet.APPLICATION_PARAMETER);
if(appParamRaw != null)
appParams = new BluetoothMapAppParams(appParamRaw);
- } catch (Exception e) {
- Log.e(TAG, "request headers error");
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
- }
-
- if(D) Log.d(TAG,"type = " + type + ", name = " + name);
- if (type.equals(TYPE_MESSAGE_UPDATE)) {
- if(V) {
- Log.d(TAG,"TYPE_MESSAGE_UPDATE:");
+ if(D) Log.d(TAG,"type = " + type + ", name = " + name);
+ if (type.equals(TYPE_MESSAGE_UPDATE)) {
+ if(V) {
+ Log.d(TAG,"TYPE_MESSAGE_UPDATE:");
+ }
+ return updateInbox();
+ }else if(type.equals(TYPE_SET_NOTIFICATION_REGISTRATION)) {
+ if(V) {
+ Log.d(TAG,"TYPE_SET_NOTIFICATION_REGISTRATION: NotificationStatus: "
+ + appParams.getNotificationStatus());
+ }
+ return mObserver.setNotificationRegistration(appParams.getNotificationStatus());
+ }else if(type.equals(TYPE_SET_MESSAGE_STATUS)) {
+ if(V) {
+ Log.d(TAG,"TYPE_SET_MESSAGE_STATUS: StatusIndicator: "
+ + appParams.getStatusIndicator()
+ + ", StatusValue: " + appParams.getStatusValue());
+ }
+ return setMessageStatus(name, appParams);
+ } else if (type.equals(TYPE_MESSAGE)) {
+ if(V) {
+ Log.d(TAG,"TYPE_MESSAGE: Transparet: " + appParams.getTransparent()
+ + ", retry: " + appParams.getRetry()
+ + ", charset: " + appParams.getCharset());
+ }
+ return pushMessage(op, name, appParams);
}
- return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
- }else if(type.equals(TYPE_SET_NOTIFICATION_REGISTRATION)) {
- if(V) {
- Log.d(TAG,"TYPE_SET_NOTIFICATION_REGISTRATION: NotificationStatus: " + appParams.getNotificationStatus());
+ } catch (RemoteException e){
+ //reload the providerClient and return error
+ try {
+ mProviderClient = acquireUnstableContentProviderOrThrow();
+ }catch (RemoteException e2){
+ //should not happen
}
- return setNotificationRegistration(appParams);
- }else if(type.equals(TYPE_SET_MESSAGE_STATUS)) {
- if(V) {
- Log.d(TAG,"TYPE_SET_MESSAGE_STATUS: StatusIndicator: " + appParams.getStatusIndicator() + ", StatusValue: " + appParams.getStatusValue());
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }catch (Exception e) {
+
+ if(D) {
+ Log.e(TAG, "Exception occured while handling request",e);
+ } else {
+ Log.e(TAG, "Exception occured while handling request");
}
- return setMessageStatus(name, appParams);
- } else if (type.equals(TYPE_MESSAGE)) {
- if(V) {
- Log.d(TAG,"TYPE_MESSAGE: Transparet: " + appParams.getTransparent() + ", Retry: " + appParams.getRetry());
- Log.d(TAG," charset: " + appParams.getCharset());
+ if(mIsAborted) {
+ return ResponseCodes.OBEX_HTTP_OK;
+ } else {
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
- return pushMessage(op, name, appParams);
+ }
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+ private int updateInbox() throws RemoteException{
+ if (mAccount != null) {
+ BluetoothMapFolderElement inboxFolder = mCurrentFolder.getEmailFolderByName(
+ BluetoothMapContract.FOLDER_NAME_INBOX);
+ if (inboxFolder != null) {
+ long accountId = mAccountId;
+ if (D) Log.d(TAG,"updateInbox inbox=" + inboxFolder.getName() + "id="
+ + inboxFolder.getEmailFolderId());
+
+ final Bundle extras = new Bundle(2);
+ if (accountId != -1) {
+ if (D) Log.d(TAG,"updateInbox accountId=" + accountId);
+ extras.putLong(BluetoothMapContract.EXTRA_UPDATE_FOLDER_ID,
+ inboxFolder.getEmailFolderId());
+ extras.putLong(BluetoothMapContract.EXTRA_UPDATE_ACCOUNT_ID, accountId);
+ } else {
+ // Only error code allowed on an UpdateInbox is OBEX_HTTP_NOT_IMPLEMENTED,
+ // i.e. if e.g. update not allowed on the mailbox
+ if (D) Log.d(TAG,"updateInbox accountId=0 -> OBEX_HTTP_NOT_IMPLEMENTED");
+ return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
+ }
+
+ Uri emailUri = Uri.parse(mBaseEmailUriString);
+ if (D) Log.d(TAG,"updateInbox in: " + emailUri.toString());
+ try {
+ if (D) Log.d(TAG,"updateInbox call()...");
+ Bundle myBundle = mProviderClient.call(BluetoothMapContract.METHOD_UPDATE_FOLDER, null, extras);
+ if (myBundle != null)
+ return ResponseCodes.OBEX_HTTP_OK;
+ else {
+ if (D) Log.d(TAG,"updateInbox call failed");
+ return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
+ }
+ } catch (RemoteException e){
+ mProviderClient = acquireUnstableContentProviderOrThrow();
+ return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
+ }catch (NullPointerException e) {
+ if(D) Log.e(TAG, "UpdateInbox - if uri or method is null", e);
+ return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
+
+ } catch (IllegalArgumentException e) {
+ if(D) Log.e(TAG, "UpdateInbox - if uri is not known", e);
+ return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
+ }
+ }
}
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ return ResponseCodes.OBEX_HTTP_OK;
}
- private int setNotificationRegistration(BluetoothMapAppParams appParams) {
- // Forward the request to the MNS thread as a message - including the MAS instance ID.
- Handler mns = mMnsClient.getMessageHandler();
- if(mns != null) {
- Message msg = Message.obtain(mns);
- msg.what = BluetoothMnsObexClient.MSG_MNS_NOTIFICATION_REGISTRATION;
- msg.arg1 = 0; // TODO: Add correct MAS ID, as specified in the SDP record.
- msg.arg2 = appParams.getNotificationStatus();
- msg.sendToTarget();
- if(D) Log.d(TAG,"MSG_MNS_NOTIFICATION_REGISTRATION");
- return ResponseCodes.OBEX_HTTP_OK;
+ private BluetoothMapFolderElement getFolderElementFromName(String folderName) {
+ BluetoothMapFolderElement folderElement = null;
+
+ if(folderName == null || folderName.trim().isEmpty() ) {
+ folderElement = mCurrentFolder;
+ if(D) Log.d(TAG, "no folder name supplied, setting folder to current: "
+ + folderElement.getName());
} else {
- return ResponseCodes.OBEX_HTTP_UNAVAILABLE; // This should not happen.
+ folderElement = mCurrentFolder.getSubFolder(folderName);
+ if(D) Log.d(TAG, "Folder name: " + folderName + " resulted in this element: "
+ + folderElement.getName());
}
+ return folderElement;
}
private int pushMessage(final Operation op, String folderName, BluetoothMapAppParams appParams) {
if(appParams.getCharset() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) {
- if(D) Log.d(TAG, "Missing charset - unable to decode message content. appParams.getCharset() = " + appParams.getCharset());
+ if(D) Log.d(TAG, "pushMessage: Missing charset - unable to decode message content. " +
+ "appParams.getCharset() = " + appParams.getCharset());
return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
}
+ InputStream bMsgStream = null;
try {
- if(folderName == null || folderName.trim().isEmpty()) {
- folderName = mCurrentFolder.getName();
+ BluetoothMapFolderElement folderElement = getFolderElementFromName(folderName);
+ if(folderElement == null) {
+ Log.w(TAG,"pushMessage: folderElement == null - sending OBEX_HTTP_PRECON_FAILED");
+ return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
+ } else {
+ folderName = folderElement.getName();
}
- folderName = folderName.toLowerCase();
- if(!folderName.equals("outbox") && !folderName.equals("draft")) {
- if(D) Log.d(TAG, "Push message only allowed to outbox and draft. folderName: " + folderName);
+ if (!folderName.equals(BluetoothMapContract.FOLDER_NAME_OUTBOX) &&
+ !folderName.equals(BluetoothMapContract.FOLDER_NAME_DRAFT)) {
+ if(D) Log.d(TAG, "pushMessage: Is only allowed to outbox and draft. " +
+ "folderName=" + folderName);
return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
}
+
/* - Read out the message
* - Decode into a bMessage
* - send it.
*/
- InputStream bMsgStream;
BluetoothMapbMessage message;
bMsgStream = op.openInputStream();
- message = BluetoothMapbMessage.parse(bMsgStream, appParams.getCharset()); // Decode the messageBody
+ // Decode the messageBody
+ message = BluetoothMapbMessage.parse(bMsgStream, appParams.getCharset());
// Send message
- BluetoothMapContentObserver observer = mMnsClient.getContentObserver();
- if (observer == null) {
- return ResponseCodes.OBEX_HTTP_UNAVAILABLE; // Should not happen.
+ if (mObserver == null || message == null) {
+ // Should not happen except at shutdown.
+ if(D) Log.w(TAG, "mObserver or parsed message not available" );
+ return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
}
- long handle = observer.pushMessage(message, folderName, appParams);
+ if ((message.getType().equals(TYPE.EMAIL) && (folderElement.getEmailFolderId() == -1)) ||
+ ((message.getType().equals(TYPE.SMS_GSM) || message.getType().equals(TYPE.SMS_CDMA) ||
+ message.getType().equals(TYPE.MMS)) && !folderElement.hasSmsMmsContent()) ) {
+ if(D) Log.w(TAG, "Wrong message type recieved" );
+ return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
+ }
+
+ long handle = mObserver.pushMessage(message, folderElement, appParams, mBaseEmailUriString);
if (D) Log.d(TAG, "pushMessage handle: " + handle);
if (handle < 0) {
+ if(D) Log.w(TAG, "Message handle not created" );
return ResponseCodes.OBEX_HTTP_UNAVAILABLE; // Should not happen.
}
HeaderSet replyHeaders = new HeaderSet();
@@ -272,14 +501,34 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
replyHeaders.setHeader(HeaderSet.NAME, handleStr);
op.sendHeaders(replyHeaders);
- bMsgStream.close();
+ } catch (RemoteException e) {
+ //reload the providerClient and return error
+ try {
+ mProviderClient = acquireUnstableContentProviderOrThrow();
+ }catch (RemoteException e2){
+ //should not happen
+ }
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
} catch (IllegalArgumentException e) {
- if(D) Log.w(TAG, "Wrongly formatted bMessage received", e);
+ if (D) Log.e(TAG, "Wrongly formatted bMessage received", e);
return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
+ } catch (IOException e) {
+ if (D) Log.e(TAG, "Exception occured: ", e);
+ if(mIsAborted == true) {
+ if(D) Log.d(TAG, "PushMessage Operation Aborted");
+ return ResponseCodes.OBEX_HTTP_OK;
+ } else {
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
} catch (Exception e) {
- // TODO: Change to IOException after debug
- Log.e(TAG, "Exception occured: ", e);
+ if (D) Log.e(TAG, "Exception:", e);
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ } finally {
+ if(bMsgStream != null) {
+ try {
+ bMsgStream.close();
+ } catch (IOException e) {}
+ }
}
return ResponseCodes.OBEX_HTTP_OK;
}
@@ -295,25 +544,33 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
msgHandle == null) {
return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
}
- BluetoothMapContentObserver observer = mMnsClient.getContentObserver();
- if (observer == null) {
+ if (mObserver == null) {
+ if(D) Log.d(TAG, "Error: no mObserver!");
return ResponseCodes.OBEX_HTTP_UNAVAILABLE; // Should not happen.
}
try {
handle = BluetoothMapUtils.getCpHandle(msgHandle);
msgType = BluetoothMapUtils.getMsgTypeFromHandle(msgHandle);
+ if(D)Log.d(TAG,"setMessageStatus. Handle:" + handle+", MsgType: "+ msgType);
} catch (NumberFormatException e) {
Log.w(TAG, "Wrongly formatted message handle: " + msgHandle);
return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
}
if( indicator == BluetoothMapAppParams.STATUS_INDICATOR_DELETED) {
- if (!observer.setMessageStatusDeleted(handle, msgType, value)) {
+ if (!mObserver.setMessageStatusDeleted(handle, msgType, mCurrentFolder,
+ mBaseEmailUriString, value)) {
return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
}
- } else /* BluetoothMapAppParams.STATUS_INDICATOR_READE */ {
- if (!observer.setMessageStatusRead(handle, msgType, value)) {
+ } else /* BluetoothMapAppParams.STATUS_INDICATOR_READ */ {
+ try{
+ if (!mObserver.setMessageStatusRead(handle, msgType, mBaseEmailUriString, value)) {
+ if(D)Log.d(TAG,"not able to update the message");
+ return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
+ }
+ }catch(RemoteException e) {
+ if(D) Log.e(TAG,"Error in setMessageStatusRead()", e);
return ResponseCodes.OBEX_HTTP_UNAVAILABLE;
}
}
@@ -329,13 +586,17 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
try {
folderName = (String)request.getHeader(HeaderSet.NAME);
} catch (Exception e) {
- Log.e(TAG, "request headers error");
+ if(D) {
+ Log.e(TAG, "request headers error" , e);
+ } else {
+ Log.e(TAG, "request headers error");
+ }
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
if (V) logHeader(request);
if (D) Log.d(TAG, "onSetPath name is " + folderName + " backup: " + backup
- + "create: " + create);
+ + " create: " + create);
if(backup == true){
if(mCurrentFolder.getParent() != null)
@@ -364,15 +625,22 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
if (mCallback != null) {
Message msg = Message.obtain(mCallback);
msg.what = BluetoothMapService.MSG_SERVERSESSION_CLOSE;
+ msg.arg1 = mMasId;
msg.sendToTarget();
if (D) Log.d(TAG, "onClose(): msg MSG_SERVERSESSION_CLOSE sent out.");
+
+ }
+ if(mProviderClient != null){
+ mProviderClient.release();
+ mProviderClient = null;
}
+
}
@Override
public int onGet(Operation op) {
notifyUpdateWakeLock();
- sIsAborted = false;
+ mIsAborted = false;
HeaderSet request;
String type;
String name;
@@ -400,8 +668,7 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
", ListStartOffset = " + appParams.getStartOffset());
}
return sendFolderListingRsp(op, appParams); // Block until all packets have been send.
- }
- else if (type.equals(TYPE_GET_MESSAGE_LISTING)){
+ } else if (type.equals(TYPE_GET_MESSAGE_LISTING)){
if (V && appParams != null) {
Log.d(TAG,"TYPE_GET_MESSAGE_LISTING: MaxListCount = " + appParams.getMaxListCount() +
", ListStartOffset = " + appParams.getStartOffset());
@@ -416,22 +683,36 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
Log.d(TAG,"FilterPriority = " + appParams.getFilterPriority());
}
return sendMessageListingRsp(op, appParams, name); // Block until all packets have been send.
- }
- else if (type.equals(TYPE_MESSAGE)){
+ } else if (type.equals(TYPE_MESSAGE)){
if(V && appParams != null) {
- Log.d(TAG,"TYPE_MESSAGE (GET): Attachment = " + appParams.getAttachment() + ", Charset = " + appParams.getCharset() +
- ", FractionRequest = " + appParams.getFractionRequest());
+ Log.d(TAG,"TYPE_MESSAGE (GET): Attachment = " + appParams.getAttachment() +
+ ", Charset = " + appParams.getCharset() +
+ ", FractionRequest = " + appParams.getFractionRequest());
}
return sendGetMessageRsp(op, name, appParams); // Block until all packets have been send.
- }
- else {
+ } else {
Log.w(TAG, "unknown type request: " + type);
return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
}
+
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Exception:", e);
+ return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
+ } catch (ParseException e) {
+ Log.e(TAG, "Exception:", e);
+ return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
} catch (Exception e) {
- // TODO: Move to the part that actually throws exceptions, and change to the correat exception type
- Log.e(TAG, "request headers error, Exception:", e);
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ if(D) {
+ Log.e(TAG, "Exception occured while handling request",e);
+ } else {
+ Log.e(TAG, "Exception occured while handling request");
+ }
+ if(mIsAborted == true) {
+ if(D) Log.d(TAG, "onGet Operation Aborted");
+ return ResponseCodes.OBEX_HTTP_OK;
+ } else {
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
}
}
@@ -456,15 +737,18 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
HeaderSet replyHeaders = new HeaderSet();
BluetoothMapAppParams outAppParams = new BluetoothMapAppParams();
BluetoothMapMessageListing outList;
- if(folderName == null || folderName.length() == 0 ) {
- folderName = mCurrentFolder.getName();
- }
if(appParams == null){
appParams = new BluetoothMapAppParams();
appParams.setMaxListCount(1024);
appParams.setStartOffset(0);
}
+ BluetoothMapFolderElement folderToList = getFolderElementFromName(folderName);
+ if(folderToList == null) {
+ Log.w(TAG,"sendMessageListingRsp: folderToList == null - sending OBEX_HTTP_BAD_REQUEST");
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
+
// Check to see if we only need to send the size - hence no need to encode.
try {
// Open the OBEX body stream
@@ -477,15 +761,15 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
appParams.setStartOffset(0);
if(appParams.getMaxListCount() != 0) {
- outList = mOutContent.msgListing(folderName, appParams);
+ outList = mOutContent.msgListing(folderToList, appParams);
// Generate the byte stream
outAppParams.setMessageListingSize(outList.getCount());
- outBytes = outList.encode();
+ outBytes = outList.encode(mThreadIdSupport); // Include thread ID for clients that supports it.
hasUnread = outList.hasUnread();
}
else {
- listSize = mOutContent.msgListingSize(folderName, appParams);
- hasUnread = mOutContent.msgListingHasUnread(folderName, appParams);
+ listSize = mOutContent.msgListingSize(folderToList, appParams);
+ hasUnread = mOutContent.msgListingHasUnread(folderToList, appParams);
outAppParams.setMessageListingSize(listSize);
op.noBodyHeader();
}
@@ -493,8 +777,7 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
// Build the application parameter header
// let the peer know if there are unread messages in the list
- if(hasUnread)
- {
+ if(hasUnread) {
outAppParams.setNewMessage(1);
}else{
outAppParams.setNewMessage(0);
@@ -506,40 +789,39 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
} catch (IOException e) {
Log.w(TAG,"sendMessageListingRsp: IOException - sending OBEX_HTTP_BAD_REQUEST", e);
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ if(outStream != null) { try { outStream.close(); } catch (IOException ex) {} }
+ if(mIsAborted == true) {
+ if(D) Log.d(TAG, "sendMessageListingRsp Operation Aborted");
+ return ResponseCodes.OBEX_HTTP_OK;
+ } else {
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
} catch (IllegalArgumentException e) {
Log.w(TAG,"sendMessageListingRsp: IllegalArgumentException - sending OBEX_HTTP_BAD_REQUEST", e);
+ if(outStream != null) { try { outStream.close(); } catch (IOException ex) {} }
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
maxChunkSize = op.getMaxPacketSize(); // This must be called after setting the headers.
if(outBytes != null) {
try {
- while (bytesWritten < outBytes.length && sIsAborted == false) {
+ while (bytesWritten < outBytes.length && mIsAborted == false) {
bytesToWrite = Math.min(maxChunkSize, outBytes.length - bytesWritten);
outStream.write(outBytes, bytesWritten, bytesToWrite);
bytesWritten += bytesToWrite;
}
} catch (IOException e) {
- if(V) Log.w(TAG,e);
+ if(D) Log.w(TAG,e);
// We were probably aborted or disconnected
} finally {
- if(outStream != null) {
- try {
- outStream.close();
- } catch (IOException e) {
- // If an error occurs during close, there is no more cleanup to do
- }
- }
+ if(outStream != null) { try { outStream.close(); } catch (IOException e) {} }
}
- if(bytesWritten != outBytes.length)
+ if(bytesWritten != outBytes.length && !mIsAborted) {
+ Log.w(TAG,"sendMessageListingRsp: bytesWritten != outBytes.length - sending OBEX_HTTP_BAD_REQUEST");
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
- } else {
- try {
- outStream.close();
- } catch (IOException e) {
- // If an error occurs during close, there is no more cleanup to do
}
+ } else {
+ if(outStream != null) { try { outStream.close(); } catch (IOException e) {} }
}
return ResponseCodes.OBEX_HTTP_OK;
}
@@ -595,17 +877,24 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
} catch (IOException e1) {
Log.w(TAG,"sendFolderListingRsp: IOException - sending OBEX_HTTP_BAD_REQUEST Exception:", e1);
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ if(outStream != null) { try { outStream.close(); } catch (IOException e) {} }
+ if(mIsAborted == true) {
+ if(D) Log.d(TAG, "sendFolderListingRsp Operation Aborted");
+ return ResponseCodes.OBEX_HTTP_OK;
+ } else {
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
} catch (IllegalArgumentException e1) {
Log.w(TAG,"sendFolderListingRsp: IllegalArgumentException - sending OBEX_HTTP_BAD_REQUEST Exception:", e1);
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ if(outStream != null) { try { outStream.close(); } catch (IOException e) {} }
+ return ResponseCodes.OBEX_HTTP_PRECON_FAILED;
}
maxChunkSize = op.getMaxPacketSize(); // This must be called after setting the headers.
if(outBytes != null) {
try {
- while (bytesWritten < outBytes.length && sIsAborted == false) {
+ while (bytesWritten < outBytes.length && mIsAborted == false) {
bytesToWrite = Math.min(maxChunkSize, outBytes.length - bytesWritten);
outStream.write(outBytes, bytesWritten, bytesToWrite);
bytesWritten += bytesToWrite;
@@ -613,17 +902,11 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
} catch (IOException e) {
// We were probably aborted or disconnected
} finally {
- if(outStream != null) {
- try {
- outStream.close();
- } catch (IOException e) {
- // If an error occurs during close, there is no more cleanup to do
- }
- }
+ if(outStream != null) { try { outStream.close(); } catch (IOException e) {} }
}
if(V)
Log.v(TAG,"sendFolderList sent " + bytesWritten + " bytes out of "+ outBytes.length);
- if(bytesWritten == outBytes.length)
+ if(bytesWritten == outBytes.length || mIsAborted)
return ResponseCodes.OBEX_HTTP_OK;
else
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
@@ -646,20 +929,42 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
* {@link ResponseCodes.OBEX_HTTP_BAD_REQUEST} on error.
*/
private int sendGetMessageRsp(Operation op, String handle, BluetoothMapAppParams appParams){
- OutputStream outStream ;
- byte[] outBytes;
+ OutputStream outStream = null;
+ byte[] outBytes = null;
int maxChunkSize, bytesToWrite, bytesWritten = 0;
- long msgHandle;
try {
- outBytes = mOutContent.getMessage(handle, appParams);
+ outBytes = mOutContent.getMessage(handle, appParams, mCurrentFolder);
outStream = op.openOutputStream();
+ // If it is a fraction request of Email message, set header before responding
+ if ((BluetoothMapUtils.getMsgTypeFromHandle(handle).equals(TYPE.EMAIL)) &&
+ (appParams.getFractionRequest() ==
+ BluetoothMapAppParams.FRACTION_REQUEST_FIRST)) {
+ BluetoothMapAppParams outAppParams = new BluetoothMapAppParams();;
+ HeaderSet replyHeaders = new HeaderSet();
+ outAppParams.setFractionDeliver(BluetoothMapAppParams.FRACTION_DELIVER_LAST);
+ // Build and set the application parameter header
+ replyHeaders.setHeader(HeaderSet.APPLICATION_PARAMETER,
+ outAppParams.EncodeParams());
+ op.sendHeaders(replyHeaders);
+ if(V) Log.v(TAG,"sendGetMessageRsp fractionRequest - " +
+ "set FRACTION_DELIVER_LAST header");
+ }
+
} catch (IOException e) {
Log.w(TAG,"sendGetMessageRsp: IOException - sending OBEX_HTTP_BAD_REQUEST", e);
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ if(outStream != null) { try { outStream.close(); } catch (IOException ex) {} }
+ if(mIsAborted == true) {
+ if(D) Log.d(TAG, "sendGetMessageRsp Operation Aborted");
+ return ResponseCodes.OBEX_HTTP_OK;
+ } else {
+ return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
+ }
} catch (IllegalArgumentException e) {
- Log.w(TAG,"sendGetMessageRsp: IllegalArgumentException (e.g. invalid handle) - sending OBEX_HTTP_BAD_REQUEST", e);
+ Log.w(TAG,"sendGetMessageRsp: IllegalArgumentException (e.g. invalid handle) - " +
+ "sending OBEX_HTTP_BAD_REQUEST", e);
+ if(outStream != null) { try { outStream.close(); } catch (IOException ex) {} }
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
}
@@ -667,23 +972,20 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
if(outBytes != null) {
try {
- while (bytesWritten < outBytes.length && sIsAborted == false) {
+ while (bytesWritten < outBytes.length && mIsAborted == false) {
bytesToWrite = Math.min(maxChunkSize, outBytes.length - bytesWritten);
outStream.write(outBytes, bytesWritten, bytesToWrite);
bytesWritten += bytesToWrite;
}
} catch (IOException e) {
// We were probably aborted or disconnected
- } finally {
- if(outStream != null) {
- try {
- outStream.close();
- } catch (IOException e) {
- // If an error occurs during close, there is no more cleanup to do
- }
+ if(D && e.getMessage().equals("Abort Received")) {
+ Log.w(TAG, "getMessage() Aborted...", e);
}
+ } finally {
+ if(outStream != null) { try { outStream.close(); } catch (IOException e) {} }
}
- if(bytesWritten == outBytes.length)
+ if(bytesWritten == outBytes.length || mIsAborted)
return ResponseCodes.OBEX_HTTP_OK;
else
return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
@@ -693,9 +995,11 @@ public class BluetoothMapObexServer extends ServerRequestHandler {
}
private void notifyUpdateWakeLock() {
- Message msg = Message.obtain(mCallback);
- msg.what = BluetoothMapService.MSG_ACQUIRE_WAKE_LOCK;
- msg.sendToTarget();
+ if(mCallback != null) {
+ Message msg = Message.obtain(mCallback);
+ msg.what = BluetoothMapService.MSG_ACQUIRE_WAKE_LOCK;
+ msg.sendToTarget();
+ }
}
private static final void logHeader(HeaderSet hs) {
diff --git a/src/com/android/bluetooth/map/BluetoothMapService.java b/src/com/android/bluetooth/map/BluetoothMapService.java
index 7d42b29fc..077e2a3b6 100755
--- a/src/com/android/bluetooth/map/BluetoothMapService.java
+++ b/src/com/android/bluetooth/map/BluetoothMapService.java
@@ -1,5 +1,5 @@
/*
-* Copyright (C) 2013 Samsung System LSI
+* Copyright (C) 2014 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
@@ -17,10 +17,19 @@ package com.android.bluetooth.map;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import javax.obex.ServerSession;
+
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
@@ -31,21 +40,29 @@ import android.bluetooth.BluetoothMap;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter.MalformedMimeTypeException;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.ParcelUuid;
+import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
+import android.util.SparseArray;
import android.provider.Settings;
+import android.provider.Telephony.Sms;
import android.content.IntentFilter;
import android.content.BroadcastReceiver;
+import android.database.ContentObserver;
import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
-
+import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder;
+import com.android.bluetooth.opp.BluetoothOppTransferHistory;
+import com.android.bluetooth.opp.BluetoothShare;
+import com.android.bluetooth.opp.Constants;
public class BluetoothMapService extends ProfileService {
private static final String TAG = "BluetoothMapService";
@@ -57,31 +74,20 @@ public class BluetoothMapService extends ProfileService {
* DEBUG log: "setprop log.tag.BluetoothMapService VERBOSE"
*/
- public static final boolean DEBUG = true;
+ public static final boolean DEBUG = true; //TODO: set to false
- public static final boolean VERBOSE = false;
-
- /**
- * Intent indicating incoming obex authentication request which is from
- * PCE(Carkit)
- */
- public static final String AUTH_CHALL_ACTION = "com.android.bluetooth.map.authchall";
+ public static final boolean VERBOSE = true; //TODO: set to false
/**
* Intent indicating timeout for user confirmation, which is sent to
* BluetoothMapActivity
*/
public static final String USER_CONFIRM_TIMEOUT_ACTION =
- "com.android.bluetooth.map.userconfirmtimeout";
- private static final int USER_CONFIRM_TIMEOUT_VALUE = 30000;
+ "com.android.bluetooth.map.USER_CONFIRM_TIMEOUT";
+ private static final int USER_CONFIRM_TIMEOUT_VALUE = 25000;
- /**
- * Intent Extra name indicating session key which is sent from
- * BluetoothMapActivity
- */
- public static final String EXTRA_SESSION_KEY = "com.android.bluetooth.map.sessionkey";
-
- public static final String THIS_PACKAGE_NAME = "com.android.bluetooth";
+ /** Intent indicating that the email settings activity should be opened*/
+ public static final String ACTION_SHOW_MAPS_EMAIL_SETTINGS = "android.btmap.intent.action.SHOW_MAPS_EMAIL_SETTINGS";
public static final int MSG_SERVERSESSION_CLOSE = 5000;
@@ -89,11 +95,12 @@ public class BluetoothMapService extends ProfileService {
public static final int MSG_SESSION_DISCONNECTED = 5002;
- public static final int MSG_OBEX_AUTH_CHALL = 5003;
+ public static final int MSG_MAS_CONNECT = 5003; // Send at MAS connect, including the MAS_ID
+ public static final int MSG_MAS_CONNECT_CANCEL = 5004; // Send at auth. declined
- public static final int MSG_ACQUIRE_WAKE_LOCK = 5004;
+ public static final int MSG_ACQUIRE_WAKE_LOCK = 5005;
- public static final int MSG_RELEASE_WAKE_LOCK = 5005;
+ public static final int MSG_RELEASE_WAKE_LOCK = 5006;
private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH;
@@ -105,36 +112,50 @@ public class BluetoothMapService extends ProfileService {
private static final int DISCONNECT_MAP = 3;
+ private static final int SHUTDOWN = 4;
+
private static final int RELEASE_WAKE_LOCK_DELAY = 10000;
private PowerManager.WakeLock mWakeLock = null;
- private BluetoothAdapter mAdapter;
-
- private SocketAcceptThread mAcceptThread = null;
+ private static final int UPDATE_MAS_INSTANCES = 5;
- private BluetoothMapAuthenticator mAuth = null;
+ public static final int UPDATE_MAS_INSTANCES_ACCOUNT_ADDED = 0;
+ public static final int UPDATE_MAS_INSTANCES_ACCOUNT_REMOVED = 1;
+ public static final int UPDATE_MAS_INSTANCES_ACCOUNT_RENAMED = 2;
+ public static final int UPDATE_MAS_INSTANCES_ACCOUNT_DISCONNECT = 3;
- private BluetoothMapObexServer mMapServer;
+ private static final int MAS_ID_SMS_MMS = 0;
- private ServerSession mServerSession = null;
+ private BluetoothAdapter mAdapter;
private BluetoothMnsObexClient mBluetoothMnsObexClient = null;
- private BluetoothServerSocket mServerSocket = null;
-
- private BluetoothSocket mConnSocket = null;
+ /* mMasInstances: A list of the active MasInstances with the key being the MasId */
+ private SparseArray<BluetoothMapMasInstance> mMasInstances =
+ new SparseArray<BluetoothMapMasInstance>(1);
+ /* mMasInstanceMap: A list of the active MasInstances with the key being the account */
+ private HashMap<BluetoothMapEmailSettingsItem, BluetoothMapMasInstance> mMasInstanceMap =
+ new HashMap<BluetoothMapEmailSettingsItem, BluetoothMapMasInstance>(1);
- private BluetoothDevice mRemoteDevice = null;
+ private BluetoothDevice mRemoteDevice = null; // The remote connected device - protect access
+ private ArrayList<BluetoothMapEmailSettingsItem> mEnabledAccounts = null;
private static String sRemoteDeviceName = null;
- private volatile boolean mInterrupted;
-
private int mState;
+ private BluetoothMapEmailAppObserver mAppObserver = null;
+ private AlarmManager mAlarmManager = null;
- private boolean isWaitingAuthorization = false;
- private boolean removeTimeoutMsg = false;
+ private boolean mIsWaitingAuthorization = false;
+ private boolean mRemoveTimeoutMsg = false;
+ private boolean mTrust = false; // Temp. fix for missing BluetoothDevice.getTrustState()
+ private boolean mAccountChanged = false;
+
+ // package and class name to which we send intent to check phone book access permission
+ private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings";
+ private static final String ACCESS_AUTHORITY_CLASS =
+ "com.android.settings.bluetooth.BluetoothPermissionRequest";
private static final ParcelUuid[] MAP_UUIDS = {
BluetoothUuid.MAP,
@@ -143,138 +164,56 @@ public class BluetoothMapService extends ProfileService {
public BluetoothMapService() {
mState = BluetoothMap.STATE_DISCONNECTED;
- }
- private void startRfcommSocketListener() {
- if (DEBUG) Log.d(TAG, "Map Service startRfcommSocketListener");
-
- if (mAcceptThread == null) {
- mAcceptThread = new SocketAcceptThread();
- mAcceptThread.setName("BluetoothMapAcceptThread");
- mAcceptThread.start();
- }
}
- private final boolean initSocket() {
- if (DEBUG) Log.d(TAG, "Map Service initSocket");
- boolean initSocketOK = false;
- final int CREATE_RETRY_TIME = 10;
-
- // It's possible that create will fail in some cases. retry for 10 times
- for (int i = 0; (i < CREATE_RETRY_TIME) && !mInterrupted; i++) {
- initSocketOK = true;
- try {
- // It is mandatory for MSE to support initiation of bonding and
- // encryption.
- mServerSocket = mAdapter.listenUsingEncryptedRfcommWithServiceRecord
- ("MAP SMS/MMS", BluetoothUuid.MAS.getUuid());
+ private final void closeService() {
+ if (DEBUG) Log.d(TAG, "MAP Service closeService in");
- } catch (IOException e) {
- Log.e(TAG, "Error create RfcommServerSocket " + e.toString());
- initSocketOK = false;
- }
- if (!initSocketOK) {
- // Need to break out of this loop if BT is being turned off.
- if (mAdapter == null) break;
- int state = mAdapter.getState();
- if ((state != BluetoothAdapter.STATE_TURNING_ON) &&
- (state != BluetoothAdapter.STATE_ON)) {
- Log.w(TAG, "initServerSocket failed as BT is (being) turned off");
- break;
- }
- try {
- if (VERBOSE) Log.v(TAG, "wait 300 ms");
- Thread.sleep(300);
- } catch (InterruptedException e) {
- Log.e(TAG, "socketAcceptThread thread was interrupted (3)");
- }
- } else {
- break;
- }
- }
- if (mInterrupted) {
- initSocketOK = false;
- // close server socket to avoid resource leakage
- closeServerSocket();
+ if (mBluetoothMnsObexClient != null) {
+ mBluetoothMnsObexClient.shutdown();
+ mBluetoothMnsObexClient = null;
}
- if (initSocketOK) {
- if (VERBOSE) Log.v(TAG, "Succeed to create listening socket ");
-
- } else {
- Log.e(TAG, "Error to create listening socket after " + CREATE_RETRY_TIME + " try");
+ for(int i=0, c=mMasInstances.size(); i < c; i++) {
+ mMasInstances.valueAt(i).shutdown();
}
- return initSocketOK;
- }
+ mMasInstances.clear();
- private final synchronized void closeServerSocket() {
- // exit SocketAcceptThread early
- if (mServerSocket != null) {
- try {
- // this will cause mServerSocket.accept() return early with IOException
- mServerSocket.close();
- mServerSocket = null;
- } catch (IOException ex) {
- Log.e(TAG, "Close Server Socket error: " + ex);
- }
- }
- }
- private final synchronized void closeConnectionSocket() {
- if (mConnSocket != null) {
- try {
- mConnSocket.close();
- mConnSocket = null;
- } catch (IOException e) {
- Log.e(TAG, "Close Connection Socket error: " + e.toString());
- }
+ if (mSessionStatusHandler != null) {
+ mSessionStatusHandler.removeCallbacksAndMessages(null);
}
- }
-
- private final void closeService() {
- if (DEBUG) Log.d(TAG, "MAP Service closeService in");
-
- // exit initSocket early
- mInterrupted = true;
- closeServerSocket();
- if (mAcceptThread != null) {
- try {
- mAcceptThread.shutdown();
- mAcceptThread.join();
- mAcceptThread = null;
- } catch (InterruptedException ex) {
- Log.w(TAG, "mAcceptThread close error" + ex);
- }
- }
+ mIsWaitingAuthorization = false;
+ mTrust = false;
+ setState(BluetoothMap.STATE_DISCONNECTED);
if (mWakeLock != null) {
mWakeLock.release();
+ if(VERBOSE)Log.i(TAG, "CloseService(): Release Wake Lock");
mWakeLock = null;
}
+ mRemoteDevice = null;
- if (mServerSession != null) {
- mServerSession.close();
- mServerSession = null;
- }
-
- if (mBluetoothMnsObexClient != null) {
- mBluetoothMnsObexClient.shutdown();
- mBluetoothMnsObexClient = null;
- }
-
- closeConnectionSocket();
+ if (VERBOSE) Log.v(TAG, "MAP Service closeService out");
+ }
- if (mSessionStatusHandler != null) {
- mSessionStatusHandler.removeCallbacksAndMessages(null);
+ /**
+ * Starts the RFComm listerner threads for each MAS
+ * @throws IOException
+ */
+ private final void startRfcommSocketListeners() {
+ for(int i=0, c=mMasInstances.size(); i < c; i++) {
+ mMasInstances.valueAt(i).startRfcommSocketListener();
}
- isWaitingAuthorization = false;
-
- if (VERBOSE) Log.v(TAG, "MAP Service closeService out");
}
- private final void startObexServerSession() throws IOException {
- if (DEBUG) Log.d(TAG, "Map Service startObexServerSession");
+ /**
+ * Start a MAS instance for SMS/MMS and each e-mail account.
+ */
+ private final void startObexServerSessions() {
+ if (DEBUG) Log.d(TAG, "Map Service START ObexServerSessions()");
// acquire the wakeLock before start Obex transaction thread
if (mWakeLock == null) {
@@ -283,157 +222,93 @@ public class BluetoothMapService extends ProfileService {
"StartingObexMapTransaction");
mWakeLock.setReferenceCounted(false);
mWakeLock.acquire();
+ if(VERBOSE)Log.i(TAG, "startObexSessions(): Acquire Wake Lock");
}
- mBluetoothMnsObexClient = new BluetoothMnsObexClient(this, mRemoteDevice,
- mSessionStatusHandler);
- mMapServer = new BluetoothMapObexServer(mSessionStatusHandler, this,
- mBluetoothMnsObexClient);
- synchronized (this) {
- // We need to get authentication now that obex server is up
- mAuth = new BluetoothMapAuthenticator(mSessionStatusHandler);
- mAuth.setChallenged(false);
- mAuth.setCancelled(false);
+ if(mBluetoothMnsObexClient == null) {
+ mBluetoothMnsObexClient = new BluetoothMnsObexClient(mRemoteDevice, mSessionStatusHandler);
+ }
+
+ boolean connected = false;
+ for(int i=0, c=mMasInstances.size(); i < c; i++) {
+ try {
+ if(mMasInstances.valueAt(i)
+ .startObexServerSession(mBluetoothMnsObexClient) == true) {
+ connected = true;
+ }
+ } catch (IOException e) {
+ Log.w(TAG,"IOException occured while starting an obexServerSession restarting the listener",e);
+ mMasInstances.valueAt(i).restartObexServerSession();
+ } catch (RemoteException e) {
+ Log.w(TAG,"RemoteException occured while starting an obexServerSession restarting the listener",e);
+ mMasInstances.valueAt(i).restartObexServerSession();
+ }
+ }
+ if(connected) {
+ setState(BluetoothMap.STATE_CONNECTED);
}
- // setup RFCOMM transport
- BluetoothMapRfcommTransport transport = new BluetoothMapRfcommTransport(mConnSocket);
- mServerSession = new ServerSession(transport, mMapServer, mAuth);
- setState(BluetoothMap.STATE_CONNECTED);
mSessionStatusHandler.removeMessages(MSG_RELEASE_WAKE_LOCK);
mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler
- .obtainMessage(MSG_RELEASE_WAKE_LOCK), RELEASE_WAKE_LOCK_DELAY);
+ .obtainMessage(MSG_RELEASE_WAKE_LOCK), RELEASE_WAKE_LOCK_DELAY);
if (VERBOSE) {
- Log.v(TAG, "startObexServerSession() success!");
+ Log.v(TAG, "startObexServerSessions() success!");
}
}
- private void stopObexServerSession() {
- if (DEBUG) Log.d(TAG, "MAP Service stopObexServerSession");
-
- mSessionStatusHandler.removeMessages(MSG_ACQUIRE_WAKE_LOCK);
- mSessionStatusHandler.removeMessages(MSG_RELEASE_WAKE_LOCK);
-
- // Release the wake lock if obex transaction is over
- if (mWakeLock != null) {
- mWakeLock.release();
- mWakeLock = null;
- }
-
- if (mServerSession != null) {
- mServerSession.close();
- mServerSession = null;
- }
-
- mAcceptThread = null;
-
- if(mBluetoothMnsObexClient != null) {
- mBluetoothMnsObexClient.shutdown();
- mBluetoothMnsObexClient = null;
- }
- closeConnectionSocket();
-
- // Last obex transaction is finished, we start to listen for incoming
- // connection again
- if (mAdapter.isEnabled()) {
- startRfcommSocketListener();
- }
- setState(BluetoothMap.STATE_DISCONNECTED);
+ public Handler getHandler() {
+ return mSessionStatusHandler;
}
-
-
/**
- * A thread that runs in the background waiting for remote rfcomm
- * connect.Once a remote socket connected, this thread shall be
- * shutdown.When the remote disconnect,this thread shall run again waiting
- * for next request.
+ * Restart a MAS instances.
+ * @param masId use -1 to stop all instances
*/
- private class SocketAcceptThread extends Thread {
+ private void stopObexServerSessions(int masId) {
+ if (DEBUG) Log.d(TAG, "MAP Service STOP ObexServerSessions()");
- private boolean stopped = false;
+ boolean lastMasInst = true;
- @Override
- public void run() {
- BluetoothServerSocket serverSocket;
- if (mServerSocket == null) {
- if (!initSocket()) {
- return;
+ if(masId != -1) {
+ for(int i=0, c=mMasInstances.size(); i < c; i++) {
+ BluetoothMapMasInstance masInst = mMasInstances.valueAt(i);
+ if(masInst.getMasId() != masId && masInst.isStarted()) {
+ lastMasInst = false;
}
}
+ } // Else just close down it all
- while (!stopped) {
- try {
- if (DEBUG) Log.d(TAG, "Accepting socket connection...");
- serverSocket = mServerSocket;
- if(serverSocket == null) {
- Log.w(TAG, "mServerSocket is null");
- break;
- }
- mConnSocket = serverSocket.accept();
- if (DEBUG) Log.d(TAG, "Accepted socket connection...");
- synchronized (BluetoothMapService.this) {
- if (mConnSocket == null) {
- Log.w(TAG, "mConnSocket is null");
- break;
- }
- mRemoteDevice = mConnSocket.getRemoteDevice();
- }
- if (mRemoteDevice == null) {
- Log.i(TAG, "getRemoteDevice() = null");
- break;
- }
-
- sRemoteDeviceName = mRemoteDevice.getName();
- // In case getRemoteName failed and return null
- if (TextUtils.isEmpty(sRemoteDeviceName)) {
- sRemoteDeviceName = getString(R.string.defaultname);
- }
- boolean trust = mRemoteDevice.getTrustState();
- if (DEBUG) Log.d(TAG, "GetTrustState() = " + trust);
-
-
- if (trust) {
- try {
- if (DEBUG) Log.d(TAG, "incoming connection accepted from: "
- + sRemoteDeviceName + " automatically as trusted device");
- startObexServerSession();
- } catch (IOException ex) {
- Log.e(TAG, "catch exception starting obex server session"
- + ex.toString());
- }
- } else {
- Intent intent = new
- Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST);
- intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
- BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice);
-
- isWaitingAuthorization = true;
- sendOrderedBroadcast(intent, BLUETOOTH_ADMIN_PERM);
-
- if (DEBUG) Log.d(TAG, "waiting for authorization for connection from: "
- + sRemoteDeviceName);
- //Queue USER_TIMEOUT to disconnect MAP OBEX session. If user doesn't
- //accept or reject authorization request
- removeTimeoutMsg = true;
- mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler
- .obtainMessage(USER_TIMEOUT), USER_CONFIRM_TIMEOUT_VALUE);
+ /* Shutdown the MNS client - currently must happen before MAS close */
+ if(mBluetoothMnsObexClient != null && lastMasInst) {
+ mBluetoothMnsObexClient.shutdown();
+ mBluetoothMnsObexClient = null;
+ }
+ BluetoothMapMasInstance masInst = mMasInstances.get(masId); // returns null for -1
+ if(masInst != null) {
+ masInst.restartObexServerSession();
+ } else {
+ for(int i=0, c=mMasInstances.size(); i < c; i++) {
+ mMasInstances.valueAt(i).restartObexServerSession();
+ }
+ }
- }
- stopped = true; // job done ,close this thread;
- } catch (IOException ex) {
- stopped=true;
- if (VERBOSE) Log.v(TAG, "Accept exception: " + ex.toString());
- }
+ if(lastMasInst) {
+ setState(BluetoothMap.STATE_DISCONNECTED);
+ mTrust = false;
+ mRemoteDevice = null;
+ if(mAccountChanged) {
+ updateMasInstances(UPDATE_MAS_INSTANCES_ACCOUNT_DISCONNECT);
}
}
- void shutdown() {
- stopped = true;
- interrupt();
+ // Release the wake lock at disconnect
+ if (mWakeLock != null && lastMasInst) {
+ mSessionStatusHandler.removeMessages(MSG_ACQUIRE_WAKE_LOCK);
+ mSessionStatusHandler.removeMessages(MSG_RELEASE_WAKE_LOCK);
+ mWakeLock.release();
+ if(VERBOSE)Log.i(TAG, "stopObexServerSessions(): Release Wake Lock");
}
}
@@ -443,23 +318,35 @@ public class BluetoothMapService extends ProfileService {
if (VERBOSE) Log.v(TAG, "Handler(): got msg=" + msg.what);
switch (msg.what) {
+ case UPDATE_MAS_INSTANCES:
+ updateMasInstancesHandler();
+ break;
case START_LISTENER:
if (mAdapter.isEnabled()) {
- startRfcommSocketListener();
+ startRfcommSocketListeners();
}
break;
+ case MSG_MAS_CONNECT:
+ onConnectHandler(msg.arg1);
+ break;
+ case MSG_MAS_CONNECT_CANCEL:
+ stopObexServerSessions(-1);
+ break;
case USER_TIMEOUT:
- Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice);
- intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
- BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS);
- sendBroadcast(intent, BLUETOOTH_PERM);
- isWaitingAuthorization = false;
- removeTimeoutMsg = false;
- stopObexServerSession();
+ if(mIsWaitingAuthorization){
+ Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL);
+ intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice);
+ intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
+ BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS);
+ sendBroadcast(intent);
+ cancelUserTimeoutAlarm();
+ mIsWaitingAuthorization = false;
+ stopObexServerSessions(-1);
+ }
break;
case MSG_SERVERSESSION_CLOSE:
- stopObexServerSession();
+ stopObexServerSessions(msg.arg1);
break;
case MSG_SESSION_ESTABLISHED:
break;
@@ -469,25 +356,33 @@ public class BluetoothMapService extends ProfileService {
case DISCONNECT_MAP:
disconnectMap((BluetoothDevice)msg.obj);
break;
+ case SHUTDOWN:
+ /* Ensure to call close from this handler to avoid starting new stuff
+ because of pending messages */
+ closeService();
+ break;
case MSG_ACQUIRE_WAKE_LOCK:
+ if(VERBOSE)Log.i(TAG, "Acquire Wake Lock request message");
if (mWakeLock == null) {
PowerManager pm = (PowerManager)getSystemService(
Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
"StartingObexMapTransaction");
mWakeLock.setReferenceCounted(false);
+ }
+ if(!mWakeLock.isHeld()) {
mWakeLock.acquire();
- Log.w(TAG, "Acquire Wake Lock");
+ if(DEBUG)Log.i(TAG, " Acquired Wake Lock by message");
}
mSessionStatusHandler.removeMessages(MSG_RELEASE_WAKE_LOCK);
mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler
.obtainMessage(MSG_RELEASE_WAKE_LOCK), RELEASE_WAKE_LOCK_DELAY);
break;
case MSG_RELEASE_WAKE_LOCK:
+ if(VERBOSE)Log.i(TAG, "Release Wake Lock request message");
if (mWakeLock != null) {
mWakeLock.release();
- mWakeLock = null;
- Log.w(TAG, "Release Wake Lock");
+ if(DEBUG) Log.i(TAG, " Released Wake Lock by message");
}
break;
default:
@@ -496,8 +391,34 @@ public class BluetoothMapService extends ProfileService {
}
};
+ private void onConnectHandler(int masId) {
+ if(mIsWaitingAuthorization == true || mRemoteDevice == null) {
+ return;
+ }
+ BluetoothMapMasInstance masInst = mMasInstances.get(masId);
+ // getTrustState() is not implemented, use local cache
+ // boolean trust = mRemoteDevice.getTrustState(); // Need to ensure we are still trusted
+ boolean trust = mTrust;
+ if (DEBUG) Log.d(TAG, "GetTrustState() = " + trust);
- public int getState() {
+ if (trust) {
+ try {
+ if (DEBUG) Log.d(TAG, "incoming connection accepted from: "
+ + sRemoteDeviceName + " automatically as trusted device");
+ if(mBluetoothMnsObexClient != null
+ && masInst != null) {
+ masInst.startObexServerSession(mBluetoothMnsObexClient);
+ } else {
+ startObexServerSessions();
+ }
+ } catch (IOException ex) {
+ Log.e(TAG, "catch IOException starting obex server session", ex);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "catch RemoteException starting obex server session", ex);
+ }
+ }
+ }
+ public int getState() {
return mState;
}
@@ -542,17 +463,7 @@ public class BluetoothMapService extends ProfileService {
if (getRemoteDevice().equals(device)) {
switch (mState) {
case BluetoothMap.STATE_CONNECTED:
- if (mServerSession != null) {
- mServerSession.close();
- mServerSession = null;
- }
- if(mBluetoothMnsObexClient != null) {
- mBluetoothMnsObexClient.shutdown();
- mBluetoothMnsObexClient = null;
- }
- closeConnectionSocket();
-
- setState(BluetoothMap.STATE_DISCONNECTED, BluetoothMap.RESULT_CANCELED);
+ sendShutdownMessage();
result = true;
break;
default:
@@ -630,40 +541,328 @@ public class BluetoothMapService extends ProfileService {
filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY);
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
+ filter.addAction(ACTION_SHOW_MAPS_EMAIL_SETTINGS);
+ filter.addAction(USER_CONFIRM_TIMEOUT_ACTION);
+
+ // We need two filters, since Type only applies to the ACTION_MESSAGE_SENT
+ IntentFilter filterMessageSent = new IntentFilter();
+ filterMessageSent.addAction(BluetoothMapContentObserver.ACTION_MESSAGE_SENT);
+ try{
+ filterMessageSent.addDataType("message/*");
+ } catch (MalformedMimeTypeException e) {
+ Log.e(TAG, "Wrong mime type!!!", e);
+ }
+
try {
registerReceiver(mMapReceiver, filter);
+ registerReceiver(mMapReceiver, filterMessageSent);
} catch (Exception e) {
Log.w(TAG,"Unable to register map receiver",e);
}
- mInterrupted = false;
mAdapter = BluetoothAdapter.getDefaultAdapter();
+ mAppObserver = new BluetoothMapEmailAppObserver(this, this);
+
+ mEnabledAccounts = mAppObserver.getEnabledAccountItems();
+ // Uses mEnabledAccounts, hence getEnabledAccountItems() must be called before this.
+ createMasInstances();
+
// start RFCOMM listener
mSessionStatusHandler.sendMessage(mSessionStatusHandler
.obtainMessage(START_LISTENER));
+
return true;
}
+ /**
+ * Call this to trigger an update of the MAS instance list.
+ * No changes will be applied unless in disconnected state
+ */
+ public void updateMasInstances(int action) {
+ mSessionStatusHandler.obtainMessage (UPDATE_MAS_INSTANCES,
+ action, 0).sendToTarget();
+ }
+
+ /**
+ * Update the active MAS Instances according the difference between mEnabledDevices
+ * and the current list of accounts.
+ * Will only make changes if state is disconnected.
+ *
+ * How it works:
+ * 1) Build lists of account changes from last update of mEnabledAccounts.
+ * newAccounts - accounts that have been enabled since mEnabledAccounts
+ * was last updated.
+ * removedAccounts - Accounts that is on mEnabledAccounts, but no longer
+ * enabled.
+ * enabledAccounts - A new list of all enabled accounts.
+ * 2) Stop and remove all MasInstances on the remove list
+ * 3) Add and start MAS instances for accounts on the new list.
+ * Called at:
+ * - Each change in accounts
+ * - Each disconnect - before MasInstances restart.
+ *
+ * @return true is any changes are made, false otherwise.
+ */
+ private boolean updateMasInstancesHandler(){
+ if(DEBUG)Log.d(TAG,"updateMasInstancesHandler() state = " + getState());
+ boolean changed = false;
+
+ if(getState() == BluetoothMap.STATE_DISCONNECTED) {
+ ArrayList<BluetoothMapEmailSettingsItem> newAccountList = mAppObserver.getEnabledAccountItems();
+ ArrayList<BluetoothMapEmailSettingsItem> newAccounts = null;
+ ArrayList<BluetoothMapEmailSettingsItem> removedAccounts = null;
+ newAccounts = new ArrayList<BluetoothMapEmailSettingsItem>();
+ removedAccounts = mEnabledAccounts; // reuse the current enabled list, to track removed accounts
+ for(BluetoothMapEmailSettingsItem account: newAccountList) {
+ if(!removedAccounts.remove(account)) {
+ newAccounts.add(account);
+ }
+ }
+
+ if(removedAccounts != null) {
+ /* Remove all disabled/removed accounts */
+ for(BluetoothMapEmailSettingsItem account : removedAccounts) {
+ BluetoothMapMasInstance masInst = mMasInstanceMap.remove(account);
+ if(DEBUG)Log.d(TAG," Removing account: " + account + " masInst = " + masInst);
+ if(masInst != null) {
+ masInst.shutdown();
+ mMasInstances.remove(masInst.getMasId());
+ changed = true;
+ }
+ }
+ }
+
+ if(newAccounts != null) {
+ /* Add any newly created accounts */
+ for(BluetoothMapEmailSettingsItem account : newAccounts) {
+ if(DEBUG)Log.d(TAG," Adding account: " + account);
+ int masId = getNextMasId();
+ BluetoothMapMasInstance newInst =
+ new BluetoothMapMasInstance(this,
+ this,
+ account,
+ masId,
+ false);
+ mMasInstances.append(masId, newInst);
+ mMasInstanceMap.put(account, newInst);
+ changed = true;
+ /* Start the new instance */
+ if (mAdapter.isEnabled()) {
+ newInst.startRfcommSocketListener();
+ }
+ }
+ }
+ mEnabledAccounts = newAccountList;
+ if(VERBOSE) {
+ Log.d(TAG," Enabled accounts:");
+ for(BluetoothMapEmailSettingsItem account : mEnabledAccounts) {
+ Log.d(TAG, " " + account);
+ }
+ Log.d(TAG," Active MAS instances:");
+ for(int i=0, c=mMasInstances.size(); i < c; i++) {
+ BluetoothMapMasInstance masInst = mMasInstances.valueAt(i);
+ Log.d(TAG, " " + masInst);
+ }
+ }
+ mAccountChanged = false;
+ } else {
+ mAccountChanged = true;
+ }
+ return changed;
+ }
+
+ /**
+ * Will return the next MasId to use.
+ * Will ensure the key returned is greater than the largest key in use.
+ * Unless the key 255 is in use, in which case the first free masId
+ * will be returned.
+ * @return
+ */
+ private int getNextMasId() {
+ /* Find the largest masId in use */
+ int largestMasId = 0;
+ for(int i=0, c=mMasInstances.size(); i < c; i++) {
+ int masId = mMasInstances.keyAt(i);
+ if(masId > largestMasId) {
+ largestMasId = masId;
+ }
+ }
+ if(largestMasId < 0xff) {
+ return largestMasId + 1;
+ }
+ /* If 0xff is already in use, wrap and choose the first free
+ * MasId. */
+ for(int i = 1; i <= 0xff; i++) {
+ if(mMasInstances.get(i) == null) {
+ return i;
+ }
+ }
+ return 0xff; // This will never happen, as we only allow 10 e-mail accounts to be enabled
+ }
+
+ private void createMasInstances() {
+ int masId = MAS_ID_SMS_MMS;
+
+ // Add the SMS/MMS instance
+ BluetoothMapMasInstance smsMmsInst =
+ new BluetoothMapMasInstance(this,
+ this,
+ null,
+ masId,
+ true);
+ mMasInstances.append(masId, smsMmsInst);
+ mMasInstanceMap.put(null, smsMmsInst);
+
+ // get list of accounts already set to be visible through MAP
+ for(BluetoothMapEmailSettingsItem account : mEnabledAccounts) {
+ masId++; // SMS/MMS is masId=0, increment before adding next
+ BluetoothMapMasInstance newInst =
+ new BluetoothMapMasInstance(this,
+ this,
+ account,
+ masId,
+ false);
+ mMasInstances.append(masId, newInst);
+ mMasInstanceMap.put(account, newInst);
+ }
+ }
+
@Override
protected boolean stop() {
if (DEBUG) Log.d(TAG, "stop()");
try {
unregisterReceiver(mMapReceiver);
+ mAppObserver.shutdown();
} catch (Exception e) {
Log.w(TAG,"Unable to unregister map receiver",e);
}
setState(BluetoothMap.STATE_DISCONNECTED, BluetoothMap.RESULT_CANCELED);
- closeService();
+ sendShutdownMessage();
return true;
}
public boolean cleanup() {
if (DEBUG) Log.d(TAG, "cleanup()");
setState(BluetoothMap.STATE_DISCONNECTED, BluetoothMap.RESULT_CANCELED);
+ // TODO: Change to use message? - do we need to wait for completion?
closeService();
return true;
}
+ /**
+ * Called from each MAS instance when a connection is received.
+ * @param remoteDevice The device connecting
+ * @param masInst a reference to the calling MAS instance.
+ * @return
+ */
+ public boolean onConnect(BluetoothDevice remoteDevice, BluetoothMapMasInstance masInst) {
+
+ boolean sendIntent=false;
+ // As this can be called from each MasInstance, we need to lock access to member variables
+ synchronized(this) {
+ if(mRemoteDevice == null) {
+ mRemoteDevice = remoteDevice;
+ sRemoteDeviceName = mRemoteDevice.getName();
+ // In case getRemoteName failed and return null
+ if (TextUtils.isEmpty(sRemoteDeviceName)) {
+ sRemoteDeviceName = getString(R.string.defaultname);
+ }
+
+ if(mTrust == false) {
+ sendIntent = true;
+ mIsWaitingAuthorization = true;
+ setUserTimeoutAlarm();
+ }
+ } else if (!mRemoteDevice.equals(remoteDevice)) {
+ Log.w(TAG, "Unexpected connection from a second Remote Device received. name: " +
+ ((remoteDevice==null)?"unknown":remoteDevice.getName()));
+ return false; /* The connecting device is different from what is already
+ connected, reject the connection. */
+ } // Else second connection to same device, just continue
+ }
+
+
+ if(sendIntent == true) {
+ /* This will trigger */
+ Intent intent = new
+ Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST);
+ intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS);
+ intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
+ BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice);
+ sendBroadcast(intent, BLUETOOTH_ADMIN_PERM);
+
+ if (DEBUG) Log.d(TAG, "waiting for authorization for connection from: "
+ + sRemoteDeviceName);
+ //Queue USER_TIMEOUT to disconnect MAP OBEX session. If user doesn't
+ //accept or reject authorization request
+
+
+
+
+ } else {
+ /* Signal to the service that we have a incoming connection. */
+ sendConnectMessage(masInst.getMasId());
+ }
+ return true;
+ };
+
+
+ private void setUserTimeoutAlarm(){
+ if(DEBUG)Log.d(TAG,"SetUserTimeOutAlarm()");
+ if(mAlarmManager == null){
+ mAlarmManager =(AlarmManager) this.getSystemService (Context.ALARM_SERVICE);
+ }
+ mRemoveTimeoutMsg = true;
+ Intent timeoutIntent =
+ new Intent(USER_CONFIRM_TIMEOUT_ACTION);
+ PendingIntent pIntent = PendingIntent.getBroadcast(this, 0, timeoutIntent, 0);
+ mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+USER_CONFIRM_TIMEOUT_VALUE,pIntent);
+ }
+
+ private void cancelUserTimeoutAlarm(){
+ if(DEBUG)Log.d(TAG,"cancelUserTimeOutAlarm()");
+ Intent intent = new Intent(this, BluetoothMapService.class);
+ PendingIntent sender = PendingIntent.getBroadcast(this, 0, intent, 0);
+ AlarmManager alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
+ alarmManager.cancel(sender);
+ mRemoveTimeoutMsg = false;
+ }
+
+ private void sendConnectMessage(int masId) {
+ if(mSessionStatusHandler != null) {
+ Message msg = mSessionStatusHandler.obtainMessage(MSG_MAS_CONNECT, masId, 0);
+ msg.sendToTarget();
+ } // Can only be null during shutdown
+ }
+ private void sendConnectTimeoutMessage() {
+ if (DEBUG) Log.d(TAG, "sendConnectTimeoutMessage()");
+ if(mSessionStatusHandler != null) {
+ Message msg = mSessionStatusHandler.obtainMessage(USER_TIMEOUT);
+ msg.sendToTarget();
+ } // Can only be null during shutdown
+ }
+ private void sendConnectCancelMessage() {
+ if(mSessionStatusHandler != null) {
+ Message msg = mSessionStatusHandler.obtainMessage(MSG_MAS_CONNECT_CANCEL);
+ msg.sendToTarget();
+ } // Can only be null during shutdown
+ }
+
+ private void sendShutdownMessage() {
+ /* Any pending messages are no longer valid.
+ To speed up things, simply delete them. */
+ if (mRemoveTimeoutMsg) {
+ Intent timeoutIntent =
+ new Intent(USER_CONFIRM_TIMEOUT_ACTION);
+ sendBroadcast(timeoutIntent, BLUETOOTH_PERM);
+ mIsWaitingAuthorization = false;
+ cancelUserTimeoutAlarm();
+ }
+ mSessionStatusHandler.removeCallbacksAndMessages(null);
+ // Request release of all resources
+ mSessionStatusHandler.obtainMessage(SHUTDOWN).sendToTarget();
+ }
private MapBroadcastReceiver mMapReceiver = new MapBroadcastReceiver();
@@ -676,71 +875,78 @@ public class BluetoothMapService extends ProfileService {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR);
if (state == BluetoothAdapter.STATE_TURNING_OFF) {
- if (DEBUG) Log.d(TAG, "STATE_TURNING_OFF removeTimeoutMsg:" + removeTimeoutMsg);
- // Send any pending timeout now, as this service will be destroyed.
- if (removeTimeoutMsg) {
- mSessionStatusHandler.removeMessages(USER_TIMEOUT);
-
- Intent timeoutIntent =
- new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL);
- timeoutIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice);
- timeoutIntent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
- BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS);
- sendBroadcast(timeoutIntent, BLUETOOTH_PERM);
- isWaitingAuthorization = false;
- removeTimeoutMsg = false;
- stopObexServerSession();
- }
-
- // Release all resources
- closeService();
+ if (DEBUG) Log.d(TAG, "STATE_TURNING_OFF");
+ sendShutdownMessage();
} else if (state == BluetoothAdapter.STATE_ON) {
if (DEBUG) Log.d(TAG, "STATE_ON");
- mInterrupted = false;
// start RFCOMM listener
mSessionStatusHandler.sendMessage(mSessionStatusHandler
.obtainMessage(START_LISTENER));
}
+ }else if (action.equals(USER_CONFIRM_TIMEOUT_ACTION)){
+ if (DEBUG) Log.d(TAG, "USER_CONFIRM_TIMEOUT ACTION Received.");
+ // send us self a message about the timeout.
+ sendConnectTimeoutMessage();
} else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) {
int requestType = intent.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS);
if (DEBUG) Log.d(TAG, "Received ACTION_CONNECTION_ACCESS_REPLY:" +
- requestType + "isWaitingAuthorization:" + isWaitingAuthorization);
- if ((!isWaitingAuthorization) ||
+ requestType + "isWaitingAuthorization:" + mIsWaitingAuthorization);
+ if ((!mIsWaitingAuthorization) ||
(requestType != BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS)) {
// this reply is not for us
return;
}
- isWaitingAuthorization = false;
- if (removeTimeoutMsg) {
+ mIsWaitingAuthorization = false;
+ if (mRemoveTimeoutMsg) {
mSessionStatusHandler.removeMessages(USER_TIMEOUT);
- removeTimeoutMsg = false;
+ cancelUserTimeoutAlarm();
+ setState(BluetoothMap.STATE_DISCONNECTED);
}
if (intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT,
BluetoothDevice.CONNECTION_ACCESS_NO) ==
BluetoothDevice.CONNECTION_ACCESS_YES) {
- //bluetooth connection accepted by user
+ // Bluetooth connection accepted by user
if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) {
- boolean result = mRemoteDevice.setTrust(true);
- if (DEBUG) Log.d(TAG, "setTrust() result=" + result);
- }
- try {
- if (mConnSocket != null) {
- // start obex server and rfcomm connection
- startObexServerSession();
- } else {
- stopObexServerSession();
- }
- } catch (IOException ex) {
- Log.e(TAG, "Caught the error: " + ex.toString());
+ // Not implemented in BluetoothDevice
+ //boolean result = mRemoteDevice.setTrust(true);
+ //if (DEBUG) Log.d(TAG, "setTrust() result=" + result);
}
+ mTrust = true;
+ sendConnectMessage(-1); // -1 indicates all MAS instances
} else {
- stopObexServerSession();
+ // Auth. declined by user, serverSession should not be running, but
+ // call stop anyway to restart listener.
+ mTrust = false;
+ sendConnectCancelMessage();
+ }
+ } else if (action.equals(ACTION_SHOW_MAPS_EMAIL_SETTINGS)) {
+ Log.v(TAG, "Received ACTION_SHOW_MAPS_EMAIL_SETTINGS.");
+
+ Intent in = new Intent(context, BluetoothMapEmailSettings.class);
+ in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ context.startActivity(in);
+ } else if (action.equals(BluetoothMapContentObserver.ACTION_MESSAGE_SENT)) {
+ BluetoothMapMasInstance masInst = null;
+ int result = getResultCode();
+ boolean handled = false;
+ if(mMasInstances != null && (masInst = mMasInstances.get(MAS_ID_SMS_MMS)) != null) {
+ intent.putExtra(BluetoothMapContentObserver.EXTRA_MESSAGE_SENT_RESULT, result);
+ if(masInst.handleSmsSendIntent(context, intent)) {
+ // The intent was handled by the mas instance it self
+ handled = true;
+ }
+ }
+ if(handled == false)
+ {
+ /* We do not have a connection to a device, hence we need to move
+ the SMS to the correct folder. */
+ BluetoothMapContentObserver.actionMessageSentDisconnected(context, intent, result);
}
} else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED) &&
- isWaitingAuthorization) {
+ mIsWaitingAuthorization) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (mRemoteDevice == null || device == null) {
@@ -750,7 +956,7 @@ public class BluetoothMapService extends ProfileService {
if (DEBUG) Log.d(TAG,"ACL disconnected for "+ device);
- if (mRemoteDevice.equals(device) && removeTimeoutMsg) {
+ if (mRemoteDevice.equals(device) && mRemoveTimeoutMsg) {
// Send any pending timeout now, as ACL got disconnected.
mSessionStatusHandler.removeMessages(USER_TIMEOUT);
@@ -760,9 +966,9 @@ public class BluetoothMapService extends ProfileService {
timeoutIntent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS);
sendBroadcast(timeoutIntent, BLUETOOTH_PERM);
- isWaitingAuthorization = false;
- removeTimeoutMsg = false;
- stopObexServerSession();
+ mIsWaitingAuthorization = false;
+ mRemoveTimeoutMsg = false;
+
}
}
}
@@ -868,5 +1074,5 @@ public class BluetoothMapService extends ProfileService {
if (service == null) return BluetoothProfile.PRIORITY_UNDEFINED;
return service.getPriority(device);
}
- };
+ }
}
diff --git a/src/com/android/bluetooth/map/BluetoothMapSmsPdu.java b/src/com/android/bluetooth/map/BluetoothMapSmsPdu.java
index 2070c2693..5b7b8db04 100644
--- a/src/com/android/bluetooth/map/BluetoothMapSmsPdu.java
+++ b/src/com/android/bluetooth/map/BluetoothMapSmsPdu.java
@@ -52,7 +52,7 @@ public class BluetoothMapSmsPdu {
public static int SMS_TYPE_CDMA = 2;
- /* TODO: We need to handle the SC-address mentioned in errata 4335.
+ /* We need to handle the SC-address mentioned in errata 4335.
* Since the definition could be read in three different ways, I have asked
* the car working group for clarification, and are awaiting confirmation that
* this clarification will go into the MAP spec:
@@ -64,25 +64,25 @@ public class BluetoothMapSmsPdu {
public static class SmsPdu {
- private byte[] data;
- private byte[] scAddress = {0}; // At the moment we do not use the scAddress, hence set the length to 0.
- private int userDataMsgOffset = 0;
- private int encoding;
- private int languageTable;
- private int languageShiftTable;
- private int type;
+ private byte[] mData;
+ private byte[] mScAddress = {0}; // At the moment we do not use the scAddress, hence set the length to 0.
+ private int mUserDataMsgOffset = 0;
+ private int mEncoding;
+ private int mLanguageTable;
+ private int mLanguageShiftTable;
+ private int mType;
/* Members used for pdu decoding */
- private int userDataSeptetPadding = INVALID_VALUE;
- private int msgSeptetCount = 0;
+ private int mUserDataSeptetPadding = INVALID_VALUE;
+ private int mMsgSeptetCount = 0;
SmsPdu(byte[] data, int type){
- this.data = data;
- this.encoding = INVALID_VALUE;
- this.type = type;
- this.languageTable = INVALID_VALUE;
- this.languageShiftTable = INVALID_VALUE;
- this.userDataMsgOffset = gsmSubmitGetTpUdOffset(); // Assume no user data header
+ this.mData = data;
+ this.mEncoding = INVALID_VALUE;
+ this.mType = type;
+ this.mLanguageTable = INVALID_VALUE;
+ this.mLanguageShiftTable = INVALID_VALUE;
+ this.mUserDataMsgOffset = gsmSubmitGetTpUdOffset(); // Assume no user data header
}
/**
@@ -93,48 +93,48 @@ public class BluetoothMapSmsPdu {
* @param languageTable
*/
SmsPdu(byte[]data, int encoding, int type, int languageTable){
- this.data = data;
- this.encoding = encoding;
- this.type = type;
- this.languageTable = languageTable;
+ this.mData = data;
+ this.mEncoding = encoding;
+ this.mType = type;
+ this.mLanguageTable = languageTable;
}
public byte[] getData(){
- return data;
+ return mData;
}
public byte[] getScAddress(){
- return scAddress;
+ return mScAddress;
}
public void setEncoding(int encoding) {
- this.encoding = encoding;
+ this.mEncoding = encoding;
}
public int getEncoding(){
- return encoding;
+ return mEncoding;
}
public int getType(){
- return type;
+ return mType;
}
public int getUserDataMsgOffset() {
- return userDataMsgOffset;
+ return mUserDataMsgOffset;
}
/** The user data message payload size in bytes - excluding the user data header. */
public int getUserDataMsgSize() {
- return data.length - userDataMsgOffset;
+ return mData.length - mUserDataMsgOffset;
}
public int getLanguageShiftTable() {
- return languageShiftTable;
+ return mLanguageShiftTable;
}
public int getLanguageTable() {
- return languageTable;
+ return mLanguageTable;
}
public int getUserDataSeptetPadding() {
- return userDataSeptetPadding;
+ return mUserDataSeptetPadding;
}
public int getMsgSeptetCount() {
- return msgSeptetCount;
+ return mMsgSeptetCount;
}
@@ -157,7 +157,7 @@ public class BluetoothMapSmsPdu {
* parameter length, and offset + 2 is the first byte of the parameter data.
*/
private int cdmaGetParameterOffset(byte parameterId) {
- ByteArrayInputStream pdu = new ByteArrayInputStream(data);
+ ByteArrayInputStream pdu = new ByteArrayInputStream(mData);
int offset = 0;
boolean found = false;
@@ -191,7 +191,7 @@ public class BluetoothMapSmsPdu {
private final static byte BEARER_DATA_MSG_ID = 0x00;
private int cdmaGetSubParameterOffset(byte subParameterId) {
- ByteArrayInputStream pdu = new ByteArrayInputStream(data);
+ ByteArrayInputStream pdu = new ByteArrayInputStream(mData);
int offset = 0;
boolean found = false;
offset = cdmaGetParameterOffset(BEARER_DATA) + 2; // Add to offset the BEARER_DATA parameter id and length bytes
@@ -230,28 +230,39 @@ public class BluetoothMapSmsPdu {
* - A time stamp is not mandatory.
*/
int offset;
+ if(mData == null) {
+ throw new IllegalArgumentException("Unable to convert PDU to Deliver type");
+ }
offset = cdmaGetParameterOffset(DESTINATION_ADDRESS);
- data[offset] = ORIGINATING_ADDRESS;
+ if(mData.length < offset) {
+ throw new IllegalArgumentException("Unable to convert PDU to Deliver type");
+ }
+ mData[offset] = ORIGINATING_ADDRESS;
+
offset = cdmaGetParameterOffset(DESTINATION_SUB_ADDRESS);
- data[offset] = ORIGINATING_SUB_ADDRESS;
+ if(mData.length < offset) {
+ throw new IllegalArgumentException("Unable to convert PDU to Deliver type");
+ }
+ mData[offset] = ORIGINATING_SUB_ADDRESS;
offset = cdmaGetSubParameterOffset(BEARER_DATA_MSG_ID);
-// if(data != null && data.length > 2) {
- int tmp = data[offset+2] & 0xff; // Skip the subParam ID and length, and read the first byte.
+ if(mData.length > (2+offset)) {
+ int tmp = mData[offset+2] & 0xff; // Skip the subParam ID and length, and read the first byte.
// Mask out the type
tmp &= 0x0f;
// Set the new type
tmp |= ((BearerData.MESSAGE_TYPE_DELIVER << 4) & 0xf0);
// Store the result
- data[offset+2] = (byte) tmp;
+ mData[offset+2] = (byte) tmp;
-// }
+ } else {
+ throw new IllegalArgumentException("Unable to convert PDU to Deliver type");
+ }
/* TODO: Do we need to change anything in the user data? Not sure if the user data is
* just encoded using GSM encoding, or it is an actual GSM submit PDU embedded
* in the user data?
*/
-
}
private static final byte TP_MIT_DELIVER = 0x00; // bit 0 and 1
@@ -264,18 +275,18 @@ public class BluetoothMapSmsPdu {
/* calculate the offset to TP_PID.
* The TP-DA has variable length, and the length excludes the 2 byte length and type headers.
* The TP-DA is two bytes within the PDU */
- int offset = 2 + ((data[2]+1) & 0xff)/2 + 2; // data[2] is the number of semi-octets in the phone number (ceil result)
- if((offset > data.length) || (offset > (2 + 12))) // max length of TP_DA is 12 bytes + two byte offset.
+ int offset = 2 + ((mData[2]+1) & 0xff)/2 + 2; // data[2] is the number of semi-octets in the phone number (ceil result)
+ if((offset > mData.length) || (offset > (2 + 12))) // max length of TP_DA is 12 bytes + two byte offset.
throw new IllegalArgumentException("wrongly formatted gsm submit PDU. offset = " + offset);
return offset;
}
public int gsmSubmitGetTpDcs() {
- return data[gsmSubmitGetTpDcsOffset()] & 0xff;
+ return mData[gsmSubmitGetTpDcsOffset()] & 0xff;
}
public boolean gsmSubmitHasUserDataHeader() {
- return ((data[0] & 0xff) & TP_UDHI_MASK) == TP_UDHI_MASK;
+ return ((mData[0] & 0xff) & TP_UDHI_MASK) == TP_UDHI_MASK;
}
private int gsmSubmitGetTpDcsOffset() {
@@ -283,7 +294,7 @@ public class BluetoothMapSmsPdu {
}
private int gsmSubmitGetTpUdlOffset() {
- switch(((data[0] & 0xff) & (0x08 | 0x04))>>2) {
+ switch(((mData[0] & 0xff) & (0x08 | 0x04))>>2) {
case 0: // Not TP-VP present
return gsmSubmitGetTpPidOffset() + 2;
case 1: // TP-VP relative format
@@ -299,7 +310,7 @@ public class BluetoothMapSmsPdu {
}
public void gsmDecodeUserDataHeader() {
- ByteArrayInputStream pdu = new ByteArrayInputStream(data);
+ ByteArrayInputStream pdu = new ByteArrayInputStream(mData);
pdu.skip(gsmSubmitGetTpUdlOffset());
int userDataLength = pdu.read();
@@ -307,7 +318,7 @@ public class BluetoothMapSmsPdu {
int userDataHeaderLength = pdu.read();
// This part is only needed to extract the language info, hence only needed for 7 bit encoding
- if(encoding == SmsConstants.ENCODING_7BIT)
+ if(mEncoding == SmsConstants.ENCODING_7BIT)
{
byte[] udh = new byte[userDataHeaderLength];
try {
@@ -316,30 +327,30 @@ public class BluetoothMapSmsPdu {
Log.w(TAG, "unable to read userDataHeader", e);
}
SmsHeader userDataHeader = SmsHeader.fromByteArray(udh);
- languageTable = userDataHeader.languageTable;
- languageShiftTable = userDataHeader.languageShiftTable;
+ mLanguageTable = userDataHeader.languageTable;
+ mLanguageShiftTable = userDataHeader.languageShiftTable;
int headerBits = (userDataHeaderLength + 1) * 8;
int headerSeptets = headerBits / 7;
headerSeptets += (headerBits % 7) > 0 ? 1 : 0;
- userDataSeptetPadding = (headerSeptets * 7) - headerBits;
- msgSeptetCount = userDataLength - headerSeptets;
+ mUserDataSeptetPadding = (headerSeptets * 7) - headerBits;
+ mMsgSeptetCount = userDataLength - headerSeptets;
}
- userDataMsgOffset = gsmSubmitGetTpUdOffset() + userDataHeaderLength + 1; // Add the byte containing the length
+ mUserDataMsgOffset = gsmSubmitGetTpUdOffset() + userDataHeaderLength + 1; // Add the byte containing the length
}
else
{
- userDataSeptetPadding = 0;
- msgSeptetCount = userDataLength;
- userDataMsgOffset = gsmSubmitGetTpUdOffset();
+ mUserDataSeptetPadding = 0;
+ mMsgSeptetCount = userDataLength;
+ mUserDataMsgOffset = gsmSubmitGetTpUdOffset();
}
if(V) {
- Log.v(TAG, "encoding:" + encoding);
- Log.v(TAG, "msgSeptetCount:" + msgSeptetCount);
- Log.v(TAG, "userDataSeptetPadding:" + userDataSeptetPadding);
- Log.v(TAG, "languageShiftTable:" + languageShiftTable);
- Log.v(TAG, "languageTable:" + languageTable);
- Log.v(TAG, "userDataMsgOffset:" + userDataMsgOffset);
+ Log.v(TAG, "encoding:" + mEncoding);
+ Log.v(TAG, "msgSeptetCount:" + mMsgSeptetCount);
+ Log.v(TAG, "userDataSeptetPadding:" + mUserDataSeptetPadding);
+ Log.v(TAG, "languageShiftTable:" + mLanguageShiftTable);
+ Log.v(TAG, "languageTable:" + mLanguageTable);
+ Log.v(TAG, "userDataMsgOffset:" + mUserDataMsgOffset);
}
}
@@ -393,7 +404,7 @@ public class BluetoothMapSmsPdu {
int userDataLength = 0;
try {
newPdu.write(TP_MIT_DELIVER | TP_MMS_NO_MORE | TP_RP_NO_REPLY_PATH | TP_SRI_NO_REPORT
- | (data[0] & 0xff) & TP_UDHI_MASK);
+ | (mData[0] & 0xff) & TP_UDHI_MASK);
encodedAddress = PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength(originator);
if(encodedAddress != null) {
int padding = (encodedAddress[encodedAddress.length-1] & 0xf0) == 0xf0 ? 1 : 0;
@@ -405,29 +416,29 @@ public class BluetoothMapSmsPdu {
newPdu.write(0x81); /* International type */
}
- newPdu.write(data[gsmSubmitGetTpPidOffset()]);
- newPdu.write(data[gsmSubmitGetTpDcsOffset()]);
+ newPdu.write(mData[gsmSubmitGetTpPidOffset()]);
+ newPdu.write(mData[gsmSubmitGetTpDcsOffset()]);
// Generate service center time stamp
gsmWriteDate(newPdu, date);
- userDataLength = (data[gsmSubmitGetTpUdlOffset()] & 0xff);
+ userDataLength = (mData[gsmSubmitGetTpUdlOffset()] & 0xff);
newPdu.write(userDataLength);
// Copy the pdu user data - keep in mind that the userDataLength is not the length in bytes for 7-bit encoding.
- newPdu.write(data, gsmSubmitGetTpUdOffset(), data.length - gsmSubmitGetTpUdOffset());
+ newPdu.write(mData, gsmSubmitGetTpUdOffset(), mData.length - gsmSubmitGetTpUdOffset());
} catch (IOException e) {
Log.e(TAG, "", e);
throw new IllegalArgumentException("Failed to change type to deliver PDU.");
}
- data = newPdu.toByteArray();
+ mData = newPdu.toByteArray();
}
/* SMS encoding to bmessage strings */
/** get the encoding type as a bMessage string */
public String getEncodingString(){
- if(type == SMS_TYPE_GSM)
+ if(mType == SMS_TYPE_GSM)
{
- switch(encoding){
+ switch(mEncoding){
case SmsMessage.ENCODING_7BIT:
- if(languageTable == 0)
+ if(mLanguageTable == 0)
return "G-7BIT";
else
return "G-7BITEXT";
@@ -440,7 +451,7 @@ public class BluetoothMapSmsPdu {
return "";
}
} else /* SMS_TYPE_CDMA */ {
- switch(encoding){
+ switch(mEncoding){
case SmsMessage.ENCODING_7BIT:
return "C-7ASCII";
case SmsMessage.ENCODING_8BIT:
@@ -583,13 +594,6 @@ public class BluetoothMapSmsPdu {
return deliverPdus;
}
- public static void testSendRawPdu(SmsPdu pdu){
- if(pdu.getType() == SMS_TYPE_CDMA){
- /* TODO: Try to send the message using SmsManager.sendData()?*/
- }else {
-
- }
- }
/**
* The decoding only supports decoding the actual textual content of the PDU received
@@ -706,8 +710,6 @@ public class BluetoothMapSmsPdu {
+ (dataCodingScheme & 0xff));
}
- /* TODO: This is NOT good design - to have the pdu class being depending on these two function calls.
- * - move the encoding extraction into the pdu class */
pdu.setEncoding(encodingType);
pdu.gsmDecodeUserDataHeader();
diff --git a/src/com/android/bluetooth/map/BluetoothMapUtils.java b/src/com/android/bluetooth/map/BluetoothMapUtils.java
index e57cf16b1..d0b7e2de8 100644
--- a/src/com/android/bluetooth/map/BluetoothMapUtils.java
+++ b/src/com/android/bluetooth/map/BluetoothMapUtils.java
@@ -14,6 +14,8 @@
*/
package com.android.bluetooth.map;
+import android.util.Log;
+
/**
* Various utility methods and generic defines that can be used throughout MAPS
@@ -21,16 +23,18 @@ package com.android.bluetooth.map;
public class BluetoothMapUtils {
private static final String TAG = "MapUtils";
+ private static final boolean D = BluetoothMapService.DEBUG;
private static final boolean V = BluetoothMapService.VERBOSE;
- /* We use the upper 5 bits for the type mask - avoid using the top bit, since it
- * indicates a negative value, hence corrupting the formatter when converting to
- * type String. (I really miss the unsigned type in Java:))
+ /* We use the upper 4 bits for the type mask.
+ * TODO: When more types are needed, consider just using a number
+ * in stead of a bit to indicate the message type. Then 4
+ * bit can be use for 16 different message types.
*/
- private static final long HANDLE_TYPE_MASK = 0xf<<59;
- private static final long HANDLE_TYPE_MMS_MASK = 0x1<<59;
- private static final long HANDLE_TYPE_EMAIL_MASK = 0x2<<59;
- private static final long HANDLE_TYPE_SMS_GSM_MASK = 0x4<<59;
- private static final long HANDLE_TYPE_SMS_CDMA_MASK = 0x8<<59;
+ private static final long HANDLE_TYPE_MASK = (((long)0xf)<<60);
+ private static final long HANDLE_TYPE_MMS_MASK = (((long)0x1)<<60);
+ private static final long HANDLE_TYPE_EMAIL_MASK = (((long)0x2)<<60);
+ private static final long HANDLE_TYPE_SMS_GSM_MASK = (((long)0x4)<<60);
+ private static final long HANDLE_TYPE_SMS_CDMA_MASK = (((long)0x8)<<60);
/**
* This enum is used to convert from the bMessage type property to a type safe
@@ -43,27 +47,46 @@ public class BluetoothMapUtils {
MMS
}
+ public static String getLongAsString(long v) {
+ char[] result = new char[16];
+ int v1 = (int) (v & 0xffffffff);
+ int v2 = (int) ((v>>32) & 0xffffffff);
+ int c;
+ for (int i = 0; i < 8; i++) {
+ c = v2 & 0x0f;
+ c += (c < 10) ? '0' : ('A'-10);
+ result[7 - i] = (char) c;
+ v2 >>= 4;
+ c = v1 & 0x0f;
+ c += (c < 10) ? '0' : ('A'-10);
+ result[15 - i] = (char)c;
+ v1 >>= 4;
+ }
+ return new String(result);
+ }
+
/**
* Convert a Content Provider handle and a Messagetype into a unique handle
* @param cpHandle content provider handle
* @param messageType message type (TYPE_MMS/TYPE_SMS_GSM/TYPE_SMS_CDMA/TYPE_EMAIL)
* @return String Formatted Map Handle
*/
- static public String getMapHandle(long cpHandle, TYPE messageType){
+ public static String getMapHandle(long cpHandle, TYPE messageType){
String mapHandle = "-1";
switch(messageType)
{
+
case MMS:
- mapHandle = String.format("%016X",(cpHandle | HANDLE_TYPE_MMS_MASK));
+ mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_MMS_MASK);
break;
case SMS_GSM:
- mapHandle = String.format("%016X",cpHandle | HANDLE_TYPE_SMS_GSM_MASK);
+ mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_SMS_GSM_MASK);
break;
case SMS_CDMA:
- mapHandle = String.format("%016X",cpHandle | HANDLE_TYPE_SMS_CDMA_MASK);
+ mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_SMS_CDMA_MASK);
break;
case EMAIL:
- mapHandle = String.format("%016X",(cpHandle | HANDLE_TYPE_EMAIL_MASK)); //TODO correct when email support is implemented
+ mapHandle = getLongAsString(cpHandle | HANDLE_TYPE_EMAIL_MASK);
break;
default:
throw new IllegalArgumentException("Message type not supported");
@@ -88,8 +111,11 @@ public class BluetoothMapUtils {
static public long getCpHandle(String mapHandle)
{
long cpHandle = getMsgHandleAsLong(mapHandle);
+ if(D)Log.d(TAG,"-> MAP handle:"+mapHandle);
/* remove masks as the call should already know what type of message this handle is for */
cpHandle &= ~HANDLE_TYPE_MASK;
+ if(D)Log.d(TAG,"->CP handle:"+cpHandle);
+
return cpHandle;
}
diff --git a/src/com/android/bluetooth/map/BluetoothMapbMessage.java b/src/com/android/bluetooth/map/BluetoothMapbMessage.java
index de3de663e..54b72cbb2 100644
--- a/src/com/android/bluetooth/map/BluetoothMapbMessage.java
+++ b/src/com/android/bluetooth/map/BluetoothMapbMessage.java
@@ -33,42 +33,41 @@ import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
public abstract class BluetoothMapbMessage {
protected static String TAG = "BluetoothMapbMessage";
- protected static final boolean D = false;
- protected static final boolean V = false;
+ protected static final boolean D = BluetoothMapService.DEBUG;
+ protected static final boolean V = BluetoothMapService.VERBOSE;
+
private static final String VERSION = "VERSION:1.0";
public static int INVALID_VALUE = -1;
- protected int appParamCharset = BluetoothMapAppParams.INVALID_VALUE_PARAMETER;
-
- // TODO: Reevaluate if strings are the best types for the members.
+ protected int mAppParamCharset = BluetoothMapAppParams.INVALID_VALUE_PARAMETER;
/* BMSG attributes */
- private String status = null; // READ/UNREAD
- protected TYPE type = null; // SMS/MMS/EMAIL
+ private String mStatus = null; // READ/UNREAD
+ protected TYPE mType = null; // SMS/MMS/EMAIL
- private String folder = null;
+ private String mFolder = null;
/* BBODY attributes */
- private long partId = INVALID_VALUE;
- protected String encoding = null;
- protected String charset = null;
- private String language = null;
+ private long mPartId = INVALID_VALUE;
+ protected String mEncoding = null;
+ protected String mCharset = null;
+ private String mLanguage = null;
- private int bMsgLength = INVALID_VALUE;
+ private int mBMsgLength = INVALID_VALUE;
- private ArrayList<vCard> originator = null;
- private ArrayList<vCard> recipient = null;
+ private ArrayList<vCard> mOriginator = null;
+ private ArrayList<vCard> mRecipient = null;
public static class vCard {
/* VCARD attributes */
- private String version;
- private String name = null;
- private String formattedName = null;
- private String[] phoneNumbers = {};
- private String[] emailAddresses = {};
- private int envLevel = 0;
+ private String mVersion;
+ private String mName = null;
+ private String mFormattedName = null;
+ private String[] mPhoneNumbers = {};
+ private String[] mEmailAddresses = {};
+ private int mEnvLevel = 0;
/**
* Construct a version 3.0 vCard
@@ -80,13 +79,13 @@ public abstract class BluetoothMapbMessage {
*/
public vCard(String name, String formattedName, String[] phoneNumbers,
String[] emailAddresses, int envLevel) {
- this.envLevel = envLevel;
- this.version = "3.0";
- this.name = name != null ? name : "";
- this.formattedName = formattedName != null ? formattedName : "";
+ this.mEnvLevel = envLevel;
+ this.mVersion = "3.0";
+ this.mName = name != null ? name : "";
+ this.mFormattedName = formattedName != null ? formattedName : "";
setPhoneNumbers(phoneNumbers);
if (emailAddresses != null)
- this.emailAddresses = emailAddresses;
+ this.mEmailAddresses = emailAddresses;
}
/**
@@ -98,12 +97,12 @@ public abstract class BluetoothMapbMessage {
*/
public vCard(String name, String[] phoneNumbers,
String[] emailAddresses, int envLevel) {
- this.envLevel = envLevel;
- this.version = "2.1";
- this.name = name != null ? name : "";
+ this.mEnvLevel = envLevel;
+ this.mVersion = "2.1";
+ this.mName = name != null ? name : "";
setPhoneNumbers(phoneNumbers);
if (emailAddresses != null)
- this.emailAddresses = emailAddresses;
+ this.mEmailAddresses = emailAddresses;
}
/**
@@ -114,12 +113,12 @@ public abstract class BluetoothMapbMessage {
* @param emailAddresses a String[] of email addresses
*/
public vCard(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses) {
- this.version = "3.0";
- this.name = name != null ? name : "";
- this.formattedName = formattedName != null ? formattedName : "";
+ this.mVersion = "3.0";
+ this.mName = (name != null) ? name : "";
+ this.mFormattedName = (formattedName != null) ? formattedName : "";
setPhoneNumbers(phoneNumbers);
if (emailAddresses != null)
- this.emailAddresses = emailAddresses;
+ this.mEmailAddresses = emailAddresses;
}
/**
@@ -129,49 +128,69 @@ public abstract class BluetoothMapbMessage {
* @param emailAddresses a String[] of email addresses
*/
public vCard(String name, String[] phoneNumbers, String[] emailAddresses) {
- this.version = "2.1";
- this.name = name != null ? name : "";
+ this.mVersion = "2.1";
+ this.mName = name != null ? name : "";
setPhoneNumbers(phoneNumbers);
if (emailAddresses != null)
- this.emailAddresses = emailAddresses;
+ this.mEmailAddresses = emailAddresses;
}
private void setPhoneNumbers(String[] numbers) {
- if(numbers != null && numbers.length > 0)
- {
- phoneNumbers = new String[numbers.length];
+ if(numbers != null && numbers.length > 0) {
+ mPhoneNumbers = new String[numbers.length];
for(int i = 0, n = numbers.length; i < n; i++){
- phoneNumbers[i] = PhoneNumberUtils.extractNetworkPortion(numbers[i]);
+ String networkNumber = PhoneNumberUtils.extractNetworkPortion(numbers[i]);
+ /* extractNetworkPortion can return N if the number is a service "number" = a string
+ * with the a name in (i.e. "Some-Tele-company" would return N because of the N in compaNy)
+ * Hence we need to check if the number is actually a string with alpha chars.
+ * */
+ Boolean alpha = PhoneNumberUtils.stripSeparators(numbers[i]).matches("[0-9]*[a-zA-Z]+[0-9]*");
+ if(networkNumber != null && networkNumber.length() > 1 && !alpha) {
+ mPhoneNumbers[i] = networkNumber;
+ } else {
+ mPhoneNumbers[i] = numbers[i];
+ }
}
}
}
public String getFirstPhoneNumber() {
- if(phoneNumbers.length > 0) {
- return phoneNumbers[0];
+ if(mPhoneNumbers.length > 0) {
+ return mPhoneNumbers[0];
} else
- throw new IllegalArgumentException("No Phone number");
+ return null;
}
public int getEnvLevel() {
- return envLevel;
+ return mEnvLevel;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getFirstEmail() {
+ if(mEmailAddresses.length > 0) {
+ return mEmailAddresses[0];
+ } else
+ return null;
}
public void encode(StringBuilder sb)
{
sb.append("BEGIN:VCARD").append("\r\n");
- sb.append("VERSION:").append(version).append("\r\n");
- if(version.equals("3.0") && formattedName != null)
+ sb.append("VERSION:").append(mVersion).append("\r\n");
+ if(mVersion.equals("3.0") && mFormattedName != null)
{
- sb.append("FN:").append(formattedName).append("\r\n");
+ sb.append("FN:").append(mFormattedName).append("\r\n");
}
- if (name != null)
- sb.append("N:").append(name).append("\r\n");
- for(String phoneNumber : phoneNumbers)
+ if (mName != null)
+ sb.append("N:").append(mName).append("\r\n");
+ for(String phoneNumber : mPhoneNumbers)
{
sb.append("TEL:").append(phoneNumber).append("\r\n");
}
- for(String emailAddress : emailAddresses)
+ for(String emailAddress : mEmailAddresses)
{
sb.append("EMAIL:").append(emailAddress).append("\r\n");
}
@@ -181,7 +200,7 @@ public abstract class BluetoothMapbMessage {
/**
* Parse a vCard from a BMgsReader, where a line containing "BEGIN:VCARD" have just been read.
* @param reader
- * @param originator
+ * @param mOriginator
* @return
*/
public static vCard parseVcard(BMsgReader reader, int envLevel) {
@@ -252,6 +271,7 @@ public abstract class BluetoothMapbMessage {
* as the Bluetooth MAP spec. illustrates vCards using tab alignment, hence actually
* showing an invalid vCard format...
* If we read such a folded line, the folded part will be skipped in the parser
+ * UPDATE: Check if we actually do unfold before parsing the input stream
*/
ByteArrayOutputStream output = new ByteArrayOutputStream();
@@ -478,8 +498,10 @@ public abstract class BluetoothMapbMessage {
newBMsg = new BluetoothMapbMessageSms();
break;
case MMS:
+ newBMsg = new BluetoothMapbMessageMms();
+ break;
case EMAIL:
- newBMsg = new BluetoothMapbMessageMmsEmail();
+ newBMsg = new BluetoothMapbMessageEmail();
break;
default:
break;
@@ -500,7 +522,7 @@ public abstract class BluetoothMapbMessage {
if(newBMsg == null)
throw new IllegalArgumentException("Missing bMessage TYPE: - unable to parse body-content");
newBMsg.setType(type);
- newBMsg.appParamCharset = appParamCharset;
+ newBMsg.mAppParamCharset = appParamCharset;
if(folder != null)
newBMsg.setCompleteFolder(folder);
if(statusFound)
@@ -519,6 +541,8 @@ public abstract class BluetoothMapbMessage {
/* TODO: Do we need to validate the END:* tags? They are only needed if someone puts additional info
* below the END:MSG - in which case we don't handle it.
+ * We need to parse the message based on the length field, to ensure MAP 1.0 compatibility,
+ * since this spec. do not suggest to escape the end-tag if it occurs inside the message text.
*/
try {
@@ -537,9 +561,9 @@ public abstract class BluetoothMapbMessage {
while(line.contains("BEGIN:VCARD")){
if(D) Log.d(TAG,"Decoding recipient vCard level " + level);
- if(recipient == null)
- recipient = new ArrayList<vCard>(1);
- recipient.add(vCard.parseVcard(reader, level));
+ if(mRecipient == null)
+ mRecipient = new ArrayList<vCard>(1);
+ mRecipient.add(vCard.parseVcard(reader, level));
line = reader.getLineEnforce();
}
if(line.contains("BEGIN:BENV")) {
@@ -560,7 +584,7 @@ public abstract class BluetoothMapbMessage {
String arg[] = line.split(":");
if (arg != null && arg.length == 2) {
try {
- partId = Long.parseLong(arg[1].trim());
+ mPartId = Long.parseLong(arg[1].trim());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Wrong value in 'PARTID': " + arg[1]);
}
@@ -571,7 +595,8 @@ public abstract class BluetoothMapbMessage {
else if(line.contains("ENCODING:")) {
String arg[] = line.split(":");
if (arg != null && arg.length == 2) {
- encoding = arg[1].trim(); // TODO: Validate ?
+ mEncoding = arg[1].trim();
+ // If needed validation will be done when the value is used
} else {
throw new IllegalArgumentException("Missing value for 'ENCODING': " + line);
}
@@ -579,7 +604,8 @@ public abstract class BluetoothMapbMessage {
else if(line.contains("CHARSET:")) {
String arg[] = line.split(":");
if (arg != null && arg.length == 2) {
- charset = arg[1].trim(); // TODO: Validate ?
+ mCharset = arg[1].trim();
+ // If needed validation will be done when the value is used
} else {
throw new IllegalArgumentException("Missing value for 'CHARSET': " + line);
}
@@ -587,7 +613,8 @@ public abstract class BluetoothMapbMessage {
else if(line.contains("LANGUAGE:")) {
String arg[] = line.split(":");
if (arg != null && arg.length == 2) {
- language = arg[1].trim(); // TODO: Validate ?
+ mLanguage = arg[1].trim();
+ // If needed validation will be done when the value is used
} else {
throw new IllegalArgumentException("Missing value for 'LANGUAGE': " + line);
}
@@ -596,7 +623,7 @@ public abstract class BluetoothMapbMessage {
String arg[] = line.split(":");
if (arg != null && arg.length == 2) {
try {
- bMsgLength = Integer.parseInt(arg[1].trim());
+ mBMsgLength = Integer.parseInt(arg[1].trim());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Wrong value in 'LENGTH': " + arg[1]);
}
@@ -605,21 +632,23 @@ public abstract class BluetoothMapbMessage {
}
}
else if(line.contains("BEGIN:MSG")) {
- if(bMsgLength == INVALID_VALUE)
- throw new IllegalArgumentException("Missing value for 'LENGTH'. Unable to read remaining part of the message");
- // For SMS: Encoding of MSG is always UTF-8 compliant, regardless of any properties, since PDUs are encodes as hex-strings
+ if(mBMsgLength == INVALID_VALUE)
+ throw new IllegalArgumentException("Missing value for 'LENGTH'. " +
+ "Unable to read remaining part of the message");
+ /* For SMS: Encoding of MSG is always UTF-8 compliant, regardless of any properties,
+ since PDUs are encodes as hex-strings */
/* PTS has a bug regarding the message length, and sets it 2 bytes too short, hence
* using the length field to determine the amount of data to read, might not be the
* best solution.
* Since errata ???(bluetooth.org is down at the moment) introduced escaping of END:MSG
* in the actual message content, it is now safe to use the END:MSG tag as terminator,
* and simply ignore the length field.*/
- byte[] rawData = reader.getDataBytes(bMsgLength - (line.getBytes().length + 2)); // 2 added to compensate for the removed \r\n
+ byte[] rawData = reader.getDataBytes(mBMsgLength - (line.getBytes().length + 2)); // 2 added to compensate for the removed \r\n
String data;
try {
data = new String(rawData, "UTF-8");
if(V) {
- Log.v(TAG,"MsgLength: " + bMsgLength);
+ Log.v(TAG,"MsgLength: " + mBMsgLength);
Log.v(TAG,"line.getBytes().length: " + line.getBytes().length);
String debug = line.replaceAll("\\n", "<LF>\n");
debug = debug.replaceAll("\\r", "<CR>");
@@ -666,47 +695,47 @@ public abstract class BluetoothMapbMessage {
public void setStatus(boolean read) {
if(read)
- this.status = "READ";
+ this.mStatus = "READ";
else
- this.status = "UNREAD";
+ this.mStatus = "UNREAD";
}
public void setType(TYPE type) {
- this.type = type;
+ this.mType = type;
}
/**
* @return the type
*/
public TYPE getType() {
- return type;
+ return mType;
}
public void setCompleteFolder(String folder) {
- this.folder = folder;
+ this.mFolder = folder;
}
public void setFolder(String folder) {
- this.folder = "telecom/msg/" + folder;
+ this.mFolder = "telecom/msg/" + folder;
}
public String getFolder() {
- return folder;
+ return mFolder;
}
public void setEncoding(String encoding) {
- this.encoding = encoding;
+ this.mEncoding = encoding;
}
public ArrayList<vCard> getOriginators() {
- return originator;
+ return mOriginator;
}
public void addOriginator(vCard originator) {
- if(this.originator == null)
- this.originator = new ArrayList<vCard>();
- this.originator.add(originator);
+ if(this.mOriginator == null)
+ this.mOriginator = new ArrayList<vCard>();
+ this.mOriginator.add(originator);
}
/**
@@ -717,9 +746,9 @@ public abstract class BluetoothMapbMessage {
* @param emailAddresses
*/
public void addOriginator(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses) {
- if(originator == null)
- originator = new ArrayList<vCard>();
- originator.add(new vCard(name, formattedName, phoneNumbers, emailAddresses));
+ if(mOriginator == null)
+ mOriginator = new ArrayList<vCard>();
+ mOriginator.add(new vCard(name, formattedName, phoneNumbers, emailAddresses));
}
/** Add a version 2.1 vCard with only a name.
@@ -729,31 +758,31 @@ public abstract class BluetoothMapbMessage {
* @param emailAddresses
*/
public void addOriginator(String name, String[] phoneNumbers, String[] emailAddresses) {
- if(originator == null)
- originator = new ArrayList<vCard>();
- originator.add(new vCard(name, phoneNumbers, emailAddresses));
+ if(mOriginator == null)
+ mOriginator = new ArrayList<vCard>();
+ mOriginator.add(new vCard(name, phoneNumbers, emailAddresses));
}
public ArrayList<vCard> getRecipients() {
- return recipient;
+ return mRecipient;
}
public void setRecipient(vCard recipient) {
- if(this.recipient == null)
- this.recipient = new ArrayList<vCard>();
- this.recipient.add(recipient);
+ if(this.mRecipient == null)
+ this.mRecipient = new ArrayList<vCard>();
+ this.mRecipient.add(recipient);
}
public void addRecipient(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses) {
- if(recipient == null)
- recipient = new ArrayList<vCard>();
- recipient.add(new vCard(name, formattedName, phoneNumbers, emailAddresses));
+ if(mRecipient == null)
+ mRecipient = new ArrayList<vCard>();
+ mRecipient.add(new vCard(name, formattedName, phoneNumbers, emailAddresses));
}
public void addRecipient(String name, String[] phoneNumbers, String[] emailAddresses) {
- if(recipient == null)
- recipient = new ArrayList<vCard>();
- recipient.add(new vCard(name, phoneNumbers, emailAddresses));
+ if(mRecipient == null)
+ mRecipient = new ArrayList<vCard>();
+ mRecipient.add(new vCard(name, phoneNumbers, emailAddresses));
}
/**
@@ -811,30 +840,32 @@ public abstract class BluetoothMapbMessage {
byte[] msgStart, msgEnd;
sb.append("BEGIN:BMSG").append("\r\n");
sb.append(VERSION).append("\r\n");
- sb.append("STATUS:").append(status).append("\r\n");
- sb.append("TYPE:").append(type.name()).append("\r\n");
- if(folder.length() > 512)
- sb.append("FOLDER:").append(folder.substring(folder.length()-512, folder.length())).append("\r\n");
+ sb.append("STATUS:").append(mStatus).append("\r\n");
+ sb.append("TYPE:").append(mType.name()).append("\r\n");
+ if(mFolder.length() > 512)
+ sb.append("FOLDER:").append(mFolder.substring(mFolder.length()-512, mFolder.length())).append("\r\n");
else
- sb.append("FOLDER:").append(folder).append("\r\n");
- if(originator != null){
- for(vCard element : originator)
+ sb.append("FOLDER:").append(mFolder).append("\r\n");
+ if(mOriginator != null){
+ for(vCard element : mOriginator)
element.encode(sb);
}
- /* TODO: Do we need the three levels of env? - e.g. for e-mail. - we do have a level in the
- * vCards that could be used to determine the the levels of the envelope.
+ /* If we need the three levels of env. at some point - we do have a level in the
+ * vCards that could be used to determine the levels of the envelope.
*/
sb.append("BEGIN:BENV").append("\r\n");
- if(recipient != null){
- for(vCard element : recipient)
+ if(mRecipient != null){
+ for(vCard element : mRecipient) {
+ if(V) Log.v(TAG, "encodeGeneric: recipient email" + element.getFirstEmail());
element.encode(sb);
+ }
}
sb.append("BEGIN:BBODY").append("\r\n");
- if(encoding != null && encoding != "")
- sb.append("ENCODING:").append(encoding).append("\r\n");
- if(charset != null && charset != "")
- sb.append("CHARSET:").append(charset).append("\r\n");
+ if(mEncoding != null && mEncoding != "")
+ sb.append("ENCODING:").append(mEncoding).append("\r\n");
+ if(mCharset != null && mCharset != "")
+ sb.append("CHARSET:").append(mCharset).append("\r\n");
int length = 0;
diff --git a/src/com/android/bluetooth/map/BluetoothMapbMessageEmail.java b/src/com/android/bluetooth/map/BluetoothMapbMessageEmail.java
new file mode 100644
index 000000000..d329d45bf
--- /dev/null
+++ b/src/com/android/bluetooth/map/BluetoothMapbMessageEmail.java
@@ -0,0 +1,79 @@
+/*
+* Copyright (C) 2013 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.map;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+import java.util.UUID;
+
+import com.android.bluetooth.map.BluetoothMapSmsPdu.SmsPdu;
+
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import android.util.Base64;
+import android.util.Log;
+
+
+public class BluetoothMapbMessageEmail extends BluetoothMapbMessage {
+
+ private String mEmailBody = null;
+
+ public void setEmailBody(String emailBody) {
+ this.mEmailBody = emailBody;
+ this.mCharset = "UTF-8";
+ this.mEncoding = "8bit";
+ }
+
+ public String getEmailBody() {
+ return mEmailBody;
+ }
+
+ public void parseMsgPart(String msgPart) {
+ if (mEmailBody == null)
+ mEmailBody = msgPart;
+ else
+ mEmailBody += msgPart;
+ }
+
+ /**
+ * Set initial values before parsing - will be called is a message body is found
+ * during parsing.
+ */
+ public void parseMsgInit() {
+ // Not used for e-mail
+ }
+
+ public byte[] encode() throws UnsupportedEncodingException
+ {
+ ArrayList<byte[]> bodyFragments = new ArrayList<byte[]>();
+
+ /* Store the messages in an ArrayList to be able to handle the different message types in a generic way.
+ * We use byte[] since we need to extract the length in bytes. */
+ if(mEmailBody != null) {
+ String tmpBody = mEmailBody.replaceAll("END:MSG", "/END\\:MSG"); // Replace any occurrences of END:MSG with \END:MSG
+ bodyFragments.add(tmpBody.getBytes("UTF-8"));
+ } else {
+ Log.e(TAG, "Email has no body - this should not be possible");
+ bodyFragments.add(new byte[0]); // An empty message - this should not be possible
+ }
+ return encodeGeneric(bodyFragments);
+ }
+
+}
diff --git a/src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java b/src/com/android/bluetooth/map/BluetoothMapbMessageMms.java
index 0fcba3bbe..8c9a39d0a 100644
--- a/src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java
+++ b/src/com/android/bluetooth/map/BluetoothMapbMessageMms.java
@@ -16,6 +16,7 @@ package com.android.bluetooth.map;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -28,48 +29,76 @@ import android.text.util.Rfc822Tokenizer;
import android.util.Base64;
import android.util.Log;
-public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
+public class BluetoothMapbMessageMms extends BluetoothMapbMessage {
public static class MimePart {
- public long _id = INVALID_VALUE; /* The _id from the content provider, can be used to sort the parts if needed */
- public String contentType = null; /* The mime type, e.g. text/plain */
- public String contentId = null;
- public String contentLocation = null;
- public String contentDisposition = null;
- public String partName = null; /* e.g. text_1.txt*/
- public String charsetName = null; /* This seems to be a number e.g. 106 for UTF-8 CharacterSets
+ public long mId = INVALID_VALUE; /* The _id from the content provider, can be used to sort the parts if needed */
+ public String mContentType = null; /* The mime type, e.g. text/plain */
+ public String mContentId = null;
+ public String mContentLocation = null;
+ public String mContentDisposition = null;
+ public String mPartName = null; /* e.g. text_1.txt*/
+ public String mCharsetName = null; /* This seems to be a number e.g. 106 for UTF-8 CharacterSets
holds a method for the mapping. */
- public String fileName = null; /* Do not seem to be used */
- public byte[] data = null; /* The raw un-encoded data e.g. the raw jpeg data or the text.getBytes("utf-8") */
+ public String mFileName = null; /* Do not seem to be used */
+ public byte[] mData = null; /* The raw un-encoded data e.g. the raw jpeg data or the text.getBytes("utf-8") */
+ String getDataAsString() {
+ String result = null;
+ String charset = mCharsetName;
+ // Figure out if we support the charset, else fall back to UTF-8, as this is what
+ // the MAP specification suggest to use, and is compatible with US-ASCII.
+ if(charset == null){
+ charset = "UTF-8";
+ } else {
+ charset = charset.toUpperCase();
+ try {
+ if(Charset.isSupported(charset) == false) {
+ charset = "UTF-8";
+ }
+ } catch (IllegalCharsetNameException e) {
+ Log.w(TAG, "Received unknown charset: " + charset + " - using UTF-8.");
+ charset = "UTF-8";
+ }
+ }
+ try{
+ result = new String(mData, charset);
+ } catch (UnsupportedEncodingException e) {
+ /* This cannot happen unless Charset.isSupported() is out of sync with String */
+ try{
+ result = new String(mData, "UTF-8");
+ } catch (UnsupportedEncodingException e2) {/* This cannot happen */}
+ }
+ return result;
+ }
public void encode(StringBuilder sb, String boundaryTag, boolean last) throws UnsupportedEncodingException {
sb.append("--").append(boundaryTag).append("\r\n");
- if(contentType != null)
- sb.append("Content-Type: ").append(contentType);
- if(charsetName != null)
- sb.append("; ").append("charset=\"").append(charsetName).append("\"");
+ if(mContentType != null)
+ sb.append("Content-Type: ").append(mContentType);
+ if(mCharsetName != null)
+ sb.append("; ").append("charset=\"").append(mCharsetName).append("\"");
sb.append("\r\n");
- if(contentLocation != null)
- sb.append("Content-Location: ").append(contentLocation).append("\r\n");
- if(contentId != null)
- sb.append("Content-ID: ").append(contentId).append("\r\n");
- if(contentDisposition != null)
- sb.append("Content-Disposition: ").append(contentDisposition).append("\r\n");
- if(data != null) {
+ if(mContentLocation != null)
+ sb.append("Content-Location: ").append(mContentLocation).append("\r\n");
+ if(mContentId != null)
+ sb.append("Content-ID: ").append(mContentId).append("\r\n");
+ if(mContentDisposition != null)
+ sb.append("Content-Disposition: ").append(mContentDisposition).append("\r\n");
+ if(mData != null) {
/* TODO: If errata 4176 is adopted in the current form (it is not in either 1.1 or 1.2),
- the below is not allowed, Base64 should be used for text. */
+ the below use of UTF-8 is not allowed, Base64 should be used for text. */
- if(contentType != null &&
- (contentType.toUpperCase().contains("TEXT") ||
- contentType.toUpperCase().contains("SMIL") )) {
+ if(mContentType != null &&
+ (mContentType.toUpperCase().contains("TEXT") ||
+ mContentType.toUpperCase().contains("SMIL") )) {
sb.append("Content-Transfer-Encoding: 8BIT\r\n\r\n"); // Add the header split empty line
- sb.append(new String(data,"UTF-8")).append("\r\n");
+ sb.append(new String(mData,"UTF-8")).append("\r\n");
}
else {
sb.append("Content-Transfer-Encoding: Base64\r\n\r\n"); // Add the header split empty line
- sb.append(Base64.encodeToString(data, Base64.DEFAULT)).append("\r\n");
+ sb.append(Base64.encodeToString(mData, Base64.DEFAULT)).append("\r\n");
}
}
if(last) {
@@ -78,14 +107,14 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
}
public void encodePlainText(StringBuilder sb) throws UnsupportedEncodingException {
- if(contentType != null && contentType.toUpperCase().contains("TEXT")) {
- sb.append(new String(data,"UTF-8")).append("\r\n");
- } else if(contentType != null && contentType.toUpperCase().contains("/SMIL")) {
+ if(mContentType != null && mContentType.toUpperCase().contains("TEXT")) {
+ sb.append(new String(mData,"UTF-8")).append("\r\n");
+ } else if(mContentType != null && mContentType.toUpperCase().contains("/SMIL")) {
/* Skip the smil.xml, as no-one knows what it is. */
} else {
/* Not a text part, just print the filename or part name if they exist. */
- if(partName != null)
- sb.append("<").append(partName).append(">\r\n");
+ if(mPartName != null)
+ sb.append("<").append(mPartName).append(">\r\n");
else
sb.append("<").append("attachment").append(">\r\n");
}
@@ -111,7 +140,8 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
private String getBoundary() {
if(boundary == null)
- boundary = "----" + UUID.randomUUID();
+ // Include "=_" as these cannot occur in quoted printable text
+ boundary = "--=_" + UUID.randomUUID();
return boundary;
}
@@ -122,9 +152,23 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
return parts;
}
+ public String getMessageAsText() {
+ StringBuilder sb = new StringBuilder();
+ if(subject != null && !subject.isEmpty()) {
+ sb.append("<Sub:").append(subject).append("> ");
+ }
+ if(parts != null) {
+ for(MimePart part : parts) {
+ if(part.mContentType.toUpperCase().contains("TEXT")) {
+ sb.append(new String(part.mData));
+ }
+ }
+ }
+ return sb.toString();
+ }
public MimePart addMimePart() {
if(parts == null)
- parts = new ArrayList<BluetoothMapbMessageMmsEmail.MimePart>();
+ parts = new ArrayList<BluetoothMapbMessageMms.MimePart>();
MimePart newPart = new MimePart();
parts.add(newPart);
return newPart;
@@ -237,19 +281,24 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
return includeAttachments;
}
public void updateCharset() {
- charset = null;
- for(MimePart part : parts) {
- if(part.contentType != null &&
- part.contentType.toUpperCase().contains("TEXT")) {
- charset = "UTF-8";
- break;
+ if(parts != null) {
+ mCharset = null;
+ for(MimePart part : parts) {
+ if(part.mContentType != null &&
+ part.mContentType.toUpperCase().contains("TEXT")) {
+ mCharset = "UTF-8";
+ if(V) Log.v(TAG,"Charset set to UTF-8");
+ break;
+ }
}
}
}
public int getSize() {
int message_size = 0;
- for(MimePart part : parts) {
- message_size += part.data.length;
+ if(parts != null) {
+ for(MimePart part : parts) {
+ message_size += part.mData.length;
+ }
}
return message_size;
}
@@ -299,9 +348,9 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
sb.append("Date: ").append(getDateString()).append("\r\n");
/* According to RFC-2822 headers must use US-ASCII, where the MAP specification states
* UTF-8 should be used for the entire <bmessage-body-content>. We let the MAP specification
- * take precedence above the RFC-2822. The code to
+ * take precedence above the RFC-2822.
*/
- /* If we are to use US-ASCII anyway, here are the code for it.
+ /* If we are to use US-ASCII anyway, here is the code for it for base64.
if (subject != null){
// Use base64 encoding for the subject, as it may contain non US-ASCII characters or other
// illegal (RFC822 header), and android do not seem to have encoders/decoders for quoted-printables
@@ -311,13 +360,14 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
}*/
if (subject != null)
sb.append("Subject: ").append(subject).append("\r\n");
+ if(from == null)
+ sb.append("From: \r\n");
if(from != null)
encodeHeaderAddresses(sb, "From: ", from); // This includes folding if needed.
if(sender != null)
encodeHeaderAddresses(sb, "Sender: ", sender); // This includes folding if needed.
/* For MMS one recipient(to, cc or bcc) must exists, if none: 'To: undisclosed-
* recipients:;' could be used.
- * TODO: Is this a valid solution for E-Mail?
*/
if(to == null && cc == null && bcc == null)
sb.append("To: undisclosed-recipients:;\r\n");
@@ -383,14 +433,16 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
encoding = "8BIT"; // The encoding used
encodeHeaders(sb);
- if(getIncludeAttachments() == false) {
- for(MimePart part : parts) {
- part.encodePlainText(sb); /* We call encode on all parts, to include a tag, where an attachment is missing. */
- }
- } else {
- for(MimePart part : parts) {
- count++;
- part.encode(sb, getBoundary(), (count == parts.size()));
+ if(parts != null) {
+ if(getIncludeAttachments() == false) {
+ for(MimePart part : parts) {
+ part.encodePlainText(sb); /* We call encode on all parts, to include a tag, where an attachment is missing. */
+ }
+ } else {
+ for(MimePart part : parts) {
+ count++;
+ part.encode(sb, getBoundary(), (count == parts.size()));
+ }
}
}
@@ -415,12 +467,13 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
*/
private String parseMmsHeaders(String hdrPart) {
String[] headers = hdrPart.split("\r\n");
+ if(D) Log.d(TAG,"Header count=" + headers.length);
String header;
hasHeaders = false;
for(int i = 0, c = headers.length; i < c; i++) {
header = headers[i];
-
+ if(D) Log.d(TAG,"Header[" + i + "]: " + header);
/* We need to figure out if any headers are present, in cases where devices do not follow the e-mail RFCs.
* Skip empty lines, and then parse headers until a non-header line is found, at which point we treat the
* remaining as plain text.
@@ -441,39 +494,34 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
String headerValue = headerParts[1].trim();
// Address headers
- /* TODO: If this is empty, the MSE needs to fill it in before sending the message.
- * This happens when sending the MMS, not sure what happens for e-mail.
+ /* If this is empty, the MSE needs to fill it in before sending the message.
+ * This happens when sending the MMS.
*/
if(headerType.contains("FROM")) {
Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
from = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
- }
- else if(headerType.contains("TO")) {
+ } else if(headerType.contains("TO")) {
Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
to = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
- }
- else if(headerType.contains("CC")) {
+ } else if(headerType.contains("CC")) {
Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
cc = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
- }
- else if(headerType.contains("BCC")) {
+ } else if(headerType.contains("BCC")) {
Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
bcc = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
- }
- else if(headerType.contains("REPLY-TO")) {
+ } else if(headerType.contains("REPLY-TO")) {
Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
replyTo = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
- }// Other headers
- else if(headerType.contains("SUBJECT")) {
+ } else if(headerType.contains("SUBJECT")) { // Other headers
subject = headerValue;
- }
- else if(headerType.contains("MESSAGE-ID")) {
+ } else if(headerType.contains("MESSAGE-ID")) {
messageId = headerValue;
- }
- else if(headerType.contains("DATE")) {
- /* TODO: Do we need the date? */
- }
- else if(headerType.contains("CONTENT-TYPE")) {
+ } else if(headerType.contains("DATE")) {
+ /* The date is not needed, as the time stamp will be set in the DB
+ * when the message is send. */
+ } else if(headerType.contains("MIME-VERSION")) {
+ /* The mime version is not needed */
+ } else if(headerType.contains("CONTENT-TYPE")) {
String[] contentTypeParts = headerValue.split(";");
contentType = contentTypeParts[0];
// Extract the boundary if it exists
@@ -481,14 +529,17 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
{
if(contentTypeParts[j].contains("boundary")) {
boundary = contentTypeParts[j].split("boundary[\\s]*=", 2)[1].trim();
- boundary = boundary.replaceAll("\"", ""); // " is allowed around a boundary, but is not allowed as part of the boundary
+ // removing quotes from boundary string
+ if ((boundary.charAt(0) == '\"') && (boundary.charAt(boundary.length()-1) == '\"'))
+ boundary = boundary.substring(1, boundary.length()-1);
+ if(D) Log.d(TAG,"Boundary tag=" + boundary);
+ } else if(contentTypeParts[j].contains("charset")) {
+ mCharset = contentTypeParts[j].split("charset[\\s]*=", 2)[1].trim();
}
}
- }
- else if(headerType.contains("CONTENT-TRANSFER-ENCODING")) {
+ } else if(headerType.contains("CONTENT-TRANSFER-ENCODING")) {
encoding = headerValue;
- }
- else {
+ } else {
if(D) Log.w(TAG,"Skipping unknown header: " + headerType + " (" + header + ")");
}
}
@@ -497,71 +548,88 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
private void parseMmsMimePart(String partStr) {
String[] parts = partStr.split("\r\n\r\n", 2); // Split the header from the body
- String body;
MimePart newPart = addMimePart();
String partEncoding = encoding; /* Use the overall encoding as default */
+ String body;
+
+ String[] headers = parts[0].split("\r\n");
+ if(D) Log.d(TAG, "parseMmsMimePart: headers count=" + headers.length);
+
if(parts.length != 2) {
body = partStr;
} else {
- body = parts[1];
- String[] headers = parts[0].split("\r\n");
-
for(String header : headers) {
- if(header.length() == 0)
+ // Skip empty lines(the \r\n after the boundary tag) and endBoundary tags
+ if((header.length() == 0) || (header.trim().isEmpty()) || header.trim().equals("--"))
continue;
- if(header.trim() == "" || header.trim().equals("--")) // Skip empty lines(the \r\n after the boundary tag) and endBoundary tags
- continue;
String[] headerParts = header.split(":",2);
if(headerParts.length != 2) {
- //throw new IllegalArgumentException("part-Header not formatted correctly: " + header);
- // If we find a part without headers, treat the entire content as body
- body = partStr;
- break;
+ if(D) Log.w(TAG, "part-Header not formatted correctly: ");
+ continue;
}
+ if(D) Log.d(TAG, "parseMmsMimePart: header=" + header);
String headerType = headerParts[0].toUpperCase();
String headerValue = headerParts[1].trim();
if(headerType.contains("CONTENT-TYPE")) {
- // TODO: extract charset? Only UTF-8 is allowed for TEXT typed parts
- newPart.contentType = headerValue;
- Log.d(TAG, "*** CONTENT-TYPE: " + newPart.contentType);
+ String[] contentTypeParts = headerValue.split(";");
+ newPart.mContentType = contentTypeParts[0];
+ // Extract the boundary if it exists
+ for(int j=1, n=contentTypeParts.length; j<n; j++)
+ {
+ String value = contentTypeParts[j].toLowerCase();
+ if(value.contains("charset")) {
+ newPart.mCharsetName = value.split("charset[\\s]*=", 2)[1].trim();
+ }
+ }
}
else if(headerType.contains("CONTENT-LOCATION")) {
- // This is used if the smil refers to a file name in its src=
- newPart.contentLocation = headerValue;
- newPart.partName = headerValue;
+ // This is used if the smil refers to a file name in its src
+ newPart.mContentLocation = headerValue;
+ newPart.mPartName = headerValue;
}
else if(headerType.contains("CONTENT-TRANSFER-ENCODING")) {
partEncoding = headerValue;
}
else if(headerType.contains("CONTENT-ID")) {
- // This is used if the smil refers to a cid:<xxx> in it's src=
- newPart.contentId = headerValue;
+ // This is used if the smil refers to a cid:<xxx> in it's src
+ newPart.mContentId = headerValue;
}
else if(headerType.contains("CONTENT-DISPOSITION")) {
- // This is used if the smil refers to a cid:<xxx> in it's src=
- newPart.contentDisposition = headerValue;
+ // This is used if the smil refers to a cid:<xxx> in it's src
+ newPart.mContentDisposition = headerValue;
}
else {
if(D) Log.w(TAG,"Skipping unknown part-header: " + headerType + " (" + header + ")");
}
}
+ body = parts[1];
+ if(body.length() > 2) {
+ if(body.charAt(body.length()-2) == '\r'
+ && body.charAt(body.length()-2) == '\n') {
+ body = body.substring(0, body.length()-2);
+ }
+ }
}
// Now for the body
- newPart.data = decodeBody(body, partEncoding);
+ newPart.mData = decodeBody(body, partEncoding, newPart.mCharsetName);
}
private void parseMmsMimeBody(String body) {
MimePart newPart = addMimePart();
- newPart.data = decodeBody(body, encoding);
+ newPart.mCharsetName = mCharset;
+ newPart.mData = decodeBody(body, encoding, mCharset);
}
- private byte[] decodeBody(String body, String encoding) {
+ private byte[] decodeBody(String body, String encoding, String charset) {
if(encoding != null && encoding.toUpperCase().contains("BASE64")) {
return Base64.decode(body, Base64.DEFAULT);
- } else {
+ } else if(encoding != null && encoding.toUpperCase().contains("QUOTED-PRINTABLE")) {
+ return quotedPrintableToUtf8(body, charset);
+ }else{
// TODO: handle other encoding types? - here we simply store the string data as bytes
try {
+
return body.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
// This will never happen, as UTF-8 is mandatory on Android platforms
@@ -594,8 +662,8 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
// If we have some text not being a header, add it to the message body.
if(remaining != null) {
messageBody = remaining + messageParts[1];
- }
- else {
+ if(D) Log.d(TAG, "parseMms remaining=" + remaining );
+ } else {
messageBody = messageParts[1];
}
}
@@ -607,17 +675,113 @@ public class BluetoothMapbMessageMmsEmail extends BluetoothMapbMessage {
setTextOnly(true);
if(contentType == null)
contentType = "text/plain";
- parts.get(0).contentType = contentType;
+ parts.get(0).mContentType = contentType;
}
else
{
mimeParts = messageBody.split("--" + boundary);
+ if(D) Log.d(TAG, "mimePart count=" + mimeParts.length);
+ // Part 0 is the message to clients not capable of decoding MIME
for(int i = 1; i < mimeParts.length - 1; i++) {
String part = mimeParts[i];
if (part != null && (part.length() > 0))
parseMmsMimePart(part);
+ }
+ }
+ }
+
+ /**
+ * Convert a quoted-printable encoded string to a UTF-8 string:
+ * - Remove any soft line breaks: "=<CRLF>"
+ * - Convert all "=xx" to the corresponding byte
+ * @param text quoted-printable encoded UTF-8 text
+ * @return decoded UTF-8 string
+ */
+ public static byte[] quotedPrintableToUtf8(String text, String charset) {
+ byte[] output = new byte[text.length()]; // We allocate for the worst case memory need
+ byte[] input = null;
+ try {
+ input = text.getBytes("US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ /* This cannot happen as "US-ASCII" is supported for all Java implementations */ }
+
+ if(input == null){
+ return "".getBytes();
+ }
+
+ int in, out, stopCnt = input.length-2; // Leave room for peaking the next two bytes
+
+ /* Algorithm:
+ * - Search for token, copying all non token chars
+ * */
+ for(in=0, out=0; in < stopCnt; in++){
+ byte b0 = input[in];
+ if(b0 == '=') {
+ byte b1 = input[++in];
+ byte b2 = input[++in];
+ if(b1 == '\r' && b2 == '\n') {
+ continue; // soft line break, remove all tree;
+ }
+ if(((b1 >= '0' && b1 <= '9') || (b1 >= 'A' && b1 <= 'F') || (b1 >= 'a' && b1 <= 'f')) &&
+ ((b2 >= '0' && b2 <= '9') || (b2 >= 'A' && b2 <= 'F') || (b2 >= 'a' && b2 <= 'f'))) {
+ if(V)Log.v(TAG, "Found hex number: " + String.format("%c%c", b1, b2));
+ if(b1 <= '9') b1 = (byte) (b1 - '0');
+ else if (b1 <= 'F') b1 = (byte) (b1 - 'A' + 10);
+ else if (b1 <= 'f') b1 = (byte) (b1 - 'a' + 10);
+
+ if(b2 <= '9') b2 = (byte) (b2 - '0');
+ else if (b2 <= 'F') b2 = (byte) (b2 - 'A' + 10);
+ else if (b2 <= 'f') b2 = (byte) (b2 - 'a' + 10);
+
+ if(V)Log.v(TAG, "Resulting nibble values: " + String.format("b1=%x b2=%x", b1, b2));
+
+ output[out++] = (byte)(b1<<4 | b2); // valid hex char, append
+ if(V)Log.v(TAG, "Resulting value: " + String.format("0x%2x", output[out-1]));
+ continue;
+ }
+ Log.w(TAG, "Received wrongly quoted printable encoded text. Continuing at best effort...");
+ /* If we get a '=' without either a hex value or CRLF following, just add it and
+ * rewind the in counter. */
+ output[out++] = b0;
+ in -= 2;
+ continue;
+ } else {
+ output[out++] = b0;
+ continue;
+ }
+ }
+
+ // Just add any remaining characters. If they contain any encoding, it is invalid,
+ // and best effort would be just to display the characters.
+ while (in < input.length) {
+ output[out++] = input[in++];
+ }
+
+ String result = null;
+ // Figure out if we support the charset, else fall back to UTF-8, as this is what
+ // the MAP specification suggest to use, and is compatible with US-ASCII.
+ if(charset == null){
+ charset = "UTF-8";
+ } else {
+ charset = charset.toUpperCase();
+ try {
+ if(Charset.isSupported(charset) == false) {
+ charset = "UTF-8";
+ }
+ } catch (IllegalCharsetNameException e) {
+ Log.w(TAG, "Received unknown charset: " + charset + " - using UTF-8.");
+ charset = "UTF-8";
+ }
}
+ try{
+ result = new String(output, 0, out, charset);
+ } catch (UnsupportedEncodingException e) {
+ /* This cannot happen unless Charset.isSupported() is out of sync with String */
+ try{
+ result = new String(output, 0, out, "UTF-8");
+ } catch (UnsupportedEncodingException e2) {/* This cannot happen */}
}
+ return result.getBytes(); /* return the result as "UTF-8" bytes */
}
/* Notes on SMIL decoding (from http://tools.ietf.org/html/rfc2557):
diff --git a/src/com/android/bluetooth/map/BluetoothMapbMessageSms.java b/src/com/android/bluetooth/map/BluetoothMapbMessageSms.java
index 8107bd8fe..9f57f60f4 100644
--- a/src/com/android/bluetooth/map/BluetoothMapbMessageSms.java
+++ b/src/com/android/bluetooth/map/BluetoothMapbMessageSms.java
@@ -24,29 +24,29 @@ import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
public class BluetoothMapbMessageSms extends BluetoothMapbMessage {
- private ArrayList<SmsPdu> smsBodyPdus = null;
- private String smsBody = null;
+ private ArrayList<SmsPdu> mSmsBodyPdus = null;
+ private String mSmsBody = null;
public void setSmsBodyPdus(ArrayList<SmsPdu> smsBodyPdus) {
- this.smsBodyPdus = smsBodyPdus;
- this.charset = null;
+ this.mSmsBodyPdus = smsBodyPdus;
+ this.mCharset = null;
if(smsBodyPdus.size() > 0)
- this.encoding = smsBodyPdus.get(0).getEncodingString();
+ this.mEncoding = smsBodyPdus.get(0).getEncodingString();
}
public String getSmsBody() {
- return smsBody;
+ return mSmsBody;
}
public void setSmsBody(String smsBody) {
- this.smsBody = smsBody;
- this.charset = "UTF-8";
- this.encoding = null;
+ this.mSmsBody = smsBody;
+ this.mCharset = "UTF-8";
+ this.mEncoding = null;
}
@Override
public void parseMsgPart(String msgPart) {
- if(appParamCharset == BluetoothMapAppParams.CHARSET_NATIVE) {
+ if(mAppParamCharset == BluetoothMapAppParams.CHARSET_NATIVE) {
if(D) Log.d(TAG, "Decoding \"" + msgPart + "\" as native PDU");
byte[] msgBytes = decodeBinary(msgPart);
if(msgBytes.length > 0 &&
@@ -56,16 +56,16 @@ public class BluetoothMapbMessageSms extends BluetoothMapbMessage {
throw new IllegalArgumentException("Only submit PDUs are supported");
}
- smsBody += BluetoothMapSmsPdu.decodePdu(msgBytes,
- type == TYPE.SMS_CDMA ? BluetoothMapSmsPdu.SMS_TYPE_CDMA
+ mSmsBody += BluetoothMapSmsPdu.decodePdu(msgBytes,
+ mType == TYPE.SMS_CDMA ? BluetoothMapSmsPdu.SMS_TYPE_CDMA
: BluetoothMapSmsPdu.SMS_TYPE_GSM);
} else {
- smsBody += msgPart;
+ mSmsBody += msgPart;
}
}
@Override
public void parseMsgInit() {
- smsBody = "";
+ mSmsBody = "";
}
public byte[] encode() throws UnsupportedEncodingException
@@ -75,16 +75,16 @@ public class BluetoothMapbMessageSms extends BluetoothMapbMessage {
/* Store the messages in an ArrayList to be able to handle the different message types in a generic way.
* We use byte[] since we need to extract the length in bytes.
*/
- if(smsBody != null) {
- String tmpBody = smsBody.replaceAll("END:MSG", "/END\\:MSG"); // Replace any occurrences of END:MSG with \END:MSG
+ if(mSmsBody != null) {
+ String tmpBody = mSmsBody.replaceAll("END:MSG", "/END\\:MSG"); // Replace any occurrences of END:MSG with \END:MSG
bodyFragments.add(tmpBody.getBytes("UTF-8"));
- }else if (smsBodyPdus != null && smsBodyPdus.size() > 0) {
- for (SmsPdu pdu : smsBodyPdus) {
+ }else if (mSmsBodyPdus != null && mSmsBodyPdus.size() > 0) {
+ for (SmsPdu pdu : mSmsBodyPdus) {
// This cannot(must not) contain END:MSG
bodyFragments.add(encodeBinary(pdu.getData(),pdu.getScAddress()).getBytes("UTF-8"));
}
} else {
- bodyFragments.add(new byte[0]); // TODO: Is this allowed? (An empty message)
+ bodyFragments.add(new byte[0]); // An empty message - no text
}
return encodeGeneric(bodyFragments);
diff --git a/src/com/android/bluetooth/map/BluetoothMnsObexClient.java b/src/com/android/bluetooth/map/BluetoothMnsObexClient.java
index 6518a00de..1e2c1839a 100644
--- a/src/com/android/bluetooth/map/BluetoothMnsObexClient.java
+++ b/src/com/android/bluetooth/map/BluetoothMnsObexClient.java
@@ -1,5 +1,5 @@
/*
-* Copyright (C) 2013 Samsung System LSI
+* Copyright (C) 2014 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
@@ -16,21 +16,17 @@ package com.android.bluetooth.map;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
-import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelUuid;
import android.util.Log;
+import android.util.SparseBooleanArray;
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
-import javax.obex.ApplicationParameter;
import javax.obex.ClientOperation;
import javax.obex.ClientSession;
import javax.obex.HeaderSet;
@@ -47,53 +43,48 @@ import javax.obex.ResponseCodes;
public class BluetoothMnsObexClient {
private static final String TAG = "BluetoothMnsObexClient";
- private static final boolean D = false;
- private static final boolean V = false;
+ private static final boolean D = BluetoothMapService.DEBUG;
+ private static final boolean V = BluetoothMapService.VERBOSE;
private ObexTransport mTransport;
- private Context mContext;
public Handler mHandler = null;
private volatile boolean mWaitingForRemote;
private static final String TYPE_EVENT = "x-bt/MAP-event-report";
private ClientSession mClientSession;
private boolean mConnected = false;
BluetoothDevice mRemoteDevice;
+ private SparseBooleanArray mRegisteredMasIds = new SparseBooleanArray(1);
+
+ private HeaderSet mHsConnect = null;
private Handler mCallback = null;
- private BluetoothMapContentObserver mObserver;
- private boolean mObserverRegistered = false;
// Used by the MAS to forward notification registrations
public static final int MSG_MNS_NOTIFICATION_REGISTRATION = 1;
+ public static final int MSG_MNS_SEND_EVENT = 2;
- public static final ParcelUuid BluetoothUuid_ObexMns =
+ public static final ParcelUuid BLUETOOTH_UUID_OBEX_MNS =
ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
- public BluetoothMnsObexClient(Context context, BluetoothDevice remoteDevice,
- Handler callback) {
+ public BluetoothMnsObexClient(BluetoothDevice remoteDevice, Handler callback) {
if (remoteDevice == null) {
throw new NullPointerException("Obex transport is null");
}
+ mRemoteDevice = remoteDevice;
HandlerThread thread = new HandlerThread("BluetoothMnsObexClient");
thread.start();
+ /* This will block until the looper have started, hence it will be safe to use it,
+ when the constructor completes */
Looper looper = thread.getLooper();
mHandler = new MnsObexClientHandler(looper);
- mContext = context;
- mRemoteDevice = remoteDevice;
mCallback = callback;
- mObserver = new BluetoothMapContentObserver(mContext);
- mObserver.init();
}
public Handler getMessageHandler() {
return mHandler;
}
- public BluetoothMapContentObserver getContentObserver() {
- return mObserver;
- }
-
private final class MnsObexClientHandler extends Handler {
private MnsObexClientHandler(Looper looper) {
super(looper);
@@ -105,6 +96,9 @@ public class BluetoothMnsObexClient {
case MSG_MNS_NOTIFICATION_REGISTRATION:
handleRegistration(msg.arg1 /*masId*/, msg.arg2 /*status*/);
break;
+ case MSG_MNS_SEND_EVENT:
+ sendEventHandler((byte[])msg.obj/*byte[]*/, msg.arg1 /*masId*/);
+ break;
default:
break;
}
@@ -156,7 +150,7 @@ public class BluetoothMnsObexClient {
*/
public void shutdown() {
/* should shutdown handler thread first to make sure
- * handleRegistration won't be called when disconnet
+ * handleRegistration won't be called when disconnect
*/
if (mHandler != null) {
// Shut down the thread
@@ -171,58 +165,50 @@ public class BluetoothMnsObexClient {
/* Disconnect if connected */
disconnect();
- if(mObserverRegistered) {
- mObserver.unregisterObserver();
- mObserverRegistered = false;
- }
- if (mObserver != null) {
- mObserver.deinit();
- mObserver = null;
- }
+ mRegisteredMasIds.clear();
}
- private HeaderSet hsConnect = null;
-
+ /**
+ * We store a list of registered MasIds only to control connect/disconnect
+ * @param masId
+ * @param notificationStatus
+ */
public void handleRegistration(int masId, int notificationStatus){
- Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")");
-
- if((isConnected() == false) &&
- (notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES)) {
- Log.d(TAG, "handleRegistration: connect");
- connect();
- }
+ if(D) Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")");
if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_NO) {
- // Unregister - should we disconnect, or keep the connection? - the spec. says nothing about this.
- if(mObserverRegistered == true) {
- mObserver.unregisterObserver();
- mObserverRegistered = false;
- disconnect();
- }
+ mRegisteredMasIds.delete(masId);
} else if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) {
/* Connect if we do not have a connection, and start the content observers providing
* this thread as Handler.
*/
- synchronized (this) {
- if(mObserverRegistered == false && mObserver != null) {
- mObserver.registerObserver(this, masId);
- mObserverRegistered = true;
- }
+ if(isConnected() == false) {
+ if(D) Log.d(TAG, "handleRegistration: connect");
+ connect();
}
+ mRegisteredMasIds.put(masId, true); // We don't use the value for anything
+ }
+ if(mRegisteredMasIds.size() == 0) {
+ // No more registrations - disconnect
+ if(D) Log.d(TAG, "handleRegistration: disconnect");
+ disconnect();
}
}
public void connect() {
- Log.d(TAG, "handleRegistration: connect 2");
+
+ mConnected = true;
BluetoothSocket btSocket = null;
try {
+ // TODO: Why insecure? - is it because the link is already encrypted?
btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord(
- BluetoothUuid_ObexMns.getUuid());
+ BLUETOOTH_UUID_OBEX_MNS.getUuid());
btSocket.connect();
} catch (IOException e) {
Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e);
// TODO: do we need to report error somewhere?
+ mConnected = false;
return;
}
@@ -230,12 +216,12 @@ public class BluetoothMnsObexClient {
try {
mClientSession = new ClientSession(mTransport);
- mConnected = true;
} catch (IOException e1) {
Log.e(TAG, "OBEX session create error " + e1.getMessage());
+ mConnected = false;
}
if (mConnected && mClientSession != null) {
- mConnected = false;
+ boolean connected = false;
HeaderSet hs = new HeaderSet();
// bb582b41-420c-11db-b0de-0800200c9a66
byte[] mnsTarget = { (byte) 0xbb, (byte) 0x58, (byte) 0x2b, (byte) 0x41,
@@ -248,19 +234,36 @@ public class BluetoothMnsObexClient {
mWaitingForRemote = true;
}
try {
- hsConnect = mClientSession.connect(hs);
+ mHsConnect = mClientSession.connect(hs);
if (D) Log.d(TAG, "OBEX session created");
- mConnected = true;
+ connected = true;
} catch (IOException e) {
Log.e(TAG, "OBEX session connect error " + e.getMessage());
}
+ mConnected = connected;
}
synchronized (this) {
mWaitingForRemote = false;
}
}
- public int sendEvent(byte[] eventBytes, int masInstanceId) {
+ /**
+ * Call this method to queue an event report to be send to the MNS server.
+ * @param eventBytes the encoded event data.
+ * @param masInstanceId the MasId of the instance sending the event.
+ */
+ public void sendEvent(byte[] eventBytes, int masInstanceId) {
+ // We need to check for null, to handle shutdown.
+ if(mHandler != null) {
+ Message msg = mHandler.obtainMessage(MSG_MNS_SEND_EVENT, masInstanceId, 0, eventBytes);
+ if(msg != null) {
+ msg.sendToTarget();
+ }
+ }
+ notifyUpdateWakeLock();
+ }
+
+ private int sendEventHandler(byte[] eventBytes, int masInstanceId) {
boolean error = false;
int responseCode = -1;
@@ -273,8 +276,6 @@ public class BluetoothMnsObexClient {
return responseCode;
}
- notifyUpdateWakeLock();
-
request = new HeaderSet();
BluetoothMapAppParams appParams = new BluetoothMapAppParams();
appParams.setMasInstanceId(masInstanceId);
@@ -286,9 +287,9 @@ public class BluetoothMnsObexClient {
request.setHeader(HeaderSet.TYPE, TYPE_EVENT);
request.setHeader(HeaderSet.APPLICATION_PARAMETER, appParams.EncodeParams());
- if (hsConnect.mConnectionID != null) {
+ if (mHsConnect.mConnectionID != null) {
request.mConnectionID = new byte[4];
- System.arraycopy(hsConnect.mConnectionID, 0, request.mConnectionID, 0, 4);
+ System.arraycopy(mHsConnect.mConnectionID, 0, request.mConnectionID, 0, 4);
} else {
Log.w(TAG, "sendEvent: no connection ID");
}
@@ -377,8 +378,10 @@ public class BluetoothMnsObexClient {
}
private void notifyUpdateWakeLock() {
- Message msg = Message.obtain(mCallback);
- msg.what = BluetoothMapService.MSG_ACQUIRE_WAKE_LOCK;
- msg.sendToTarget();
+ if(mCallback != null) {
+ Message msg = Message.obtain(mCallback);
+ msg.what = BluetoothMapService.MSG_ACQUIRE_WAKE_LOCK;
+ msg.sendToTarget();
+ }
}
}
diff --git a/tests/Android.mk b/tests/Android.mk
index 22f117a46..9f8e26eed 100755
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -5,11 +5,12 @@ include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_CERTIFICATE := platform
-LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_JAVA_LIBRARIES := android.test.runner telephony-common mms-common
LOCAL_STATIC_JAVA_LIBRARIES := com.android.emailcommon
# Include all test java files.
LOCAL_SRC_FILES := $(call all-java-files-under, src)
+# LOCAL_SRC_FILES := src/com/android/bluetooth/tests/BluetoothMapContentTest.java
LOCAL_PACKAGE_NAME := BluetoothProfileTests
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 17b07cfcc..657c85840 100755
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -18,12 +18,11 @@
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.NET_ADMIN" />
<uses-permission android:name="android.permission.CALL_PRIVILEGED" />
+ <uses-permission android:name="com.android.permission.HANDOVER_STATUS" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.NET_TUNNELING" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
- <uses-permission android:name="android.permission.CONNECTIVITY_INTERNAL" />
- <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
<uses-permission android:name="android.permission.BLUETOOTH_STACK" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
@@ -37,16 +36,21 @@
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.WRITE_SMS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
-
+ <uses-permission android:name="com.android.permission.WHITELIST_BLUETOOTH_DEVICE" />
<uses-permission android:name="com.android.email.permission.ACCESS_PROVIDER"/>
+ <!-- For testing Email content access -->
+ <uses-permission android:name="com.android.email.permission.BT_ACCESS_PROVIDER"/>
+ <uses-permission android:name="com.android.email.permission.READ_ATTACHMENT"/>
<!-- For PBAP Owner Vcard Info -->
<uses-permission android:name="android.permission.READ_PROFILE"/>
<!-- We add an application tag here just so that we can indicate that
this package needs to link against the android.test library,
which is needed when building test cases. -->
- <application>
+
+ <application
+ android:allowBackup="true" >
<uses-library android:name="android.test.runner" />
<uses-permission android:name="android.permission.ACCESS_BLUETOOTH_SHARE" />
<uses-permission android:name="com.android.permission.WHITELIST_BLUETOOTH_DEVICE" />
diff --git a/tests/src/com/android/bluetooth/tests/BluetoothMapbMessageTest.java b/tests/src/com/android/bluetooth/tests/BluetoothMapbMessageTest.java
index 14c41da21..1fcec4e66 100755
--- a/tests/src/com/android/bluetooth/tests/BluetoothMapbMessageTest.java
+++ b/tests/src/com/android/bluetooth/tests/BluetoothMapbMessageTest.java
@@ -20,7 +20,7 @@ import com.android.bluetooth.map.BluetoothMapAppParams;
import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
import com.android.bluetooth.map.BluetoothMapSmsPdu;
import com.android.bluetooth.map.BluetoothMapbMessage;
-import com.android.bluetooth.map.BluetoothMapbMessageMmsEmail;
+import com.android.bluetooth.map.BluetoothMapbMessageMms;
import com.android.bluetooth.map.BluetoothMapbMessageSms;
import org.apache.http.message.BasicHeaderValueFormatter;
import org.apache.http.message.BasicHeaderElement;
@@ -421,7 +421,7 @@ public class BluetoothMapbMessageTest extends AndroidTestCase {
* Test encoding of a simple MMS text message (UTF8). This validates most parameters.
*/
public void testMmsEncodeText() {
- BluetoothMapbMessageMmsEmail msg = new BluetoothMapbMessageMmsEmail();
+ BluetoothMapbMessageMms msg = new BluetoothMapbMessageMms();
String str1 =
"BEGIN:BMSG\r\n" +
"VERSION:1.0\r\n" +
@@ -467,11 +467,11 @@ public class BluetoothMapbMessageTest extends AndroidTestCase {
msg.addTo("Jørn Hansen", "bonde@email.add");
msg.addCc("Jens Hansen", "bonde@email.add");
msg.addFrom("Jørn Hansen", "bonde@email.add");
- BluetoothMapbMessageMmsEmail.MimePart part = msg.addMimePart();
- part.partName = "partNameText";
- part.contentType ="dsfajfdlk/text/asdfafda";
+ BluetoothMapbMessageMms.MimePart part = msg.addMimePart();
+ part.mPartName = "partNameText";
+ part.mContentType ="dsfajfdlk/text/asdfafda";
try {
- part.data = new String("This is a short message\r\n").getBytes("UTF-8");
+ part.mData = new String("This is a short message\r\n").getBytes("UTF-8");
}
catch (UnsupportedEncodingException e) {
if(D) Log.e(TAG, "UnsupportedEncodingException should never happen???", e);
@@ -479,9 +479,9 @@ public class BluetoothMapbMessageTest extends AndroidTestCase {
}
part = msg.addMimePart();
- part.partName = "partNameimage";
- part.contentType = "dsfajfdlk/image/asdfafda";
- part.data = null;
+ part.mPartName = "partNameimage";
+ part.mContentType = "dsfajfdlk/image/asdfafda";
+ part.mData = null;
msg.setStatus(false);
msg.setType(TYPE.MMS);