From 326b5e610063ac24c0ba467ac585bd4c7f618a67 Mon Sep 17 00:00:00 2001 From: Casper Bonde Date: Thu, 24 Jul 2014 13:47:23 +0200 Subject: BT MAP: added support for email sharing over BT - added support for Emails - added activity to do setup of the email accounts to share - added improved handling of MMS, SMS and Email - Many optimizations to speed (especially getMessageListing) - fixed wakelock problem - fixed user timeout problem when user do not react to msg access request - added missing privileges - support for Quoted Printable format - added accountId in test case URIs - fixed problem with service numbers being strings - fixed problem with read flag in getMessage - added support for transparent flag in Email Push - added missing send-event for non-imap accounts - set attachment size to 0 if text-only message - fixed double send for sms messages with retry - removed secondary phone numbers from recipient/originator - removed insert-address-token in MMS messages - fixed null-pointer exception in settings (missing extra in intent) - send text-only mms as sms (workaround to make it cheaper) - fixed rejection of native and fraction requests - better handling of unknown message types in push - fixed problem with possible illigal xml chars in message listing - added missing WRITE_APN_SETTINGS permission to manifest - fixed problem with notifications when pushing to folders other than OUTBOX - removed debugging code - added support for threadId - fixed permission problems - changed to use ContentProviderClients for Email app access - fixed names for member vars UPDATE: Moved the MAP E-mail API to the bluetooth package. UPDATE: Added check for the presense of MMS parts. This is needed due to a change in the MMS app/subsystem, where deleted messages gets corrupted. Signed-off-by: Casper Bonde Change-Id: Ib5dbe7c2d5c0ba8d978ae843d840028592e3cab4 --- Android.mk | 13 +- AndroidManifest.xml | 10 + lib/mapapi/BluetoothMapContract.java | 557 +++++++ lib/mapapi/BluetoothMapEmailProvider.java | 696 +++++++++ res/layout/bluetooth_map_email_settings.xml | 42 + .../bluetooth_map_email_settings_account_group.xml | 48 + .../bluetooth_map_email_settings_account_item.xml | 43 + res/layout/bluetooth_transfer_item.xml | 1 + res/values/strings.xml | 12 +- .../bluetooth/map/BluetoothMapAppParams.java | 178 ++- .../bluetooth/map/BluetoothMapAuthenticator.java | 88 -- .../android/bluetooth/map/BluetoothMapContent.java | 1590 +++++++++++++------- .../bluetooth/map/BluetoothMapContentObserver.java | 1577 ++++++++++++++----- .../map/BluetoothMapEmailAppObserver.java | 287 ++++ .../bluetooth/map/BluetoothMapEmailSettings.java | 56 + .../map/BluetoothMapEmailSettingsAdapter.java | 347 +++++ .../map/BluetoothMapEmailSettingsDataHolder.java | 23 + .../map/BluetoothMapEmailSettingsItem.java | 172 +++ .../map/BluetoothMapEmailSettingsLoader.java | 202 +++ .../bluetooth/map/BluetoothMapFolderElement.java | 209 ++- .../bluetooth/map/BluetoothMapMasInstance.java | 386 +++++ .../bluetooth/map/BluetoothMapMessageListing.java | 30 +- .../map/BluetoothMapMessageListingElement.java | 237 +-- .../bluetooth/map/BluetoothMapObexServer.java | 608 ++++++-- .../android/bluetooth/map/BluetoothMapService.java | 894 ++++++----- .../android/bluetooth/map/BluetoothMapSmsPdu.java | 162 +- .../android/bluetooth/map/BluetoothMapUtils.java | 52 +- .../bluetooth/map/BluetoothMapbMessage.java | 253 ++-- .../bluetooth/map/BluetoothMapbMessageEmail.java | 79 + .../bluetooth/map/BluetoothMapbMessageMms.java | 807 ++++++++++ .../map/BluetoothMapbMessageMmsEmail.java | 643 -------- .../bluetooth/map/BluetoothMapbMessageSms.java | 38 +- .../bluetooth/map/BluetoothMnsObexClient.java | 133 +- tests/Android.mk | 3 +- tests/AndroidManifest.xml | 12 +- .../bluetooth/tests/BluetoothMapbMessageTest.java | 18 +- 36 files changed, 7783 insertions(+), 2723 deletions(-) create mode 100644 lib/mapapi/BluetoothMapContract.java create mode 100644 lib/mapapi/BluetoothMapEmailProvider.java create mode 100644 res/layout/bluetooth_map_email_settings.xml create mode 100644 res/layout/bluetooth_map_email_settings_account_group.xml create mode 100644 res/layout/bluetooth_map_email_settings_account_item.xml delete mode 100644 src/com/android/bluetooth/map/BluetoothMapAuthenticator.java create mode 100644 src/com/android/bluetooth/map/BluetoothMapEmailAppObserver.java create mode 100644 src/com/android/bluetooth/map/BluetoothMapEmailSettings.java create mode 100644 src/com/android/bluetooth/map/BluetoothMapEmailSettingsAdapter.java create mode 100644 src/com/android/bluetooth/map/BluetoothMapEmailSettingsDataHolder.java create mode 100644 src/com/android/bluetooth/map/BluetoothMapEmailSettingsItem.java create mode 100644 src/com/android/bluetooth/map/BluetoothMapEmailSettingsLoader.java create mode 100644 src/com/android/bluetooth/map/BluetoothMapMasInstance.java create mode 100644 src/com/android/bluetooth/map/BluetoothMapbMessageEmail.java create mode 100644 src/com/android/bluetooth/map/BluetoothMapbMessageMms.java delete mode 100644 src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java diff --git a/Android.mk b/Android.mk index 53b2810ff..2b8a2ad12 100644 --- a/Android.mk +++ b/Android.mk @@ -3,6 +3,17 @@ 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) @@ -10,7 +21,7 @@ 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 @@ + @@ -233,8 +234,17 @@ android:enabled="@bool/profile_supported_map" > + + + + + *
  • Message data set containing lists of messages.
  • + *
  • 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.
  • + *
  • Folder data set with the folder structure for the messages. Each message is linked to an + * entry in this data set.
  • + * + * + * 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: + *
    <provider  android:authorities="[PROVIDER AUTHORITY]"
    +              android:exported="true"
    +              android:enabled="true"
    +              android:permission="android.permission.BLUETOOTH_MAP">
    + *   ...
    + *      <intent-filter>
    +           <action android:name="android.content.action.BLEUETOOT_MAP_PROVIDER" />
    +        </intent-filter>
    + *   ...
    + *   </provider>
    + * [PROVIDER AUTHORITY] shall be the providers authority value which implements this
    + * contract. Only a single authority shall be used. The android.permission.BLUETOOTH_MAP
    + * permission is needed for the provider.
    + */
    +public final class BluetoothMapContract {
    +    /**
    +     * Constructor - should not be used
    +     */
    +    private BluetoothMapContract(){
    +      /* class should not be instantiated */
    +    }
    +
    +    /**
    +     * Provider interface that should be used as intent-filter action in the provider section
    +     * of the manifest file.
    +     */
    +    public static final String PROVIDER_INTERFACE = "android.bluetooth.action.BLUETOOTH_MAP_PROVIDER";
    +
    +    /**
    +     * The Bluetooth Message Access profile allows a remote BT-MAP client to trigger
    +     * an update of a folder for a specific e-mail account, register for reception
    +     * of new messages from the server.
    +     *
    +     * Additionally the Bluetooth Message Access profile allows a remote BT-MAP client
    +     * to push a message to a folder - e.g. outbox or draft. The Bluetooth profile
    +     * implementation will place a new message in one of these existing folders through
    +     * the content provider.
    +     *
    +     * ContentProvider.call() is used for these purposes, and the METHOD_UPDATE_FOLDER
    +     * method name shall trigger an update of the specified folder for a specified
    +     * account.
    +     *
    +     * This shall be a non blocking call simply starting the update, and the update should
    +     * both send and receive messages, depending on what makes sense for the specified
    +     * folder.
    +     * Bundle extra parameter will carry two INTEGER (long) values:
    +     *   EXTRA_UPDATE_ACCOUNT_ID containing the account_id
    +     *   EXTRA_UPDATE_FOLDER_ID containing the folder_id of the folder to update
    +     *
    +     * The status for send complete of messages shall be reported by updating the sent-flag
    +     * and e.g. for outbox messages, move them to the sent folder in the message table of the
    +     * content provider and trigger a change notification to any attached content observer.
    +     */
    +    public static final String METHOD_UPDATE_FOLDER = "UpdateFolder";
    +    public static final String EXTRA_UPDATE_ACCOUNT_ID = "UpdateAccountId";
    +    public static final String EXTRA_UPDATE_FOLDER_ID = "UpdateFolderId";
    +
    +    /**
    +     * These column names are used as last path segment of the URI (getLastPathSegment()).
    +     * Access to a specific row in the tables is done by using the where-clause, hence
    +     * support for .../#id if not needed for the Email clients.
    +     * The URI format for accessing the tables are as follows:
    +     *   content://ProviderAuthority/TABLE_ACCOUNT
    +     *   content://ProviderAuthority/account_id/TABLE_MESSAGE
    +     *   content://ProviderAuthority/account_id/TABLE_FOLDER
    +     */
    +
    +    /**
    +     * Build URI representing the given Accounts data-set in a
    +     * bluetooth provider. When queried, the direct URI for the account
    +     * with the given accountID is returned.
    +     */
    +    public static Uri buildAccountUri(String authority) {
    +        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
    +                .authority(authority).appendPath(TABLE_ACCOUNT).build();
    +    }
    +    /**
    +     * Build URI representing the given Account data-set with specific Id in a
    +     * Bluetooth provider. When queried, the direct URI for the account
    +     * with the given accountID is returned.
    +     */
    +    public static Uri buildAccountUriwithId(String authority, String accountId) {
    +        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
    +                .authority(authority)
    +                .appendPath(TABLE_ACCOUNT)
    +                .appendPath(accountId)
    +                .build();
    +    }
    +    /**
    +     * Build URI representing the entire Message table in a
    +     * bluetooth provider.
    +     */
    +    public static Uri buildMessageUri(String authority) {
    +        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
    +                .authority(authority)
    +                .appendPath(TABLE_MESSAGE)
    +                .build();
    +    }
    +    /**
    +     * Build URI representing the given Message data-set in a
    +     * bluetooth provider. When queried, the URI for the Messages
    +     * with the given accountID is returned.
    +     */
    +    public static Uri buildMessageUri(String authority, String accountId) {
    +        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
    +                .authority(authority)
    +                .appendPath(accountId)
    +                .appendPath(TABLE_MESSAGE)
    +                .build();
    +    }
    +    /**
    +     * Build URI representing the given Message data-set with specific messageId in a
    +     * bluetooth provider. When queried, the direct URI for the account
    +     * with the given accountID is returned.
    +     */
    +    public static Uri buildMessageUriWithId(String authority, String accountId,String messageId) {
    +        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
    +                .authority(authority)
    +                .appendPath(accountId)
    +                .appendPath(TABLE_MESSAGE)
    +                .appendPath(messageId)
    +                .build();
    +    }
    +    /**
    +     * Build URI representing the given Message data-set in a
    +     * bluetooth provider. When queried, the direct URI for the account
    +     * with the given accountID is returned.
    +     */
    +    public static Uri buildFolderUri(String authority, String accountId) {
    +        return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
    +                .authority(authority)
    +                .appendPath(accountId)
    +                .appendPath(TABLE_FOLDER)
    +                .build();
    +    }
    +
    +    /**
    +     *  @hide
    +     */
    +    public static final String TABLE_ACCOUNT = "Account";
    +    public static final String TABLE_MESSAGE = "Message";
    +    public static final String TABLE_FOLDER  = "Folder";
    +
    +    /**
    +     * Mandatory folders for the Bluetooth message access profile.
    +     * The email client shall at least implement the following root folders.
    +     * E.g. as a mapping for them such that the naming will match the underlying
    +     * matching folder ID's.
    +     */
    +    public static final String FOLDER_NAME_INBOX   = "inbox";
    +    public static final String FOLDER_NAME_OUTBOX  = "outbox";
    +    public static final String FOLDER_NAME_SENT    = "sent";
    +    public static final String FOLDER_NAME_DELETED = "deleted";
    +    public static final String FOLDER_NAME_DRAFT   = "draft";
    +
    +
    +    /**
    +     * To push RFC2822 encoded messages into a folder and read RFC2822 encoded messages from
    +     * a folder, the openFile() interface will be used as follows:
    +     * Open a file descriptor to a message.
    +     * Two modes supported for read: With and without attachments.
    +     * One mode exist for write and the actual content will be with or without
    +     * attachments.
    +     *
    +     * mode will be "r" for read and "w" for write, never "rw".
    +     *
    +     * URI format:
    +     * The URI scheme is as follows.
    +     * For reading messages with attachments:
    +     *   content://ProviderAuthority/account_id/TABLE_MESSAGE/msgId
    +     *   Note: This shall be an offline operation, including only message parts and attachments
    +     *         already downloaded to the device.
    +     *
    +     * For reading messages without attachments:
    +     *   content://ProviderAuthority/account_id/TABLE_MESSAGE/msgId/FILE_MSG_NO_ATTACHMENTS
    +     *   Note: This shall be an offline operation, including only message parts already
    +     *         downloaded to the device.
    +     *
    +     * For downloading and reading messages with attachments:
    +     *   content://ProviderAuthority/account_id/TABLE_MESSAGE/msgId/FILE_MSG_DOWNLOAD
    +     *   Note: This shall download the message content and all attachments if possible,
    +     *         else throw an IOException.
    +     *
    +     * For downloading and reading messages without attachments:
    +     *   content://ProviderAuthority/account_id/TABLE_MESSAGE/msgId/FILE_MSG_DOWNLOAD_NO_ATTACHMENTS
    +     *   Note: This shall download the message content if possible, else throw an IOException.
    +     *
    +     * When reading from the file descriptor, the content provider shall return a stream
    +     * of bytes containing a RFC2822 encoded message, as if the message was send to an email
    +     * server.
    +     *
    +     * When a byte stream is written to the file descriptor, the content provider shall
    +     * decode the RFC2822 encoded data and insert the message into the TABLE_MESSAGE at the ID
    +     * supplied in URI - additionally the message content shall be stored in the underlying
    +     * data base structure as if the message was received from an email server. The Message ID
    +     * will be created using a insert on the TABLE_MESSAGE prior to calling openFile().
    +     * Hence the procedure for inserting a message is:
    +     *  - uri/msgId = insert(uri, value: folderId=xxx)
    +     *  - fd = openFile(uri/msgId)
    +     *  - fd.write (RFC2822 encoded data)
    +     *
    +     *  The Bluetooth Message Access Client might not know what to put into the From:
    +     *  header nor have a valid time stamp, hence the content provider shall check
    +     *  if the From: and Date: headers have been set by the message written, else
    +     *  it must fill in appropriate values.
    +     */
    +    public static final String FILE_MSG_NO_ATTACHMENTS = "NO_ATTACHMENTS";
    +    public static final String FILE_MSG_DOWNLOAD = "DOWNLOAD";
    +    public static final String FILE_MSG_DOWNLOAD_NO_ATTACHMENTS = "DOWNLOAD_NO_ATTACHMENTS";
    +
    +    /**
    +     * Account Table
    +     * The columns needed to supply account information.
    +     * The e-mail client app may choose to expose all e-mails as being from the same account,
    +     * but it is not recommended, as this would be a violation of the Bluetooth specification.
    +     * The Bluetooth Message Access settings activity will provide the user the ability to
    +     * change the FLAG_EXPOSE values for each account in this table.
    +     * The Bluetooth Message Access service will read the values when Bluetooth is turned on,
    +     * and again on every notified change through the content observer interface.
    +     */
    +    public interface AccountColumns {
    +
    +        /**
    +         * The unique ID for a row.
    +         * 

    Type: INTEGER (long)

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

    Type: TEXT

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

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

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

    Type: INTEGER (long)

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

    Type: INTEGER (long)

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

    Type: TEXT

    + * read-only. + */ + public static final String SUBJECT = "subject"; + + /** + * Message Read flag + *

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

    + * read/write + */ + public static final String FLAG_READ = "flag_read"; + + /** + * Message Priority flag + *

    Type: INTEGER (boolean) normal priority = 0, high priority = 1

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

    Type: INTEGER see RECEPTION_STATE_ constants below

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

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

    + * read-only + */ + public static final String FLAG_ATTACHMENT = "flag_attachment"; + + /** The overall size in bytes of the attachments of the message. + *

    Type: INTEGER

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

    Type: INTEGER

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

    Type: INTEGER (boolean) no DRM = 0, DRM protected = 1

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

    Type: TEXT

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

    Type: TEXT

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

    Type: TEXT

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

    Type: TEXT

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

    Type: TEXT

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

    Type: INTEGER (long)

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

    Type: INTEGER (long)

    + * read-only + */ + public static final String ACCOUNT_ID = "account_id"; + + /** + * The ID identify the thread a message belongs to. If no thread id is available, + * set value to "-1" + *

    Type: INTEGER (long)

    + * read-only + */ + public static final String THREAD_ID = "thread_id"; + } + + /** + * Indicates 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. + *

    Type: INTEGER (long)

    + * read-only + */ + public static final String _ID = "_id"; + + /** + * The folder display name to present to the user. + *

    Type: TEXT

    + * read-only + */ + public static final String NAME = "name"; + + /** + * The _id-key to the account this folder refers to. + *

    Type: INTEGER (long)

    + * read-only + */ + public static final String ACCOUNT_ID = "account_id"; + + /** + * The _id-key to the parent folder. -1 for root folders. + *

    Type: INTEGER (long)

    + * 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 { + /** + * 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 { + /** + * 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 { + /** + * 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 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 ParcelFileDescriptor openInversePipeHelper(final Uri uri, final String mimeType, + final Bundle opts, final T args, final PipeDataReader func) + throws FileNotFoundException { + try { + final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe(); + + AsyncTask task = new AsyncTask() { + @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 + * @return the converted projection + */ + protected String[] convertProjection(String[] projection, Map projectionMap) { + String[] newProjection = new String[projection.length]; + for(int i = 0; i < projection.length; i++) { + newProjection[i] = projectionMap.get(projection[i]) + " as " + projection[i]; + } + return newProjection; + } + + /** + * This query needs to map from the data used in the e-mail client to BluetoothMapContract type of data. + */ + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + long callingId = Binder.clearCallingIdentity(); + try { + String accountId = null; + 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 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 @@ + + + + + + + + + + + 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 @@ + + + + + + + + + + + \ 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 @@ + + + + + + + + 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 @@ Bluetooth share: Sending %1$s - Bluetooth share: Sent %1$s - + Bluetooth share: Sent %1$s + 100% complete Bluetooth share: File %1$s not sent @@ -224,4 +224,12 @@ + + Save + Cancel + Select the email accounts you want to share through Bluetooth. You still have to accept any acceess to the accounts when connecting. + Slots left: + Application Icon + Bluetooth Message Sharing Settings + Cannot select account. 0 slots left 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 list = bmList.getList(); + int listSize = list.size(); + Cursor tmpCursor = null; + for(int x=0;x>> 32)); + return result; } - } - private Map mMsgListSms = - Collections.synchronizedMap(new HashMap()); + @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 mMsgListMms = - Collections.synchronizedMap(new HashMap()); + private Map mMsgListSms = new HashMap(); + + private Map mMsgListMms = new HashMap(); + + private Map mMsgListEmail = new HashMap(); + + 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 msgListSms = new HashMap(); + if(mEnableSmsMms) { - Cursor c = mResolver.query(Sms.CONTENT_URI, - SMS_PROJECTION, null, null, null); + HashMap msgListSms = new HashMap(); - 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 msgListMms = new HashMap(); + HashMap msgListMms = new HashMap(); - 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 msgListEmail = new HashMap(); + 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 msgListSms = new HashMap(); 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 msgListMms = new HashMap(); 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 msgListEmail = new HashMap(); + + 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 recipients = new HashSet(); 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 recipients = new HashSet(); 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 mPushMsgList = Collections.synchronizedMap(new HashMap()); - 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 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 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 recipients = new HashSet(); 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, ""); + } + } + // 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, ""); + } + 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, ""); } - 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 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 deliveryIntents = new ArrayList(msgInfo.parts); ArrayList sentIntents = new ArrayList(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> mFullList; + private LinkedHashMap mObserverMap = new LinkedHashMap(); + 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 newAccountList = mLoader.parseAccounts(app); + ArrayList oldAccountList = mFullList.get(app); + ArrayList addedAccountList = + (ArrayList)newAccountList.clone(); + ArrayList 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 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 containing all enabled accounts + */ + public ArrayList getEnabledAccountItems(){ + if(D)Log.d(TAG,"getEnabledAccountItems()\n"); + ArrayList list = new ArrayList(); + for(BluetoothMapEmailSettingsItem app:mFullList.keySet()){ + ArrayList 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 containing all accounts + */ + public ArrayList getAllAccountItems(){ + if(D)Log.d(TAG,"getAllAccountItems()\n"); + ArrayList list = new ArrayList(); + for(BluetoothMapEmailSettingsItem app:mFullList.keySet()){ + ArrayList 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> 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 mPositionArray; + private LinkedHashMap> mProupList; + private ArrayList mMainGroup; + private int[] mGroupStatus; + /* number of accounts possible to share */ + private int mSlotsLeft = 10; + + + public BluetoothMapEmailSettingsAdapter(Activity act, + ExpandableListView listView, + LinkedHashMap> 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(); + for (Map.Entry> 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 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 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 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 mCheckedChilds = new HashMap(); +} + 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{ + 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> parsePackages(boolean includeIcon) { + + LinkedHashMap> groups = + new LinkedHashMap>(); + 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 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 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 parseAccounts(BluetoothMapEmailSettingsItem app) { + Cursor c = null; + if(D) Log.d(TAG,"Adding app "+app.getPackageName()); + ArrayList children = new ArrayList(); + // 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 subFolders; + private String mName; + private BluetoothMapFolderElement mParent = null; + private boolean mHasSmsMmsContent = false; + private long mEmailFolderId = -1; + private HashMap 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(); + public BluetoothMapFolderElement( String name, BluetoothMapFolderElement parrent){ + this.mName = name; + this.mParent = parrent; + mSubFolders = new HashMap(); } 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,9 +77,67 @@ 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 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 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(); // Return an empty list + } } else { if(offset > list.size()) { list = new ArrayList(); 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= 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 mMasInstances = + new SparseArray(1); + /* mMasInstanceMap: A list of the active MasInstances with the key being the account */ + private HashMap mMasInstanceMap = + new HashMap(1); - private BluetoothDevice mRemoteDevice = null; + private BluetoothDevice mRemoteDevice = null; // The remote connected device - protect access + private ArrayList 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 newAccountList = mAppObserver.getEnabledAccountItems(); + ArrayList newAccounts = null; + ArrayList removedAccounts = null; + newAccounts = new ArrayList(); + 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 originator = null; - private ArrayList recipient = null; + private ArrayList mOriginator = null; + private ArrayList 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(1); - recipient.add(vCard.parseVcard(reader, level)); + if(mRecipient == null) + mRecipient = new ArrayList(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", "\n"); debug = debug.replaceAll("\\r", ""); @@ -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 getOriginators() { - return originator; + return mOriginator; } public void addOriginator(vCard originator) { - if(this.originator == null) - this.originator = new ArrayList(); - this.originator.add(originator); + if(this.mOriginator == null) + this.mOriginator = new ArrayList(); + 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(); - originator.add(new vCard(name, formattedName, phoneNumbers, emailAddresses)); + if(mOriginator == null) + mOriginator = new ArrayList(); + 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(); - originator.add(new vCard(name, phoneNumbers, emailAddresses)); + if(mOriginator == null) + mOriginator = new ArrayList(); + mOriginator.add(new vCard(name, phoneNumbers, emailAddresses)); } public ArrayList getRecipients() { - return recipient; + return mRecipient; } public void setRecipient(vCard recipient) { - if(this.recipient == null) - this.recipient = new ArrayList(); - this.recipient.add(recipient); + if(this.mRecipient == null) + this.mRecipient = new ArrayList(); + this.mRecipient.add(recipient); } public void addRecipient(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses) { - if(recipient == null) - recipient = new ArrayList(); - recipient.add(new vCard(name, formattedName, phoneNumbers, emailAddresses)); + if(mRecipient == null) + mRecipient = new ArrayList(); + mRecipient.add(new vCard(name, formattedName, phoneNumbers, emailAddresses)); } public void addRecipient(String name, String[] phoneNumbers, String[] emailAddresses) { - if(recipient == null) - recipient = new ArrayList(); - recipient.add(new vCard(name, phoneNumbers, emailAddresses)); + if(mRecipient == null) + mRecipient = new ArrayList(); + 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 bodyFragments = new ArrayList(); + + /* 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/BluetoothMapbMessageMms.java b/src/com/android/bluetooth/map/BluetoothMapbMessageMms.java new file mode 100644 index 000000000..8c9a39d0a --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapbMessageMms.java @@ -0,0 +1,807 @@ +/* +* 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.nio.charset.IllegalCharsetNameException; +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 android.text.util.Rfc822Token; +import android.text.util.Rfc822Tokenizer; +import android.util.Base64; +import android.util.Log; + +public class BluetoothMapbMessageMms extends BluetoothMapbMessage { + + public static class MimePart { + 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 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(mContentType != null) + sb.append("Content-Type: ").append(mContentType); + if(mCharsetName != null) + sb.append("; ").append("charset=\"").append(mCharsetName).append("\""); + sb.append("\r\n"); + 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 use of UTF-8 is not allowed, Base64 should be used for text. */ + + 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(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(mData, Base64.DEFAULT)).append("\r\n"); + } + } + if(last) { + sb.append("--").append(boundaryTag).append("--").append("\r\n"); + } + } + + public void encodePlainText(StringBuilder sb) throws UnsupportedEncodingException { + 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(mPartName != null) + sb.append("<").append(mPartName).append(">\r\n"); + else + sb.append("<").append("attachment").append(">\r\n"); + } + } + } + + private long date = INVALID_VALUE; + private String subject = null; + private ArrayList from = null; // Shall not be empty + private ArrayList sender = null; // Shall not be empty + private ArrayList to = null; // Shall not be empty + private ArrayList cc = null; // Can be empty + private ArrayList bcc = null; // Can be empty + private ArrayList replyTo = null;// Can be empty + private String messageId = null; + private ArrayList parts = null; + private String contentType = null; + private String boundary = null; + private boolean textOnly = false; + private boolean includeAttachments; + private boolean hasHeaders = false; + private String encoding = null; + + private String getBoundary() { + if(boundary == null) + // Include "=_" as these cannot occur in quoted printable text + boundary = "--=_" + UUID.randomUUID(); + return boundary; + } + + /** + * @return the parts + */ + public ArrayList getMimeParts() { + return parts; + } + + public String getMessageAsText() { + StringBuilder sb = new StringBuilder(); + if(subject != null && !subject.isEmpty()) { + sb.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(); + MimePart newPart = new MimePart(); + parts.add(newPart); + return newPart; + } + public String getDateString() { + SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); + Date dateObj = new Date(date); + return format.format(dateObj); // Format according to RFC 2822 page 14 + } + public long getDate() { + return date; + } + public void setDate(long date) { + this.date = date; + } + public String getSubject() { + return subject; + } + public void setSubject(String subject) { + this.subject = subject; + } + public ArrayList getFrom() { + return from; + } + public void setFrom(ArrayList from) { + this.from = from; + } + public void addFrom(String name, String address) { + if(this.from == null) + this.from = new ArrayList(1); + this.from.add(new Rfc822Token(name, address, null)); + } + public ArrayList getSender() { + return sender; + } + public void setSender(ArrayList sender) { + this.sender = sender; + } + public void addSender(String name, String address) { + if(this.sender == null) + this.sender = new ArrayList(1); + this.sender.add(new Rfc822Token(name,address,null)); + } + public ArrayList getTo() { + return to; + } + public void setTo(ArrayList to) { + this.to = to; + } + public void addTo(String name, String address) { + if(this.to == null) + this.to = new ArrayList(1); + this.to.add(new Rfc822Token(name, address, null)); + } + public ArrayList getCc() { + return cc; + } + public void setCc(ArrayList cc) { + this.cc = cc; + } + public void addCc(String name, String address) { + if(this.cc == null) + this.cc = new ArrayList(1); + this.cc.add(new Rfc822Token(name, address, null)); + } + public ArrayList getBcc() { + return bcc; + } + public void setBcc(ArrayList bcc) { + this.bcc = bcc; + } + public void addBcc(String name, String address) { + if(this.bcc == null) + this.bcc = new ArrayList(1); + this.bcc.add(new Rfc822Token(name, address, null)); + } + public ArrayList getReplyTo() { + return replyTo; + } + public void setReplyTo(ArrayList replyTo) { + this.replyTo = replyTo; + } + public void addReplyTo(String name, String address) { + if(this.replyTo == null) + this.replyTo = new ArrayList(1); + this.replyTo.add(new Rfc822Token(name, address, null)); + } + public void setMessageId(String messageId) { + this.messageId = messageId; + } + public String getMessageId() { + return messageId; + } + public void setContentType(String contentType) { + this.contentType = contentType; + } + public String getContentType() { + return contentType; + } + public void setTextOnly(boolean textOnly) { + this.textOnly = textOnly; + } + public boolean getTextOnly() { + return textOnly; + } + public void setIncludeAttachments(boolean includeAttachments) { + this.includeAttachments = includeAttachments; + } + public boolean getIncludeAttachments() { + return includeAttachments; + } + public void updateCharset() { + 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; + if(parts != null) { + for(MimePart part : parts) { + message_size += part.mData.length; + } + } + return message_size; + } + + /** + * Encode an address header, and perform folding if needed. + * @param sb The stringBuilder to write to + * @param headerName The RFC 2822 header name + * @param addresses the reformatted address substrings to encode. + */ + public void encodeHeaderAddresses(StringBuilder sb, String headerName, + ArrayList addresses) { + /* TODO: Do we need to encode the addresses if they contain illegal characters? + * This depends of the outcome of errata 4176. The current spec. states to use UTF-8 + * where possible, but the RFCs states to use US-ASCII for the headers - hence encoding + * would be needed to support non US-ASCII characters. But the MAP spec states not to + * use any encoding... */ + int partLength, lineLength = 0; + lineLength += headerName.getBytes().length; + sb.append(headerName); + for(Rfc822Token address : addresses) { + partLength = address.toString().getBytes().length+1; + // Add folding if needed + if(lineLength + partLength >= 998) // max line length in RFC2822 + { + sb.append("\r\n "); // Append a FWS (folding whitespace) + lineLength = 0; + } + sb.append(address.toString()).append(";"); + lineLength += partLength; + } + sb.append("\r\n"); + } + + public void encodeHeaders(StringBuilder sb) throws UnsupportedEncodingException + { + /* TODO: From RFC-4356 - about the RFC-(2)822 headers: + * "Current Internet Message format requires that only 7-bit US-ASCII + * characters be present in headers. Non-7-bit characters in an address + * domain must be encoded with [IDN]. If there are any non-7-bit + * characters in the local part of an address, the message MUST be + * rejected. Non-7-bit characters elsewhere in a header MUST be encoded + * according to [Hdr-Enc]." + * We need to add the address encoding in encodeHeaderAddresses, but it is not + * straight forward, as it is unclear how to do this. */ + if (date != INVALID_VALUE) + 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 . We let the MAP specification + * take precedence above the RFC-2822. + */ + /* 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 + sb.append("Subject:").append("=?utf-8?B?"); + sb.append(Base64.encodeToString(subject.getBytes("utf-8"), Base64.DEFAULT)); + sb.append("?=\r\n"); + }*/ + 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. + */ + if(to == null && cc == null && bcc == null) + sb.append("To: undisclosed-recipients:;\r\n"); + if(to != null) + encodeHeaderAddresses(sb, "To: ", to); // This includes folding if needed. + if(cc != null) + encodeHeaderAddresses(sb, "Cc: ", cc); // This includes folding if needed. + if(bcc != null) + encodeHeaderAddresses(sb, "Bcc: ", bcc); // This includes folding if needed. + if(replyTo != null) + encodeHeaderAddresses(sb, "Reply-To: ", replyTo); // This includes folding if needed. + if(includeAttachments == true) + { + if(messageId != null) + sb.append("Message-Id: ").append(messageId).append("\r\n"); + if(contentType != null) + sb.append("Content-Type: ").append(contentType).append("; boundary=").append(getBoundary()).append("\r\n"); + } + sb.append("\r\n"); // If no headers exists, we still need two CRLF, hence keep it out of the if above. + } + + /* Notes on MMS + * ------------ + * According to rfc4356 all headers of a MMS converted to an E-mail must use + * 7-bit encoding. According the the MAP specification only 8-bit encoding is + * allowed - hence the bMessage-body should contain no SMTP headers. (Which makes + * sense, since the info is already present in the bMessage properties.) + * The result is that no information from RFC4356 is needed, since it does not + * describe any mapping between MMS content and E-mail content. + * Suggestion: + * Clearly state in the MAP specification that + * only the actual message content should be included in the . + * Correct the Example to not include the E-mail headers, and in stead show how to + * include a picture or another binary attachment. + * + * If the headers should be included, clearly state which, as the example clearly shows + * that some of the headers should be excluded. + * Additionally it is not clear how to handle attachments. There is a parameter in the + * get message to include attachments, but since only 8-bit encoding is allowed, + * (hence neither base64 nor binary) there is no mechanism to embed the attachment in + * the . + * + * UPDATE: Errata 4176 allows the needed encoding typed inside the + * including Base64 and Quoted Printables - hence it is possible to encode non-us-ascii + * messages - e.g. pictures and utf-8 strings with non-us-ascii content. + * It have not yet been adopted, but since the comments clearly suggest that it is allowed + * to use encoding schemes for non-text parts, it is still not clear what to do about non + * US-ASCII text in the headers. + * */ + + /** + * Encode the bMessage as a MMS + * @return + * @throws UnsupportedEncodingException + */ + public byte[] encodeMms() throws UnsupportedEncodingException + { + ArrayList bodyFragments = new ArrayList(); + StringBuilder sb = new StringBuilder(); + int count = 0; + String mmsBody; + + encoding = "8BIT"; // The encoding used + + encodeHeaders(sb); + 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())); + } + } + } + + mmsBody = sb.toString(); + + if(mmsBody != null) { + String tmpBody = mmsBody.replaceAll("END:MSG", "/END\\:MSG"); // Replace any occurrences of END:MSG with \END:MSG + bodyFragments.add(tmpBody.getBytes("UTF-8")); + } else { + bodyFragments.add(new byte[0]); + } + + return encodeGeneric(bodyFragments); + } + + + /** + * Try to parse the hdrPart string as e-mail headers. + * @param hdrPart The string to parse. + * @return Null if the entire string were e-mail headers. The part of the string in which + * no headers were found. + */ + 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. + */ + if(header.trim() == "") + continue; + String[] headerParts = header.split(":",2); + if(headerParts.length != 2) { + // We treat the remaining content as plain text. + StringBuilder remaining = new StringBuilder(); + for(; i < c; i++) + remaining.append(headers[i]); + + return remaining.toString(); + } + + String headerType = headerParts[0].toUpperCase(); + String headerValue = headerParts[1].trim(); + + // Address headers + /* 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(Arrays.asList(tokens)); + } else if(headerType.contains("TO")) { + Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); + to = new ArrayList(Arrays.asList(tokens)); + } else if(headerType.contains("CC")) { + Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); + cc = new ArrayList(Arrays.asList(tokens)); + } else if(headerType.contains("BCC")) { + Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); + bcc = new ArrayList(Arrays.asList(tokens)); + } else if(headerType.contains("REPLY-TO")) { + Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); + replyTo = new ArrayList(Arrays.asList(tokens)); + } else if(headerType.contains("SUBJECT")) { // Other headers + subject = headerValue; + } else if(headerType.contains("MESSAGE-ID")) { + messageId = headerValue; + } 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 + for(int j=1, n=contentTypeParts.length; j in it's src + newPart.mContentId = headerValue; + } + else if(headerType.contains("CONTENT-DISPOSITION")) { + // This is used if the smil refers to a cid: 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.mData = decodeBody(body, partEncoding, newPart.mCharsetName); + } + + private void parseMmsMimeBody(String body) { + MimePart newPart = addMimePart(); + newPart.mCharsetName = mCharset; + newPart.mData = decodeBody(body, encoding, mCharset); + } + + private byte[] decodeBody(String body, String encoding, String charset) { + if(encoding != null && encoding.toUpperCase().contains("BASE64")) { + return Base64.decode(body, Base64.DEFAULT); + } 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 + } + } + return null; + } + + private void parseMms(String message) { + /* Overall strategy for decoding: + * 1) split on first empty line to extract the header + * 2) unfold and parse headers + * 3) split on boundary to split into parts (or use the remaining as a part, + * if part is not found) + * 4) parse each part + * */ + String[] messageParts; + String[] mimeParts; + String remaining = null; + String messageBody = null; + message = message.replaceAll("\\r\\n[ \\\t]+", ""); // Unfold + messageParts = message.split("\r\n\r\n", 2); // Split the header from the body + if(messageParts.length != 2) { + // Handle entire message as plain text + messageBody = message; + } + else + { + remaining = parseMmsHeaders(messageParts[0]); + // If we have some text not being a header, add it to the message body. + if(remaining != null) { + messageBody = remaining + messageParts[1]; + if(D) Log.d(TAG, "parseMms remaining=" + remaining ); + } else { + messageBody = messageParts[1]; + } + } + + if(boundary == null) + { + // If the boundary is not set, handle as non-multi-part + parseMmsMimeBody(messageBody); + setTextOnly(true); + if(contentType == null) + contentType = "text/plain"; + 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: "=" + * - 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): + * src="filename.jpg" refers to a part with Content-Location: filename.jpg + * src="cid:1234@hest.net" refers to a part with Content-ID:<1234@hest.net>*/ + @Override + public void parseMsgPart(String msgPart) { + parseMms(msgPart); + + } + + @Override + public void parseMsgInit() { + // Not used for e-mail + + } + + @Override + public byte[] encode() throws UnsupportedEncodingException { + return encodeMms(); + } + +} diff --git a/src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java b/src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java deleted file mode 100644 index 0fcba3bbe..000000000 --- a/src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java +++ /dev/null @@ -1,643 +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 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 android.text.util.Rfc822Token; -import android.text.util.Rfc822Tokenizer; -import android.util.Base64; -import android.util.Log; - -public class BluetoothMapbMessageMmsEmail 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 - 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 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("\""); - 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) { - /* 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. */ - - if(contentType != null && - (contentType.toUpperCase().contains("TEXT") || - contentType.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"); - } - 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"); - } - } - if(last) { - sb.append("--").append(boundaryTag).append("--").append("\r\n"); - } - } - - 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")) { - /* 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"); - else - sb.append("<").append("attachment").append(">\r\n"); - } - } - } - - private long date = INVALID_VALUE; - private String subject = null; - private ArrayList from = null; // Shall not be empty - private ArrayList sender = null; // Shall not be empty - private ArrayList to = null; // Shall not be empty - private ArrayList cc = null; // Can be empty - private ArrayList bcc = null; // Can be empty - private ArrayList replyTo = null;// Can be empty - private String messageId = null; - private ArrayList parts = null; - private String contentType = null; - private String boundary = null; - private boolean textOnly = false; - private boolean includeAttachments; - private boolean hasHeaders = false; - private String encoding = null; - - private String getBoundary() { - if(boundary == null) - boundary = "----" + UUID.randomUUID(); - return boundary; - } - - /** - * @return the parts - */ - public ArrayList getMimeParts() { - return parts; - } - - public MimePart addMimePart() { - if(parts == null) - parts = new ArrayList(); - MimePart newPart = new MimePart(); - parts.add(newPart); - return newPart; - } - public String getDateString() { - SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); - Date dateObj = new Date(date); - return format.format(dateObj); // Format according to RFC 2822 page 14 - } - public long getDate() { - return date; - } - public void setDate(long date) { - this.date = date; - } - public String getSubject() { - return subject; - } - public void setSubject(String subject) { - this.subject = subject; - } - public ArrayList getFrom() { - return from; - } - public void setFrom(ArrayList from) { - this.from = from; - } - public void addFrom(String name, String address) { - if(this.from == null) - this.from = new ArrayList(1); - this.from.add(new Rfc822Token(name, address, null)); - } - public ArrayList getSender() { - return sender; - } - public void setSender(ArrayList sender) { - this.sender = sender; - } - public void addSender(String name, String address) { - if(this.sender == null) - this.sender = new ArrayList(1); - this.sender.add(new Rfc822Token(name,address,null)); - } - public ArrayList getTo() { - return to; - } - public void setTo(ArrayList to) { - this.to = to; - } - public void addTo(String name, String address) { - if(this.to == null) - this.to = new ArrayList(1); - this.to.add(new Rfc822Token(name, address, null)); - } - public ArrayList getCc() { - return cc; - } - public void setCc(ArrayList cc) { - this.cc = cc; - } - public void addCc(String name, String address) { - if(this.cc == null) - this.cc = new ArrayList(1); - this.cc.add(new Rfc822Token(name, address, null)); - } - public ArrayList getBcc() { - return bcc; - } - public void setBcc(ArrayList bcc) { - this.bcc = bcc; - } - public void addBcc(String name, String address) { - if(this.bcc == null) - this.bcc = new ArrayList(1); - this.bcc.add(new Rfc822Token(name, address, null)); - } - public ArrayList getReplyTo() { - return replyTo; - } - public void setReplyTo(ArrayList replyTo) { - this.replyTo = replyTo; - } - public void addReplyTo(String name, String address) { - if(this.replyTo == null) - this.replyTo = new ArrayList(1); - this.replyTo.add(new Rfc822Token(name, address, null)); - } - public void setMessageId(String messageId) { - this.messageId = messageId; - } - public String getMessageId() { - return messageId; - } - public void setContentType(String contentType) { - this.contentType = contentType; - } - public String getContentType() { - return contentType; - } - public void setTextOnly(boolean textOnly) { - this.textOnly = textOnly; - } - public boolean getTextOnly() { - return textOnly; - } - public void setIncludeAttachments(boolean includeAttachments) { - this.includeAttachments = includeAttachments; - } - public boolean getIncludeAttachments() { - return includeAttachments; - } - public void updateCharset() { - charset = null; - for(MimePart part : parts) { - if(part.contentType != null && - part.contentType.toUpperCase().contains("TEXT")) { - charset = "UTF-8"; - break; - } - } - } - public int getSize() { - int message_size = 0; - for(MimePart part : parts) { - message_size += part.data.length; - } - return message_size; - } - - /** - * Encode an address header, and perform folding if needed. - * @param sb The stringBuilder to write to - * @param headerName The RFC 2822 header name - * @param addresses the reformatted address substrings to encode. - */ - public void encodeHeaderAddresses(StringBuilder sb, String headerName, - ArrayList addresses) { - /* TODO: Do we need to encode the addresses if they contain illegal characters? - * This depends of the outcome of errata 4176. The current spec. states to use UTF-8 - * where possible, but the RFCs states to use US-ASCII for the headers - hence encoding - * would be needed to support non US-ASCII characters. But the MAP spec states not to - * use any encoding... */ - int partLength, lineLength = 0; - lineLength += headerName.getBytes().length; - sb.append(headerName); - for(Rfc822Token address : addresses) { - partLength = address.toString().getBytes().length+1; - // Add folding if needed - if(lineLength + partLength >= 998) // max line length in RFC2822 - { - sb.append("\r\n "); // Append a FWS (folding whitespace) - lineLength = 0; - } - sb.append(address.toString()).append(";"); - lineLength += partLength; - } - sb.append("\r\n"); - } - - public void encodeHeaders(StringBuilder sb) throws UnsupportedEncodingException - { - /* TODO: From RFC-4356 - about the RFC-(2)822 headers: - * "Current Internet Message format requires that only 7-bit US-ASCII - * characters be present in headers. Non-7-bit characters in an address - * domain must be encoded with [IDN]. If there are any non-7-bit - * characters in the local part of an address, the message MUST be - * rejected. Non-7-bit characters elsewhere in a header MUST be encoded - * according to [Hdr-Enc]." - * We need to add the address encoding in encodeHeaderAddresses, but it is not - * straight forward, as it is unclear how to do this. */ - if (date != INVALID_VALUE) - 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 . We let the MAP specification - * take precedence above the RFC-2822. The code to - */ - /* If we are to use US-ASCII anyway, here are the code for it. - 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 - sb.append("Subject:").append("=?utf-8?B?"); - sb.append(Base64.encodeToString(subject.getBytes("utf-8"), Base64.DEFAULT)); - sb.append("?=\r\n"); - }*/ - if (subject != null) - sb.append("Subject: ").append(subject).append("\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"); - if(to != null) - encodeHeaderAddresses(sb, "To: ", to); // This includes folding if needed. - if(cc != null) - encodeHeaderAddresses(sb, "Cc: ", cc); // This includes folding if needed. - if(bcc != null) - encodeHeaderAddresses(sb, "Bcc: ", bcc); // This includes folding if needed. - if(replyTo != null) - encodeHeaderAddresses(sb, "Reply-To: ", replyTo); // This includes folding if needed. - if(includeAttachments == true) - { - if(messageId != null) - sb.append("Message-Id: ").append(messageId).append("\r\n"); - if(contentType != null) - sb.append("Content-Type: ").append(contentType).append("; boundary=").append(getBoundary()).append("\r\n"); - } - sb.append("\r\n"); // If no headers exists, we still need two CRLF, hence keep it out of the if above. - } - - /* Notes on MMS - * ------------ - * According to rfc4356 all headers of a MMS converted to an E-mail must use - * 7-bit encoding. According the the MAP specification only 8-bit encoding is - * allowed - hence the bMessage-body should contain no SMTP headers. (Which makes - * sense, since the info is already present in the bMessage properties.) - * The result is that no information from RFC4356 is needed, since it does not - * describe any mapping between MMS content and E-mail content. - * Suggestion: - * Clearly state in the MAP specification that - * only the actual message content should be included in the . - * Correct the Example to not include the E-mail headers, and in stead show how to - * include a picture or another binary attachment. - * - * If the headers should be included, clearly state which, as the example clearly shows - * that some of the headers should be excluded. - * Additionally it is not clear how to handle attachments. There is a parameter in the - * get message to include attachments, but since only 8-bit encoding is allowed, - * (hence neither base64 nor binary) there is no mechanism to embed the attachment in - * the . - * - * UPDATE: Errata 4176 allows the needed encoding typed inside the - * including Base64 and Quoted Printables - hence it is possible to encode non-us-ascii - * messages - e.g. pictures and utf-8 strings with non-us-ascii content. - * It have not yet been adopted, but since the comments clearly suggest that it is allowed - * to use encoding schemes for non-text parts, it is still not clear what to do about non - * US-ASCII text in the headers. - * */ - - /** - * Encode the bMessage as a MMS - * @return - * @throws UnsupportedEncodingException - */ - public byte[] encodeMms() throws UnsupportedEncodingException - { - ArrayList bodyFragments = new ArrayList(); - StringBuilder sb = new StringBuilder(); - int count = 0; - String mmsBody; - - 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())); - } - } - - mmsBody = sb.toString(); - - if(mmsBody != null) { - String tmpBody = mmsBody.replaceAll("END:MSG", "/END\\:MSG"); // Replace any occurrences of END:MSG with \END:MSG - bodyFragments.add(tmpBody.getBytes("UTF-8")); - } else { - bodyFragments.add(new byte[0]); - } - - return encodeGeneric(bodyFragments); - } - - - /** - * Try to parse the hdrPart string as e-mail headers. - * @param hdrPart The string to parse. - * @return Null if the entire string were e-mail headers. The part of the string in which - * no headers were found. - */ - private String parseMmsHeaders(String hdrPart) { - String[] headers = hdrPart.split("\r\n"); - String header; - hasHeaders = false; - - for(int i = 0, c = headers.length; i < c; i++) { - header = headers[i]; - - /* 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. - */ - if(header.trim() == "") - continue; - String[] headerParts = header.split(":",2); - if(headerParts.length != 2) { - // We treat the remaining content as plain text. - StringBuilder remaining = new StringBuilder(); - for(; i < c; i++) - remaining.append(headers[i]); - - return remaining.toString(); - } - - String headerType = headerParts[0].toUpperCase(); - 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(headerType.contains("FROM")) { - Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); - from = new ArrayList(Arrays.asList(tokens)); - } - else if(headerType.contains("TO")) { - Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); - to = new ArrayList(Arrays.asList(tokens)); - } - else if(headerType.contains("CC")) { - Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); - cc = new ArrayList(Arrays.asList(tokens)); - } - else if(headerType.contains("BCC")) { - Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); - bcc = new ArrayList(Arrays.asList(tokens)); - } - else if(headerType.contains("REPLY-TO")) { - Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); - replyTo = new ArrayList(Arrays.asList(tokens)); - }// Other headers - else if(headerType.contains("SUBJECT")) { - subject = headerValue; - } - 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")) { - String[] contentTypeParts = headerValue.split(";"); - contentType = contentTypeParts[0]; - // Extract the boundary if it exists - for(int j=1, n=contentTypeParts.length; j in it's src= - newPart.contentId = headerValue; - } - else if(headerType.contains("CONTENT-DISPOSITION")) { - // This is used if the smil refers to a cid: in it's src= - newPart.contentDisposition = headerValue; - } - else { - if(D) Log.w(TAG,"Skipping unknown part-header: " + headerType + " (" + header + ")"); - } - } - } - // Now for the body - newPart.data = decodeBody(body, partEncoding); - } - - private void parseMmsMimeBody(String body) { - MimePart newPart = addMimePart(); - newPart.data = decodeBody(body, encoding); - } - - private byte[] decodeBody(String body, String encoding) { - if(encoding != null && encoding.toUpperCase().contains("BASE64")) { - return Base64.decode(body, Base64.DEFAULT); - } 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 - } - } - return null; - } - - private void parseMms(String message) { - /* Overall strategy for decoding: - * 1) split on first empty line to extract the header - * 2) unfold and parse headers - * 3) split on boundary to split into parts (or use the remaining as a part, - * if part is not found) - * 4) parse each part - * */ - String[] messageParts; - String[] mimeParts; - String remaining = null; - String messageBody = null; - message = message.replaceAll("\\r\\n[ \\\t]+", ""); // Unfold - messageParts = message.split("\r\n\r\n", 2); // Split the header from the body - if(messageParts.length != 2) { - // Handle entire message as plain text - messageBody = message; - } - else - { - remaining = parseMmsHeaders(messageParts[0]); - // If we have some text not being a header, add it to the message body. - if(remaining != null) { - messageBody = remaining + messageParts[1]; - } - else { - messageBody = messageParts[1]; - } - } - - if(boundary == null) - { - // If the boundary is not set, handle as non-multi-part - parseMmsMimeBody(messageBody); - setTextOnly(true); - if(contentType == null) - contentType = "text/plain"; - parts.get(0).contentType = contentType; - } - else - { - mimeParts = messageBody.split("--" + boundary); - for(int i = 1; i < mimeParts.length - 1; i++) { - String part = mimeParts[i]; - if (part != null && (part.length() > 0)) - parseMmsMimePart(part); - } - } - } - - /* Notes on SMIL decoding (from http://tools.ietf.org/html/rfc2557): - * src="filename.jpg" refers to a part with Content-Location: filename.jpg - * src="cid:1234@hest.net" refers to a part with Content-ID:<1234@hest.net>*/ - @Override - public void parseMsgPart(String msgPart) { - parseMms(msgPart); - - } - - @Override - public void parseMsgInit() { - // Not used for e-mail - - } - - @Override - public byte[] encode() throws UnsupportedEncodingException { - return encodeMms(); - } - -} 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 smsBodyPdus = null; - private String smsBody = null; + private ArrayList mSmsBodyPdus = null; + private String mSmsBody = null; public void setSmsBodyPdus(ArrayList 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 @@ + - - @@ -37,16 +36,21 @@ - + + + + - + + 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); -- cgit v1.2.3