diff options
author | Matthew Xie <mattx@google.com> | 2013-07-18 18:18:36 -0700 |
---|---|---|
committer | Zhihai Xu <zhihaixu@google.com> | 2013-08-09 18:44:53 -0700 |
commit | fd6603b8bf9ed72dcc8bd59aaef3209251b6e17c (patch) | |
tree | fecaf3c95adce97dc5176cc341d903fe488d5edf | |
parent | bb1ac417208c8e283f9b5b49f4413856500ed0f9 (diff) | |
download | android_packages_apps_Bluetooth-fd6603b8bf9ed72dcc8bd59aaef3209251b6e17c.tar.gz android_packages_apps_Bluetooth-fd6603b8bf9ed72dcc8bd59aaef3209251b6e17c.tar.bz2 android_packages_apps_Bluetooth-fd6603b8bf9ed72dcc8bd59aaef3209251b6e17c.zip |
Bluetooth MAP profile - sms and mms support initial check-in
bug:10116530
Change-Id: If9ce878d71c1e1b12416014c433da03b3033e158
23 files changed, 8583 insertions, 1 deletions
diff --git a/Android.mk b/Android.mk index b0606df35..4c24df918 100644 --- a/Android.mk +++ b/Android.mk @@ -10,7 +10,7 @@ LOCAL_PACKAGE_NAME := Bluetooth LOCAL_CERTIFICATE := platform LOCAL_JNI_SHARED_LIBRARIES := libbluetooth_jni -LOCAL_JAVA_LIBRARIES := javax.obex +LOCAL_JAVA_LIBRARIES := javax.obex telephony-common mms-common LOCAL_STATIC_JAVA_LIBRARIES := com.android.vcard LOCAL_REQUIRED_MODULES := libbluetooth_jni bluetooth.default diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4a363dd57..017bd88c0 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -50,6 +50,13 @@ <uses-permission android:name="android.permission.MANAGE_USERS"/> <uses-permission android:name="com.google.android.gallery3d.permission.GALLERY_PROVIDER"/> <uses-permission android:name="com.android.gallery3d.permission.GALLERY_PROVIDER"/> + <uses-permission android:name="android.permission.MMS_SEND_OUTBOX_MSG"/> + <uses-permission android:name="android.permission.RECEIVE_SMS" /> + <uses-permission android:name="android.permission.READ_PHONE_STATE" /> + <uses-permission android:name="android.permission.SEND_SMS" /> + <uses-permission android:name="android.permission.READ_SMS" /> + <uses-permission android:name="android.permission.WRITE_SMS" /> + <uses-permission android:name="android.permission.READ_CONTACTS" /> <!-- For PBAP Owner Vcard Info --> <uses-permission android:name="android.permission.READ_PROFILE"/> @@ -220,6 +227,34 @@ <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </receiver> + <activity android:name=".map.BluetoothMapActivity" + android:process="@string/process" + android:excludeFromRecents="true" + android:theme="@*android:style/Theme.Holo.Dialog.Alert" + android:enabled="@bool/profile_supported_map"> + <intent-filter> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + <service + android:process="@string/process" + android:name=".map.BluetoothMapService" + android:enabled="@bool/profile_supported_map" > + <intent-filter> + <action android:name="android.bluetooth.IBluetoothMap" /> + </intent-filter> + </service> + <receiver + android:process="@string/process" + android:exported="true" + android:name=".map.BluetoothMapReceiver" + android:enabled="@bool/profile_supported_map"> + <intent-filter> + <action android:name="android.bluetooth.adapter.action.STATE_CHANGED"/> + <action android:name="android.bluetooth.device.action.CONNECTION_ACCESS_REPLY" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </receiver> <service android:process="@string/process" android:name = ".gatt.GattService" diff --git a/res/values/config.xml b/res/values/config.xml index af9092660..24eeb2c53 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -23,4 +23,5 @@ <bool name="profile_supported_gatt">true</bool> <bool name="pbap_include_photos_in_vcard">false</bool> <bool name="pbap_use_profile_for_owner_vcard">true</bool> + <bool name="profile_supported_map">true</bool> </resources> diff --git a/res/values/strings_map.xml b/res/values/strings_map.xml new file mode 100644 index 000000000..ba30b9969 --- /dev/null +++ b/res/values/strings_map.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="map_session_key_dialog_title">Type session key for %1$s</string> + <string name="map_session_key_dialog_header">Bluetooth session key required</string> + <string name="map_acceptance_timeout_message">There was time out to accept connection with %1$s</string> + <string name="map_authentication_timeout_message">There was time out to input session key with %1$s</string> + <string name="map_auth_notif_ticker">Obex authentication request</string> + <!-- Notification title when a Bluetooth device wants to pair with us --> + <string name="map_auth_notif_title">Session Key</string> + <!-- Notification message when a Bluetooth device wants to pair with us --> + <string name="map_auth_notif_message">Type session key for %1$s</string> + <string name="map_defaultname">Carkit</string> + <string name="map_unknownName">Unknown name</string> + <string name="map_localPhoneName">My name</string> + <string name="map_defaultnumber">000000</string> +</resources> diff --git a/src/com/android/bluetooth/map/BluetoothMapActivity.java b/src/com/android/bluetooth/map/BluetoothMapActivity.java new file mode 100644 index 000000000..d415eef21 --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapActivity.java @@ -0,0 +1,284 @@ + +/* +* 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 com.android.bluetooth.R; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.preference.Preference; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Button; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.text.InputFilter.LengthFilter; + +import com.android.internal.app.AlertActivity; +import com.android.internal.app.AlertController; + +/** + * MapActivity shows two dialogues: One for accepting incoming map request and + * the other prompts the user to enter a session key for authentication with a + * remote Bluetooth device. + */ +public class BluetoothMapActivity extends AlertActivity implements + DialogInterface.OnClickListener, Preference.OnPreferenceChangeListener, TextWatcher { + private static final String TAG = "BluetoothMapActivity"; + + private static final boolean V = BluetoothMapService.VERBOSE; + + private static final int BLUETOOTH_OBEX_AUTHKEY_MAX_LENGTH = 16; + + private static final int DIALOG_YES_NO_AUTH = 1; + + private static final String KEY_USER_TIMEOUT = "user_timeout"; + + private View mView; + + private EditText mKeyView; + + private TextView messageView; + + private String mSessionKey = ""; + + private int mCurrentDialog; + + private Button mOkButton; + + private CheckBox mAlwaysAllowed; + + private boolean mTimeout = false; + + private boolean mAlwaysAllowedValue = true; + + private static final int DISMISS_TIMEOUT_DIALOG = 0; + + private static final int DISMISS_TIMEOUT_DIALOG_VALUE = 2000; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!BluetoothMapService.USER_CONFIRM_TIMEOUT_ACTION.equals(intent.getAction())) { + return; + } + onTimeout(); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent i = getIntent(); + String action = i.getAction(); + if (action.equals(BluetoothMapService.AUTH_CHALL_ACTION)) { + showMapDialog(DIALOG_YES_NO_AUTH); + mCurrentDialog = DIALOG_YES_NO_AUTH; + } else { + Log.e(TAG, "Error: this activity may be started only with intent " + + "MAP_ACCESS_REQUEST or MAP_AUTH_CHALL "); + finish(); + } + registerReceiver(mReceiver, new IntentFilter( + BluetoothMapService.USER_CONFIRM_TIMEOUT_ACTION)); + } + + private void showMapDialog(int id) { + final AlertController.AlertParams p = mAlertParams; + switch (id) { + case DIALOG_YES_NO_AUTH: + p.mIconId = android.R.drawable.ic_dialog_info; + p.mTitle = getString(R.string.map_session_key_dialog_header); + p.mView = createView(DIALOG_YES_NO_AUTH); + p.mPositiveButtonText = getString(android.R.string.ok); + p.mPositiveButtonListener = this; + p.mNegativeButtonText = getString(android.R.string.cancel); + p.mNegativeButtonListener = this; + setupAlert(); + mOkButton = mAlert.getButton(DialogInterface.BUTTON_POSITIVE); + mOkButton.setEnabled(false); + break; + default: + break; + } + } + + private String createDisplayText(final int id) { + String mRemoteName = BluetoothMapService.getRemoteDeviceName(); + switch (id) { + case DIALOG_YES_NO_AUTH: + String mMessage2 = getString(R.string.map_session_key_dialog_title, mRemoteName); + return mMessage2; + default: + return null; + } + } + + private View createView(final int id) { + switch (id) { + case DIALOG_YES_NO_AUTH: + mView = getLayoutInflater().inflate(R.layout.auth, null); + messageView = (TextView)mView.findViewById(R.id.message); + messageView.setText(createDisplayText(id)); + mKeyView = (EditText)mView.findViewById(R.id.text); + mKeyView.addTextChangedListener(this); + mKeyView.setFilters(new InputFilter[] { + new LengthFilter(BLUETOOTH_OBEX_AUTHKEY_MAX_LENGTH) + }); + return mView; + default: + return null; + } + } + + private void onPositive() { + if (!mTimeout) { + if (mCurrentDialog == DIALOG_YES_NO_AUTH) { + sendIntentToReceiver(BluetoothMapService.AUTH_RESPONSE_ACTION, + BluetoothMapService.EXTRA_SESSION_KEY, mSessionKey); + mKeyView.removeTextChangedListener(this); + } + } + mTimeout = false; + finish(); + } + + private void onNegative() { + if (mCurrentDialog == DIALOG_YES_NO_AUTH) { + sendIntentToReceiver(BluetoothMapService.AUTH_CANCELLED_ACTION, null, null); + mKeyView.removeTextChangedListener(this); + } + finish(); + } + + private void sendIntentToReceiver(final String intentName, final String extraName, + final String extraValue) { + Intent intent = new Intent(intentName); + intent.setClassName(BluetoothMapService.THIS_PACKAGE_NAME, BluetoothMapReceiver.class + .getName()); + if (extraName != null) { + intent.putExtra(extraName, extraValue); + } + sendBroadcast(intent); + } + + private void sendIntentToReceiver(final String intentName, final String extraName, + final boolean extraValue) { + Intent intent = new Intent(intentName); + intent.setClassName(BluetoothMapService.THIS_PACKAGE_NAME, BluetoothMapReceiver.class + .getName()); + if (extraName != null) { + intent.putExtra(extraName, extraValue); + } + sendBroadcast(intent); + } + + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + if (mCurrentDialog == DIALOG_YES_NO_AUTH) { + mSessionKey = mKeyView.getText().toString(); + } + onPositive(); + break; + + case DialogInterface.BUTTON_NEGATIVE: + onNegative(); + break; + default: + break; + } + } + + private void onTimeout() { + mTimeout = true; + if (mCurrentDialog == DIALOG_YES_NO_AUTH) { + messageView.setText(getString(R.string.map_authentication_timeout_message, + BluetoothMapService.getRemoteDeviceName())); + mKeyView.setVisibility(View.GONE); + mKeyView.clearFocus(); + mKeyView.removeTextChangedListener(this); + mOkButton.setEnabled(true); + mAlert.getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.GONE); + } + + mTimeoutHandler.sendMessageDelayed(mTimeoutHandler.obtainMessage(DISMISS_TIMEOUT_DIALOG), + DISMISS_TIMEOUT_DIALOG_VALUE); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mTimeout = savedInstanceState.getBoolean(KEY_USER_TIMEOUT); + if (V) Log.v(TAG, "onRestoreInstanceState() mTimeout: " + mTimeout); + if (mTimeout) { + onTimeout(); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(KEY_USER_TIMEOUT, mTimeout); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + unregisterReceiver(mReceiver); + } + + public boolean onPreferenceChange(Preference preference, Object newValue) { + return true; + } + + public void beforeTextChanged(CharSequence s, int start, int before, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + public void afterTextChanged(android.text.Editable s) { + if (s.length() > 0) { + mOkButton.setEnabled(true); + } + } + + private final Handler mTimeoutHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case DISMISS_TIMEOUT_DIALOG: + if (V) Log.v(TAG, "Received DISMISS_TIMEOUT_DIALOG msg."); + finish(); + break; + default: + break; + } + } + }; +} diff --git a/src/com/android/bluetooth/map/BluetoothMapAppParams.java b/src/com/android/bluetooth/map/BluetoothMapAppParams.java new file mode 100644 index 000000000..e55c61185 --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapAppParams.java @@ -0,0 +1,704 @@ +/* +* 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.ByteBuffer; +import java.nio.ByteOrder; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; + +import android.util.Log; + +/** + * This class encapsulates the appParams needed for MAP. + */ +public class BluetoothMapAppParams { + + private static final String TAG = "BluetoothMapAppParams"; + + private static final int MAX_LIST_COUNT = 0x01; + private static final int MAX_LIST_COUNT_LEN = 0x02; //, 0x0000, 0xFFFF), + private static final int START_OFFSET = 0x02; + private static final int START_OFFSET_LEN = 0x02; //, 0x0000, 0xFFFF), + private static final int FILTER_MESSAGE_TYPE = 0x03; + private static final int FILTER_MESSAGE_TYPE_LEN = 0x01; //, 0x0000, 0x000f), + private static final int FILTER_PERIOD_BEGIN = 0x04; + private static final int FILTER_PERIOD_END = 0x05; + private static final int FILTER_READ_STATUS = 0x06; + private static final int FILTER_READ_STATUS_LEN = 0x01; //, 0x0000, 0x0002), + private static final int FILTER_RECIPIENT = 0x07; + private static final int FILTER_ORIGINATOR = 0x08; + private static final int FILTER_PRIORITY = 0x09; + private static final int FILTER_PRIORITY_LEN = 0x01; //, 0x0000, 0x0002), + private static final int ATTACHMENT = 0x0A; + private static final int ATTACHMENT_LEN = 0x01; //, 0x0000, 0x0001), + private static final int TRANSPARENT = 0x0B; + private static final int TRANSPARENT_LEN = 0x01; //, 0x0000, 0x0001), + private static final int RETRY = 0x0C; + private static final int RETRY_LEN = 0x01; //, 0x0000, 0x0001), + private static final int NEW_MESSAGE = 0x0D; + private static final int NEW_MESSAGE_LEN = 0x01; //, 0x0000, 0x0001), + private static final int NOTIFICATION_STATUS = 0x0E; + private static final int NOTIFICATION_STATUS_LEN = 0x02; //, 0x0000, 0xFFFF), + private static final int MAS_INSTANCE_ID = 0x0F; + private static final int MAS_INSTANCE_ID_LEN = 0x01; //, 0x0000, 0x00FF), + private static final int PARAMETER_MASK = 0x10; + private static final int PARAMETER_MASK_LEN = 0x04; //, 0x0000, 0x0000), + private static final int FOLDER_LISTING_SIZE = 0x11; + private static final int FOLDER_LISTING_SIZE_LEN = 0x02; //, 0x0000, 0xFFFF), + private static final int MESSAGE_LISTING_SIZE = 0x12; + private static final int MESSAGE_LISTING_SIZE_LEN = 0x02; //, 0x0000, 0xFFFF), + private static final int SUBJECT_LENGTH = 0x13; + private static final int SUBJECT_LENGTH_LEN = 0x01; //, 0x0000, 0x00FF), + private static final int CHARSET = 0x14; + private static final int CHARSET_LEN = 0x01; //, 0x0000, 0x0001), + private static final int FRACTION_REQUEST = 0x15; + private static final int FRACTION_REQUEST_LEN = 0x01; //, 0x0000, 0x0001), + private static final int FRACTION_DELIVER = 0x16; + private static final int FRACTION_DELIVER_LEN = 0x01; //, 0x0000, 0x0001), + private static final int STATUS_INDICATOR = 0x17; + private static final int STATUS_INDICATOR_LEN = 0x01; //, 0x0000, 0x0001), + private static final int STATUS_VALUE = 0x18; + private static final int STATUS_VALUE_LEN = 0x01; //, 0x0000, 0x0001), + private static final int MSE_TIME = 0x19; + + public static final int INVALID_VALUE_PARAMETER = -1; + public static final int NOTIFICATION_STATUS_NO = 0; + public static final int NOTIFICATION_STATUS_YES = 1; + public static final int STATUS_INDICATOR_READ = 0; + public static final int STATUS_INDICATOR_DELETED = 1; + public static final int STATUS_VALUE_YES = 1; + public static final int STATUS_VALUE_NO = 0; + public static final int CHARSET_NATIVE = 0; + public static final int CHARSET_UTF8 = 1; + + 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; + + /** + * Default constructor, used to build an application parameter object to be + * encoded. By default the member variables will be initialized to + * {@link INVALID_VALUE_PARAMETER} for values, and empty strings for String + * typed members. + */ + public BluetoothMapAppParams() { + } + + /** + * Creates an application parameter object based on a application parameter + * OBEX header. The content of the {@link appParam} byte array will be + * parsed, and its content will be stored in the member variables. + * {@link INVALID_VALUE_PARAMETER} can be used to determine if a value is + * set or not, where strings will be empty, if {@link appParam} did not + * contain the parameter. + * + * @param appParams + * the byte array containing the application parameters OBEX + * header + * @throws IllegalArgumentException + * when a parameter does not respect the valid ranges specified + * in the MAP spec. + * @throws ParseException + * if a parameter string if formated incorrectly. + */ + public BluetoothMapAppParams(final byte[] appParams) + throws IllegalArgumentException, ParseException { + ParseParams(appParams); + } + + /** + * Parse an application parameter OBEX header stored in a ByteArray. + * + * @param appParams + * the byte array containing the application parameters OBEX + * header + * @throws IllegalArgumentException + * when a parameter does not respect the valid ranges specified + * in the MAP spec. + * @throws ParseException + * if a parameter string if formated incorrectly. + */ + private void ParseParams(final byte[] appParams) throws ParseException, + IllegalArgumentException { + int i = 0; + int tagId, tagLength; + ByteBuffer appParamBuf = ByteBuffer.wrap(appParams); + appParamBuf.order(ByteOrder.BIG_ENDIAN); + while (i < appParams.length) { + tagId = appParams[i++] & 0xff; // Convert to unsigned to support values above 127 + tagLength = appParams[i++] & 0xff; // Convert to unsigned to support values above 127 + switch (tagId) { + case MAX_LIST_COUNT: + if (tagLength != MAX_LIST_COUNT_LEN) { + Log.w(TAG, "MAX_LIST_COUNT: Wrong length received: " + tagLength + + " expected: " + MAX_LIST_COUNT_LEN); + break; + } + setMaxListCount(appParamBuf.getShort(i) & 0xffff); // Make it unsigned + break; + case START_OFFSET: + if (tagLength != START_OFFSET_LEN) { + Log.w(TAG, "START_OFFSET: Wrong length received: " + tagLength + " expected: " + + START_OFFSET_LEN); + break; + } + setStartOffset(appParamBuf.getShort(i) & 0xffff); // Make it unsigned + break; + case FILTER_MESSAGE_TYPE: + setFilterMessageType(appParams[i] & 0x0f); + break; + case FILTER_PERIOD_BEGIN: + setFilterPeriodBegin(new String(appParams, i, tagLength)); + break; + case FILTER_PERIOD_END: + setFilterPeriodEnd(new String(appParams, i, tagLength)); + break; + case FILTER_READ_STATUS: + setFilterReadStatus(appParams[i] & 0x03); // Lower two bits + break; + case FILTER_RECIPIENT: + setFilterRecipient(new String(appParams, i, tagLength)); + break; + case FILTER_ORIGINATOR: + setFilterOriginator(new String(appParams, i, tagLength)); + break; + case FILTER_PRIORITY: + setFilterPriority(appParams[i] & 0x03); // Lower two bits + break; + case ATTACHMENT: + setAttachment(appParams[i] & 0x01); // Lower bit + break; + case TRANSPARENT: + setTransparent(appParams[i] & 0x01); // Lower bit + break; + case RETRY: + setRetry(appParams[i] & 0x01); // Lower bit + break; + case NEW_MESSAGE: + setNewMessage(appParams[i] & 0x01); // Lower bit + break; + case NOTIFICATION_STATUS: + setNotificationStatus(appParams[i] & 0x01); // Lower bit + break; + case MAS_INSTANCE_ID: + setMasInstanceId(appParams[i] & 0xff); + break; + case PARAMETER_MASK: + setParameterMask(appParamBuf.getInt(i) & 0xffffffffL); // Make it unsigned + break; + case FOLDER_LISTING_SIZE: + setFolderListingSize(appParamBuf.getShort(i) & 0xffff); // Make it unsigned + break; + case MESSAGE_LISTING_SIZE: + setMessageListingSize(appParamBuf.getShort(i) & 0xffff); // Make it unsigned + break; + case SUBJECT_LENGTH: + setSubjectLength(appParams[i] & 0xff); + break; + case CHARSET: + setCharset(appParams[i] & 0x01); // Lower bit + break; + case FRACTION_REQUEST: + setFractionRequest(appParams[i] & 0x01); // Lower bit + break; + case FRACTION_DELIVER: + setFractionDeliver(appParams[i] & 0x01); // Lower bit + break; + case STATUS_INDICATOR: + setStatusIndicator(appParams[i] & 0x01); // Lower bit + break; + case STATUS_VALUE: + setStatusValue(appParams[i] & 0x01); // Lower bit + break; + case MSE_TIME: + setMseTime(new String(appParams, i, tagLength)); + break; + default: + // Just skip unknown Tags, no need to report error + Log.w(TAG, "Unknown TagId received ( 0x" + Integer.toString(tagId, 16) + + "), skipping..."); + break; + } + i += tagLength; // Offset to next TagId + } + } + + /** + * Get the approximate length needed to store the appParameters in a byte + * array. + * + * @return the length in bytes + * @throws UnsupportedEncodingException + * if the platform does not support UTF-8 encoding. + */ + private int getParamMaxLength() throws UnsupportedEncodingException { + int length = 0; + length += 25 * 2; // tagId + tagLength + length += 27; // fixed sizes + length += getFilterPeriodBegin() == INVALID_VALUE_PARAMETER ? 0 : 15; + length += getFilterPeriodEnd() == INVALID_VALUE_PARAMETER ? 0 : 15; + if (getFilterRecipient() != null) + length += getFilterRecipient().getBytes("UTF-8").length; + if (getFilterOriginator() != null) + length += getFilterOriginator().getBytes("UTF-8").length; + length += getMseTime() == INVALID_VALUE_PARAMETER ? 0 : 20; + return length; + } + + /** + * Encode the application parameter object to a byte array. + * + * @return a byte Array representation of the application parameter object. + * @throws UnsupportedEncodingException + * if the platform does not support UTF-8 encoding. + */ + public byte[] EncodeParams() throws UnsupportedEncodingException { + ByteBuffer appParamBuf = ByteBuffer.allocate(getParamMaxLength()); + appParamBuf.order(ByteOrder.BIG_ENDIAN); + byte[] retBuf; + + if (getMaxListCount() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) MAX_LIST_COUNT); + appParamBuf.put((byte) MAX_LIST_COUNT_LEN); + appParamBuf.putShort((short) getMaxListCount()); + } + if (getStartOffset() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) START_OFFSET); + appParamBuf.put((byte) START_OFFSET_LEN); + appParamBuf.putShort((short) getStartOffset()); + } + if (getFilterMessageType() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) FILTER_MESSAGE_TYPE); + appParamBuf.put((byte) FILTER_MESSAGE_TYPE_LEN); + appParamBuf.put((byte) getFilterMessageType()); + } + if (getFilterPeriodBegin() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) FILTER_PERIOD_BEGIN); + appParamBuf.put((byte) getFilterPeriodBeginString().getBytes("UTF-8").length); + appParamBuf.put(getFilterPeriodBeginString().getBytes("UTF-8")); + } + if (getFilterPeriodEnd() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) FILTER_PERIOD_END); + appParamBuf.put((byte) getFilterPeriodEndString().getBytes("UTF-8").length); + appParamBuf.put(getFilterPeriodEndString().getBytes("UTF-8")); + } + if (getFilterReadStatus() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) FILTER_READ_STATUS); + appParamBuf.put((byte) FILTER_READ_STATUS_LEN); + appParamBuf.put((byte) getFilterReadStatus()); + } + if (getFilterRecipient() != null) { + appParamBuf.put((byte) FILTER_RECIPIENT); + appParamBuf.put((byte) getFilterRecipient().getBytes("UTF-8").length); + appParamBuf.put(getFilterRecipient().getBytes("UTF-8")); + } + if (getFilterOriginator() != null) { + appParamBuf.put((byte) FILTER_ORIGINATOR); + appParamBuf.put((byte) getFilterOriginator().getBytes("UTF-8").length); + appParamBuf.put(getFilterOriginator().getBytes("UTF-8")); + } + if (getFilterPriority() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) FILTER_PRIORITY); + appParamBuf.put((byte) FILTER_PRIORITY_LEN); + appParamBuf.put((byte) getFilterPriority()); + } + if (getAttachment() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) ATTACHMENT); + appParamBuf.put((byte) ATTACHMENT_LEN); + appParamBuf.put((byte) getAttachment()); + } + if (getTransparent() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) TRANSPARENT); + appParamBuf.put((byte) TRANSPARENT_LEN); + appParamBuf.put((byte) getTransparent()); + } + if (getRetry() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) RETRY); + appParamBuf.put((byte) RETRY_LEN); + appParamBuf.put((byte) getRetry()); + } + if (getNewMessage() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) NEW_MESSAGE); + appParamBuf.put((byte) NEW_MESSAGE_LEN); + appParamBuf.put((byte) getNewMessage()); + } + if (getNotificationStatus() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) NOTIFICATION_STATUS); + appParamBuf.put((byte) NOTIFICATION_STATUS_LEN); + appParamBuf.putShort((short) getNotificationStatus()); + } + if (getMasInstanceId() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) MAS_INSTANCE_ID); + appParamBuf.put((byte) MAS_INSTANCE_ID_LEN); + appParamBuf.put((byte) getMasInstanceId()); + } + if (getParameterMask() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) PARAMETER_MASK); + appParamBuf.put((byte) PARAMETER_MASK_LEN); + appParamBuf.putInt((int) getParameterMask()); + } + if (getFolderListingSize() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) FOLDER_LISTING_SIZE); + appParamBuf.put((byte) FOLDER_LISTING_SIZE_LEN); + appParamBuf.putShort((short) getFolderListingSize()); + } + if (getMessageListingSize() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) MESSAGE_LISTING_SIZE); + appParamBuf.put((byte) MESSAGE_LISTING_SIZE_LEN); + appParamBuf.putShort((short) getMessageListingSize()); + } + if (getSubjectLength() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) SUBJECT_LENGTH); + appParamBuf.put((byte) SUBJECT_LENGTH_LEN); + appParamBuf.put((byte) getSubjectLength()); + } + if (getCharset() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) CHARSET); + appParamBuf.put((byte) CHARSET_LEN); + appParamBuf.put((byte) getCharset()); + } + if (getFractionRequest() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) FRACTION_REQUEST); + appParamBuf.put((byte) FRACTION_REQUEST_LEN); + appParamBuf.put((byte) getFractionRequest()); + } + if (getFractionDeliver() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) FRACTION_DELIVER); + appParamBuf.put((byte) FRACTION_DELIVER_LEN); + appParamBuf.put((byte) getFractionDeliver()); + } + if (getStatusIndicator() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) STATUS_INDICATOR); + appParamBuf.put((byte) STATUS_INDICATOR_LEN); + appParamBuf.put((byte) getStatusIndicator()); + } + if (getStatusValue() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) STATUS_VALUE); + appParamBuf.put((byte) STATUS_VALUE_LEN); + appParamBuf.put((byte) getStatusValue()); + } + if (getMseTime() != INVALID_VALUE_PARAMETER) { + appParamBuf.put((byte) MSE_TIME); + appParamBuf.put((byte) getMseTimeString().getBytes("UTF-8").length); + appParamBuf.put(getMseTimeString().getBytes("UTF-8")); + } + // We need to reduce the length of the array to match the content + retBuf = Arrays.copyOfRange(appParamBuf.array(), appParamBuf.arrayOffset(), + appParamBuf.arrayOffset() + appParamBuf.position()); + return retBuf; + } + + public int getMaxListCount() { + return maxListCount; + } + + 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; + } + + public int getStartOffset() { + return startOffset; + } + + 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; + } + + public int getFilterMessageType() { + return filterMessageType; + } + + 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; + } + + public long getFilterPeriodBegin() { + return filterPeriodBegin; + } + + public String getFilterPeriodBeginString() { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); + Date date = new Date(filterPeriodBegin); + return format.format(date); // Format to YYYYMMDDTHHMMSS local time + } + + public void setFilterPeriodBegin(long filterPeriodBegin) { + this.filterPeriodBegin = filterPeriodBegin; + } + + public void setFilterPeriodBegin(String filterPeriodBegin) throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); + Date date = format.parse(filterPeriodBegin); + this.filterPeriodBegin = date.getTime(); + } + + public long getFilterPeriodEnd() { + return filterPeriodEnd; + } + + public String getFilterPeriodEndString() { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); + Date date = new Date(filterPeriodEnd); + return format.format(date); // Format to YYYYMMDDTHHMMSS local time + } + + public void setFilterPeriodEnd(long filterPeriodEnd) { + this.filterPeriodEnd = filterPeriodEnd; + } + + public void setFilterPeriodEnd(String filterPeriodEnd) throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); + Date date = format.parse(filterPeriodEnd); + this.filterPeriodEnd = date.getTime(); + } + + public int getFilterReadStatus() { + return filterReadStatus; + } + + 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; + } + + public String getFilterRecipient() { + return filterRecipient; + } + + public void setFilterRecipient(String filterRecipient) { + this.filterRecipient = filterRecipient; + } + + public String getFilterOriginator() { + return filterOriginator; + } + + public void setFilterOriginator(String filterOriginator) { + this.filterOriginator = filterOriginator; + } + + public int getFilterPriority() { + return filterPriority; + } + + 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; + } + + public int getAttachment() { + return attachment; + } + + 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; + } + + public int getTransparent() { + return transparent; + } + + 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; + } + + public int getRetry() { + return retry; + } + + 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; + } + + public int getNewMessage() { + return newMessage; + } + + 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; + } + + public int getNotificationStatus() { + return notificationStatus; + } + + 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; + } + + public int getMasInstanceId() { + return masInstanceId; + } + + 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; + } + + public long getParameterMask() { + return parameterMask; + } + + 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; + } + + public int getFolderListingSize() { + return folderListingSize; + } + + 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; + } + + public int getMessageListingSize() { + return messageListingSize; + } + + 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; + } + + public int getSubjectLength() { + return subjectLength; + } + + 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; + } + + public int getCharset() { + return charset; + } + + 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; + } + + public int getFractionRequest() { + return fractionRequest; + } + + 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; + } + + public int getFractionDeliver() { + return fractionDeliver; + } + + 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; + } + + public int getStatusIndicator() { + return statusIndicator; + } + + 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; + } + + public int getStatusValue() { + return statusValue; + } + + 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; + } + + public long getMseTime() { + return mseTime; + } + + public String getMseTimeString() { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmssZ"); + Date date = new Date(getMseTime()); + return format.format(date); // Format to YYYYMMDDTHHMMSS±hhmm UTC time ± offset + } + + public void setMseTime(long mseTime) { + this.mseTime = mseTime; + } + + public void setMseTime(String mseTime) throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmssZ"); + Date date = format.parse(mseTime); + this.mseTime = date.getTime(); + } +} diff --git a/src/com/android/bluetooth/map/BluetoothMapAuthenticator.java b/src/com/android/bluetooth/map/BluetoothMapAuthenticator.java new file mode 100644 index 000000000..12f64e063 --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapAuthenticator.java @@ -0,0 +1,88 @@ +/* +* 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 isChalled"); + } + } + } + } + + 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 new file mode 100644 index 000000000..531e90b2d --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapContent.java @@ -0,0 +1,1536 @@ +/* +* 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.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; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.BaseColumns; +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.TelephonyManager; +import android.util.Log; + +import com.android.bluetooth.map.BluetoothMapUtils.TYPE; +import com.google.android.mms.pdu.CharacterSets; + +public class BluetoothMapContent { + private static final String TAG = "BluetoothMapContent"; + + private static final boolean D = true; + private static final boolean V = true; + + private static final int MASK_SUBJECT = 0x1; + private static final int MASK_DATETIME = 0x2; + private static final int MASK_SENDER_NAME = 0x4; + private static final int MASK_SENDER_ADDRESSING = 0x8; + + private static final int MASK_RECIPIENT_NAME = 0x10; + private static final int MASK_RECIPIENT_ADDRESSING = 0x20; + private static final int MASK_TYPE = 0x40; + private static final int MASK_SIZE = 0x80; + + private static final int MASK_RECEPTION_STATUS = 0x100; + private static final int MASK_TEXT = 0x200; + private static final int MASK_ATTACHMENT_SIZE = 0x400; + private static final int MASK_PRIORITY = 0x800; + + private static final int MASK_READ = 0x1000; + private static final int MASK_SENT = 0x2000; + private static final int MASK_PROTECTED = 0x4000; + private static final int MASK_REPLYTO_ADDRESSING = 0x8000; + + /* Type of MMS address. From Telephony.java it must be one of PduHeaders.BCC, */ + /* PduHeaders.CC, PduHeaders.FROM, PduHeaders.TO. These are from PduHeaders.java */ + public static final int MMS_FROM = 0x89; + public static final int MMS_TO = 0x97; + public static final int MMS_BCC = 0x81; + public static final int MMS_CC = 0x82; + + private Context mContext; + private ContentResolver mResolver; + + static final String[] SMS_PROJECTION = new String[] { + BaseColumns._ID, + Sms.THREAD_ID, + Sms.ADDRESS, + Sms.BODY, + Sms.DATE, + Sms.READ, + Sms.TYPE, + Sms.STATUS, + Sms.LOCKED, + Sms.ERROR_CODE, + }; + + static final String[] MMS_PROJECTION = new String[] { + BaseColumns._ID, + Mms.THREAD_ID, + Mms.MESSAGE_ID, + Mms.MESSAGE_SIZE, + Mms.SUBJECT, + Mms.CONTENT_TYPE, + Mms.TEXT_ONLY, + Mms.DATE, + Mms.DATE_SENT, + Mms.READ, + Mms.MESSAGE_BOX, + Mms.STATUS, + }; + + private class FilterInfo { + public static final int TYPE_SMS = 0; + public static final int TYPE_MMS = 1; + + int msgType = TYPE_SMS; + int phoneType = 0; + String phoneNum = null; + String phoneAlphaTag = null; + } + + public BluetoothMapContent(final Context context) { + mContext = context; + mResolver = mContext.getContentResolver(); + if (mResolver == null) { + Log.d(TAG, "getContentResolver failed"); + } + } + + private void addSmsEntry() { + 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))); + } + + private void printMms(Cursor c) { + if (D) Log.d(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)) + + "\n " + Mms.CONTENT_TYPE + " : " + c.getString(c.getColumnIndex(Mms.CONTENT_TYPE)) + + "\n " + Mms.TEXT_ONLY + " : " + c.getInt(c.getColumnIndex(Mms.TEXT_ONLY)) + + "\n " + Mms.DATE + " : " + c.getLong(c.getColumnIndex(Mms.DATE)) + + "\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))); + } + + 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); + Uri uriAddress = Uri.parse(uriStr); + Cursor c = mResolver.query( + uriAddress, + projection, + selection, + null, null); + + if (c.moveToFirst()) { + do { + String add = c.getString(c.getColumnIndex("address")); + Integer type = c.getInt(c.getColumnIndex("type")); + if (type == MMS_TO) { + Log.d(TAG, " recipient: " + add + " (type: " + type + ")"); + } else if (type == MMS_FROM) { + Log.d(TAG, " originator: " + add + " (type: " + type + ")"); + } else { + Log.d(TAG, " address other: " + add + " (type: " + type + ")"); + } + + } while(c.moveToNext()); + } + } + + private void printMmsPartImage(long partid) { + String uriStr = String.format("content://mms/part/%d", partid); + Uri uriAddress = Uri.parse(uriStr); + int ch; + StringBuffer sb = new StringBuffer(""); + InputStream is = null; + + try { + is = mResolver.openInputStream(uriAddress); + + while ((ch = is.read()) != -1) { + sb.append((char)ch); + } + Log.d(TAG, sb.toString()); + + } catch (IOException e) { + // do nothing for now + e.printStackTrace(); + } + } + + private void printMmsParts(long id) { + final String[] projection = null; + String selection = new String("mid=" + id); + String uriStr = String.format("content://mms/%d/part", id); + Uri uriAddress = Uri.parse(uriStr); + Cursor c = mResolver.query( + uriAddress, + projection, + selection, + null, null); + + Log.d(TAG, " parts:"); + if (c.moveToFirst()) { + do { + Long partid = c.getLong(c.getColumnIndex(BaseColumns._ID)); + String ct = c.getString(c.getColumnIndex("ct")); + String name = c.getString(c.getColumnIndex("name")); + String charset = c.getString(c.getColumnIndex("chset")); + String filename = c.getString(c.getColumnIndex("fn")); + String text = c.getString(c.getColumnIndex("text")); + Integer fd = c.getInt(c.getColumnIndex("_data")); + + Log.d(TAG, " _id : " + partid + + "\n ct : " + ct + + "\n partname : " + name + + "\n charset : " + charset + + "\n filename : " + filename + + "\n text : " + text + + "\n fd : " + fd); + + /* if (ct.equals("image/jpeg")) { */ + /* printMmsPartImage(partid); */ + /* } */ + } while(c.moveToNext()); + } + } + + public void dumpMmsTable() { + Log.d(TAG, "**** Dump of mms table ****"); + Cursor c = mResolver.query(Mms.CONTENT_URI, + MMS_PROJECTION, null, null, "_id DESC"); + if (c != null) { + 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(); + Log.d(TAG, "**** Dump of sms table ****"); + Cursor c = mResolver.query(Sms.CONTENT_URI, + SMS_PROJECTION, null, null, "_id DESC"); + if (c != null) { + 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(); + 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) { + if ((ap.getParameterMask() & MASK_PROTECTED) != 0) { + String protect = "no"; + Log.d(TAG, "setProtected: " + protect); + e.setProtect(protect); + } + } + + private void setSent(BluetoothMapMessageListingElement e, Cursor c, + 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)); + } + String sent = null; + if (msgType == 2) { + sent = "yes"; + } else { + sent = "no"; + } + Log.d(TAG, "setSent: " + sent); + e.setSent(sent); + } + } + + private void setRead(BluetoothMapMessageListingElement e, Cursor c, + FilterInfo fi, BluetoothMapAppParams ap) { + if ((ap.getParameterMask() & MASK_READ) != 0) { + 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)); + } + String setread = null; + if (read == 1) { + setread = "yes"; + } else { + setread = "no"; + } + Log.d(TAG, "setRead: " + setread); + e.setRead(setread); + } + } + + private void setPriority(BluetoothMapMessageListingElement e, Cursor c, + FilterInfo fi, BluetoothMapAppParams ap) { + if ((ap.getParameterMask() & MASK_PRIORITY) != 0) { + String priority = "no"; + Log.d(TAG, "setPriority: " + priority); + e.setPriority(priority); + } + } + + private void setAttachmentSize(BluetoothMapMessageListingElement e, Cursor c, + FilterInfo fi, BluetoothMapAppParams ap) { + if ((ap.getParameterMask() & MASK_ATTACHMENT_SIZE) != 0) { + int size = 0; + Log.d(TAG, "setAttachmentSize: " + size); + e.setAttachmentSize(size); + } + } + + private void setText(BluetoothMapMessageListingElement e, Cursor c, + FilterInfo fi, BluetoothMapAppParams ap) { + if ((ap.getParameterMask() & MASK_TEXT) != 0) { + String hasText = ""; + if (fi.msgType == FilterInfo.TYPE_SMS) { + hasText = "yes"; + } else if (fi.msgType == FilterInfo.TYPE_MMS) { + int textOnly = c.getInt(c.getColumnIndex(Mms.TEXT_ONLY)); + if (textOnly == 1) { + hasText = "yes"; + } else { + long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + String text = getTextPartsMms(id); + if (text != null && text.length() > 0) { + hasText = "yes"; + } else { + hasText = "no"; + } + } + } + Log.d(TAG, "setText: " + hasText); + e.setText(hasText); + } + } + + private void setReceptionStatus(BluetoothMapMessageListingElement e, Cursor c, + FilterInfo fi, BluetoothMapAppParams ap) { + if ((ap.getParameterMask() & MASK_RECEPTION_STATUS) != 0) { + String status = "complete"; + Log.d(TAG, "setReceptionStatus: " + status); + e.setReceptionStatus(status); + } + } + + private void setSize(BluetoothMapMessageListingElement e, Cursor c, + 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)); + size = subject.length(); + } else if (fi.msgType == FilterInfo.TYPE_MMS) { + size = c.getInt(c.getColumnIndex(Mms.MESSAGE_SIZE)); + } + Log.d(TAG, "setSize: " + size); + e.setSize(size); + } + } + + private void setType(BluetoothMapMessageListingElement e, Cursor c, + 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) { + 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; + } + Log.d(TAG, "setType: " + type); + e.setType(type); + } + } + + private void setRecipientAddressing(BluetoothMapMessageListingElement e, Cursor c, + FilterInfo fi, BluetoothMapAppParams ap) { + if ((ap.getParameterMask() & MASK_RECIPIENT_ADDRESSING) != 0) { + String address = null; + if (fi.msgType == FilterInfo.TYPE_SMS) { + int msgType = c.getInt(c.getColumnIndex(Sms.TYPE)); + if (msgType == 1) { + address = fi.phoneNum; + } else { + address = c.getString(c.getColumnIndex(Sms.ADDRESS)); + } + } else if (fi.msgType == FilterInfo.TYPE_MMS) { + long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + address = getAddressMms(mResolver, id, MMS_TO); + } + Log.d(TAG, "setRecipientAddressing: " + address); + e.setRecipientAddressing(address); + } + } + + private void setRecipientName(BluetoothMapMessageListingElement e, Cursor c, + FilterInfo fi, BluetoothMapAppParams ap) { + if ((ap.getParameterMask() & MASK_RECIPIENT_NAME) != 0) { + String name = null; + if (fi.msgType == 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); + } else { + name = fi.phoneAlphaTag; + } + } 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); + } + Log.d(TAG, "setRecipientName: " + name); + e.setRecipientName(name); + } + } + + private void setSenderAddressing(BluetoothMapMessageListingElement e, Cursor c, + 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)); + } else { + address = fi.phoneNum; + } + } else if (fi.msgType == FilterInfo.TYPE_MMS) { + long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + address = getAddressMms(mResolver, id, MMS_FROM); + } + Log.d(TAG, "setSenderAddressing: " + address); + e.setSenderAddressing(address); + } + } + + private void setSenderName(BluetoothMapMessageListingElement e, Cursor c, + FilterInfo fi, BluetoothMapAppParams ap) { + if ((ap.getParameterMask() & MASK_SENDER_NAME) != 0) { + String name = null; + if (fi.msgType == 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); + } else { + name = fi.phoneAlphaTag; + } + } 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); + } + Log.d(TAG, "setSenderName: " + 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; */ + /* } */ + } + 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); + Uri uriAddress = Uri.parse(uriStr); + Cursor c = mResolver.query(uriAddress, null, selection, + null, null); + + if (c != null && c.moveToFirst()) { + do { + String ct = c.getString(c.getColumnIndex("ct")); + if (ct.equals("text/plain")) { + text += c.getString(c.getColumnIndex("text")); + } + } while(c.moveToNext()); + } + if (c != null) { + c.close(); + } + return text; + } + + private void setSubject(BluetoothMapMessageListingElement e, Cursor c, + 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 (subject == null || subject.length() == 0) { + /* Get subject from mms text body parts - if any exists */ + long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + subject = getTextPartsMms(id); + } + } + if (subject != null) { + subject = subject.substring(0, Math.min(subject.length(), + subLength)); + } + 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)); + Log.d(TAG, "setHandle: " + handle); + e.setHandle(handle); + } + + private BluetoothMapMessageListingElement element(Cursor c, FilterInfo fi, + 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); + return e; + } + + private String getContactNameFromPhone(String phone) { + String name = null; + + Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, + Uri.encode(phone)); + + String[] projection = {Contacts._ID, Contacts.DISPLAY_NAME}; + String selection = Contacts.IN_VISIBLE_GROUP + "=1"; + String orderBy = Contacts.DISPLAY_NAME + " ASC"; + + Cursor c = mResolver.query(uri, projection, selection, null, orderBy); + + if (c != null && c.getCount() >= 1) { + c.moveToFirst(); + name = c.getString(c.getColumnIndex(Contacts.DISPLAY_NAME)); + } + + c.close(); + return name; + } + + 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); + 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")); + } + + if (c != null) { + c.close(); + } + return addr; + } + + 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)) { + Log.d(TAG, "match recipient phone = " + phone); + res = true; + } else { + String name = getContactNameFromPhone(phone); + if (name != null && name.length() > 0 && name.matches(recip)) { + Log.d(TAG, "match recipient name = " + name); + res = true; + } else { + res = false; + } + } + } else { + res = false; + } + return res; + } + + private boolean matchRecipientSms(Cursor c, FilterInfo fi, String recip) { + boolean res; + int msgType = c.getInt(c.getColumnIndex(Sms.TYPE)); + if (msgType == 1) { + String phone = fi.phoneNum; + String name = fi.phoneAlphaTag; + if (phone != null && phone.length() > 0 && phone.matches(recip)) { + Log.d(TAG, "match recipient phone = " + phone); + res = true; + } else if (name != null && name.length() > 0 && name.matches(recip)) { + Log.d(TAG, "match recipient name = " + name); + res = true; + } else { + res = false; + } + } + else { + String phone = c.getString(c.getColumnIndex(Sms.ADDRESS)); + if (phone != null && phone.length() > 0) { + if (phone.matches(recip)) { + Log.d(TAG, "match recipient phone = " + phone); + res = true; + } else { + String name = getContactNameFromPhone(phone); + if (name != null && name.length() > 0 && name.matches(recip)) { + Log.d(TAG, "match recipient name = " + name); + res = true; + } else { + res = false; + } + } + } else { + res = false; + } + } + return res; + } + + private boolean matchRecipient(Cursor c, FilterInfo fi, BluetoothMapAppParams ap) { + boolean res; + String recip = ap.getFilterRecipient(); + if (recip != null && recip.length() > 0) { + recip = recip.replace("*", ".*"); + recip = ".*" + recip + ".*"; + if (fi.msgType == FilterInfo.TYPE_SMS) { + res = matchRecipientSms(c, fi, recip); + } else if (fi.msgType == FilterInfo.TYPE_MMS) { + res = matchRecipientMms(c, fi, recip); + } else { + Log.d(TAG, "Unknown msg type: " + fi.msgType); + res = false; + } + } else { + res = true; + } + return res; + } + + private boolean matchOriginatorMms(Cursor c, FilterInfo fi, String orig) { + boolean res; + long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + String phone = getAddressMms(mResolver, id, MMS_FROM); + if (phone != null && phone.length() > 0) { + if (phone.matches(orig)) { + Log.d(TAG, "match originator phone = " + phone); + res = true; + } else { + String name = getContactNameFromPhone(phone); + if (name != null && name.length() > 0 && name.matches(orig)) { + Log.d(TAG, "match originator name = " + name); + res = true; + } else { + res = false; + } + } + } else { + res = false; + } + return res; + } + + private boolean matchOriginatorSms(Cursor c, FilterInfo fi, String orig) { + boolean res; + int msgType = c.getInt(c.getColumnIndex(Sms.TYPE)); + if (msgType == 1) { + String phone = c.getString(c.getColumnIndex(Sms.ADDRESS)); + if (phone !=null && phone.length() > 0) { + if (phone.matches(orig)) { + Log.d(TAG, "match originator phone = " + phone); + res = true; + } else { + String name = getContactNameFromPhone(phone); + if (name != null && name.length() > 0 && name.matches(orig)) { + Log.d(TAG, "match originator name = " + name); + res = true; + } else { + res = false; + } + } + } else { + res = false; + } + } + else { + String phone = fi.phoneNum; + String name = fi.phoneAlphaTag; + if (phone != null && phone.length() > 0 && phone.matches(orig)) { + Log.d(TAG, "match originator phone = " + phone); + res = true; + } else if (name != null && name.length() > 0 && name.matches(orig)) { + Log.d(TAG, "match originator name = " + name); + res = true; + } else { + res = false; + } + } + return res; + } + + private boolean matchOriginator(Cursor c, FilterInfo fi, BluetoothMapAppParams ap) { + boolean res; + String orig = ap.getFilterOriginator(); + if (orig != null && orig.length() > 0) { + orig = orig.replace("*", ".*"); + orig = ".*" + orig + ".*"; + if (fi.msgType == FilterInfo.TYPE_SMS) { + res = matchOriginatorSms(c, fi, orig); + } else if (fi.msgType == FilterInfo.TYPE_MMS) { + res = matchOriginatorMms(c, fi, orig); + } else { + Log.d(TAG, "Unknown msg type: " + fi.msgType); + res = false; + } + } else { + res = true; + } + return res; + } + + private boolean matchAddresses(Cursor c, FilterInfo fi, BluetoothMapAppParams ap) { + if (matchOriginator(c, fi, ap) && matchRecipient(c, fi, ap)) { + return true; + } else { + return false; + } + } + + 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"; + } + + return where; + } + + 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"; + } + + return where; + } + + private String setWhereFilterFolderType(String folder, FilterInfo fi) { + String where = ""; + if (fi.msgType == FilterInfo.TYPE_SMS) { + where = setWhereFilterFolderTypeSms(folder); + } else if (fi.msgType == FilterInfo.TYPE_MMS) { + where = setWhereFilterFolderTypeMms(folder); + } + + return where; + } + + private String setWhereFilterReadStatus(BluetoothMapAppParams ap) { + String where = ""; + if (ap.getFilterReadStatus() != -1) { + if ((ap.getFilterReadStatus() & 0x01) != 0) { + where = " AND read=0 "; + } + + if ((ap.getFilterReadStatus() & 0x02) != 0) { + where = " AND 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 ((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); + } + } + + return where; + } + + private String setWhereFilterPhones(String str) { + String where = ""; + str = str.replace("*", "%"); + + Cursor c = mResolver.query(ContactsContract.Contacts.CONTENT_URI, null, + ContactsContract.Contacts.DISPLAY_NAME + " like ?", + new String[]{str}, + ContactsContract.Contacts.DISPLAY_NAME + " ASC"); + + while (c != null && c.moveToNext()) { + String contactId = c.getString(c.getColumnIndex(ContactsContract.Contacts._ID)); + + Cursor p = mResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, + ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?", + new String[]{contactId}, + null); + + while (p != null && p.moveToNext()) { + String number = p.getString( + p.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)); + + where += " address = " + "'" + number + "'"; + if (!p.isLast()) { + where += " OR "; + } + } + if (!c.isLast()) { + where += " OR "; + } + p.close(); + } + c.close(); + + if (str != null && str.length() > 0) { + if (where.length() > 0) { + where += " OR "; + } + where += " address like " + "'" + str + "'"; + } + + return where; + } + + private String setWhereFilterOriginator(BluetoothMapAppParams ap, + FilterInfo fi) { + String where = ""; + String orig = ap.getFilterOriginator(); + + 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)"; + } + } + + return where; + } + + private String setWhereFilterRecipient(BluetoothMapAppParams ap, + FilterInfo fi) { + String where = ""; + String recip = ap.getFilterRecipient(); + + 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)"; + } + } + + return where; + } + + private String setWhereFilter(String folder, FilterInfo fi, BluetoothMapAppParams ap) { + String where = ""; + + where += setWhereFilterFolderType(folder, fi); + where += setWhereFilterReadStatus(ap); + where += setWhereFilterPeriod(ap, fi); + /* where += setWhereFilterOriginator(ap, fi); */ + /* where += setWhereFilterRecipient(ap, fi); */ + + Log.d(TAG, "where: " + where); + + return where; + } + + private boolean smsSelected(FilterInfo fi, BluetoothMapAppParams ap) { + int msgType = ap.getFilterMessageType(); + int phoneType = fi.phoneType; + + if (msgType == -1) + return true; + if ((msgType & 0x03) == 0) + return true; + + if (((msgType & 0x01) == 0) && (phoneType == TelephonyManager.PHONE_TYPE_GSM)) + return true; + + if (((msgType & 0x02) == 0) && (phoneType == TelephonyManager.PHONE_TYPE_CDMA)) + return true; + + return false; + } + + private boolean mmsSelected(FilterInfo fi, BluetoothMapAppParams ap) { + int msgType = ap.getFilterMessageType(); + + if (msgType == -1) + return true; + + if ((msgType & 0x08) == 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(); + Log.d(TAG, "phone type = " + fi.phoneType + + " phone num = " + fi.phoneNum + + " phone alpha tag = " + fi.phoneAlphaTag); + } + } + + public BluetoothMapMessageListing msgListing(String folder, BluetoothMapAppParams ap) { + Log.d(TAG, "msgListing: folder = " + folder); + BluetoothMapMessageListing bmList = new BluetoothMapMessageListing(); + BluetoothMapMessageListingElement e = null; + + /* Cache some info used throughout filtering */ + FilterInfo fi = new FilterInfo(); + setFilterInfo(fi); + + if (smsSelected(fi, ap)) { + fi.msgType = FilterInfo.TYPE_SMS; + + String where = setWhereFilter(folder, fi, ap); + + Cursor c = mResolver.query(Sms.CONTENT_URI, + SMS_PROJECTION, where, null, "date DESC"); + + if (c != null) { + while (c.moveToNext()) { + if (matchAddresses(c, fi, ap)) { + printSms(c); + e = element(c, 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); + bmList.add(e); + } + } + c.close(); + } + } + + /* Enable this if post sorting and segmenting needed */ + bmList.sort(); + bmList.segment(ap.getMaxListCount(), ap.getStartOffset()); + + return bmList; + } + + public int msgListingSize(String folder, BluetoothMapAppParams ap) { + Log.d(TAG, "msgListingSize: folder = " + folder); + int cnt = 0; + + /* Cache some info used throughout filtering */ + FilterInfo fi = new FilterInfo(); + setFilterInfo(fi); + + if (smsSelected(fi, ap)) { + String where = setWhereFilter(folder, fi, ap); + + Cursor c = mResolver.query(Sms.CONTENT_URI, + SMS_PROJECTION, where, null, "date DESC"); + + if (c != null) { + cnt = c.getCount(); + c.close(); + } + } + Log.d(TAG, "msgListingSize: size = " + cnt); + return cnt; + } + + /** + * Get the folder name of an SMS message or MMS message. + * @param c the cursor pointing at the message + * @return the folder name. + */ + private String getFolderName(int type, int threadId) { + + if(threadId == -1) + return "deleted"; + + switch(type) { + case 1: + return "inbox"; + case 2: + return "send"; + case 3: + return "draft"; + case 4: // Just name outbox, failed and queued "outbox" + case 5: + case 6: + return "outbox"; + } + return ""; + } + + public byte[] getMessage(String handle, int charset) throws UnsupportedEncodingException{ + TYPE type = BluetoothMapUtils.getMsgTypeFromHandle(handle); + long id = BluetoothMapUtils.getCpHandle(handle); + switch(type) { + case SMS_GSM: + case SMS_CDMA: + return getSmsMessage(id, charset); + case MMS: + return getMmsMessage(id); + case EMAIL: + throw new IllegalArgumentException("Email not implemented - invalid message handle."); + } + throw new IllegalArgumentException("Invalid message handle."); + } + + private void setVCardFromPhoneNumber(BluetoothMapbMessage message, String phone, boolean incoming) { + String contactId = null, contactName = null; + String[] phoneNumbers = null; + String[] emailAddresses = null; + Cursor p; + + Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, + Uri.encode(phone)); + + String[] projection = {Contacts._ID, Contacts.DISPLAY_NAME}; + String selection = Contacts.IN_VISIBLE_GROUP + "=1"; + String orderBy = Contacts._ID + " ASC"; + + // Get the contact _ID and name + p = mResolver.query(uri, projection, selection, null, orderBy); + if (p != null && p.getCount() >= 1) { + p.moveToFirst(); + contactId = p.getString(p.getColumnIndex(Contacts._ID)); + contactName = p.getString(p.getColumnIndex(Contacts.DISPLAY_NAME)); + } + p.close(); + + // Bail out if we are unable to find a contact, based on the phone number + if(contactId == null) { + phoneNumbers = new String[1]; + phoneNumbers[0] = phone; + } + else { + // Fetch all contact phone numbers + p = mResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, + ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?", + new String[]{contactId}, + null); + if(p != null) { + int i = 0; + phoneNumbers = new String[p.getCount()]; + while (p != null && p.moveToNext()) { + String number = p.getString( + p.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)); + phoneNumbers[i++] = number; + } + p.close(); + } + + // Fetch contact e-mail addresses + p = mResolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, null, + ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?", + new String[]{contactId}, + null); + if(p != null) { + int i = 0; + emailAddresses = new String[p.getCount()]; + while (p != null && p.moveToNext()) { + String emailAddress = p.getString( + p.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)); + emailAddresses[i++] = emailAddress; + } + p.close(); + } + } + if(incoming == true) + message.addOriginator(contactName, contactName, phoneNumbers, emailAddresses); // Use version 3.0 as we only have a formatted name + else + message.addRecipient(contactName, contactName, phoneNumbers, emailAddresses); // Use version 3.0 as we only have a formatted name + } + + public static final int MAP_MESSAGE_CHARSET_NATIVE = 0; + public static final int MAP_MESSAGE_CHARSET_UTF8 = 1; + + public byte[] getSmsMessage(long id, int charset) throws UnsupportedEncodingException{ + int type, threadId; + long time = -1; + String msgBody; + BluetoothMapbMessageSms message = new BluetoothMapbMessageSms(); + TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); + Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, "_ID = " + id, null, null); + + if(c != null && c.moveToFirst()) + { + + if(V) Log.d(TAG,"c.count: " + c.getCount()); + + if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) { + message.setType(TYPE.SMS_GSM); + } else if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) { + message.setType(TYPE.SMS_CDMA); + } + + String read = c.getString(c.getColumnIndex(Sms.READ)); + if (read.equalsIgnoreCase("1")) + message.setStatus(true); + else + message.setStatus(false); + + type = c.getInt(c.getColumnIndex(Sms.TYPE)); + threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); + message.setFolder(getFolderName(type, threadId)); + + msgBody = c.getString(c.getColumnIndex(Sms.BODY)); + + String phone = c.getString(c.getColumnIndex(Sms.ADDRESS)); + + if(type == 1) // Inbox message needs to set the vCard as originator + setVCardFromPhoneNumber(message, phone, true); + else // Other messages sets the vCard as the recipient + setVCardFromPhoneNumber(message, phone, false); + + if(charset == MAP_MESSAGE_CHARSET_NATIVE) { + if(type == 1) + message.setSmsBodyPdus(BluetoothMapSmsPdu.getSubmitPdus(msgBody, phone)); + else + message.setSmsBodyPdus(BluetoothMapSmsPdu.getDeliverPdus(msgBody, phone, time)); // TODO: No support for GSM + } else /*if (charset == MAP_MESSAGE_CHARSET_UTF8)*/ { + message.setSmsBody(msgBody); + } + + c.close(); + + return message.encode(); + } + throw new IllegalArgumentException("SMS handle not found"); + } + + private void extractMmsAddresses(long id, BluetoothMapbMessageMmsEmail message) { + final String[] projection = null; + String selection = new String("msg_id=" + id); + String uriStr = String.format("content://mms/%d/addr", id); + Uri uriAddress = Uri.parse(uriStr); + Cursor c = mResolver.query( + uriAddress, + projection, + selection, + null, null); + /* TODO: Change the setVCard...() to return the vCard, and use the name in message.addXxx() */ + if (c.moveToFirst()) { + do { + String address = c.getString(c.getColumnIndex("address")); + Integer type = c.getInt(c.getColumnIndex("type")); + switch(type) { + case MMS_FROM: + setVCardFromPhoneNumber(message, address, false); + message.addFrom(null, address); + break; + case MMS_TO: + setVCardFromPhoneNumber(message, address, true); + message.addTo(null, address); + break; + case MMS_CC: + setVCardFromPhoneNumber(message, address, true); + message.addCc(null, address); + break; + case MMS_BCC: + setVCardFromPhoneNumber(message, address, true); + message.addBcc(null, address); + default: + break; + } + } while(c.moveToNext()); + } + } + + private byte[] readMmsDataPart(long partid) { + String uriStr = String.format("content://mms/part/%d", partid); + Uri uriAddress = Uri.parse(uriStr); + InputStream is = null; + ByteArrayOutputStream os = new ByteArrayOutputStream(); + int bufferSize = 8192; + byte[] buffer = new byte[bufferSize]; + byte[] retVal = null; + + try { + is = mResolver.openInputStream(uriAddress); + int len = 0; + while ((len = is.read(buffer)) != -1) { + os.write(buffer, 0, len); // We need to specify the len, as it can be != bufferSize + } + retVal = os.toByteArray(); + } catch (IOException e) { + // do nothing for now + Log.w(TAG,"Error reading part data",e); + } finally { + try { + os.close(); + is.close(); + } catch (IOException e) { + } + } + return retVal; + } + + private void extractMmsParts(long id, BluetoothMapbMessageMmsEmail message) + { + final String[] projection = null; + String selection = new String("mid=" + id); + String uriStr = String.format("content://mms/%d/part", id); + Uri uriAddress = Uri.parse(uriStr); + BluetoothMapbMessageMmsEmail.MimePart part; + Cursor c = mResolver.query( + uriAddress, + projection, + selection, + null, null); + + if (c.moveToFirst()) { + do { + Long partId = c.getLong(c.getColumnIndex(BaseColumns._ID)); + String contentType = c.getString(c.getColumnIndex("ct")); + String name = c.getString(c.getColumnIndex("name")); + String charset = c.getString(c.getColumnIndex("chset")); + String filename = c.getString(c.getColumnIndex("fn")); + String text = c.getString(c.getColumnIndex("text")); + Integer fd = c.getInt(c.getColumnIndex("_data")); + + if(D)Log.d(TAG, " _id : " + partId + + "\n ct : " + contentType + + "\n partname : " + name + + "\n charset : " + charset + + "\n filename : " + filename + + "\n text : " + text + + "\n fd : " + fd); + part = message.addMimePart(); + part.contentType = contentType; + part.partName = name; + try { + if(text != null) { + part.data = text.getBytes("UTF-8"); + part.charsetName = "utf-8"; + } + else { + part.data = readMmsDataPart(partId); + if(charset != null) + part.charsetName = CharacterSets.getMimeName(Integer.parseInt(charset)); + } + } catch (NumberFormatException e) { + Log.d(TAG,"extractMmsParts",e); + part.data = null; + part.charsetName = null; + } catch (UnsupportedEncodingException e) { + Log.d(TAG,"extractMmsParts",e); + part.data = null; + part.charsetName = null; + } finally { + } + part.fileName = filename; + } while(c.moveToNext()); + } + message.updateCharset(); + } + + public byte[] getMmsMessage(long id) throws UnsupportedEncodingException { + int msgBox, threadId; + BluetoothMapbMessageMmsEmail message = new BluetoothMapbMessageMmsEmail(); + Cursor c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION, "_ID = " + id, null, null); + if(c != null && c.moveToFirst()) + { + message.setType(TYPE.MMS); + + // The MMS info: + String read = c.getString(c.getColumnIndex(Mms.READ)); + if (read.equalsIgnoreCase("1")) + message.setStatus(true); + else + message.setStatus(false); + + msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); + threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); + message.setFolder(getFolderName(msgBox, threadId)); + + message.setSubject(c.getString(c.getColumnIndex(Mms.SUBJECT))); + message.setMessageId(c.getString(c.getColumnIndex(Mms.MESSAGE_ID))); + message.setContentType(c.getString(c.getColumnIndex(Mms.CONTENT_TYPE))); + message.setDate(c.getLong(c.getColumnIndex(Mms.DATE)) * 1000L); + // c.getInt(c.getColumnIndex(Mms.TEXT_ONLY)); - TODO: Do we need this + // c.getLong(c.getColumnIndex(Mms.DATE_SENT)); - this is never used + // c.getInt(c.getColumnIndex(Mms.STATUS)); - don't know what this is + + // The parts + extractMmsParts(id, message); + + // The addresses + extractMmsAddresses(id, message); + + c.close(); + + return message.encode(); + } + else if(c != null) { + c.close(); + } + + throw new IllegalArgumentException("MMS handle not found"); + } + +} diff --git a/src/com/android/bluetooth/map/BluetoothMapContentObserver.java b/src/com/android/bluetooth/map/BluetoothMapContentObserver.java new file mode 100644 index 000000000..ed090ea1d --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapContentObserver.java @@ -0,0 +1,1168 @@ +/* +* 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.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.xmlpull.v1.XmlSerializer; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.provider.BaseColumns; +import android.provider.Telephony; +import android.provider.Telephony.Mms; +import android.provider.Telephony.MmsSms; +import android.provider.Telephony.Sms; +import android.provider.Telephony.Sms.Inbox; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.telephony.SmsManager; +import android.telephony.SmsMessage; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.util.Xml; + +import com.android.bluetooth.map.BluetoothMapUtils.TYPE; +import com.android.bluetooth.map.BluetoothMapbMessageMmsEmail.MimePart; +import com.google.android.mms.pdu.PduHeaders; + +public class BluetoothMapContentObserver { + private static final String TAG = "BluetoothMapContentObserver"; + + private static final boolean D = true; + private static final boolean V = true; + + private Context mContext; + private ContentResolver mResolver; + private BluetoothMnsObexClient mMnsClient; + private int mMasId; + + public static final int DELETED_THREAD_ID = -1; + + /* X-Mms-Message-Type field types. These are from PduHeaders.java */ + public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84; + + private TYPE mSmsType; + + static final String[] SMS_PROJECTION = new String[] { + BaseColumns._ID, + Sms.THREAD_ID, + Sms.ADDRESS, + Sms.BODY, + Sms.DATE, + Sms.READ, + Sms.TYPE, + Sms.STATUS, + Sms.LOCKED, + Sms.ERROR_CODE, + }; + + static final String[] MMS_PROJECTION = new String[] { + BaseColumns._ID, + Mms.THREAD_ID, + Mms.MESSAGE_ID, + Mms.MESSAGE_SIZE, + Mms.SUBJECT, + Mms.CONTENT_TYPE, + Mms.TEXT_ONLY, + Mms.DATE, + Mms.DATE_SENT, + Mms.READ, + Mms.MESSAGE_BOX, + Mms.MESSAGE_TYPE, + Mms.STATUS, + }; + + public BluetoothMapContentObserver(final Context context) { + mContext = context; + mResolver = mContext.getContentResolver(); + + mSmsType = getSmsType(); + } + + private TYPE getSmsType() { + TYPE smsType = null; + TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); + + if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) { + smsType = TYPE.SMS_GSM; + } else if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) { + smsType = TYPE.SMS_CDMA; + } + + return smsType; + } + + private final ContentObserver mObserver = 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.toString() + " selfchange: " + selfChange); + + handleMsgListChanges(); + } + }; + + private static final String folderSms[] = { + "", + "inbox", + "sent", + "draft", + "outbox", + "outbox", + "outbox", + "inbox", + "inbox", + }; + + private static final String folderMms[] = { + "", + "inbox", + "sent", + "draft", + "outbox", + }; + + private class Event { + String eventType; + long handle; + String folder; + String oldFolder; + TYPE msgType; + + public Event(String eventType, long handle, String folder, + String oldFolder, TYPE msgType) { + String PATH = "telecom/msg/"; + this.eventType = eventType; + this.handle = handle; + if (folder != null) { + this.folder = PATH + folder; + } else { + this.folder = null; + } + if (oldFolder != null) { + this.oldFolder = PATH + oldFolder; + } else { + this.oldFolder = null; + } + this.msgType = msgType; + } + + public byte[] encode() throws UnsupportedEncodingException { + StringWriter sw = new StringWriter(); + XmlSerializer xmlEvtReport = Xml.newSerializer(); + try { + xmlEvtReport.setOutput(sw); + xmlEvtReport.startDocument(null, null); + xmlEvtReport.text("\n"); + xmlEvtReport.startTag("", "Map-event-report"); + xmlEvtReport.attribute("", "version", "1.0"); + + xmlEvtReport.startTag("", "event"); + xmlEvtReport.attribute("", "type", eventType); + xmlEvtReport.attribute("", "handle", BluetoothMapUtils.getMapHandle(handle, msgType)); + if (folder != null) { + xmlEvtReport.attribute("", "folder", folder); + } + if (oldFolder != null) { + xmlEvtReport.attribute("", "old_folder", oldFolder); + } + xmlEvtReport.attribute("", "msg_type", msgType.name()); + xmlEvtReport.endTag("", "event"); + + xmlEvtReport.endTag("", "Map-event-report"); + xmlEvtReport.endDocument(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + if (V) System.out.println(sw.toString()); + + return sw.toString().getBytes("UTF-8"); + } + } + + private class Msg { + long id; + int type; + + public Msg(long id, int type) { + this.id = id; + this.type = type; + } + } + + private Map<Long, Msg> mMsgListSms = + Collections.synchronizedMap(new HashMap<Long, Msg>()); + + private Map<Long, Msg> mMsgListMms = + Collections.synchronizedMap(new HashMap<Long, Msg>()); + + public void registerObserver(BluetoothMnsObexClient mns, int masId) { + if (V) Log.d(TAG, "registerObserver"); + /* Use MmsSms Uri since the Sms Uri is not notified on deletes */ + mMasId = masId; + mMnsClient = mns; + mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver); + initMsgList(); + } + + public void unregisterObserver() { + if (V) Log.d(TAG, "unregisterObserver"); + mResolver.unregisterContentObserver(mObserver); + } + + private void sendEvent(Event evt) { + Log.d(TAG, "sendEvent: " + evt.eventType + " " + evt.handle + " " + + evt.folder + " " + evt.oldFolder + " " + evt.msgType.name()); + + if (mMnsClient == null) { + Log.d(TAG, "sendEvent: No MNS client registered - don't send event"); + return; + } + + try { + mMnsClient.sendEvent(evt.encode(), mMasId); + } catch (UnsupportedEncodingException ex) { + /* do nothing */ + } + } + + private void initMsgList() { + if (V) Log.d(TAG, "initMsgList"); + + mMsgListSms.clear(); + mMsgListMms.clear(); + + HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>(); + + Cursor c = mResolver.query(Sms.CONTENT_URI, + SMS_PROJECTION, null, null, null); + + if (c != null && c.moveToFirst()) { + do { + long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + int type = c.getInt(c.getColumnIndex(Sms.TYPE)); + + Msg msg = new Msg(id, type); + msgListSms.put(id, msg); + } while (c.moveToNext()); + c.close(); + } + + mMsgListSms = msgListSms; + + HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>(); + + c = mResolver.query(Mms.CONTENT_URI, + MMS_PROJECTION, 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)); + + Msg msg = new Msg(id, type); + msgListMms.put(id, msg); + } while (c.moveToNext()); + c.close(); + } + + mMsgListMms = msgListMms; + } + + private void handleMsgListChangesSms() { + if (V) Log.d(TAG, "handleMsgListChangesSms"); + + HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>(); + + Cursor c = mResolver.query(Sms.CONTENT_URI, + SMS_PROJECTION, null, null, null); + + synchronized(mMsgListSms) { + if (c != null && c.moveToFirst()) { + do { + long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + int type = c.getInt(c.getColumnIndex(Sms.TYPE)); + + Msg msg = mMsgListSms.remove(id); + + if (msg == null) { + /* New message */ + msg = new Msg(id, type); + msgListSms.put(id, msg); + + if (folderSms[type].equals("inbox")) { + Event evt = new Event("NewMessage", 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); + msg.type = type; + } + msgListSms.put(id, msg); + } + } while (c.moveToNext()); + c.close(); + } + + for (Msg msg : mMsgListSms.values()) { + Event evt = new Event("MessageDeleted", msg.id, "deleted", + folderSms[msg.type], mSmsType); + sendEvent(evt); + } + + mMsgListSms = msgListSms; + } + } + + private void handleMsgListChangesMms() { + if (V) Log.d(TAG, "handleMsgListChangesMms"); + + HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>(); + + Cursor c = mResolver.query(Mms.CONTENT_URI, + MMS_PROJECTION, null, null, null); + + synchronized(mMsgListMms) { + if (c != null && c.moveToFirst()) { + do { + long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); + int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE)); + + Msg msg = mMsgListMms.remove(id); + + if (msg == null) { + /* New message - only notify on retrieve conf */ + if (folderMms[type].equals("inbox") && + mtype != MESSAGE_TYPE_RETRIEVE_CONF) { + continue; + } + + msg = new Msg(id, type); + msgListMms.put(id, msg); + + if (folderMms[type].equals("inbox")) { + Event evt = new Event("NewMessage", id, folderMms[type], + null, TYPE.MMS); + 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); + msg.type = type; + + if (folderMms[type].equals("sent")) { + evt = new Event("SendingSuccess", id, + folderSms[type], null, TYPE.MMS); + sendEvent(evt); + } + } + msgListMms.put(id, msg); + } + } while (c.moveToNext()); + c.close(); + } + + for (Msg msg : mMsgListMms.values()) { + Event evt = new Event("MessageDeleted", msg.id, "deleted", + folderMms[msg.type], TYPE.MMS); + sendEvent(evt); + } + + mMsgListMms = msgListMms; + } + } + + private void handleMsgListChanges() { + handleMsgListChangesSms(); + handleMsgListChangesMms(); + } + + private boolean deleteMessageMms(long handle) { + boolean res = false; + Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); + Cursor c = mResolver.query(uri, null, null, null, null); + if (c != null && c.moveToFirst()) { + /* Move to deleted folder, or delete if already in deleted folder */ + 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); + } else { + /* Delete from observer message list to avoid delete notifications */ + mMsgListMms.remove(handle); + /* Delete message */ + mResolver.delete(uri, null, null); + } + res = true; + } + if (c != null) { + c.close(); + } + 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); + Cursor c = mResolver.query(uri, null, null, null, null); + + if (c != null && c.moveToFirst()) { + int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); + if (threadId == DELETED_THREAD_ID) { + /* 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)); + int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); + if (msgBox == Mms.MESSAGE_BOX_INBOX) { + address = BluetoothMapContent.getAddressMms(mResolver, id, + BluetoothMapContent.MMS_FROM); + } else { + address = BluetoothMapContent.getAddressMms(mResolver, id, + BluetoothMapContent.MMS_TO); + } + Set<String> recipients = new HashSet<String>(); + recipients.addAll(Arrays.asList(address)); + updateThreadIdMms(uri, Telephony.Threads.getOrCreateThreadId(mContext, recipients)); + } else { + Log.d(TAG, "Message not in deleted folder: handle " + handle + + " threadId " + threadId); + } + res = true; + } + if (c != null) { + c.close(); + } + return res; + } + + private boolean deleteMessageSms(long handle) { + boolean res = false; + Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); + Cursor c = mResolver.query(uri, null, null, null, null); + + if (c != null && c.moveToFirst()) { + /* 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) { + /* Set deleted thread id */ + ContentValues contentValues = new ContentValues(); + contentValues.put(Sms.THREAD_ID, DELETED_THREAD_ID); + mResolver.update(uri, contentValues, null, null); + } else { + /* Delete from observer message list to avoid delete notifications */ + mMsgListSms.remove(handle); + /* Delete message */ + mResolver.delete(uri, null, null); + } + res = true; + } + if (c != null) { + c.close(); + } + 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); + Cursor c = mResolver.query(uri, null, null, null, null); + + if (c != null && c.moveToFirst()) { + int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); + if (threadId == DELETED_THREAD_ID) { + String address = c.getString(c.getColumnIndex(Sms.ADDRESS)); + Set<String> recipients = new HashSet<String>(); + recipients.addAll(Arrays.asList(address)); + updateThreadIdSms(uri, Telephony.Threads.getOrCreateThreadId(mContext, recipients)); + } else { + Log.d(TAG, "Message not in deleted folder: handle " + handle + + " threadId " + threadId); + } + res = true; + } + if (c != null) { + c.close(); + } + return res; + } + + public boolean setMessageStatusDeleted(long handle, TYPE type, 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); + } + } + return res; + } + + public boolean setMessageStatusRead(long handle, TYPE type, int statusValue) { + boolean res = true; + + if (D) Log.d(TAG, "setMessageStatusRead: handle " + handle + + " type " + type + " value " + statusValue); + + /* Approved MAP spec errata 3445 states that read status initiated */ + /* 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); + Cursor c = mResolver.query(uri, null, null, null, null); + + ContentValues contentValues = new ContentValues(); + contentValues.put(Sms.READ, statusValue); + mResolver.update(uri, contentValues, null, null); + } else if (type == TYPE.MMS) { + Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); + Cursor c = mResolver.query(uri, null, null, null, null); + + ContentValues contentValues = new ContentValues(); + contentValues.put(Mms.READ, statusValue); + mResolver.update(uri, contentValues, null, null); + } + + return res; + } + + private class PushMsgInfo { + long id; + int transparent; + int retry; + String phone; + Uri uri; + int parts; + int partsSent; + int partsDelivered; + boolean resend; + + public PushMsgInfo(long id, int transparent, + int retry, String phone, Uri uri) { + this.id = id; + this.transparent = transparent; + this.retry = retry; + this.phone = phone; + this.uri = uri; + this.resend = false; + }; + } + + private Map<Long, PushMsgInfo> mPushMsgList = + Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>()); + + public long pushMessage(BluetoothMapbMessage msg, String folder, + BluetoothMapAppParams ap) throws IllegalArgumentException { + if (D) Log.d(TAG, "pushMessage"); + ArrayList<BluetoothMapbMessage.vCard> recipientList = msg.getRecipients(); + int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ? + 0 : ap.getTransparent(); + int retry = ap.getRetry(); + int charset = ap.getCharset(); + long handle = -1; + + if (recipientList == null) { + 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 */ + if (folder.equals("outbox")) { + handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMmsEmail)msg); + } + break; + } + case SMS_GSM: //fall-through + case 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; + } + + 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); + } + break; + } + case EMAIL: + { + break; + } + } + + } + } + + /* If multiple recipients return handle of last */ + return handle; + } + + + + public long sendMmsMessage(String folder,String to_address, BluetoothMapbMessageMmsEmail msg) { + /* + *strategy: + *1) parse message into parts + *if folder is outbox/drafts: + *2) push message to draft + *if folder is outbox: + *3) move message to outbox (to trigger the mms app to add msg to pending_messages list) + *4) send intent to mms app in order to wake it up. + *else if folder !outbox: + *1) push message to folder + * */ + if (folder != null && (folder.equalsIgnoreCase("outbox")|| folder.equalsIgnoreCase("drafts"))) { + 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); + } + return handle; + } else { + /* not allowed to push mms to anything but outbox/drafts */ + throw new IllegalArgumentException("Cannot push message to other folders than outbox/drafts"); + } + + } + + + 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); + 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"); + } + queryResult.close(); + }else { + Log.d(TAG, "Could not move draft to outbox "); + } + } + } + private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMmsEmail msg) { + /** + * strategy: + * 1) parse msg into parts + header + * 2) create thread id (abuse the ease of adding an SMS to get id for thread) + * 3) push parts into content://mms/parts/ table + * 3) + */ + + ContentValues values = new ContentValues(); + values.put("msg_box", folder); + + values.put("read", 0); + values.put("seen", 0); + 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); + // Get thread id + Set<String> recipients = new HashSet<String>(); + recipients.addAll(Arrays.asList(to_address)); + values.put("thread_id", Telephony.Threads.getOrCreateThreadId(mContext, recipients)); + Uri uri = Uri.parse("content://mms"); + + ContentResolver cr = mContext.getContentResolver(); + uri = cr.insert(uri, values); + + if (uri == null) { + // unable to insert MMS + Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri); + return -1; + } + + long handle = Long.parseLong(uri.getLastPathSegment()); + if (V){ + Log.v(TAG, " NEW URI " + uri.toString()); + } + try { + for(MimePart part : msg.getMimeParts()) { + values.clear(); + if(part.contentType != null && part.contentType.toUpperCase().contains("TEXT")) { + values.put("seq", 0); + values.put("ct", "text/plain"); + values.put("name", "null"); + values.put("chset", 106); + values.put("cd", "null"); + values.put("fn", part.partName); + values.put("name", part.partName); + values.put("cid", "<smil>"); + values.put("cl", part.partName); + values.put("ctt_s", "null"); + values.put("ctt_t", "null"); + values.put("_data", "null"); + values.put("text", new String(part.data, "UTF-8")); + uri = Uri.parse("content://mms/" + handle + "/part"); + uri = cr.insert(uri, values); + + } else if (part.contentType != null && part.contentType.toUpperCase().contains("SMIL")){ + + values.put("seq", -1); + values.put("ct", "application/smil"); + values.put("cid", "<smil>"); + values.put("cl", "smil.xml"); + 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); + + }else /*VIDEO/AUDIO/IMAGE*/ { + writeMmsDataPart(handle, part.contentType, part.partName, part.data); + } + if (uri != null && V){ + Log.v(TAG, "Added part with content-type: "+ part.contentType + " to Uri: " + uri.toString()); + } + } + } catch (UnsupportedEncodingException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + values.clear(); + values.put("contact_id", "null"); + values.put("address", "insert-address-token"); + values.put("type", BluetoothMapContent.MMS_FROM); + values.put("charset", 106); + + uri = Uri.parse("content://mms/" + handle + "/addr"); + uri = cr.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); + + uri = Uri.parse("content://mms/" + handle + "/addr"); + uri = cr.insert(uri, values); + if (uri != null && V){ + Log.v(TAG, " NEW URI " + uri.toString()); + } + return handle; + } + + + private void writeMmsDataPart(long handle, String contentType, String name, byte[] data) throws IOException{ + ContentValues values = new ContentValues(); + values.put("mid", handle); + values.put("ct", contentType); + values.put("cid", "<smil>"); + values.put("cl", name); + values.put("fn", name); + values.put("name", name); + Uri partUri = Uri.parse("content://mms/" + handle + "/part"); + Uri res = mResolver.insert(partUri, values); + + // Add data to part + OutputStream os = mResolver.openOutputStream(res); + ByteArrayInputStream is = new ByteArrayInputStream(data); + byte[] buffer = new byte[256]; + for (int len=0; (len=is.read(buffer)) != -1;) + { + os.write(buffer, 0, len); + } + os.close(); + is.close(); + } + + + public void sendMessage(PushMsgInfo msgInfo, String msgBody) { + + SmsManager smsMng = SmsManager.getDefault(); + ArrayList<String> parts = smsMng.divideMessage(msgBody); + msgInfo.parts = parts.size(); + + ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts); + ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts); + + 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)); + } + + Log.d(TAG, "sendMessage to " + msgInfo.phone); + + smsMng.sendMultipartTextMessage(msgInfo.phone, null, parts, sentIntents, + deliveryIntents); + } + + private static final String ACTION_MESSAGE_DELIVERY = + "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY"; + private static final String ACTION_MESSAGE_SENT = + "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT"; + + private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver(); + + 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"); + + public void register() { + Handler handler = new Handler(); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_MESSAGE_DELIVERY); + intentFilter.addAction(ACTION_MESSAGE_SENT); + mContext.registerReceiver(this, intentFilter, null, handler); + } + + public void unregister() { + try { + mContext.unregisterReceiver(this); + } catch (IllegalArgumentException e) { + /* do nothing */ + } + } + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + long handle = intent.getLongExtra("HANDLE", -1); + PushMsgInfo msgInfo = mPushMsgList.get(handle); + + Log.d(TAG, "onReceive: action" + action); + + if (msgInfo == null) { + Log.d(TAG, "onReceive: no msgInfo found for handle " + handle); + return; + } + + if (action.equals(ACTION_MESSAGE_SENT)) { + msgInfo.partsSent++; + if (msgInfo.partsSent == msgInfo.parts) { + actionMessageSent(context, intent, msgInfo); + } + } else if (action.equals(ACTION_MESSAGE_DELIVERY)) { + msgInfo.partsDelivered++; + if (msgInfo.partsDelivered == msgInfo.parts) { + actionMessageDelivery(context, intent, msgInfo); + } + } else { + Log.d(TAG, "onReceive: Unknown action " + action); + } + } + + private void actionMessageSent(Context context, Intent intent, + PushMsgInfo msgInfo) { + int result = getResultCode(); + boolean delete = false; + + if (result == Activity.RESULT_OK) { + 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"); + } + } else { + delete = true; + } + + Event evt = new Event("SendingSuccess", msgInfo.id, + folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType); + sendEvent(evt); + + } else { + if (msgInfo.retry == 1) { + /* Notify failure, but keep message in outbox for resending */ + msgInfo.resend = true; + Event evt = new Event("SendingFailure", 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"); + } + } else { + delete = true; + } + + Event evt = new Event("SendingFailure", msgInfo.id, + folderSms[Sms.MESSAGE_TYPE_FAILED], null, mSmsType); + sendEvent(evt); + } + } + + if (delete == true) { + /* Delete from Observer message list to avoid delete notifications */ + mMsgListSms.remove(msgInfo.id); + + /* Delete from DB */ + mResolver.delete(msgInfo.uri, null, null); + } + } + + 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(); + + Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null); + + try { + if (cursor.moveToFirst()) { + 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); + + ContentValues contentValues = new ContentValues(2); + + contentValues.put(Sms.STATUS, status); + contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis()); + mResolver.update(updateUri, contentValues, null, null); + } else { + Log.d(TAG, "Can't find message for status update: " + messageUri); + } + } finally { + cursor.close(); + } + + if (status == 0) { + Event evt = new Event("DeliverySuccess", msgInfo.id, + folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType); + sendEvent(evt); + } else { + Event evt = new Event("DeliveryFailure", msgInfo.id, + folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType); + sendEvent(evt); + } + + mPushMsgList.remove(msgInfo.id); + } + } + + private void registerPhoneServiceStateListener() { + TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); + tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE); + } + + private void unRegisterPhoneServiceStateListener() { + TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); + tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE); + } + + private void resendPendingMessages() { + /* Send pending messages in outbox */ + String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX; + Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, + null); + + if (c != null && c.moveToFirst()) { + do { + long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + String msgBody = c.getString(c.getColumnIndex(Sms.BODY)); + PushMsgInfo msgInfo = mPushMsgList.get(id); + if (msgInfo == null || msgInfo.resend == false) { + continue; + } + sendMessage(msgInfo, msgBody); + } while (c.moveToNext()); + c.close(); + } + } + + private void failPendingMessages() { + /* Move pending messages from outbox to failed */ + String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX; + Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, + null); + + if (c != null && c.moveToFirst()) { + do { + long id = c.getLong(c.getColumnIndex(BaseColumns._ID)); + String msgBody = c.getString(c.getColumnIndex(Sms.BODY)); + PushMsgInfo msgInfo = mPushMsgList.get(id); + if (msgInfo == null || msgInfo.resend == false) { + continue; + } + Sms.moveMessageToFolder(mContext, msgInfo.uri, + Sms.MESSAGE_TYPE_FAILED, 0); + } while (c.moveToNext()); + c.close(); + } + } + + private void removeDeletedMessages() { + /* Remove messages from virtual "deleted" folder (thread_id -1) */ + mResolver.delete(Uri.parse("content://sms/"), + "thread_id = " + DELETED_THREAD_ID, null); + } + + private PhoneStateListener mPhoneListener = new PhoneStateListener() { + @Override + public void onServiceStateChanged(ServiceState serviceState) { + Log.d(TAG, "Phone service state change: " + serviceState.getState()); + if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) { + resendPendingMessages(); + } + } + }; + + public void init() { + mSmsBroadcastReceiver.register(); + registerPhoneServiceStateListener(); + } + + public void deinit() { + mSmsBroadcastReceiver.unregister(); + unRegisterPhoneServiceStateListener(); + failPendingMessages(); + removeDeletedMessages(); + } +} diff --git a/src/com/android/bluetooth/map/BluetoothMapFolderElement.java b/src/com/android/bluetooth/map/BluetoothMapFolderElement.java new file mode 100644 index 000000000..d3909ddad --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapFolderElement.java @@ -0,0 +1,133 @@ +/* +* 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.IOException; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; + +import org.xmlpull.v1.XmlSerializer; + +import android.util.Xml; + +/** + * @author cbonde + * + */ +public class BluetoothMapFolderElement { + private String name; + private BluetoothMapFolderElement parent = null; + private ArrayList<BluetoothMapFolderElement> subFolders; + + public BluetoothMapFolderElement( String name, BluetoothMapFolderElement parrent ){ + this.name = name; + this.parent = parrent; + subFolders = new ArrayList<BluetoothMapFolderElement>(); + } + + public String getName() { + return name; + } + + /** + * Fetch the parent folder. + * @return the parent folder or null if we are at the root folder. + */ + public BluetoothMapFolderElement getParent() { + return parent; + } + + /** + * Fetch the root folder. + * @return the parent folder or null if we are at the root folder. + */ + public BluetoothMapFolderElement getRoot() { + BluetoothMapFolderElement rootFolder = this; + while(rootFolder.getParent() != null) + rootFolder = rootFolder.getParent(); + return rootFolder; + } + + /** + * Add a 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); + return newFolder; + } + + /** + * Fetch the number of sub folders. + * @return returns the number of sub folders. + */ + public int getSubFolderCount(){ + return subFolders.size(); + } + + /** + * Returns the subFolder element matching the supplied folder name. + * @param folderName the name of the subFolder to find. + * @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; + } + + public byte[] encode(int offset, int count) throws UnsupportedEncodingException { + StringWriter sw = new StringWriter(); + XmlSerializer xmlMsgElement = Xml.newSerializer(); + int i, stopIndex; + if(offset > subFolders.size()) + throw new IllegalArgumentException("FolderListingEncode: offset > subFolders.size()"); + + stopIndex = offset + count; + if(stopIndex > subFolders.size()) + stopIndex = subFolders.size(); + + try { + xmlMsgElement.setOutput(sw); + xmlMsgElement.startDocument(null, null); + xmlMsgElement.text("\n"); + xmlMsgElement.startTag("", "folder-listing"); + xmlMsgElement.attribute("", "version", "1.0"); + for(i = offset; i<stopIndex; i++) + { + xmlMsgElement.startTag("", "folder"); + xmlMsgElement.attribute("", "name", subFolders.get(i).getName()); + xmlMsgElement.endTag("", "folder"); + } + xmlMsgElement.endTag("", "folder-listing"); + xmlMsgElement.endDocument(); + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return sw.toString().getBytes("UTF-8"); + } +} diff --git a/src/com/android/bluetooth/map/BluetoothMapMessageListing.java b/src/com/android/bluetooth/map/BluetoothMapMessageListing.java new file mode 100644 index 000000000..0e5ba97d7 --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapMessageListing.java @@ -0,0 +1,92 @@ +/* +* 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.IOException; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.xmlpull.v1.XmlSerializer; + +import android.util.Log; +import android.util.Xml; + +public class BluetoothMapMessageListing { + + private static final String TAG = "BluetoothMapMessageListing"; + private List<BluetoothMapMessageListingElement> list; + + public BluetoothMapMessageListing(){ + list = new ArrayList<BluetoothMapMessageListingElement>(); + } + public void add(BluetoothMapMessageListingElement element) { + list.add(element); + } + + /** + * Used to fetch the number of BluetoothMapMessageListingElement elements in the list. + * @return the number of elements in the list. + */ + public int getCount() { + return list.size(); + } + /** + * Encode the list of BluetoothMapMessageListingElement(s) into a UTF-8 + * formatted XML-string in a trimmed byte array + * + * @return a reference to the encoded byte array. + * @throws UnsupportedEncodingException + * if UTF-8 encoding is unsupported on the platform. + */ + public byte[] encode() throws UnsupportedEncodingException { + StringWriter sw = new StringWriter(); + XmlSerializer xmlMsgElement = Xml.newSerializer(); + try { + xmlMsgElement.setOutput(sw); + xmlMsgElement.startDocument(null, null); + xmlMsgElement.startTag("", "MAP-msg-listing"); + xmlMsgElement.attribute("", "version", "1.0"); + // Do the XML encoding of list + for (BluetoothMapMessageListingElement element : list) { + element.encode(xmlMsgElement); // Append the list element + } + xmlMsgElement.endTag("", "MAP-msg-listing"); + xmlMsgElement.endDocument(); + } catch (IllegalArgumentException e) { + Log.w(TAG, e.toString()); + } catch (IllegalStateException e) { + Log.w(TAG, e.toString()); + } catch (IOException e) { + Log.w(TAG, e.toString()); + } + return sw.toString().getBytes("UTF-8"); + } + + public void sort() { + Collections.sort(list); + } + + public void segment(int count, int offset) { + count = Math.min(count, list.size()); + if (offset + count <= list.size()) { + list = list.subList(offset, offset + count); + } else { + list = null; + } + } +} diff --git a/src/com/android/bluetooth/map/BluetoothMapMessageListingElement.java b/src/com/android/bluetooth/map/BluetoothMapMessageListingElement.java new file mode 100644 index 000000000..1870486bf --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapMessageListingElement.java @@ -0,0 +1,256 @@ +/* +* 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.IOException; +import java.io.StringWriter; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.xmlpull.v1.XmlSerializer; + +import android.telephony.PhoneNumberUtils; +import android.util.Log; +import android.util.Xml; + +import com.android.bluetooth.map.BluetoothMapUtils.TYPE; + +public class BluetoothMapMessageListingElement + implements Comparable<BluetoothMapMessageListingElement> { + + private static final String TAG = "BluetoothMapMessageListingElement"; + private static final boolean D = true; + private static final boolean V = true; + + private long handle = 0; + 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; + + public long getHandle() { + return handle; + } + + public void setHandle(long handle) { + this.handle = handle; + } + + public long getDateTime() { + return dateTime; + } + + public String getDateTimeString() { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); + Date date = new Date(dateTime); + return format.format(date); // Format to YYYYMMDDTHHMMSS local time + } + + public void setDateTime(long dateTime) { + this.dateTime = dateTime; + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public String getSenderName() { + return senderName; + } + + public void setSenderName(String senderName) { + this.senderName = senderName; + } + + public String getSenderAddressing() { + return senderAddressing; + } + + 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 + } + } + + public String getReplyToAddressing() { + return replytoAddressing; + } + + public void setReplytoAddressing(String replytoAddressing) { + this.replytoAddressing = replytoAddressing; + } + + public String getRecipientName() { + return recipientName; + } + + public void setRecipientName(String recipientName) { + this.recipientName = recipientName; + } + + public String getRecipientAddressing() { + return recipientAddressing; + } + + public void setRecipientAddressing(String recipientAddressing) { + this.recipientAddressing = recipientAddressing; + } + + public TYPE getType() { + return type; + } + + public void setType(TYPE type) { + this.type = type; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getReceptionStatus() { + return receptionStatus; + } + + public void setReceptionStatus(String receptionStatus) { + this.receptionStatus = receptionStatus; + } + + public int getAttachmentSize() { + return attachmentSize; + } + + public void setAttachmentSize(int attachmentSize) { + this.attachmentSize = attachmentSize; + } + + public String getPriority() { + return priority; + } + + public void setPriority(String priority) { + this.priority = priority; + } + + public String getRead() { + return read; + } + + public void setRead(String read) { + this.read = read; + } + + public String getSent() { + return sent; + } + + public void setSent(String sent) { + this.sent = sent; + } + + public String getProtect() { + return protect; + } + + public void setProtect(String protect) { + this.protect = protect; + } + + public int compareTo(BluetoothMapMessageListingElement e) { + if (this.dateTime < e.dateTime) { + return 1; + } else if (this.dateTime > e.dateTime) { + return -1; + } else { + return 0; + } + } + + /* Encode the MapMessageListingElement into the StringBuilder reference. + * */ + public void encode(XmlSerializer xmlMsgElement) throws IllegalArgumentException, + IllegalStateException, IOException + { + // contruct the XML tag for a single msg in the msglisting + xmlMsgElement.startTag("", "msg"); + xmlMsgElement.attribute("", "handle", BluetoothMapUtils.getMapHandle(handle, type)); + xmlMsgElement.attribute("", "subject", subject); + xmlMsgElement.attribute("", "datetime", this.getDateTimeString()); + if (senderName != null) + xmlMsgElement.attribute("", "sender_name", senderName); + if (senderAddressing != null) + xmlMsgElement.attribute("", "sender_addressing", senderAddressing); + if (replytoAddressing != null) + xmlMsgElement.attribute("", "replyto_addressing",replytoAddressing); + if (recipientName != null) + xmlMsgElement.attribute("", "recipient_name",recipientName); + if (recipientAddressing != null) + xmlMsgElement.attribute("", "recipient_addressing", recipientAddressing); + if (type != null) + xmlMsgElement.attribute("", "type", type.name()); + if (size != -1) + xmlMsgElement.attribute("", "size", Integer.toString(size)); + if (text != null) + xmlMsgElement.attribute("", "text", text); + if (receptionStatus != null) + xmlMsgElement.attribute("", "reception_status", receptionStatus); + if (attachmentSize != -1) + xmlMsgElement.attribute("", "attachment_size", Integer.toString(attachmentSize)); + if (priority != null) + xmlMsgElement.attribute("", "priority", priority); + if (read != null) + xmlMsgElement.attribute("", "read", read); + if (sent != null) + xmlMsgElement.attribute("", "sent", sent); + if (protect != null) + xmlMsgElement.attribute("", "protect", protect); + xmlMsgElement.endTag("", "msg"); + } +} + + diff --git a/src/com/android/bluetooth/map/BluetoothMapObexServer.java b/src/com/android/bluetooth/map/BluetoothMapObexServer.java new file mode 100644 index 000000000..c1a82c2d4 --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapObexServer.java @@ -0,0 +1,695 @@ +/* +* 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.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Calendar; + +import javax.obex.HeaderSet; +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 { + + private static final String TAG = "BluetoothMapObexServer"; + + private static final boolean D = BluetoothMapService.DEBUG; + private static final boolean V = BluetoothMapService.VERBOSE; + + private static final int UUID_LENGTH = 16; + + // 128 bit UUID for MAP + private static final byte[] MAP_TARGET = new byte[] { + (byte)0xBB, (byte)0x58, (byte)0x2B, (byte)0x40, + (byte)0x42, (byte)0x0C, (byte)0x11, (byte)0xDB, + (byte)0xB0, (byte)0xDE, (byte)0x08, (byte)0x00, + (byte)0x20, (byte)0x0C, (byte)0x9A, (byte)0x66 + }; + + /* Message types */ + private static final String TYPE_GET_FOLDER_LISTING = "x-obex/folder-listing"; + private static final String TYPE_GET_MESSAGE_LISTING = "x-bt/MAP-msg-listing"; + private static final String TYPE_MESSAGE = "x-bt/message"; + private static final String TYPE_SET_MESSAGE_STATUS = "x-bt/messageStatus"; + private static final String TYPE_SET_NOTIFICATION_REGISTRATION = "x-bt/MAP-NotificationRegistration"; + private static final String TYPE_SEND_EVENT = "x-bt/MAP-event-report"; + private static final String TYPE_MESSAGE_UPDATE = "x-bt/MAP-messageUpdate"; + + private BluetoothMapFolderElement mCurrentFolder; + + private Handler mCallback = null; + + private Context mContext; + + public static boolean sIsAborted = false; + + BluetoothMapContent mOutContent; + + public BluetoothMapObexServer(Handler callback, Context context) { + super(); + mCallback = callback; + mContext = context; + mOutContent = new BluetoothMapContent(mContext); + + buildFolderStructure(); /* Build the default folder structure, and set + mCurrentFolder to root folder */ + } + + /** + * Build the default minimal folder structure, as defined in the MAP specification. + */ + private void buildFolderStructure(){ + 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"); + } + + @Override + public int onConnect(final HeaderSet request, HeaderSet reply) { + if (D) Log.d(TAG, "onConnect():"); + if (V) logHeader(request); + try { + byte[] uuid = (byte[])request.getHeader(HeaderSet.TARGET); + if (uuid == null) { + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + if (D) Log.d(TAG, "onConnect(): uuid=" + Arrays.toString(uuid)); + + if (uuid.length != UUID_LENGTH) { + Log.w(TAG, "Wrong UUID length"); + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + for (int i = 0; i < UUID_LENGTH; i++) { + if (uuid[i] != MAP_TARGET[i]) { + Log.w(TAG, "Wrong UUID"); + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + } + reply.setHeader(HeaderSet.WHO, uuid); + } catch (IOException e) { + Log.e(TAG, e.toString()); + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + + try { + byte[] remote = (byte[])request.getHeader(HeaderSet.WHO); + if (remote != null) { + if (D) Log.d(TAG, "onConnect(): remote=" + Arrays.toString(remote)); + reply.setHeader(HeaderSet.TARGET, remote); + } + } catch (IOException e) { + Log.e(TAG, e.toString()); + 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(); + + return ResponseCodes.OBEX_HTTP_OK; + } + + @Override + public void onDisconnect(final HeaderSet req, final HeaderSet resp) { + if (D) Log.d(TAG, "onDisconnect(): enter"); + if (V) logHeader(req); + + resp.responseCode = ResponseCodes.OBEX_HTTP_OK; + if (mCallback != null) { + Message msg = Message.obtain(mCallback); + msg.what = BluetoothMapService.MSG_SESSION_DISCONNECTED; + msg.sendToTarget(); + if (V) Log.v(TAG, "onDisconnect(): msg MSG_SESSION_DISCONNECTED sent out."); + } + } + + @Override + public int onAbort(HeaderSet request, HeaderSet reply) { + if (D) Log.d(TAG, "onAbort(): enter."); + sIsAborted = true; + return ResponseCodes.OBEX_HTTP_OK; + } + + @Override + public int onPut(final Operation op) { + if (D) Log.d(TAG, "onPut(): enter"); + HeaderSet request = null; + String type, name; + byte[] appParamRaw; + BluetoothMapAppParams appParams = null; + + 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:"); + } + return ResponseCodes.OBEX_HTTP_OK; + }else if(type.equals(TYPE_SET_NOTIFICATION_REGISTRATION)) { + if(V) { + Log.d(TAG,"TYPE_SET_NOTIFICATION_REGISTRATION: NotificationStatus: " + appParams.getNotificationStatus()); + } + 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 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()); + } + return pushMessage(op, name, appParams); + + } + + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + + private int setNotificationRegistration(BluetoothMapAppParams appParams) { + // Forward the request to the MNS thread as a message - including the MAS instance ID. + Handler mns = BluetoothMnsObexClient.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(); + return ResponseCodes.OBEX_HTTP_OK; + } else { + return ResponseCodes.OBEX_HTTP_UNAVAILABLE; // This should not happen. + } + } + + 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()); + return ResponseCodes.OBEX_HTTP_PRECON_FAILED; + } + try { + if(folderName == null || folderName.equals("")) { + folderName = mCurrentFolder.getName(); + } + if(!folderName.equals("outbox") && !folderName.equals("draft")) { + if(D) Log.d(TAG, "Push message only allowed to outbox and draft. folderName: " + folderName); + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + /* TODO: + * - Read out the message - OK + * - Decode into a bMessage - OK + * - push to draft or send. + */ + InputStream bMsgStream; + BluetoothMapbMessage message; + bMsgStream = op.openInputStream(); + message = BluetoothMapbMessage.parse(bMsgStream, appParams.getCharset()); // Decode the messageBody + // Send message + BluetoothMapContentObserver observer = BluetoothMnsObexClient.getContentObserver(); + if (observer == null) { + return ResponseCodes.OBEX_HTTP_UNAVAILABLE; // Should not happen. + } + + long handle = observer.pushMessage(message, folderName, appParams); + if (D) Log.d(TAG, "pushMessage handle: " + handle); + if (handle < 0) { + return ResponseCodes.OBEX_HTTP_UNAVAILABLE; // Should not happen. + } + HeaderSet replyHeaders = new HeaderSet(); + String handleStr = BluetoothMapUtils.getMapHandle(handle, message.getType()); + if (D) Log.d(TAG, "handleStr: " + handleStr + " message.getType(): " + message.getType()); + replyHeaders.setHeader(HeaderSet.NAME, handleStr); + op.sendHeaders(replyHeaders); + } catch (IllegalArgumentException e) { + if(D) Log.w(TAG, "Wrongly formatted bMessage received", e); + return ResponseCodes.OBEX_HTTP_PRECON_FAILED; + } catch (Exception e) { + // TODO: Change to IOException after debug + Log.e(TAG, "Exception occured: ", e); + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + return ResponseCodes.OBEX_HTTP_OK; + } + + private int setMessageStatus(String msgHandle, BluetoothMapAppParams appParams) { + int indicator = appParams.getStatusIndicator(); + int value = appParams.getStatusValue(); + if(indicator == BluetoothMapAppParams.INVALID_VALUE_PARAMETER || + value == BluetoothMapAppParams.INVALID_VALUE_PARAMETER || + msgHandle == null) { + return ResponseCodes.OBEX_HTTP_PRECON_FAILED; + } + BluetoothMapContentObserver observer = BluetoothMnsObexClient.getContentObserver(); + if (observer == null) { + return ResponseCodes.OBEX_HTTP_UNAVAILABLE; // Should not happen. + } + + long handle = BluetoothMapUtils.getCpHandle(msgHandle); + BluetoothMapUtils.TYPE msgType = BluetoothMapUtils.getMsgTypeFromHandle(msgHandle); + if( indicator == BluetoothMapAppParams.STATUS_INDICATOR_DELETED) { + if (!observer.setMessageStatusDeleted(handle, msgType, value)) { + return ResponseCodes.OBEX_HTTP_UNAVAILABLE; + } + } else /* BluetoothMapAppParams.STATUS_INDICATOR_READE */ { + if (!observer.setMessageStatusRead(handle, msgType, value)) { + return ResponseCodes.OBEX_HTTP_UNAVAILABLE; + } + } + return ResponseCodes.OBEX_HTTP_OK; + } + + @Override + public int onSetPath(final HeaderSet request, final HeaderSet reply, final boolean backup, + final boolean create) { + String folderName; + BluetoothMapFolderElement folder; + try { + folderName = (String)request.getHeader(HeaderSet.NAME); + } catch (Exception e) { + 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); + + if(backup == true){ + if(mCurrentFolder.getParent() != null) + mCurrentFolder = mCurrentFolder.getParent(); + else + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + + if (folderName == null || folderName == "") { + if(backup == false) + mCurrentFolder = mCurrentFolder.getRoot(); + } + else { + folder = mCurrentFolder.getSubFolder(folderName); + if(folder != null) + mCurrentFolder = folder; + else + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + if (V) Log.d(TAG, "Current Folder: " + mCurrentFolder.getName()); + return ResponseCodes.OBEX_HTTP_OK; + } + + @Override + public void onClose() { + if (mCallback != null) { + Message msg = Message.obtain(mCallback); + msg.what = BluetoothMapService.MSG_SERVERSESSION_CLOSE; + msg.sendToTarget(); + if (D) Log.d(TAG, "onClose(): msg MSG_SERVERSESSION_CLOSE sent out."); + } + } + + @Override + public int onGet(Operation op) { + sIsAborted = false; + HeaderSet request; + String type; + String name; + byte[] appParamRaw = null; + BluetoothMapAppParams appParams = null; + 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); + + if (V) logHeader(request); + if (D) Log.d(TAG, "OnGet type is " + type + " name is " + name); + + if (type == null) { + if (V) Log.d(TAG, "type is null?" + type); + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + + if (type.equals(TYPE_GET_FOLDER_LISTING)) { + if (V && appParams != null) { + Log.d(TAG,"TYPE_GET_FOLDER_LISTING: MaxListCount = " + appParams.getMaxListCount() + + ", ListStartOffset = " + appParams.getStartOffset()); + } + return sendFolderListingRsp(op, appParams); // Block until all packets have been send. + } + 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()); + Log.d(TAG,"SubjectLength = " + appParams.getSubjectLength() + ", ParameterMask = " + + appParams.getParameterMask()); + Log.d(TAG,"FilterMessageType = " + appParams.getFilterMessageType() + + ", FilterPeriodBegin = " + appParams.getFilterPeriodBegin()); + Log.d(TAG,"FilterPeriodEnd = " + appParams.getFilterPeriodBegin() + + ", FilterReadStatus = " + appParams.getFilterReadStatus()); + Log.d(TAG,"FilterRecipient = " + appParams.getFilterRecipient() + + ", FilterOriginator = " + appParams.getFilterOriginator()); + Log.d(TAG,"FilterPriority = " + appParams.getFilterPriority()); + } + return sendMessageListingRsp(op, appParams, name); // Block until all packets have been send. + } + 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()); + } + return sendGetMessageRsp(op, name, appParams); // Block until all packets have been send. + } + else { + Log.w(TAG, "unknown type request: " + type); + return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE; + } + } 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; + } + } + + /** + * Generate and send the message listing response based on an application + * parameter header. This function call will block until complete or aborted + * by the peer. Fragmentation of packets larger than the obex packet size + * will be handled by this function. + * + * @param op + * The OBEX operation. + * @param appParams + * The application parameter header + * @return {@link ResponseCodes.OBEX_HTTP_OK} on success or + * {@link ResponseCodes.OBEX_HTTP_BAD_REQUEST} on error. + */ + private int sendMessageListingRsp(Operation op, BluetoothMapAppParams appParams, String folderName){ + OutputStream outStream = null; + byte[] outBytes = null; + int maxChunkSize, bytesToWrite, bytesWritten = 0, listSize; + HeaderSet replyHeaders = new HeaderSet(); + BluetoothMapAppParams outAppParams = new BluetoothMapAppParams(); + BluetoothMapMessageListing outList; + if (folderName == null) { + folderName = mCurrentFolder.getName(); + } + if (appParams == null){ + appParams = new BluetoothMapAppParams(); + appParams.setMaxListCount(1024); + appParams.setStartOffset(0); + } + + // TODO: Check to see if we only need to send the size - hence no need to encode. + try { + // Open the OBEX body stream + outStream = op.openOutputStream(); + + if(appParams.getMaxListCount() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) + appParams.setMaxListCount(1024); + + if(appParams.getStartOffset() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) + appParams.setStartOffset(0); + + if(appParams.getMaxListCount() != 0) { + outList = mOutContent.msgListing(folderName, appParams); + // Generate the byte stream + outAppParams.setMessageListingSize(outList.getCount()); +// if(outList.getCount() != 0) { + outBytes = outList.encode(); +// } else { +// op.noBodyHeader(); + // TODO: Remove after test + //Log.w(TAG,"sendMessageListingRsp: Empty list - sending OBEX_HTTP_BAD_REQUEST"); + //return ResponseCodes.OBEX_HTTP_BAD_REQUEST; +// } + } + else { + listSize = mOutContent.msgListingSize(folderName, appParams); + outAppParams.setMessageListingSize(listSize); + op.noBodyHeader(); + } + + // Build the application parameter header + outAppParams.setNewMessage(0); // TODO: set depending on new messages + outAppParams.setMseTime(Calendar.getInstance().getTime().getTime()); + + replyHeaders.setHeader(HeaderSet.APPLICATION_PARAMETER, outAppParams.EncodeParams()); + + op.sendHeaders(replyHeaders); + + } catch (IOException e) { + Log.w(TAG,"sendMessageListingRsp: IOException - sending OBEX_HTTP_BAD_REQUEST", e); + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } catch (IllegalArgumentException e) { + Log.w(TAG,"sendMessageListingRsp: IllegalArgumentException - sending OBEX_HTTP_BAD_REQUEST", e); + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } catch (Exception e){ + Log.w(TAG, "Exception:", e); + // TODO: REMOVE AFTER TEST!! + } + + maxChunkSize = op.getMaxPacketSize(); // This must be called after setting the headers. + if (outBytes != null) { + try { + while (bytesWritten < outBytes.length && sIsAborted == false) { + bytesToWrite = Math.min(maxChunkSize, outBytes.length - bytesWritten); + outStream.write(outBytes, bytesWritten, bytesToWrite); + bytesWritten += bytesToWrite; + } + } catch (IOException e) { + if(V) Log.w(TAG,e); + // We were probably aborted or disconnected + } catch (Exception e){ + if(V) Log.w(TAG,e); + // TODO: REMOVE AFTER TEST!! + } finally { + if (outStream != null) { + try { + outStream.close(); + } catch (IOException e) { + // If an error occurs during close, there is no more cleanup to do + } + } + } + if (bytesWritten != outBytes.length) + 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 + } + } + return ResponseCodes.OBEX_HTTP_OK; + } + + /** + * Generate and send the Folder listing response based on an application + * parameter header. This function call will block until complete or aborted + * by the peer. Fragmentation of packets larger than the obex packet size + * will be handled by this function. + * + * @param op + * The OBEX operation. + * @param appParams + * The application parameter header + * @return {@link ResponseCodes.OBEX_HTTP_OK} on success or + * {@link ResponseCodes.OBEX_HTTP_BAD_REQUEST} on error. + */ + private int sendFolderListingRsp(Operation op, BluetoothMapAppParams appParams){ + OutputStream outStream = null; + byte[] outBytes = null; + BluetoothMapAppParams outAppParams = new BluetoothMapAppParams(); + int maxChunkSize, bytesWritten = 0; + HeaderSet replyHeaders = new HeaderSet(); + int bytesToWrite, maxListCount, listStartOffset; + if(appParams == null){ + appParams = new BluetoothMapAppParams(); + appParams.setMaxListCount(1024); + } + + if(V) + Log.v(TAG,"sendFolderList for " + mCurrentFolder.getName()); + + try { + maxListCount = appParams.getMaxListCount(); + listStartOffset = appParams.getStartOffset(); + + if(listStartOffset == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) + listStartOffset = 0; + + if(maxListCount == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) + maxListCount = 1024; + + if(maxListCount != 0) + { + outBytes = mCurrentFolder.encode(listStartOffset, maxListCount); + outStream = op.openOutputStream(); + } + + // Build and set the application parameter header + outAppParams.setFolderListingSize(mCurrentFolder.getSubFolderCount()); + replyHeaders.setHeader(HeaderSet.APPLICATION_PARAMETER, outAppParams.EncodeParams()); + op.sendHeaders(replyHeaders); + + } catch (IOException e1) { + Log.w(TAG,"sendFolderListingRsp: IOException - sending OBEX_HTTP_BAD_REQUEST Exception:", e1); + 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; + } + + maxChunkSize = op.getMaxPacketSize(); // This must be called after setting the headers. + + if(outBytes != null) { + try { + while (bytesWritten < outBytes.length && sIsAborted == 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(V) + Log.v(TAG,"sendFolderList sent " + bytesWritten + " bytes out of "+ outBytes.length); + if(bytesWritten == outBytes.length) + return ResponseCodes.OBEX_HTTP_OK; + else + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + + return ResponseCodes.OBEX_HTTP_OK; + } + + /** + * Generate and send the Folder listing response based on an application + * parameter header. This function call will block until complete or aborted + * by the peer. Fragmentation of packets larger than the obex packet size + * will be handled by this function. + * + * @param op + * The OBEX operation. + * @param appParams + * The application parameter header + * @return {@link ResponseCodes.OBEX_HTTP_OK} on success or + * {@link ResponseCodes.OBEX_HTTP_BAD_REQUEST} on error. + */ + private int sendGetMessageRsp(Operation op, String name, BluetoothMapAppParams appParams){ + OutputStream outStream ; + byte[] outBytes; + int maxChunkSize, bytesToWrite, bytesWritten = 0; + long msgHandle; + + try { + outBytes = mOutContent.getMessage(name, appParams.getCharset()); + outStream = op.openOutputStream(); + + } catch (IOException e) { + Log.w(TAG,"sendGetMessageRsp: IOException - sending OBEX_HTTP_BAD_REQUEST", e); + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } catch (IllegalArgumentException e) { + Log.w(TAG,"sendGetMessageRsp: IllegalArgumentException (e.g. invalid handle) - sending OBEX_HTTP_BAD_REQUEST", e); + 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) { + 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(bytesWritten == outBytes.length) + return ResponseCodes.OBEX_HTTP_OK; + else + return ResponseCodes.OBEX_HTTP_BAD_REQUEST; + } + + return ResponseCodes.OBEX_HTTP_OK; + } + + + public static final void logHeader(HeaderSet hs) { + Log.v(TAG, "Dumping HeaderSet " + hs.toString()); + try { + Log.v(TAG, "CONNECTION_ID : " + hs.getHeader(HeaderSet.CONNECTION_ID)); + Log.v(TAG, "NAME : " + hs.getHeader(HeaderSet.NAME)); + Log.v(TAG, "TYPE : " + hs.getHeader(HeaderSet.TYPE)); + Log.v(TAG, "TARGET : " + hs.getHeader(HeaderSet.TARGET)); + Log.v(TAG, "WHO : " + hs.getHeader(HeaderSet.WHO)); + Log.v(TAG, "APPLICATION_PARAMETER : " + hs.getHeader(HeaderSet.APPLICATION_PARAMETER)); + } catch (IOException e) { + Log.e(TAG, "dump HeaderSet error " + e); + } + Log.v(TAG, "NEW!!! Dumping HeaderSet END"); + } +} diff --git a/src/com/android/bluetooth/map/BluetoothMapReceiver.java b/src/com/android/bluetooth/map/BluetoothMapReceiver.java new file mode 100644 index 000000000..7363e0048 --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapReceiver.java @@ -0,0 +1,64 @@ +/* +* 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.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class BluetoothMapReceiver extends BroadcastReceiver { + + private static final String TAG = "BluetoothMapReceiver"; + + private static final boolean V = BluetoothMapService.VERBOSE; + + @Override + public void onReceive(Context context, Intent intent) { + if (V) Log.v(TAG, "MapReceiver onReceive "); + + Intent in = new Intent(); + in.putExtras(intent); + in.setClass(context, BluetoothMapService.class); + String action = intent.getAction(); + in.putExtra("action", action); + if (V) Log.v(TAG,"***********action = " + action); + + boolean startService = true; + if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); + in.putExtra(BluetoothAdapter.EXTRA_STATE, state); + if (V) Log.v(TAG,"***********state = " + state); + if ((state == BluetoothAdapter.STATE_TURNING_ON) + || (state == BluetoothAdapter.STATE_OFF)) { + //FIX: We turn on MAP after BluetoothAdapter.STATE_ON, + //but we turn off MAP right after BluetoothAdapter.STATE_TURNING_OFF + startService = false; + } + } else { + // Don't forward intent unless device has bluetooth and bluetooth is enabled. + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter == null || !adapter.isEnabled()) { + startService = false; + } + } + if (startService) { + if (V) Log.v(TAG,"***********Calling start service!!!! with action = " + in.getAction()); + context.startService(in); + } + } +} diff --git a/src/com/android/bluetooth/map/BluetoothMapRfcommTransport.java b/src/com/android/bluetooth/map/BluetoothMapRfcommTransport.java new file mode 100644 index 000000000..90437d8f7 --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapRfcommTransport.java @@ -0,0 +1,72 @@ +/* +* 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.bluetooth.BluetoothSocket; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.obex.ObexTransport; + +public class BluetoothMapRfcommTransport implements ObexTransport { + private BluetoothSocket mSocket = null; + + public BluetoothMapRfcommTransport(BluetoothSocket rfs) { + super(); + this.mSocket = rfs; + } + + public void close() throws IOException { + mSocket.close(); + } + + public DataInputStream openDataInputStream() throws IOException { + return new DataInputStream(openInputStream()); + } + + public DataOutputStream openDataOutputStream() throws IOException { + return new DataOutputStream(openOutputStream()); + } + + public InputStream openInputStream() throws IOException { + return mSocket.getInputStream(); + } + + public OutputStream openOutputStream() throws IOException { + return mSocket.getOutputStream(); + } + + public void connect() throws IOException { + } + + public void create() throws IOException { + } + + public void disconnect() throws IOException { + } + + public void listen() throws IOException { + } + + public boolean isConnected() throws IOException { + return true; + } + +} diff --git a/src/com/android/bluetooth/map/BluetoothMapService.java b/src/com/android/bluetooth/map/BluetoothMapService.java new file mode 100644 index 000000000..e4da10f11 --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapService.java @@ -0,0 +1,778 @@ +/* +* 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.IOException; + +import javax.obex.ServerSession; +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; +import android.bluetooth.BluetoothServerSocket; +import android.bluetooth.IBluetooth; +import android.bluetooth.IBluetoothMap; +import android.bluetooth.BluetoothUuid; +import android.bluetooth.BluetoothMap; +import android.bluetooth.BluetoothSocket; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.PowerManager; +import android.text.TextUtils; +import android.util.Log; + +import com.android.bluetooth.R; +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.AdapterService; + + +public class BluetoothMapService extends Service { + private static final String TAG = "BluetoothMapService"; + + /** + * To enable MAP DEBUG/VERBOSE logging - run below cmd in adb shell, and + * restart com.android.bluetooth process. only enable DEBUG log: + * "setprop log.tag.BluetoothMapService DEBUG"; enable both VERBOSE and + * DEBUG log: "setprop log.tag.BluetoothMapService VERBOSE" + */ + + public static final boolean DEBUG = true; + + public static final boolean VERBOSE = true; + + /** + * Intent indicating incoming obex authentication request which is from + * PCE(Carkit) + */ + public static final String AUTH_CHALL_ACTION = "com.android.bluetooth.map.authchall"; + + /** + * Intent indicating obex session key input complete by user which is sent + * from BluetoothMapActivity + */ + public static final String AUTH_RESPONSE_ACTION = "com.android.bluetooth.map.authresponse"; + + /** + * Intent indicating user canceled obex authentication session key input + * which is sent from BluetoothMapActivity + */ + public static final String AUTH_CANCELLED_ACTION = "com.android.bluetooth.map.authcancelled"; + + /** + * Intent indicating timeout for user confirmation, which is sent to + * BluetoothMapActivity + */ + public static final String USER_CONFIRM_TIMEOUT_ACTION = + "com.android.bluetooth.map.userconfirmtimeout"; + + /** + * 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"; + + public static final int MSG_SERVERSESSION_CLOSE = 5000; + + public static final int MSG_SESSION_ESTABLISHED = 5001; + + public static final int MSG_SESSION_DISCONNECTED = 5002; + + public static final int MSG_OBEX_AUTH_CHALL = 5003; + + private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; + + private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; + + private static final int START_LISTENER = 1; + + private static final int USER_TIMEOUT = 2; + + private static final int AUTH_TIMEOUT = 3; + + + private static final int USER_CONFIRM_TIMEOUT_VALUE = 30000; + + + // Ensure not conflict with Opp notification ID + private static final int NOTIFICATION_ID_ACCESS = -1000001; + + private static final int NOTIFICATION_ID_AUTH = -1000002; + + private PowerManager.WakeLock mWakeLock = null; + + private BluetoothAdapter mAdapter; + + private SocketAcceptThread mAcceptThread = null; + + private BluetoothMapAuthenticator mAuth = null; + + private BluetoothMapObexServer mMapServer; + + private ServerSession mServerSession = null; + + private BluetoothMnsObexClient mBluetoothMnsObexClient = null; + + private BluetoothServerSocket mServerSocket = null; + + private BluetoothSocket mConnSocket = null; + + private BluetoothDevice mRemoteDevice = null; + + private static String sLocalPhoneNum = null; + + private static String sLocalPhoneName = null; + + private static String sRemoteDeviceName = null; + + private boolean mHasStarted = false; + + private volatile boolean mInterrupted; + + private int mState; + + private int mStartId = -1; + + //private IBluetooth mBluetoothService; + + private boolean isWaitingAuthorization = 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"; + + public BluetoothMapService() { + mState = BluetoothMap.STATE_DISCONNECTED; + } + + @Override + public void onCreate() { + super.onCreate(); + if (VERBOSE) Log.v(TAG, "Map Service onCreate"); + + mInterrupted = false; + mAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (!mHasStarted) { + mHasStarted = true; + if (VERBOSE) Log.v(TAG, "Starting MAP service"); + + int state = mAdapter.getState(); + if (state == BluetoothAdapter.STATE_ON) { + // start RFCOMM listener + mSessionStatusHandler.sendMessage(mSessionStatusHandler + .obtainMessage(START_LISTENER)); + } + } + } + // incoming Start intent handler + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + mStartId = startId; + if (mAdapter == null) { + Log.w(TAG, "Stopping BluetoothMapService: " + + "device does not have BT or device is not ready"); + // Release all resources + closeService(); + } else { + // No need to handle the null intent case, because we have + // all restart work done in onCreate() + if (intent != null) { + parseIntent(intent); + } + } + return START_NOT_STICKY; + } + + // process the intent from receiver + private void parseIntent(final Intent intent) { + String action = intent.getStringExtra("action"); + if (VERBOSE) Log.v(TAG, "action: " + action); + + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); + if (VERBOSE) Log.v(TAG, "state: " + state); + + boolean removeTimeoutMsg = true; + // BT status have been changed check new state + if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + if (state == BluetoothAdapter.STATE_TURNING_OFF) { + // Send any pending timeout now, as this service will be destroyed. + if (mSessionStatusHandler.hasMessages(USER_TIMEOUT)) { + Intent timeoutIntent = + new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL); + timeoutIntent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS); + sendBroadcast(timeoutIntent, BLUETOOTH_ADMIN_PERM); + } + // Release all resources + closeService(); + } else { + removeTimeoutMsg = false; + } + // Authorization answer intent + } else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) { + if (!isWaitingAuthorization) { + // this reply is not for us + return; + } + + isWaitingAuthorization = false; + + if (intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT, + BluetoothDevice.CONNECTION_ACCESS_NO) == + BluetoothDevice.CONNECTION_ACCESS_YES) { + //bluetooth connection accepted by user + if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) { + boolean result = mRemoteDevice.setTrust(true); + if (VERBOSE) Log.v(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()); + } + } else { + stopObexServerSession(); + } + } else if (action.equals(AUTH_RESPONSE_ACTION)) { + String sessionkey = intent.getStringExtra(EXTRA_SESSION_KEY); + //send auth request + notifyAuthKeyInput(sessionkey); + } else if (action.equals(AUTH_CANCELLED_ACTION)) { + //user cancelled auth request + notifyAuthCancelled(); + } else { + removeTimeoutMsg = false; + } + + if (removeTimeoutMsg) { + mSessionStatusHandler.removeMessages(USER_TIMEOUT); + } + } + + @Override + public void onDestroy() { + if (VERBOSE) Log.v(TAG, "Map Service onDestroy"); + + super.onDestroy(); + setState(BluetoothMap.STATE_DISCONNECTED, BluetoothMap.RESULT_CANCELED); + if (mWakeLock != null) { + mWakeLock.release(); + mWakeLock = null; + } + closeService(); + if(mSessionStatusHandler != null) { + mSessionStatusHandler.removeCallbacksAndMessages(null); + } + } + + @Override + public IBinder onBind(Intent intent) { + if (VERBOSE) Log.v(TAG, "Map Service onBind"); + return mBinder; + } + + private void startRfcommSocketListener() { + if (VERBOSE) Log.v(TAG, "Map Service startRfcommSocketListener"); + + if (mAcceptThread == null) { + mAcceptThread = new SocketAcceptThread(); + mAcceptThread.setName("BluetoothMapAcceptThread"); + mAcceptThread.start(); + } + } + + private final boolean initSocket() { + if (VERBOSE) Log.v(TAG, "Map Service initSocket"); + + boolean initSocketOK = true; + 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++) { + try { + // It is mandatory for PSE to support initiation of bonding and + // encryption. + mServerSocket = mAdapter.listenUsingEncryptedRfcommWithServiceRecord + ("OBEX Message Access Server", BluetoothUuid.MAP.getUuid()); + + } catch (IOException e) { + Log.e(TAG, "Error create RfcommServerSocket " + e.toString()); + initSocketOK = false; + } + if (!initSocketOK) { + // Need to break out of this loop if BT is being turned off. + if (mAdapter == null) break; + int state = mAdapter.getState(); + if ((state != BluetoothAdapter.STATE_TURNING_ON) && + (state != BluetoothAdapter.STATE_ON)) { + Log.w(TAG, "initServerSocket failed as BT is (being) turned off"); + break; + } + synchronized (this) { + try { + if (VERBOSE) Log.v(TAG, "wait 300 ms"); + Thread.sleep(300); + } catch (InterruptedException e) { + Log.e(TAG, "socketAcceptThread thread was interrupted (3)"); + mInterrupted = true; + } + } + } else { + break; + } + } + + 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"); + } + return initSocketOK; + } + + private final void closeSocket(boolean server, boolean accept) throws IOException { + if (server == true) { + // Stop the possible trying to init serverSocket + mInterrupted = true; + + if (mServerSocket != null) { + mServerSocket.close(); + mServerSocket = null; + } + } + + if (accept == true) { + if (mConnSocket != null) { + mConnSocket.close(); + mConnSocket = null; + } + } + } + + private final void closeService() { + if (VERBOSE) Log.v(TAG, "Map Service closeService in"); + + try { + closeSocket(true, true); + } catch (IOException ex) { + Log.e(TAG, "CloseSocket error: " + ex); + } + + if (mAcceptThread != null) { + try { + mAcceptThread.shutdown(); + mAcceptThread.join(); + mAcceptThread = null; + } catch (InterruptedException ex) { + Log.w(TAG, "mAcceptThread close error", ex); + } + } + if (mServerSession != null) { + mServerSession.close(); + mServerSession = null; + } + if (mBluetoothMnsObexClient != null) { + try { + mBluetoothMnsObexClient.interrupt(); + mBluetoothMnsObexClient.join(); + mBluetoothMnsObexClient = null; + } catch (InterruptedException ex) { + Log.w(TAG, "mBluetoothMnsObexClient close error", ex); + } + } +// mBluetoothMnsObexClient.shutdown + + mHasStarted = false; + if (mStartId != -1 && stopSelfResult(mStartId)) { + if (VERBOSE) Log.v(TAG, "successfully stopped map service"); + mStartId = -1; + } + if (VERBOSE) Log.v(TAG, "Map Service closeService out"); + } + + private final void startObexServerSession() throws IOException { + if (VERBOSE) Log.v(TAG, "Map Service startObexServerSession"); + + // acquire the wakeLock before start Obex transaction thread + if (mWakeLock == null) { + PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "StartingObexMapTransaction"); + mWakeLock.setReferenceCounted(false); + mWakeLock.acquire(); + } + + + mMapServer = new BluetoothMapObexServer(mSessionStatusHandler, this); + synchronized (this) { + // We need to get authentication now that obex server is up + mAuth = new BluetoothMapAuthenticator(mSessionStatusHandler); + mAuth.setChallenged(false); + mAuth.setCancelled(false); + } + // setup RFCOMM transport + BluetoothMapRfcommTransport transport = new BluetoothMapRfcommTransport(mConnSocket); + mServerSession = new ServerSession(transport, mMapServer, mAuth); + mBluetoothMnsObexClient = new BluetoothMnsObexClient(this, mRemoteDevice); + mBluetoothMnsObexClient.start(); // Initiate the MNS message loop. + setState(BluetoothMap.STATE_CONNECTED); + if (VERBOSE) { + Log.v(TAG, "startObexServerSession() success!"); + } + } + + private void stopObexServerSession() { + if (VERBOSE) Log.v(TAG, "Map Service stopObexServerSession"); + + // 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.disconnect(); + mBluetoothMnsObexClient = null; + } + + try { + closeSocket(false, true); + mConnSocket = null; + } catch (IOException e) { + Log.e(TAG, "closeSocket error: " + e.toString()); + } + // Last obex transaction is finished, we start to listen for incoming + // connection again + if (mAdapter.isEnabled()) { + startRfcommSocketListener(); + } + setState(BluetoothMap.STATE_DISCONNECTED); + } + + private void notifyAuthKeyInput(final String key) { + synchronized (mAuth) { + if (key != null) { + mAuth.setSessionKey(key); + } + mAuth.setChallenged(true); + mAuth.notify(); + } + } + + private void notifyAuthCancelled() { + synchronized (mAuth) { + mAuth.setCancelled(true); + mAuth.notify(); + } + } + + /** + * A thread that runs in the background waiting for remote rfcomm + * connect.Once a remote socket connected, this thread shall be + * shutdown.When the remote disconnect,this thread shall run again waiting + * for next request. + */ + private class SocketAcceptThread extends Thread { + + private boolean stopped = false; + + @Override + public void run() { + if (mServerSocket == null) { + if (!initSocket()) { + closeService(); + return; + } + } + + while (!stopped) { + try { + if (VERBOSE) Log.v(TAG, "Accepting socket connection..."); + mConnSocket = mServerSocket.accept(); + if (VERBOSE) Log.v(TAG, "Accepted socket connection..."); + + 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 (VERBOSE) Log.v(TAG, "GetTrustState() = " + trust); + + if (trust) { + try { + if (VERBOSE) Log.v(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.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); + intent.putExtra(BluetoothDevice.EXTRA_PACKAGE_NAME, getPackageName()); + intent.putExtra(BluetoothDevice.EXTRA_CLASS_NAME, + BluetoothMapReceiver.class.getName()); + sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + isWaitingAuthorization = true; + + if (VERBOSE) Log.v(TAG, "waiting for authorization for connection from: " + + sRemoteDeviceName); + + } + stopped = true; // job done ,close this thread; + } catch (IOException ex) { + stopped=true; + if (VERBOSE) Log.v(TAG, "Accept exception: " + ex.toString()); + } + } + } + + void shutdown() { + stopped = true; + interrupt(); + } + } + + private final Handler mSessionStatusHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (VERBOSE) Log.v(TAG, "Handler(): got msg=" + msg.what); + + switch (msg.what) { + case START_LISTENER: + if (mAdapter.isEnabled()) { + startRfcommSocketListener(); + } else { + closeService();// release all resources + } + break; + case USER_TIMEOUT: + + Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL); + intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS); + sendBroadcast(intent); + isWaitingAuthorization = false; + stopObexServerSession(); + break; + case AUTH_TIMEOUT: + Intent i = new Intent(USER_CONFIRM_TIMEOUT_ACTION); + sendBroadcast(i); + removeMapNotification(NOTIFICATION_ID_AUTH); + notifyAuthCancelled(); + break; + case MSG_SERVERSESSION_CLOSE: + stopObexServerSession(); + break; + case MSG_SESSION_ESTABLISHED: + break; + case MSG_SESSION_DISCONNECTED: + // handled elsewhere + break; + case MSG_OBEX_AUTH_CHALL: + createMapNotification(AUTH_CHALL_ACTION); + mSessionStatusHandler.sendMessageDelayed(mSessionStatusHandler + .obtainMessage(AUTH_TIMEOUT), USER_CONFIRM_TIMEOUT_VALUE); + break; + default: + break; + } + } + }; + + private void setState(int state) { + setState(state, BluetoothMap.RESULT_SUCCESS); + } + + private synchronized void setState(int state, int result) { + if (state != mState) { + if (DEBUG) Log.d(TAG, "Map state " + mState + " -> " + state + ", result = " + + result); + int prevState = mState; + mState = state; + Intent intent = new Intent(BluetoothMap.MAP_STATE_CHANGED_ACTION); + intent.putExtra(BluetoothMap.MAP_PREVIOUS_STATE, prevState); + intent.putExtra(BluetoothMap.MAP_STATE, mState); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice); + sendBroadcast(intent, BLUETOOTH_PERM); + AdapterService s = AdapterService.getAdapterService(); + if (s != null) { + s.onProfileConnectionStateChanged(mRemoteDevice, BluetoothProfile.MAP, + mState, prevState); + } + } + } + + private void createMapNotification(String action) { + + NotificationManager nm = (NotificationManager) + getSystemService(Context.NOTIFICATION_SERVICE); + + // Create an intent triggered by clicking on the status icon. + Intent clickIntent = new Intent(); + clickIntent.setClass(this, BluetoothMapActivity.class); + clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + clickIntent.setAction(action); + + // Create an intent triggered by clicking on the + // "Clear All Notifications" button + Intent deleteIntent = new Intent(); + deleteIntent.setClass(this, BluetoothMapReceiver.class); + + Notification notification = null; + String name = getRemoteDeviceName(); + + if (action.equals(AUTH_CHALL_ACTION)) { + deleteIntent.setAction(AUTH_CANCELLED_ACTION); + notification = new Notification.Builder(this) + .setContentTitle(getString(R.string.auth_notif_title)) + .setContentText(getString(R.string.auth_notif_message,name)) + .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) + .build(); + + notification.flags |= Notification.FLAG_AUTO_CANCEL; + notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE; + notification.defaults = Notification.DEFAULT_SOUND; + notification.deleteIntent = PendingIntent.getBroadcast(this, 0, deleteIntent, 0); + nm.notify(NOTIFICATION_ID_AUTH, notification); + } + } + + private void removeMapNotification(int id) { + NotificationManager nm = (NotificationManager) + getSystemService(Context.NOTIFICATION_SERVICE); + nm.cancel(id); + } + + public static String getRemoteDeviceName() { + return sRemoteDeviceName; + } + + /** + * Handlers for incoming service calls + */ + private final IBluetoothMap.Stub mBinder = new IBluetoothMap.Stub() { + public int getState() { + if (DEBUG) Log.d(TAG, "getState " + mState); + + if (!Utils.checkCaller()) { + Log.w(TAG,"getState(): not allowed for non-active user"); + return BluetoothMap.STATE_DISCONNECTED; + } + + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mState; + } + + public BluetoothDevice getClient() { + if (DEBUG) Log.d(TAG, "getClient" + mRemoteDevice); + + if (!Utils.checkCaller()) { + Log.w(TAG,"getClient(): not allowed for non-active user"); + return null; + } + + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if (mState == BluetoothMap.STATE_DISCONNECTED) { + return null; + } + return mRemoteDevice; + } + + public boolean isConnected(BluetoothDevice device) { + if (!Utils.checkCaller()) { + Log.w(TAG,"isConnected(): not allowed for non-active user"); + return false; + } + + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mState == BluetoothMap.STATE_CONNECTED && mRemoteDevice.equals(device); + } + + public boolean connect(BluetoothDevice device) { + if (!Utils.checkCaller()) { + Log.w(TAG,"connect(): not allowed for non-active user"); + return false; + } + + enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + return false; + } + + public void disconnect() { + if (DEBUG) Log.d(TAG, "disconnect"); + + if (!Utils.checkCaller()) { + Log.w(TAG,"disconnect(): not allowed for non-active user"); + return; + } + + enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, + "Need BLUETOOTH_ADMIN permission"); + synchronized (BluetoothMapService.this) { + switch (mState) { + case BluetoothMap.STATE_CONNECTED: + if (mServerSession != null) { + mServerSession.close(); + mServerSession = null; + } + try { + closeSocket(false, true); + mConnSocket = null; + } catch (IOException ex) { + Log.e(TAG, "Caught the error: " + ex); + } + setState(BluetoothMap.STATE_DISCONNECTED, BluetoothMap.RESULT_CANCELED); + break; + default: + break; + } + } + } + }; +} diff --git a/src/com/android/bluetooth/map/BluetoothMapSmsPdu.java b/src/com/android/bluetooth/map/BluetoothMapSmsPdu.java new file mode 100644 index 000000000..7f87cd76b --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapSmsPdu.java @@ -0,0 +1,721 @@ +/* +* 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 static android.telephony.TelephonyManager.PHONE_TYPE_CDMA; +import static com.android.internal.telephony.SmsConstants.ENCODING_7BIT; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.Random; + +import android.telephony.PhoneNumberUtils; +import android.telephony.SmsMessage; +import android.telephony.TelephonyManager; +import android.util.Log; + +import com.android.internal.telephony.*; +/*import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; +import com.android.internal.telephony.SmsConstants;*/ +import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.SmsMessageBase; +import com.android.internal.telephony.SmsMessageBase.SubmitPduBase; +import com.android.internal.telephony.cdma.sms.*; +import com.android.internal.telephony.gsm.SmsMessage.SubmitPdu; + +public class BluetoothMapSmsPdu { + + private static final String TAG = "BluetoothMapSmsPdu"; + private static final boolean V = true; + private static int INVALID_VALUE = -1; + public static int SMS_TYPE_GSM = 1; + public static int SMS_TYPE_CDMA = 2; + + + /* TODO: 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: + * "The native format should be <sc_addr><tpdu> where <sc_addr> is <length><ton><1..10 octet of address> + * coded according to 24.011. The IEI is not to be used, as the fixed order of the data makes a type 4 LV + * information element sufficient. <length> is a single octet which value is the length of the value-field + * in octets including both the <ton> and the <address>." + * */ + + + 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; + + /* Members used for pdu decoding */ + private int userDataSeptetPadding = INVALID_VALUE; + private int msgSeptetCount = 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 + } + + /** + * Create a pdu instance based on the data generated on this device. + * @param data + * @param encoding + * @param type + * @param languageTable + */ + SmsPdu(byte[]data, int encoding, int type, int languageTable){ + this.data = data; + this.encoding = encoding; + this.type = type; + this.languageTable = languageTable; + } + public byte[] getData(){ + return data; + } + public byte[] getScAddress(){ + return scAddress; + } + public void setEncoding(int encoding) { + this.encoding = encoding; + } + public int getEncoding(){ + return encoding; + } + public int getType(){ + return type; + } + public int getUserDataMsgOffset() { + return userDataMsgOffset; + } + /** The user data message payload size in bytes - excluding the user data header. */ + public int getUserDataMsgSize() { + return data.length - userDataMsgOffset; + } + + public int getLanguageShiftTable() { + return languageShiftTable; + } + + public int getLanguageTable() { + return languageTable; + } + + public int getUserDataSeptetPadding() { + return userDataSeptetPadding; + } + + public int getMsgSeptetCount() { + return msgSeptetCount; + } + + + /* PDU parsing/modification functionality */ + private final static byte TELESERVICE_IDENTIFIER = 0x00; + private final static byte SERVICE_CATEGORY = 0x01; + private final static byte ORIGINATING_ADDRESS = 0x02; + private final static byte ORIGINATING_SUB_ADDRESS = 0x03; + private final static byte DESTINATION_ADDRESS = 0x04; + private final static byte DESTINATION_SUB_ADDRESS = 0x05; + private final static byte BEARER_REPLY_OPTION = 0x06; + private final static byte CAUSE_CODES = 0x07; + private final static byte BEARER_DATA = 0x08; + + /** + * Find and return the offset to the specified parameter ID + * @param parameterId The parameter ID to find + * @return the offset in number of bytes to the parameterID entry in the pdu data. + * The byte at the offset contains the parameter ID, the byte following contains the + * parameter length, and offset + 2 is the first byte of the parameter data. + */ + private int cdmaGetParameterOffset(byte parameterId) { + ByteArrayInputStream pdu = new ByteArrayInputStream(data); + int offset = 0; + boolean found = false; + + try { + pdu.skip(1); // Skip the message type + + while (pdu.available() > 0) { + int currentId = pdu.read(); + int currentLen = pdu.read(); + + if(currentId == parameterId) { + found = true; + break; + } + else { + pdu.skip(currentLen); + offset += 2 + currentLen; + } + } + pdu.close(); + } catch (Exception e) { + Log.e(TAG, "cdmaGetParameterOffset: ", e); + } + + if(found) + return offset; + else + return 0; + } + + private final static byte BEARER_DATA_MSG_ID = 0x00; + + private int cdmaGetSubParameterOffset(byte subParameterId) { + ByteArrayInputStream pdu = new ByteArrayInputStream(data); + int offset = 0; + boolean found = false; + offset = cdmaGetParameterOffset(BEARER_DATA) + 2; // Add to offset the BEARER_DATA parameter id and length bytes + pdu.skip(offset); + try { + + while (pdu.available() > 0) { + int currentId = pdu.read(); + int currentLen = pdu.read(); + + if(currentId == subParameterId) { + found = true; + break; + } + else { + pdu.skip(currentLen); + offset += 2 + currentLen; + } + } + pdu.close(); + } catch (Exception e) { + Log.e(TAG, "cdmaGetParameterOffset: ", e); + } + + if(found) + return offset; + else + return 0; + } + + + public void cdmaChangeToDeliverPdu(long date){ + /* Things to change: + * - Message Type in bearer data (Not the overall point-to-point type) + * - Change address ID from destination to originating (sub addresses are not used) + * - A time stamp is not mandatory. + */ + int offset; + offset = cdmaGetParameterOffset(DESTINATION_ADDRESS); + data[offset] = ORIGINATING_ADDRESS; + offset = cdmaGetParameterOffset(DESTINATION_SUB_ADDRESS); + data[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. + // 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; + +// } + //TODO: Error handling. + /* 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 + private static final byte TP_MMS_NO_MORE = 0x04; // bit 2 + private static final byte TP_RP_NO_REPLY_PATH = 0x00; // bit 7 + private static final byte TP_UDHI_MASK = 0x20; // bit 6 + private static final byte TP_SRI_NO_REPORT = 0x00; // bit 5 + + private int gsmSubmitGetTpPidOffset() { + /* calculate the offset to TP_PID and return the TP_PID byte. + * 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] & 0xff) + 2; // + if((offset > data.length) || (offset > (2 + 12))) // max length of TP_DA is 12 bytes + two byte offset + throw new IllegalArgumentException("wrongly formatted gsm submit PDU"); + return offset; + } + + public int gsmSubmitGetTpDcs() { + return data[gsmSubmitGetTpDcsOffset()] & 0xff; + } + + public boolean gsmSubmitHasUserDataHeader() { + return ((data[0] & 0xff) & TP_UDHI_MASK) == TP_UDHI_MASK; + } + + private int gsmSubmitGetTpDcsOffset() { + return gsmSubmitGetTpPidOffset() + 1; + } + + private int gsmSubmitGetTpUdlOffset() { + switch(((data[0] & 0xff) & (0x08 | 0x04))>>2) { + case 0: // Not TP-VP present + return gsmSubmitGetTpPidOffset() + 2; + case 1: // TP-VP relative format + return gsmSubmitGetTpPidOffset() + 2 + 1; + case 2: // TP-VP enhanced format + case 3: // TP-VP absolute format + break; + } + return gsmSubmitGetTpPidOffset() + 2 + 7; + } + private int gsmSubmitGetTpUdOffset() { + return gsmSubmitGetTpUdlOffset() + 1; + } + + public void gsmDecodeUserDataHeader() { + ByteArrayInputStream pdu = new ByteArrayInputStream(data); + + pdu.skip(gsmSubmitGetTpUdlOffset()); + int userDataLength = pdu.read(); + 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) + { + byte[] udh = new byte[userDataHeaderLength]; + try { + pdu.read(udh); + } catch (IOException e) { + Log.w(TAG, "unable to read userDataHeader", e); + } + SmsHeader userDataHeader = SmsHeader.fromByteArray(udh); + languageTable = userDataHeader.languageTable; + languageShiftTable = userDataHeader.languageShiftTable; + + int headerBits = (userDataHeaderLength + 1) * 8; + int headerSeptets = headerBits / 7; + headerSeptets += (headerBits % 7) > 0 ? 1 : 0; + userDataSeptetPadding = (headerSeptets * 7) - headerBits; + msgSeptetCount = userDataLength - headerSeptets; + } + userDataMsgOffset = gsmSubmitGetTpUdOffset() + userDataHeaderLength + 1; // Add the byte containing the length + } + + private void gsmWriteDate(ByteArrayOutputStream header, long time) throws UnsupportedEncodingException { + SimpleDateFormat format = new SimpleDateFormat("yyMMddHHmmss"); + Date date = new Date(time); + String timeStr = format.format(date); // Format to YYMMDDTHHMMSS UTC time + byte[] timeChars = timeStr.getBytes("US-ASCII"); + + for(int i = 0, n = timeStr.length()/2; i < n; i++) { + header.write((timeChars[i+1]-0x30) << 4 | (timeChars[i]-0x30)); // Offset from ascii char to decimal value + } + + Calendar cal = Calendar.getInstance(); + int offset = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / (15 * 60 * 1000); /* offset in quarters of an hour */ + String offsetString; + if(offset < 0) { + offsetString = String.format("%1$02d", -(offset)); + char[] offsetChars = offsetString.toCharArray(); + header.write((offsetChars[1]-0x30) << 4 | 0x40 | (offsetChars[0]-0x30)); + } + else { + offsetString = String.format("%1$02d", offset); + char[] offsetChars = offsetString.toCharArray(); + header.write((offsetChars[1]-0x30) << 4 | (offsetChars[0]-0x30)); + } + } + +/* private void gsmSubmitExtractUserData() { + int userDataLength = data[gsmSubmitGetTpUdlOffset()]; + userData = new byte[userDataLength]; + System.arraycopy(userData, 0, data, gsmSubmitGetTpUdOffset(), userDataLength); + + }*/ + + /** + * Change the GSM Submit Pdu data in this object to a deliver PDU: + * - Build the new header with deliver PDU type, originator and time stamp. + * - Extract encoding details from the submit PDU + * - Extract user data length and user data from the submitPdu + * - Build the new PDU + * @param date the time stamp to include (The value is the number of milliseconds since Jan. 1, 1970 GMT.) + * @param originator the phone number to include in the deliver PDU header. Any undesired characters, + * such as '-' will be striped from this string. + */ + public void gsmChangeToDeliverPdu(long date, String originator) + { + ByteArrayOutputStream newPdu = new ByteArrayOutputStream(22); // 22 is the max length of the deliver pdu header + byte[] encodedAddress; + 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); + encodedAddress = PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength(originator); + // Insert originator address into the header - this includes the length + newPdu.write(encodedAddress); + newPdu.write(data[gsmSubmitGetTpPidOffset()]); + newPdu.write(data[gsmSubmitGetTpDcsOffset()]); + // Generate service center time stamp + gsmWriteDate(newPdu, date); + userDataLength = (data[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()); + } catch (IOException e) { + Log.e(TAG, "", e); + throw new IllegalArgumentException("Failed to change type to deliver PDU."); // TODO: Is this the best way to handle this error? - which cannot occur... + } + data = newPdu.toByteArray(); + } + + /* SMS encoding to bmessage strings */ + /** get the encoding type as a bMessage string */ + public String getEncodingString(){ + if(type == SMS_TYPE_GSM) + { + switch(encoding){ + case SmsMessage.ENCODING_7BIT: + if(languageTable == 0) + return "G-7BIT"; + else + return "G-7BITEXT"; + case SmsMessage.ENCODING_8BIT: + return "G-8BIT"; + case SmsMessage.ENCODING_16BIT: + return "G-16BIT"; + case SmsMessage.ENCODING_UNKNOWN: + default: + return ""; + } + } else /* SMS_TYPE_CDMA */ { + switch(encoding){ + case SmsMessage.ENCODING_7BIT: + return "C-7ASCII"; + case SmsMessage.ENCODING_8BIT: + return "C-8BIT"; + case SmsMessage.ENCODING_16BIT: + return "C-UNICODE"; + case SmsMessage.ENCODING_KSC5601: + return "C-KOREAN"; + case SmsMessage.ENCODING_UNKNOWN: + default: + return ""; + } + } + } + } + + private static int sConcatenatedRef = new Random().nextInt(256); + + protected static int getNextConcatenatedRef() { + sConcatenatedRef += 1; + return sConcatenatedRef; + } + public static ArrayList<SmsPdu> getSubmitPdus(String messageText, String address){ + /* Use the generic GSM/CDMA SMS Message functionality within Android to generate the + * SMS PDU's as once generated to send the SMS message. + */ + + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); // TODO: Change to use: ((TelephonyManager)myContext.getSystemService(Context.TELEPHONY_SERVICE)) + int phoneType; + GsmAlphabet.TextEncodingDetails ted = (PHONE_TYPE_CDMA == activePhone) ? + com.android.internal.telephony.cdma.SmsMessage.calculateLength((CharSequence)messageText, false) : + com.android.internal.telephony.gsm.SmsMessage.calculateLength((CharSequence)messageText, false); + + SmsPdu newPdu; + String destinationAddress; + int msgCount = ted.msgCount; + int encoding; + int languageTable; + int languageShiftTable; + int refNumber = getNextConcatenatedRef() & 0x00FF; + ArrayList<String> smsFragments = SmsMessage.fragmentText(messageText); + ArrayList<SmsPdu> pdus = new ArrayList<SmsPdu>(msgCount); + byte[] data; + + // Default to GSM, as this code should not be used, if we neither have CDMA not GSM. + phoneType = (activePhone == PHONE_TYPE_CDMA) ? SMS_TYPE_CDMA : SMS_TYPE_GSM; + encoding = ted.codeUnitSize; + languageTable = ted.languageTable; + languageShiftTable = ted.languageShiftTable; + destinationAddress = PhoneNumberUtils.stripSeparators(address); + if(destinationAddress == null || destinationAddress.length() < 2) { + destinationAddress = "12"; // Ensure we add a number at least 2 digits as specified in the GSM spec. + } + + if(msgCount == 1){ + data = SmsMessage.getSubmitPdu(null, destinationAddress, smsFragments.get(0), false).encodedMessage; + newPdu = new SmsPdu(data, encoding, phoneType, languageTable); + pdus.add(newPdu); + } + + /* This code is a reduced copy of the actual code used in the Android SMS sub system, + * hence the comments have been left untouched. */ + for(int i = 0; i < msgCount; i++){ + SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); + concatRef.refNumber = refNumber; + concatRef.seqNumber = i + 1; // 1-based sequence + concatRef.msgCount = msgCount; + // TODO: We currently set this to true since our messaging app will never + // send more than 255 parts (it converts the message to MMS well before that). + // However, we should support 3rd party messaging apps that might need 16-bit + // references + // Note: It's not sufficient to just flip this bit to true; it will have + // ripple effects (several calculations assume 8-bit ref). + concatRef.isEightBits = true; + SmsHeader smsHeader = new SmsHeader(); + smsHeader.concatRef = concatRef; + + /* Depending on the type, call either GSM or CDMA getSubmitPdu(). The encoding + * will be determined(again) by getSubmitPdu(). + * All packets need to be encoded using the same encoding, as the bMessage + * only have one filed to describe the encoding for all messages in a concatenated + * SMS... */ + if (encoding == SmsConstants.ENCODING_7BIT) { + smsHeader.languageTable = languageTable; + smsHeader.languageShiftTable = languageShiftTable; + } + + if(phoneType == SMS_TYPE_GSM){ + data = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(null, destinationAddress, + smsFragments.get(i), false, SmsHeader.toByteArray(smsHeader), + encoding, languageTable, languageShiftTable).encodedMessage; + } else { // SMS_TYPE_CDMA + UserData uData = new UserData(); + uData.payloadStr = smsFragments.get(i); + uData.userDataHeader = smsHeader; + if (encoding == SmsConstants.ENCODING_7BIT) { + uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET; + } else { // assume UTF-16 + uData.msgEncoding = UserData.ENCODING_UNICODE_16; + } + uData.msgEncodingSet = true; + data = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(destinationAddress, + uData, false).encodedMessage; + } + newPdu = new SmsPdu(data, encoding, phoneType, languageTable); + pdus.add(newPdu); + } + + return pdus; + } + + /** + * Generate a list of deliver PDUs. The messageText and address parameters must be different from null, + * for CDMA the date can be omitted (and will be ignored if supplied) + * @param messageText The text to include. + * @param address The originator address. + * @param date The delivery time stamp. + * @return + */ + public static ArrayList<SmsPdu> getDeliverPdus(String messageText, String address, long date){ + ArrayList<SmsPdu> deliverPdus = getSubmitPdus(messageText, address); + + /* + * For CDMA the only difference between deliver and submit pdus are the messageType, + * which is set in encodeMessageId, (the higher 4 bits of the 1st byte + * of the Message identification sub parameter data.) and the address type. + * + * For GSM, a larger part of the header needs to be generated. + */ + for(SmsPdu currentPdu : deliverPdus){ + if(currentPdu.getType() == SMS_TYPE_CDMA){ + currentPdu.cdmaChangeToDeliverPdu(date); + } else { /* SMS_TYPE_GSM */ + currentPdu.gsmChangeToDeliverPdu(date, address); + } + } + + 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 + * from the MAP client. (As the Android system has no interface to send pre encoded PDUs) + * The destination address must be extracted from the bmessage vCard(s). + */ + public static String decodePdu(byte[] data, int type) { + String ret; + if(type == SMS_TYPE_CDMA) { + /* This is able to handle both submit and deliver PDUs */ + ret = com.android.internal.telephony.cdma.SmsMessage.createFromEfRecord(0, data).getMessageBody(); + } else { + /* For GSM, there is no submit pdu decoder, and most parser utils are private, and only minded for submit pdus */ + ret = gsmParseSubmitPdu(data); + } + return ret; + } + + /* At the moment we do not support using a SC-address. Use this function to strip off + * the SC-address before parsing it to the SmsPdu. (this was added in errata 4335) + */ + private static byte[] gsmStripOffScAddress(byte[] data) { + /* The format of a native GSM SMS is: <sc-address><pdu> where sc-address is: + * <length-byte><type-byte><number-bytes> */ + int addressLength = data[0] & 0xff; // Treat the byte value as an unsigned value + if(addressLength >= data.length) // TODO: We could verify that the address-length is no longer than 11 bytes + throw new IllegalArgumentException("Length of address exeeds the length of the PDU data."); + int pduLength = data.length-(1+addressLength); + byte[] newData = new byte[pduLength]; + System.arraycopy(data, 1+addressLength, newData, 0, pduLength); + return newData; + } + + private static String gsmParseSubmitPdu(byte[] data) { + /* Things to do: + * - extract hasUsrData bit + * - extract TP-DCS -> Character set, compressed etc. + * - extract user data header to get the language properties + * - extract user data + * - decode the string */ + //Strip off the SC-address before parsing + SmsPdu pdu = new SmsPdu(gsmStripOffScAddress(data), SMS_TYPE_GSM); + boolean userDataCompressed = false; + int dataCodingScheme = pdu.gsmSubmitGetTpDcs(); + int encodingType = SmsConstants.ENCODING_UNKNOWN; + String messageBody = null; + + // Look up the data encoding scheme + if ((dataCodingScheme & 0x80) == 0) { + // Bits 7..4 == 0xxx + userDataCompressed = (0 != (dataCodingScheme & 0x20)); + + if (userDataCompressed) { + Log.w(TAG, "4 - Unsupported SMS data coding scheme " + + "(compression) " + (dataCodingScheme & 0xff)); + } else { + switch ((dataCodingScheme >> 2) & 0x3) { + case 0: // GSM 7 bit default alphabet + encodingType = SmsConstants.ENCODING_7BIT; + break; + + case 2: // UCS 2 (16bit) + encodingType = SmsConstants.ENCODING_16BIT; + break; + + case 1: // 8 bit data + case 3: // reserved + Log.w(TAG, "1 - Unsupported SMS data coding scheme " + + (dataCodingScheme & 0xff)); + encodingType = SmsConstants.ENCODING_8BIT; + break; + } + } + } else if ((dataCodingScheme & 0xf0) == 0xf0) { + userDataCompressed = false; + + if (0 == (dataCodingScheme & 0x04)) { + // GSM 7 bit default alphabet + encodingType = SmsConstants.ENCODING_7BIT; + } else { + // 8 bit data + encodingType = SmsConstants.ENCODING_8BIT; + } + } else if ((dataCodingScheme & 0xF0) == 0xC0 + || (dataCodingScheme & 0xF0) == 0xD0 + || (dataCodingScheme & 0xF0) == 0xE0) { + // 3GPP TS 23.038 V7.0.0 (2006-03) section 4 + + // 0xC0 == 7 bit, don't store + // 0xD0 == 7 bit, store + // 0xE0 == UCS-2, store + + if ((dataCodingScheme & 0xF0) == 0xE0) { + encodingType = SmsConstants.ENCODING_16BIT; + } else { + encodingType = SmsConstants.ENCODING_7BIT; + } + + userDataCompressed = false; + + // bit 0x04 reserved + } else if ((dataCodingScheme & 0xC0) == 0x80) { + // 3GPP TS 23.038 V7.0.0 (2006-03) section 4 + // 0x80..0xBF == Reserved coding groups + if (dataCodingScheme == 0x84) { + // This value used for KSC5601 by carriers in Korea. + encodingType = SmsConstants.ENCODING_KSC5601; + } else { + Log.w(TAG, "5 - Unsupported SMS data coding scheme " + + (dataCodingScheme & 0xff)); + } + } else { + Log.w(TAG, "3 - Unsupported SMS data coding scheme " + + (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); + if(pdu.gsmSubmitHasUserDataHeader()) { + pdu.gsmDecodeUserDataHeader(); + } + + try { + switch (encodingType) { + case SmsConstants.ENCODING_UNKNOWN: + case SmsConstants.ENCODING_8BIT: + messageBody = null; + break; + + case SmsConstants.ENCODING_7BIT: + messageBody = GsmAlphabet.gsm7BitPackedToString(pdu.getData(), pdu.getUserDataMsgOffset(), + pdu.getMsgSeptetCount(), pdu.getUserDataSeptetPadding(), pdu.getLanguageTable(), + pdu.getLanguageShiftTable()); + + break; + + case SmsConstants.ENCODING_16BIT: + messageBody = new String(pdu.getData(), pdu.getUserDataMsgOffset(), pdu.getUserDataMsgSize(), "utf-16"); + break; + + case SmsConstants.ENCODING_KSC5601: + messageBody = new String(pdu.getData(), pdu.getUserDataMsgOffset(), pdu.getUserDataMsgSize(), "KSC5601"); + + break; + } + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Unsupported encoding type???", e); // This should never happen. + return null; + } + + return messageBody; + } + +} diff --git a/src/com/android/bluetooth/map/BluetoothMapUtils.java b/src/com/android/bluetooth/map/BluetoothMapUtils.java new file mode 100644 index 000000000..e57cf16b1 --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapUtils.java @@ -0,0 +1,116 @@ +/* +* 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; + + +/** + * Various utility methods and generic defines that can be used throughout MAPS + */ +public class BluetoothMapUtils { + + private static final String TAG = "MapUtils"; + 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:)) + */ + 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; + + /** + * This enum is used to convert from the bMessage type property to a type safe + * type. Hence do not change the names of the enum values. + */ + public enum TYPE{ + EMAIL, + SMS_GSM, + SMS_CDMA, + MMS + } + + /** + * 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){ + String mapHandle = "-1"; + switch(messageType) + { + case MMS: + mapHandle = String.format("%016X",(cpHandle | HANDLE_TYPE_MMS_MASK)); + break; + case SMS_GSM: + mapHandle = String.format("%016X",cpHandle | HANDLE_TYPE_SMS_GSM_MASK); + break; + case SMS_CDMA: + mapHandle = String.format("%016X",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 + break; + default: + throw new IllegalArgumentException("Message type not supported"); + } + return mapHandle; + + } + + /** + * Convert a handle string the the raw long representation, including the type bit. + * @param mapHandle the handle string + * @return the handle value + */ + static public long getMsgHandleAsLong(String mapHandle){ + return Long.parseLong(mapHandle, 16); + } + /** + * Convert a Map Handle into a content provider Handle + * @param mapHandle handle to convert from + * @return content provider handle without message type mask + */ + static public long getCpHandle(String mapHandle) + { + long cpHandle = getMsgHandleAsLong(mapHandle); + /* remove masks as the call should already know what type of message this handle is for */ + cpHandle &= ~HANDLE_TYPE_MASK; + return cpHandle; + } + + /** + * Extract the message type from the handle. + * @param mapHandle + * @return + */ + static public TYPE getMsgTypeFromHandle(String mapHandle) { + long cpHandle = getMsgHandleAsLong(mapHandle); + + if((cpHandle & HANDLE_TYPE_MMS_MASK) != 0) + return TYPE.MMS; + if((cpHandle & HANDLE_TYPE_EMAIL_MASK) != 0) + return TYPE.EMAIL; + if((cpHandle & HANDLE_TYPE_SMS_GSM_MASK) != 0) + return TYPE.SMS_GSM; + if((cpHandle & HANDLE_TYPE_SMS_CDMA_MASK) != 0) + return TYPE.SMS_CDMA; + + throw new IllegalArgumentException("Message type not found in handle string."); + } +} + diff --git a/src/com/android/bluetooth/map/BluetoothMapbMessage.java b/src/com/android/bluetooth/map/BluetoothMapbMessage.java new file mode 100644 index 000000000..084ebedfe --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapbMessage.java @@ -0,0 +1,778 @@ +/* +* 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.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; + +import android.telephony.PhoneNumberUtils; +import android.util.Log; +import com.android.bluetooth.map.BluetoothMapUtils.TYPE; + +public abstract class BluetoothMapbMessage { + + protected static String TAG = "BluetoothMapbMessage"; + protected static final boolean D = true; + protected static final boolean V = true; + 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. + + /* BMSG attributes */ + private String status = null; // READ/UNREAD + protected TYPE type = null; // SMS/MMS/EMAIL + + private String folder = null; + + /* BBODY attributes */ + private long partId = INVALID_VALUE; + protected String encoding = null; + protected String charset = null; + private String language = null; + + private int bMsgLength = INVALID_VALUE; + + private ArrayList<vCard> originator = null; + private ArrayList<vCard> recipient = 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; + + /** + * Construct a version 3.0 vCard + * @param name Structured + * @param formattedName Formatted name + * @param phoneNumbers a String[] of phone numbers + * @param emailAddresses a String[] of email addresses + * @param the bmessage envelope level (0 is the top/most outer level) + */ + 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 : ""; + setPhoneNumbers(phoneNumbers); + if (emailAddresses != null) + this.emailAddresses = emailAddresses; + } + + /** + * Construct a version 2.1 vCard + * @param name Structured name + * @param phoneNumbers a String[] of phone numbers + * @param emailAddresses a String[] of email addresses + * @param the bmessage envelope level (0 is the top/most outer level) + */ + public vCard(String name, String[] phoneNumbers, + String[] emailAddresses, int envLevel) { + this.envLevel = envLevel; + this.version = "2.1"; + this.name = name != null ? name : ""; + setPhoneNumbers(phoneNumbers); + if (emailAddresses != null) + this.emailAddresses = emailAddresses; + } + + /** + * Construct a version 3.0 vCard + * @param name Structured name + * @param formattedName Formatted name + * @param phoneNumbers a String[] of phone numbers + * @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 : ""; + setPhoneNumbers(phoneNumbers); + if (emailAddresses != null) + this.emailAddresses = emailAddresses; + } + + /** + * Construct a version 2.1 vCard + * @param name Structured Name + * @param phoneNumbers a String[] of phone numbers + * @param emailAddresses a String[] of email addresses + */ + public vCard(String name, String[] phoneNumbers, String[] emailAddresses) { + this.version = "2.1"; + this.name = name != null ? name : ""; + setPhoneNumbers(phoneNumbers); + if (emailAddresses != null) + this.emailAddresses = emailAddresses; + } + + private void setPhoneNumbers(String[] numbers) { + if(numbers != null && numbers.length > 0) + { + phoneNumbers = new String[numbers.length]; + for(int i = 0, n = numbers.length; i < n; i++){ + phoneNumbers[i] = PhoneNumberUtils.extractNetworkPortion(numbers[i]); + } + } + } + + public String getFirstPhoneNumber() { + if(phoneNumbers.length > 0) { + return phoneNumbers[0]; + } else + throw new IllegalArgumentException("No Phone number"); + } + + public int getEnvLevel() { + return envLevel; + } + + 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("FN:").append(formattedName).append("\r\n"); + } + if (name != null) + sb.append("N:").append(name).append("\r\n"); + for (String phoneNumber : phoneNumbers) + { + sb.append("TEL:").append(phoneNumber).append("\r\n"); + } + for (String emailAddress : emailAddresses) + { + sb.append("EMAIL:").append(emailAddress).append("\r\n"); + } + sb.append("END:VCARD").append("\r\n"); + } + + /** + * Parse a vCard from a BMgsReader, where a line containing "BEGIN:VCARD" have just been read. + * @param reader + * @param originator + * @return + */ + public static vCard parseVcard(BMsgReader reader, int envLevel) { + String formattedName = null; + String name = null; + ArrayList<String> phoneNumbers = null; + ArrayList<String> emailAddresses = null; + String[] parts; + String line = reader.getLineEnforce(); + + while(!line.contains("END:VCARD")) { + line = line.trim(); + if(line.startsWith("N:")){ + parts = line.split("[^\\\\]:"); // Split on "un-escaped" ':' + if(parts.length == 2) { + name = parts[1]; + } else + name = ""; + } + else if(line.startsWith("FN:")){ + parts = line.split("[^\\\\]:"); // Split on "un-escaped" ':' + if(parts.length == 2) { + formattedName = parts[1]; + } else + formattedName = ""; + } + else if(line.startsWith("TEL:")){ + parts = line.split("[^\\\\]:"); // Split on "un-escaped" ':' + if(parts.length == 2) { + String[] subParts = parts[1].split("[^\\\\];"); + if(phoneNumbers == null) + phoneNumbers = new ArrayList<String>(1); + phoneNumbers.add(subParts[subParts.length-1]); // only keep actual phone number + } else {} + // Empty phone number - ignore + } + else if(line.startsWith("EMAIL:")){ + parts = line.split("[^\\\\]:"); // Split on "un-escaped" : + if(parts.length == 2) { + String[] subParts = parts[1].split("[^\\\\];"); + if(emailAddresses == null) + emailAddresses = new ArrayList<String>(1); + emailAddresses.add(subParts[subParts.length-1]); // only keep actual email address + } else {} + // Empty email address entry - ignore + } + line = reader.getLineEnforce(); + } + return new vCard(name, formattedName, + phoneNumbers == null? null : phoneNumbers.toArray(new String[phoneNumbers.size()]), + emailAddresses == null ? null : emailAddresses.toArray(new String[emailAddresses.size()]), + envLevel); + } + }; + + private static class BMsgReader { + InputStream mInStream; + public BMsgReader(InputStream is) + { + this.mInStream = is; + } + + private byte[] getLineAsBytes() { + int readByte; + + /* TODO: Actually the vCard spec. allows to break lines by using a newLine + * followed by a white space character(space or tab). Not sure this is a good idea to implement + * 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 + */ + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try { + while ((readByte = mInStream.read()) != -1) { + if (readByte == '\r') { + if ((readByte = mInStream.read()) != -1 && readByte == '\n') { + if(output.size() == 0) + continue; /* Skip empty lines */ + else + break; + } else { + output.write('\r'); + } + } else if (readByte == '\n' && output.size() == 0) { + /* Empty line - skip */ + continue; + } + + output.write(readByte); + } + } catch (IOException e) { + Log.w(TAG, e); + return null; + } + return output.toByteArray(); + } + + /** + * Read a line of text from the BMessage. + * @return the next line of text, or null at end of file, or if UTF-8 is not supported. + */ + public String getLine() { + try { + byte[] line = getLineAsBytes(); + if (line.length == 0) + return null; + else + return new String(line, "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.w(TAG, e); + return null; + } + } + + /** + * same as getLine(), but throws an exception, if we run out of lines. + * Use this function when ever more lines are needed for the bMessage to be complete. + * @return the next line + */ + public String getLineEnforce() { + String line = getLine(); + if (line == null) + throw new IllegalArgumentException("Bmessage too short"); + return line; + } + + + /** + * Reads a line from the InputStream, and examines if the subString + * matches the line read. + * @param subString + * The string to match against the line. + * @throws IllegalArgumentException + * If the expected substring is not found. + * + */ + public void expect(String subString) throws IllegalArgumentException{ + String line = getLine(); + if (!line.contains(subString)) + // TODO: Should this be case insensitive? (Either use toUpper() or matches()) + throw new IllegalArgumentException("Expected \"" + subString + "\" in: \"" + line + "\""); + } + + /** + * Same as expect(String), but with two strings. + * @param subString + * @param subString2 + * @throws IllegalArgumentException + * If one or all of the strings are not found. + */ + public void expect(String subString, String subString2) throws IllegalArgumentException{ + String line = getLine(); + if(!line.contains(subString)) // TODO: Should this be case insensitive? (Either use toUpper() or matches()) + throw new IllegalArgumentException("Expected \"" + subString + "\" in: \"" + line + "\""); + if(!line.contains(subString2)) // TODO: Should this be case insensitive? (Either use toUpper() or matches()) + throw new IllegalArgumentException("Expected \"" + subString + "\" in: \"" + line + "\""); + } + + /** + * Read a part of the bMessage as raw data. + * @param length the number of bytes to read + * @return the byte[] containing the number of bytes or null if an error occurs or EOF is reached + * before length bytes have been read. + */ + public byte[] getDataBytes(int length) { + byte[] data = new byte[length]; + try { + int bytesRead; + int offset=0; + while ((bytesRead = mInStream.read(data, offset, length-offset)) != length) { + if(bytesRead == -1) + return null; + offset += bytesRead; + } + } catch (IOException e) { + Log.w(TAG, e); + return null; + } + return data; + } + }; + + public BluetoothMapbMessage() { + + } + + public static BluetoothMapbMessage parse(InputStream bMsgStream, int appParamCharset) throws IllegalArgumentException{ + BMsgReader reader = new BMsgReader(bMsgStream); + String line = ""; + BluetoothMapbMessage newBMsg = null; + reader.expect("BEGIN:BMSG"); + reader.expect("VERSION","1.0"); + boolean status = false; + boolean statusFound = false; + TYPE type = null; + String folder = null; + + line = reader.getLineEnforce(); + // Parse the properties - which end with either a VCARD or a BENV + while(!line.contains("BEGIN:VCARD") && !line.contains("BEGIN:BENV")) { + if(line.contains("STATUS")){ + String arg[] = line.split(":"); + if (arg != null && arg.length == 2) { + if (arg[1].trim().equals("READ")) { + status = true; + } else if (arg[1].trim().equals("UNREAD")) { + status =false; + } else { + throw new IllegalArgumentException("Wrong value in 'STATUS': " + arg[1]); + } + } else { + throw new IllegalArgumentException("Missing value for 'STATUS': " + line); + } + } + if(line.contains("TYPE")) { + String arg[] = line.split(":"); + if (arg != null && arg.length == 2) { + String value = arg[1].trim(); + type = TYPE.valueOf(value); // Will throw IllegalArgumentException if value is wrong + if(appParamCharset == BluetoothMapAppParams.CHARSET_NATIVE + && type != TYPE.SMS_CDMA && type != TYPE.SMS_GSM) { + throw new IllegalArgumentException("Native appParamsCharset only supported for SMS"); + } + switch(type) { + case SMS_CDMA: + case SMS_GSM: + newBMsg = new BluetoothMapbMessageSms(); + break; + case MMS: + case EMAIL: + newBMsg = new BluetoothMapbMessageMmsEmail(); + break; + default: + break; + } + } else { + throw new IllegalArgumentException("Missing value for 'TYPE':" + line); + } + } + if(line.contains("FOLDER")) { + String[] arg = line.split(":"); + if (arg != null && arg.length == 2) { + folder = arg[1].trim(); + } else { + throw new IllegalArgumentException("Missing value for 'FOLDER':" + line); + } + } + line = reader.getLineEnforce(); + } + if(newBMsg == null) + throw new IllegalArgumentException("Missing bMessage TYPE: - unable to parse body-content"); + newBMsg.setType(type); + newBMsg.appParamCharset = appParamCharset; + if(folder != null) + newBMsg.setFolder(folder); + if(statusFound) + newBMsg.setStatus(status); + + // Now check for originator VCARDs + while(line.contains("BEGIN:VCARD")){ + if(D) Log.d(TAG,"Decoding vCard"); + newBMsg.addOriginator(vCard.parseVcard(reader,0)); + line = reader.getLineEnforce(); + } + if(line.contains("BEGIN:BENV")) { + newBMsg.parseEnvelope(reader, 0); + } else + throw new IllegalArgumentException("Bmessage has no BEGIN:BENV - line:" + line); + + /* 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. + */ + return newBMsg; + } + + private void parseEnvelope(BMsgReader reader, int level) { + String line; + line = reader.getLineEnforce(); + if(D) Log.d(TAG,"Decoding envelope level " + level); + + while(line.contains("BEGIN:VCARD")){ + if(D) Log.d(TAG,"Decoding recipient vCard level " + level); + if(recipient == null) + recipient = new ArrayList<vCard>(1); + recipient.add(vCard.parseVcard(reader, level)); + line = reader.getLineEnforce(); + } + if(line.contains("BEGIN:BENV")) { + if(D) Log.d(TAG,"Decoding nested envelope"); + parseEnvelope(reader, ++level); // Nested BENV + } + if(line.contains("BEGIN:BBODY")){ + if(D) Log.d(TAG,"Decoding bbody"); + parseBody(reader); + } + } + + private void parseBody(BMsgReader reader) { + String line; + line = reader.getLineEnforce(); + while(!line.contains("END:")) { + if(line.contains("PARTID:")) { + String arg[] = line.split(":"); + if (arg != null && arg.length == 2) { + try { + partId = Long.parseLong(arg[1].trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Wrong value in 'PARTID': " + arg[1]); + } + } else { + throw new IllegalArgumentException("Missing value for 'PARTID': " + line); + } + } + else if(line.contains("ENCODING:")) { + String arg[] = line.split(":"); + if (arg != null && arg.length == 2) { + encoding = arg[1].trim(); // TODO: Validate ? + } else { + throw new IllegalArgumentException("Missing value for 'ENCODING': " + line); + } + } + else if(line.contains("CHARSET:")) { + String arg[] = line.split(":"); + if (arg != null && arg.length == 2) { + charset = arg[1].trim(); // TODO: Validate ? + } else { + throw new IllegalArgumentException("Missing value for 'CHARSET': " + line); + } + } + else if(line.contains("LANGUAGE:")) { + String arg[] = line.split(":"); + if (arg != null && arg.length == 2) { + language = arg[1].trim(); // TODO: Validate ? + } else { + throw new IllegalArgumentException("Missing value for 'LANGUAGE': " + line); + } + } + else if(line.contains("LENGTH:")) { + String arg[] = line.split(":"); + if (arg != null && arg.length == 2) { + try { + bMsgLength = Integer.parseInt(arg[1].trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Wrong value in 'LENGTH': " + arg[1]); + } + } else { + throw new IllegalArgumentException("Missing value for 'LENGTH': " + line); + } + } + 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 + /* 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 + String data; + try { + data = new String(rawData, "UTF-8"); + if(V) { + Log.v(TAG,"MsgLength: " + bMsgLength); + Log.v(TAG,"line.getBytes().length: " + line.getBytes().length); + String debug = line.replaceAll("\\n", "<LF>\n"); + debug = debug.replaceAll("\\r", "<CR>"); + Log.v(TAG,"The line: \"" + debug + "\""); + debug = data.replaceAll("\\n", "<LF>\n"); + debug = debug.replaceAll("\\r", "<CR>"); + Log.v(TAG,"The msgString: \"" + debug + "\""); + } + } catch (UnsupportedEncodingException e) { + Log.w(TAG,e); + throw new IllegalArgumentException("Unable to convert to UTF-8"); + } + /* Decoding of MSG: + * 1) split on "\r\nEND:MSG\r\n" + * 2) delete "BEGIN:MSG\r\n" for each msg + * 3) replace any occurrence of "\END:MSG" with "END:MSG" + * 4) based on charset from application properties either store as String[] or decode to raw PDUs + * */ + String messages[] = data.split("\r\nEND:MSG\r\n"); + parseMsgInit(); + for(int i = 0; i < messages.length; i++) { + messages[i] = messages[i].replaceFirst("^BEGIN:MGS\r\n", ""); + messages[i] = messages[i].replaceAll("\r\n([/]*)/END\\:MSG", "\r\n$1END:MSG"); + messages[i] = messages[i].trim(); + parseMsgPart(messages[i]); + } + } + line = reader.getLineEnforce(); + } + } + + /** + * Parse the 'message' part of <bmessage-body-content>" + * @param msgPart + */ + public abstract void parseMsgPart(String msgPart); + /** + * Set initial values before parsing - will be called is a message body is found + * during parsing. + */ + public abstract void parseMsgInit(); + + public abstract byte[] encode() throws UnsupportedEncodingException; + + public void setStatus(boolean read) { + if(read) + this.status = "READ"; + else + this.status = "UNREAD"; + } + + public void setType(TYPE type) { + this.type = type; + } + + /** + * @return the type + */ + public TYPE getType() { + return type; + } + + public void setFolder(String folder) { + this.folder = "telecom/msg/" + folder; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public ArrayList<vCard> getOriginators() { + return originator; + } + + public void addOriginator(vCard originator) { + if(this.originator == null) + this.originator = new ArrayList<vCard>(); + this.originator.add(originator); + } + + /** + * Add a version 3.0 vCard with a formatted name + * @param name e.g. Bonde;Casper + * @param formattedName e.g. "Casper Bonde" + * @param phoneNumbers + * @param emailAddresses + */ + public void addOriginator(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses) { + if(originator == null) + originator = new ArrayList<vCard>(); + originator.add(new vCard(name, formattedName, phoneNumbers, emailAddresses)); + } + + /** Add a version 2.1 vCard with only a name. + * + * @param name e.g. Bonde;Casper + * @param phoneNumbers + * @param emailAddresses + */ + public void addOriginator(String name, String[] phoneNumbers, String[] emailAddresses) { + if(originator == null) + originator = new ArrayList<vCard>(); + originator.add(new vCard(name, phoneNumbers, emailAddresses)); + } + + public ArrayList<vCard> getRecipients() { + return recipient; + } + + public void setRecipient(vCard recipient) { + if(this.recipient == null) + this.recipient = new ArrayList<vCard>(); + this.recipient.add(recipient); + } + + public void addRecipient(String name, String formattedName, String[] phoneNumbers, String[] emailAddresses) { + if(recipient == null) + recipient = new ArrayList<vCard>(); + recipient.add(new vCard(name, formattedName, phoneNumbers, emailAddresses)); + } + + public void addRecipient(String name, String[] phoneNumbers, String[] emailAddresses) { + if(recipient == null) + recipient = new ArrayList<vCard>(); + recipient.add(new vCard(name, phoneNumbers, emailAddresses)); + } + + /** + * Convert a byte[] of data to a hex string representation, converting each nibble to the corresponding + * hex char. + * NOTE: There is not need to escape instances of "\r\nEND:MSG" in the binary data represented as a string + * as only the characters [0-9] and [a-f] is used. + * @param pduData the byte-array of data. + * @param scAddressData the byte-array of the encoded sc-Address. + * @return the resulting string. + */ + protected String encodeBinary(byte[] pduData, byte[] scAddressData) { + StringBuilder out = new StringBuilder((pduData.length + scAddressData.length)*2); + for(int i = 0; i < scAddressData.length; i++) { + out.append(Integer.toString((scAddressData[i] >> 4) & 0x0f,16)); // MS-nibble first + out.append(Integer.toString( scAddressData[i] & 0x0f,16)); + } + for(int i = 0; i < pduData.length; i++) { + out.append(Integer.toString((pduData[i] >> 4) & 0x0f,16)); // MS-nibble first + out.append(Integer.toString( pduData[i] & 0x0f,16)); + /*out.append(Integer.toHexString(data[i]));*/ /* This is the same as above, but does not include the needed 0's + e.g. it converts the value 3 to "3" and not "03" */ + } + return out.toString(); + } + + /** + * Decodes a binary hex-string encoded UTF-8 string to the represented binary data set. + * @param data The string representation of the data - must have an even number of characters. + * @return the byte[] represented in the data. + */ + protected byte[] decodeBinary(String data) { + byte[] out = new byte[data.length()/2]; + String value; + if(D) Log.d(TAG,"Decoding binary data: START:" + data + ":END"); + for(int i = 0, j = 0, n = out.length; i < n; i++) + { + value = data.substring(j++, j++); // same as data.substring(2*i, 2*i+1) + out[i] = Byte.valueOf(value, 16); + } + return out; + } + + public byte[] encodeGeneric(ArrayList<byte[]> bodyFragments) throws UnsupportedEncodingException + { + StringBuilder sb = new StringBuilder(256); + 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"); + sb.append("FOLDER:").append(folder).append("\r\n"); + if(originator != null){ + for(vCard element : originator) + 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. + */ + + sb.append("BEGIN:BENV").append("\r\n"); + if(recipient != null){ + for(vCard element : recipient) + 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"); + + + int length = 0; + /* 22 is the length of the 'BEGIN:MSG' and 'END:MSG' + 3*CRLF */ + for (byte[] fragment : bodyFragments) { + length += fragment.length + 22; + } + sb.append("LENGTH:").append(length).append("\r\n"); + + // Extract the initial part of the bMessage string + msgStart = sb.toString().getBytes("UTF-8"); + + sb = new StringBuilder(31); + sb.append("END:BBODY").append("\r\n"); + sb.append("END:BENV").append("\r\n"); + sb.append("END:BMSG").append("\r\n"); + + msgEnd = sb.toString().getBytes("UTF-8"); + + try { + + ByteArrayOutputStream stream = new ByteArrayOutputStream(msgStart.length + msgEnd.length + length); + stream.write(msgStart); + + for (byte[] fragment : bodyFragments) { + stream.write("BEGIN:MSG\r\n".getBytes("UTF-8")); + stream.write(fragment); + stream.write("\r\nEND:MSG\r\n".getBytes("UTF-8")); + } + stream.write(msgEnd); + + if(V) Log.v(TAG,stream.toString("UTF-8")); + return stream.toByteArray(); + } catch (IOException e) { + Log.w(TAG,e); + return null; + } + } +} diff --git a/src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java b/src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java new file mode 100644 index 000000000..169c42dc4 --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapbMessageMmsEmail.java @@ -0,0 +1,538 @@ +/* +* 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 { + + private String mmsBody; + /** + * TODO: Determine the best way to store the MMS message content. + * @param mmsBody + */ + + 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 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); + if(last) + sb.append("--"); + sb.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(partName != null) + sb.append("Content-Location: ").append(partName).append("\r\n"); + if(data != null) { + // If errata 4176 is adopted in the current form, the below is not allowed, Base64 should be used for text + if(contentType.toUpperCase().contains("TEXT")) { + 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"); + } + } + } + } + private long date = INVALID_VALUE; + private String subject = null; + private ArrayList<Rfc822Token> from = null; // Shall not be empty + private ArrayList<Rfc822Token> sender = null; // Shall not be empty + private ArrayList<Rfc822Token> to = null; // Shall not be empty + private ArrayList<Rfc822Token> cc = null; // Can be empty + private ArrayList<Rfc822Token> bcc = null; // Can be empty + private ArrayList<Rfc822Token> replyTo = null;// Can be empty + private String messageId = null; + private ArrayList<MimePart> parts = null; + private String contentType = null; + private String boundary = null; + + /* TODO: + * - create an encoder for the parts e.g. embedded in the mimePart class + * */ + private String getBoundary() { + if(boundary == null) + boundary = "----" + UUID.randomUUID(); + return boundary; + } + + /** + * @return the parts + */ + public ArrayList<MimePart> getMimeParts() { + return parts; + } + + public MimePart addMimePart() { + if(parts == null) + parts = new ArrayList<BluetoothMapbMessageMmsEmail.MimePart>(); + 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<Rfc822Token> getFrom() { + return from; + } + public void setFrom(ArrayList<Rfc822Token> from) { + this.from = from; + } + public void addFrom(String name, String address) { + if(this.from == null) + this.from = new ArrayList<Rfc822Token>(1); + //this.from.add(formatAddress(name, address)); + this.from.add(new Rfc822Token(name, address, null)); + } + public ArrayList<Rfc822Token> getSender() { + return from; + } + public void setSender(ArrayList<Rfc822Token> sender) { + this.sender = sender; + } + public void addSender(String name, String address) { + if(this.sender == null) + this.sender = new ArrayList<Rfc822Token>(1); + //this.sender.add(formatAddress(name, address)); + this.sender.add(new Rfc822Token(name,address,null)); + } + public ArrayList<Rfc822Token> getTo() { + return to; + } + public void setTo(ArrayList<Rfc822Token> to) { + this.to = to; + } + public void addTo(String name, String address) { + if(this.to == null) + this.to = new ArrayList<Rfc822Token>(1); + //this.to.add(formatAddress(name, address)); + this.to.add(new Rfc822Token(name, address, null)); + } + public ArrayList<Rfc822Token> getCc() { + return cc; + } + public void setCc(ArrayList<Rfc822Token> cc) { + this.cc = cc; + } + public void addCc(String name, String address) { + if(this.cc == null) + this.cc = new ArrayList<Rfc822Token>(1); + //this.cc.add(formatAddress(name, address)); + this.cc.add(new Rfc822Token(name, address, null)); + } + public ArrayList<Rfc822Token> getBcc() { + return bcc; + } + public void setBcc(ArrayList<Rfc822Token> bcc) { + this.bcc = bcc; + } + public void addBcc(String name, String address) { + if(this.bcc == null) + this.bcc = new ArrayList<Rfc822Token>(1); + //this.bcc.add(formatAddress(name, address)); + this.bcc.add(new Rfc822Token(name, address, null)); + } + public ArrayList<Rfc822Token> getReplyTo() { + return replyTo; + } + public void setReplyTo(ArrayList<Rfc822Token> replyTo) { + this.replyTo = replyTo; + } + public void addReplyTo(String name, String address) { + if(this.replyTo == null) + this.replyTo = new ArrayList<Rfc822Token>(1); + //this.replyTo.add(formatAddress(name, address)); + 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 updateCharset() { + charset = null; + for(MimePart part : parts) { + if(part.contentType != null && + part.contentType.toUpperCase().contains("TEXT")) { + charset = "UTF-8"; + } + } + } + /** + * Use this to format an address according to RFC 2822. + * @param name + * @param address + * @return + */ + public static String formatAddress(String name, String address) { + StringBuilder sb = new StringBuilder(); + Boolean nameSet = false; + if(name != null && !(name = name.trim()).equals("")) { + sb.append(name.trim()); + nameSet = true; + } + if(address != null && !(address = address.trim()).equals("")) + { + if(nameSet == true) + sb.append(":"); + sb.append("<").append(address).append(">"); + } + // TODO: Throw exception of the string is larger than 996 + return sb.toString(); + } + + + /** + * 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. Create + * these using {@link formatAddress} + */ + public void encodeHeaderAddresses(StringBuilder sb, String headerName, + ArrayList<Rfc822Token> addresses) { + /* TODO: Do we need to encode the addresses if they contain illegal characters */ + 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 <bmessage-body-content>. 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(messageId != null) + sb.append("Message-Id: ").append(messageId).append("\r\n"); + if(contentType != null) + sb.append("Content-Type: ").append(contentType).append("; boundary=").append(getBoundary()); + sb.append("\r\n\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 <bmessage-body-content>. + * 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 not mechanism to embed the attachment in + * the <bmessage-body-content>. + * + * UPDATE: Errata xxx allows the needed encoding typed inside the <bmessage-body-content> + * 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. + * */ + + /** + * Encode the bMessage as a MMS + * @return + * @throws UnsupportedEncodingException + */ + public byte[] encodeMms() throws UnsupportedEncodingException + { + ArrayList<byte[]> bodyFragments = new ArrayList<byte[]>(); + StringBuilder sb = new StringBuilder(); + int count = 0; + String mmsBody; + + encoding = "8BIT"; // The encoding used + + encodeHeaders(sb); + 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]); // TODO: Is this allowed? (An empty message) + } + + return encodeGeneric(bodyFragments); + } + + private void parseMmsHeaders(String hdrPart) { + String[] headers = hdrPart.split("\r\n"); + + for(String header : headers) { + if(header.trim() == "") + continue; + String[] headerParts = header.split(":",2); + if(headerParts.length != 2) + throw new IllegalArgumentException("Header not formatted correctly: " + header); + String headerType = headerParts[0].toUpperCase(); + String headerValue = headerParts[1].trim(); + + // Address headers + // TODO: If this is empty, the MSE needs to fill it in + if(headerType.contains("FROM")) { + Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); + from = new ArrayList<Rfc822Token>(Arrays.asList(tokens)); + } + else if(headerType.contains("TO")) { + Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); + to = new ArrayList<Rfc822Token>(Arrays.asList(tokens)); + } + else if(headerType.contains("CC")) { + Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); + cc = new ArrayList<Rfc822Token>(Arrays.asList(tokens)); + } + else if(headerType.contains("BCC")) { + Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); + bcc = new ArrayList<Rfc822Token>(Arrays.asList(tokens)); + } + else if(headerType.contains("REPLY-TO")) { + Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue); + replyTo = new ArrayList<Rfc822Token>(Arrays.asList(tokens)); + }// Other headers + else if(headerType.contains("SUBJECT")) { + subject = headerValue; + } + else if(headerType.contains("MESSAGE-ID")) { + messageId = headerValue; + } + else if(headerType.contains("DATE")) { + /* XXX: Set date */ + } + else if(headerType.contains("CONTENT-TYPE")) { + String[] contentTypeParts = headerValue.split(";"); + contentType = contentTypeParts[0]; + // Extract the boundary if it exists + for(int i=1, n=contentTypeParts.length; i<n; i++) + { + if(contentTypeParts[i].contains("boundary")) { + boundary = contentTypeParts[i].split("boundary[\\s]*=", 2)[1].trim(); + } + } + } + else { + if(D) Log.w(TAG,"Skipping unknown header: " + headerType + " (" + header + ")"); + } + } + } + + private void parseMmsMimePart(String partStr) { + /**/ + String[] parts = partStr.split("\r\n\r\n", 2); // Split the header from the body + if(parts.length != 2) { + throw new IllegalArgumentException("Wrongly formatted email part - unable to locate header section"); + } + String[] headers = parts[0].split("\r\n"); + MimePart newPart = addMimePart(); + String encoding = ""; + + for(String header : headers) { + if(header.length() == 0) + continue; + + if(header.trim() == "" || header.trim().equals("--")) // Skip empty lines(the \r\n after the boundary tag) and endBoundary tags + continue; + String[] headerParts = header.split(":",2); + if(headerParts.length != 2) + throw new IllegalArgumentException("part-Header not formatted correctly: " + header); + String headerType = headerParts[0].toUpperCase(); + String headerValue = headerParts[1].trim(); + if(headerType.contains("CONTENT-TYPE")) { + // TODO: extract charset - as for + newPart.contentType = headerValue; + Log.d(TAG, "*** CONTENT-TYPE: " + newPart.contentType); + } + else if(headerType.contains("CONTENT-LOCATION")) { + // This is used if the smil refers to a file name in its src= + newPart.partName = headerValue; + } + else if(headerType.contains("CONTENT-TRANSFER-ENCODING")) { + encoding = headerValue; + } + else if(headerType.contains("CONTENT-ID")) { + // This is used if the smil refers to a cid:<xxx> in it's src= + newPart.contentId = headerValue; + } + else { + if(D) Log.w(TAG,"Skipping unknown part-header: " + headerType + " (" + header + ")"); + } + } + // Now for the body + if(encoding.toUpperCase().contains("BASE64")) { + newPart.data = Base64.decode(parts[1], Base64.DEFAULT); + } else { + // TODO: handle other encoding types? - here we simply store the string data as bytes + try { + newPart.data = parts[1].getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + // This will never happen, as UTF-8 is mandatory on Android platforms + } + } + } + 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; + 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) { + throw new IllegalArgumentException("Wrongly formatted email message - unable to locate header section"); + } + parseMmsHeaders(messageParts[0]); + mimeParts = messageParts[1].split("--" + boundary); + for(String part : mimeParts) { + 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) { + // TODO Auto-generated method stub + parseMms(msgPart); + + } + + @Override + public void parseMsgInit() { + // TODO Auto-generated method stub + + } + + @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 new file mode 100644 index 000000000..07a2a6731 --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMapbMessageSms.java @@ -0,0 +1,82 @@ +/* +* 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.util.ArrayList; + +import com.android.bluetooth.map.BluetoothMapSmsPdu.SmsPdu; +import com.android.bluetooth.map.BluetoothMapUtils.TYPE; + +public class BluetoothMapbMessageSms extends BluetoothMapbMessage { + + private ArrayList<SmsPdu> smsBodyPdus = null; + private String smsBody = null; + + public void setSmsBodyPdus(ArrayList<SmsPdu> smsBodyPdus) { + this.smsBodyPdus = smsBodyPdus; + this.charset = null; + if(smsBodyPdus.size() > 0) + this.encoding = smsBodyPdus.get(0).getEncodingString(); + } + + public String getSmsBody() { + return smsBody; + } + + public void setSmsBody(String smsBody) { + this.smsBody = smsBody; + this.charset = "UTF-8"; + this.encoding = null; + } + + @Override + public void parseMsgPart(String msgPart) { + if(appParamCharset == BluetoothMapAppParams.CHARSET_NATIVE) { + smsBody += BluetoothMapSmsPdu.decodePdu(decodeBinary(msgPart), + type == TYPE.SMS_CDMA ? BluetoothMapSmsPdu.SMS_TYPE_CDMA + : BluetoothMapSmsPdu.SMS_TYPE_GSM); + } else { + smsBody += msgPart; + } + } + @Override + public void parseMsgInit() { + smsBody = ""; + } + + public byte[] encode() throws UnsupportedEncodingException + { + ArrayList<byte[]> bodyFragments = new ArrayList<byte[]>(); + + /* Store the messages in an ArrayList to be able to handle the different message types in a generic way. + * We use byte[] since we need to extract the length in bytes. + */ + if(smsBody != null) { + String tmpBody = smsBody.replaceAll("END:MSG", "/END\\:MSG"); // Replace any occurrences of END:MSG with \END:MSG + bodyFragments.add(tmpBody.getBytes("UTF-8")); + }else if (smsBodyPdus.size() > 0) { + for (SmsPdu pdu : smsBodyPdus) { + // 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) + } + + return encodeGeneric(bodyFragments); + } + +} diff --git a/src/com/android/bluetooth/map/BluetoothMnsObexClient.java b/src/com/android/bluetooth/map/BluetoothMnsObexClient.java new file mode 100644 index 000000000..c6283f1b5 --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMnsObexClient.java @@ -0,0 +1,346 @@ +/* +* 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.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelUuid; +import android.util.Log; + +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; +import javax.obex.ObexTransport; +import javax.obex.ResponseCodes; + +/** + * + * @author Casper Bonde, Henrik Ejersbo and Kim Schulz + * + * The Message Notification Service class runs its own message handler thread, + * to avoid executing long operations on the MAP service Thread. + * This handler context is passed to the content observers, + * hence all call-backs (and thereby transmission of data) is executed + * from this thread. + */ +public class BluetoothMnsObexClient extends Thread{ + + private static final String TAG = "BluetoothMnsObexClient"; + private static final boolean D = true; + private static final boolean V = true; + + public final static int MSG_SESSION_ERROR = 1; + public final static int MSG_CONNECT_TIMEOUT = 2; + + private ObexTransport mTransport; + private Context mContext; + public static 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 static BluetoothMapContentObserver mObserver; + private boolean mObserverRegistered = false; + + private Looper mLooper = null; + // Used by the MAS to forward notification registrations + public static final int MSG_MNS_NOTIFICATION_REGISTRATION = 1; + public static final int MSG_MNS_SHUTDOWN = 2; + + public static final ParcelUuid BluetoothUuid_ObexMns = + ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB"); + + + public BluetoothMnsObexClient(Context context, BluetoothDevice remoteDevice) { + if (remoteDevice == null) { + throw new NullPointerException("Obex transport is null"); + } + mContext = context; + mRemoteDevice = remoteDevice; + } + + public static Handler getMessageHandler() { + // TODO: if mHandle is null, we should wait for it to be created. + return mHandler; + } + + public static BluetoothMapContentObserver getContentObserver() { + return mObserver; + } + + @Override + public void run() { + Looper.prepare(); + mLooper = Looper.myLooper(); + + + /* Create the context observer from within the thread to ensure the "content changed" + * events are handled in this thread. */ + mObserver = new BluetoothMapContentObserver(mContext); + mObserver.init(); + + mHandler = new Handler() { + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_MNS_NOTIFICATION_REGISTRATION: + handleRegistration(msg.arg1 /*masId*/, msg.arg2 /*status*/); + break; + case MSG_MNS_SHUTDOWN: + if(mObserverRegistered) + mObserver.unregisterObserver(); + break; + default: + break; + } + } + }; + Looper.loop(); + } + + public boolean isConnected() { + return mConnected; + } + + public void disconnect() { + try { + if (mClientSession != null) { + mClientSession.disconnect(null); + if (D) Log.d(TAG, "OBEX session disconnected"); + } + mClientSession = null; + } catch (IOException e) { + Log.w(TAG, "OBEX session disconnect error " + e.getMessage()); + } + try { + if (mClientSession != null) { + if (D) Log.d(TAG, "OBEX session close mClientSession"); + mClientSession.close(); + if (D) Log.d(TAG, "OBEX session closed"); + } + } catch (IOException e) { + Log.w(TAG, "OBEX session close error:" + e.getMessage()); + } + if (mTransport != null) { + try { + if (D) Log.d(TAG, "Close Obex Transport"); + mTransport.close(); + if (D) Log.d(TAG, "Obex Transport Closed"); + } catch (IOException e) { + Log.e(TAG, "mTransport.close error: " + e.getMessage()); + } + } + if(mObserverRegistered) { + mObserver.unregisterObserver(); + mObserverRegistered = false; + } + + mObserver.deinit(); + // Shut down the thread + if(mLooper != null) + mLooper.quit(); + interrupt(); + try { + join(); + } catch (InterruptedException e) { + if(V) Log.w(TAG, e); + } + } + + private HeaderSet hsConnect = null; + + public void handleRegistration(int masId, int notificationStatus){ + Log.d(TAG, "handleRegistration( " + masId + ", " + notificationStatus + ")"); + + if(isConnected() == false) { + Log.d(TAG, "handleRegistration: connect"); + connect(); + } + + 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(); + } + } 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. + */ + if(mObserverRegistered == false) { + mObserver.registerObserver(this, masId); + mObserverRegistered = true; + } + } + } + + public void connect() { + Log.d(TAG, "handleRegistration: connect 2"); + + BluetoothSocket btSocket = null; + try { + btSocket = mRemoteDevice.createInsecureRfcommSocketToServiceRecord( + BluetoothUuid_ObexMns.getUuid()); + btSocket.connect(); + } catch (IOException e) { + Log.e(TAG, "BtSocket Connect error " + e.getMessage(), e); + // TODO: do we need to report error somewhere? + return; + } + + mTransport = new BluetoothMnsRfcommTransport(btSocket); + + try { + mClientSession = new ClientSession(mTransport); + mConnected = true; + } catch (IOException e1) { + Log.e(TAG, "OBEX session create error " + e1.getMessage()); + } + if (mConnected && mClientSession != null) { + mConnected = false; + HeaderSet hs = new HeaderSet(); + // bb582b41-420c-11db-b0de-0800200c9a66 + byte[] mnsTarget = { (byte) 0xbb, (byte) 0x58, (byte) 0x2b, (byte) 0x41, + (byte) 0x42, (byte) 0x0c, (byte) 0x11, (byte) 0xdb, + (byte) 0xb0, (byte) 0xde, (byte) 0x08, (byte) 0x00, + (byte) 0x20, (byte) 0x0c, (byte) 0x9a, (byte) 0x66 }; + hs.setHeader(HeaderSet.TARGET, mnsTarget); + + synchronized (this) { + mWaitingForRemote = true; + } + try { + hsConnect = mClientSession.connect(hs); + if (D) Log.d(TAG, "OBEX session created"); + mConnected = true; + } catch (IOException e) { + Log.e(TAG, "OBEX session connect error " + e.getMessage()); + } + } + synchronized (this) { + mWaitingForRemote = false; + } + } + + public int sendEvent(byte[] eventBytes, int masInstanceId) { + + boolean error = false; + int responseCode = -1; + HeaderSet request; + int maxChunkSize, bytesToWrite, bytesWritten = 0; + request = new HeaderSet(); + BluetoothMapAppParams appParams = new BluetoothMapAppParams(); + appParams.setMasInstanceId(masInstanceId); + + ClientOperation putOperation = null; + OutputStream outputStream = null; + + try { + request.setHeader(HeaderSet.TYPE, TYPE_EVENT); + request.setHeader(HeaderSet.APPLICATION_PARAMETER, appParams.EncodeParams()); + + request.mConnectionID = new byte[4]; + System.arraycopy(hsConnect.mConnectionID, 0, request.mConnectionID, 0, 4); + + synchronized (this) { + mWaitingForRemote = true; + } + // Send the header first and then the body + try { + if (V) Log.v(TAG, "Send headerset Event "); + putOperation = (ClientOperation)mClientSession.put(request); + // TODO - Should this be kept or Removed + + } catch (IOException e) { + Log.e(TAG, "Error when put HeaderSet " + e.getMessage()); + error = true; + } + synchronized (this) { + mWaitingForRemote = false; + } + if (!error) { + try { + if (V) Log.v(TAG, "Send headerset Event "); + outputStream = putOperation.openOutputStream(); + } catch (IOException e) { + Log.e(TAG, "Error when opening OutputStream " + e.getMessage()); + error = true; + } + } + + if (!error) { + + maxChunkSize = putOperation.getMaxPacketSize(); + + while (bytesWritten < eventBytes.length) { + bytesToWrite = Math.min(maxChunkSize, eventBytes.length - bytesWritten); + outputStream.write(eventBytes, bytesWritten, bytesToWrite); + bytesWritten += bytesToWrite; + } + + if (bytesWritten == eventBytes.length) { + Log.i(TAG, "SendEvent finished send length" + eventBytes.length); + outputStream.close(); + } else { + error = true; + // TBD - Is Output stream close needed here + putOperation.abort(); + Log.i(TAG, "SendEvent interrupted"); + } + } + } catch (IOException e) { + handleSendException(e.toString()); + error = true; + } catch (IndexOutOfBoundsException e) { + handleSendException(e.toString()); + error = true; + } finally { + try { + if (!error) { + responseCode = putOperation.getResponseCode(); + if (responseCode != -1) { + if (V) Log.v(TAG, "Put response code " + responseCode); + if (responseCode != ResponseCodes.OBEX_HTTP_OK) { + Log.i(TAG, "Response error code is " + responseCode); + } + } + } + if (putOperation != null) { + putOperation.close(); + } + } catch (IOException e) { + Log.e(TAG, "Error when closing stream after send " + e.getMessage()); + } + } + + return responseCode; + } + + private void handleSendException(String exception) { + Log.e(TAG, "Error when sending event: " + exception); + } +} diff --git a/src/com/android/bluetooth/map/BluetoothMnsRfcommTransport.java b/src/com/android/bluetooth/map/BluetoothMnsRfcommTransport.java new file mode 100644 index 000000000..fc5d54a5d --- /dev/null +++ b/src/com/android/bluetooth/map/BluetoothMnsRfcommTransport.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 android.bluetooth.BluetoothSocket; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.obex.ObexTransport; + +public class BluetoothMnsRfcommTransport implements ObexTransport { + + private final BluetoothSocket mSocket; + + public BluetoothMnsRfcommTransport(BluetoothSocket socket) { + super(); + this.mSocket = socket; + } + + public void close() throws IOException { + mSocket.close(); + } + + public DataInputStream openDataInputStream() throws IOException { + return new DataInputStream(openInputStream()); + } + + public DataOutputStream openDataOutputStream() throws IOException { + return new DataOutputStream(openOutputStream()); + } + + public InputStream openInputStream() throws IOException { + return mSocket.getInputStream(); + } + + public OutputStream openOutputStream() throws IOException { + return mSocket.getOutputStream(); + } + + public void connect() throws IOException { + } + + public void create() throws IOException { + } + + public void disconnect() throws IOException { + } + + public void listen() throws IOException { + } + + public boolean isConnected() throws IOException { + // TODO: add implementation + return true; + } + + public String getRemoteAddress() { + if (mSocket == null) + return null; + return mSocket.getRemoteDevice().getAddress(); + } + +} |