diff options
author | AnubhavGupta <anubhavg@codeaurora.org> | 2015-02-27 19:08:36 +0530 |
---|---|---|
committer | Linux Build Service Account <lnxbuild@localhost> | 2015-10-06 03:25:43 -0600 |
commit | 442962cde4c933ba91eeeb422e48d063fa5fa0fb (patch) | |
tree | d8d1d124172a0675397899fc42f0daf211f13d0a | |
parent | db525f6a5d47ea8934dd58cffff452da89489f30 (diff) | |
download | android_packages_apps_Bluetooth-442962cde4c933ba91eeeb422e48d063fa5fa0fb.tar.gz android_packages_apps_Bluetooth-442962cde4c933ba91eeeb422e48d063fa5fa0fb.tar.bz2 android_packages_apps_Bluetooth-442962cde4c933ba91eeeb422e48d063fa5fa0fb.zip |
Bluetooth : Support for AVRCP 1.3 Controller
- Content Provider Implementation for AVRCP MetaData
- Handling of AVRCP Commands and Events
- Interface for AudioManger to support AbsVol
Change-Id: I5e1f768ddfa6c089667874c5151a412ae79052bd
Bluetooth: Avrcp Controller 1.3 Support
- send GetPlayStatus on state transition from paused/stopped
to playing.
- indentation correction
Change-Id: Ifa259038383e0652ff7dcb345367d8f67c3c80be
Bluetooth: AVRCP Controller fixes
- Reset metadata if remote does not support a particular
Element Attribute
- Fetch playerapplicationattributes on track change
- Proper update of negative values from remote
Usecase: Switch player on TG and check metadata, playerappsetting
Failure: Remote device does not call Event_APP_SETTING_CHANGED
when we switch between player. If new player does not
update metadata, we were showing stale data
Solution: Reset metadata if not supported by remote
send getplayerapplicatoin on track change
Change-Id: Ia7859261336ccdb447d330626b434fefd4abee6f
-rw-r--r--[-rwxr-xr-x] | AndroidManifest.xml | 15 | ||||
-rw-r--r-- | jni/com_android_bluetooth_avrcp_controller.cpp | 558 | ||||
-rw-r--r-- | res/values/strings.xml | 15 | ||||
-rw-r--r-- | src/com/android/bluetooth/a2dp/A2dpSinkStateMachine.java | 4 | ||||
-rw-r--r-- | src/com/android/bluetooth/avrcp/AvrcpControllerService.java | 1915 | ||||
-rw-r--r-- | src/com/android/bluetooth/avrcp/BluetoothAvrcpDataProvider.java | 382 |
6 files changed, 2877 insertions, 12 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 44613341a..0fa1b7674 100755..100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -11,6 +11,11 @@ android:description="@string/permdesc_bluetoothShareManager" android:protectionLevel="signature" /> + <permission android:name="android.permission.ACCESS_BLUETOOTH_AVRCP_CT_DATA" + android:label="@string/permlab_bluetoothAvrcpDataManager" + android:description="@string/permdesc_bluetoothAvrcpDataManager" + android:protectionLevel="signature" /> + <!-- Allows temporarily whitelisting Bluetooth addresses for sharing --> <permission android:name="com.android.permission.WHITELIST_BLUETOOTH_DEVICE" android:label="@string/permlab_bluetoothWhitelist" @@ -20,6 +25,7 @@ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.ACCESS_BLUETOOTH_SHARE" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_BLUETOOTH_AVRCP_CT_DATA" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> @@ -83,6 +89,14 @@ android:pathPrefix="/btopp" android:permission="android.permission.ACCESS_BLUETOOTH_SHARE" /> </provider> + <provider android:name=".avrcp.BluetoothAvrcpDataProvider" + android:authorities="com.android.bluetooth.avrcp" + android:exported="true" + android:process="@string/process"> + <path-permission + android:pathPrefix="/btavrcp_ct" + android:permission="android.permission.ACCESS_BLUETOOTH_AVRCP_CT_DATA" /> + </provider> <service android:process="@string/process" android:name = ".btservice.AdapterService"> @@ -297,6 +311,7 @@ <service android:process="@string/process" android:name = ".avrcp.AvrcpControllerService" + android:permission="android.permission.ACCESS_BLUETOOTH_AVRCP_CT_DATA" android:enabled="@bool/profile_supported_avrcp_controller"> <intent-filter> <action android:name="android.bluetooth.IBluetoothAvrcpController" /> diff --git a/jni/com_android_bluetooth_avrcp_controller.cpp b/jni/com_android_bluetooth_avrcp_controller.cpp index 9e32721e9..1fa588911 100644 --- a/jni/com_android_bluetooth_avrcp_controller.cpp +++ b/jni/com_android_bluetooth_avrcp_controller.cpp @@ -1,4 +1,6 @@ /* + * Copyright (c) 2015, The Linux Foundation. All rights reserved. + * Not a Contribution * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +30,18 @@ namespace android { static jmethodID method_handlePassthroughRsp; static jmethodID method_onConnectionStateChanged; +static jmethodID method_getRcFeatures; +static jmethodID method_handleGetCapabilitiesResponse; +static jmethodID method_handleListPlayerApplicationSettingsAttrib; +static jmethodID method_handleListPlayerApplicationSettingValue; +static jmethodID method_handleCurrentPlayerApplicationSettingsResponse; +static jmethodID method_handleNotificationRsp; +static jmethodID method_handleGetElementAttributes; +static jmethodID method_handleGetPlayStatus; +static jmethodID method_handleSetAbsVolume; +static jmethodID method_handleRegisterNotificationAbsVol; +static jmethodID method_handleSetPlayerApplicationResponse; + static const btrc_ctrl_interface_t *sBluetoothAvrcpInterface = NULL; static jobject mCallbacksObj = NULL; @@ -84,11 +98,323 @@ static void btavrcp_connection_state_callback(bool state, bt_bdaddr_t* bd_addr) sCallbackEnv->DeleteLocalRef(addr); } +static void btavrcp_get_rcfeatures_callback(bt_bdaddr_t *bd_addr, int features) { + jbyteArray addr; + + ALOGI("%s", __FUNCTION__); + + if (!checkCallbackThread()) { \ + ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__); \ + return; \ + } + + addr = sCallbackEnv->NewByteArray(sizeof(bt_bdaddr_t)); + if (!addr) { + ALOGE("Fail to new jbyteArray bd addr "); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + return; + } + + sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*) bd_addr); + sCallbackEnv->CallVoidMethod(mCallbacksObj, method_getRcFeatures, addr, (jint)features); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + sCallbackEnv->DeleteLocalRef(addr); +} + +static void btavrcp_getcap_rsp_callback(bt_bdaddr_t *bd_addr, int cap_id, + uint32_t* supported_values, int num_supported, uint8_t rsp_type) { + jbyteArray addr; + jintArray supported_val = NULL; + + ALOGI("%s", __FUNCTION__); + + if (!checkCallbackThread()) { \ + ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__); \ + return; \ + } + + addr = sCallbackEnv->NewByteArray(sizeof(bt_bdaddr_t)); + supported_val = sCallbackEnv->NewIntArray(num_supported); + if ((!addr)||(!supported_val)) { + ALOGE("Fail to get new array "); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + return; + } + + sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*) bd_addr); + sCallbackEnv->SetIntArrayRegion(supported_val, 0, (num_supported), (jint*)(supported_values)); + sCallbackEnv->CallVoidMethod(mCallbacksObj, method_handleGetCapabilitiesResponse, addr, + (jint)cap_id,supported_val, (jint)num_supported, (jbyte)rsp_type); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + sCallbackEnv->DeleteLocalRef(addr); + sCallbackEnv->DeleteLocalRef(supported_val); +} + +static void btavrcp_list_player_app_setting_attrib_rsp_callback(bt_bdaddr_t *bd_addr, + uint8_t* supported_attribs, int num_attrib, uint8_t rsp_type) { + jbyteArray addr; + jbyteArray supported_val; + + ALOGI("%s", __FUNCTION__); + + if (!checkCallbackThread()) { \ + ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__); \ + return; \ + } + + addr = sCallbackEnv->NewByteArray(sizeof(bt_bdaddr_t)); + supported_val = sCallbackEnv->NewByteArray(num_attrib); + if ((!addr)||(!supported_val)) { + ALOGE("Fail to get new array "); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + return; + } + + sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*) bd_addr); + sCallbackEnv->SetByteArrayRegion(supported_val, 0, (num_attrib), (jbyte*)(supported_attribs)); + sCallbackEnv->CallVoidMethod(mCallbacksObj, method_handleListPlayerApplicationSettingsAttrib, + addr, supported_val,(jint)num_attrib, (jbyte)rsp_type); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + sCallbackEnv->DeleteLocalRef(addr); + sCallbackEnv->DeleteLocalRef(supported_val); +} + +static void btavrcp_list_player_app_setting_value_rsp_callback(bt_bdaddr_t *bd_addr, + uint8_t* supported_values, uint8_t num_supported, uint8_t rsp_type) { + jbyteArray addr; + jbyteArray supported_val; + + ALOGI("%s", __FUNCTION__); + + if (!checkCallbackThread()) { \ + ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__); \ + return; \ + } + + addr = sCallbackEnv->NewByteArray(sizeof(bt_bdaddr_t)); + supported_val = sCallbackEnv->NewByteArray(num_supported); + if ((!addr)||(!supported_val)) { + ALOGE("Fail to get new array "); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + return; + } + + sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*) bd_addr); + sCallbackEnv->SetByteArrayRegion(supported_val, 0,(num_supported), (jbyte*)(supported_values)); + sCallbackEnv->CallVoidMethod(mCallbacksObj, method_handleListPlayerApplicationSettingValue, + addr, supported_val,(jbyte)num_supported, (jbyte)rsp_type); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + sCallbackEnv->DeleteLocalRef(addr); + sCallbackEnv->DeleteLocalRef(supported_val); +} + +static void btavrcp_current_player_app_setting_rsp_callback(bt_bdaddr_t *bd_addr, + uint8_t* supported_ids,uint8_t* supported_values, uint8_t num_attrib, uint8_t rsp_type) { + jbyteArray addr; + jbyteArray supported_attrib_ids; + jbyteArray supported_val; + + ALOGI("%s", __FUNCTION__); + + if (!checkCallbackThread()) { \ + ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__); \ + return; \ + } + + addr = sCallbackEnv->NewByteArray(sizeof(bt_bdaddr_t)); + supported_val = sCallbackEnv->NewByteArray(num_attrib); + supported_attrib_ids = sCallbackEnv->NewByteArray(num_attrib); + if ((!addr)||(!supported_val)) { + ALOGE("Fail to get new array "); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + return; + } + + sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*) bd_addr); + sCallbackEnv->SetByteArrayRegion(supported_val, 0, (num_attrib), (jbyte*)(supported_values)); + sCallbackEnv->SetByteArrayRegion(supported_attrib_ids, 0, (num_attrib), + (jbyte*)(supported_ids)); + sCallbackEnv->CallVoidMethod(mCallbacksObj, + method_handleCurrentPlayerApplicationSettingsResponse,addr,supported_attrib_ids, + supported_val, (jbyte)num_attrib, (jbyte)rsp_type); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + sCallbackEnv->DeleteLocalRef(addr); + sCallbackEnv->DeleteLocalRef(supported_val); + sCallbackEnv->DeleteLocalRef(supported_attrib_ids); +} + +static void btavrcp_set_player_app_setting_rsp_callback(bt_bdaddr_t *bd_addr,uint8_t rsp_type) { + jbyteArray addr; + + ALOGI("%s", __FUNCTION__); + + if (!checkCallbackThread()) { \ + ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__); \ + return; \ + } + + addr = sCallbackEnv->NewByteArray(sizeof(bt_bdaddr_t)); + if ((!addr)) { + ALOGE("Fail to get new array "); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + return; + } + + sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*) bd_addr); + sCallbackEnv->CallVoidMethod(mCallbacksObj, method_handleSetPlayerApplicationResponse, + addr,(jbyte)rsp_type); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + sCallbackEnv->DeleteLocalRef(addr); +} + +static void btavrcp_notification_rsp_callback(bt_bdaddr_t *bd_addr, uint8_t rsp_type, + int rsp_len, uint8_t* notification_rsp) { + jbyteArray addr; + jbyteArray supported_val; + + ALOGI("%s", __FUNCTION__); + + if (!checkCallbackThread()) { \ + ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__); \ + return; \ + } + + addr = sCallbackEnv->NewByteArray(sizeof(bt_bdaddr_t)); + supported_val = sCallbackEnv->NewByteArray(rsp_len); + if ((!addr)||(!supported_val)) { + ALOGE("Fail to get new array "); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + return; + } + + sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*) bd_addr); + sCallbackEnv->SetByteArrayRegion(supported_val, 0, (rsp_len), (jbyte*)(notification_rsp)); + sCallbackEnv->CallVoidMethod(mCallbacksObj, method_handleNotificationRsp, + addr, (jbyte)rsp_type, (jint)rsp_len, supported_val); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + sCallbackEnv->DeleteLocalRef(addr); + sCallbackEnv->DeleteLocalRef(supported_val); +} + +static void btavrcp_get_element_attrib_rsp_callback(bt_bdaddr_t *bd_addr, uint8_t num_attributes, + int rsp_len, uint8_t* attrib_rsp,uint8_t rsp_type) { + jbyteArray addr; + jbyteArray supported_val; + + ALOGI("%s", __FUNCTION__); + + if (!checkCallbackThread()) { \ + ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__); \ + return; \ + } + + addr = sCallbackEnv->NewByteArray(sizeof(bt_bdaddr_t)); + supported_val = sCallbackEnv->NewByteArray(rsp_len); + if ((!addr)||(!supported_val)) { + ALOGE("Fail to get new array "); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + return; + } + + sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*) bd_addr); + sCallbackEnv->SetByteArrayRegion(supported_val, 0, (rsp_len), (jbyte*)(attrib_rsp)); + sCallbackEnv->CallVoidMethod(mCallbacksObj, method_handleGetElementAttributes, + addr, (jbyte)num_attributes, (jint)rsp_len,supported_val, (jbyte)rsp_type); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + sCallbackEnv->DeleteLocalRef(addr); + sCallbackEnv->DeleteLocalRef(supported_val); +} + +static void btavrcp_get_playstatus_rsp_callback(bt_bdaddr_t *bd_addr, int param_len, + uint8_t* play_status_rsp,uint8_t rsp_type) { + jbyteArray addr; + jbyteArray supported_val; + + ALOGI("%s", __FUNCTION__); + + if (!checkCallbackThread()) { \ + ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__); \ + return; \ + } + + addr = sCallbackEnv->NewByteArray(sizeof(bt_bdaddr_t)); + supported_val = sCallbackEnv->NewByteArray(param_len); + if ((!addr)||(!supported_val)) { + ALOGE("Fail to get new array "); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + return; + } + + sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*) bd_addr); + sCallbackEnv->SetByteArrayRegion(supported_val, 0, (param_len), (jbyte*)(play_status_rsp)); + sCallbackEnv->CallVoidMethod(mCallbacksObj, method_handleGetPlayStatus, + addr, (jint)param_len, supported_val, (jbyte)rsp_type); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + sCallbackEnv->DeleteLocalRef(addr); + sCallbackEnv->DeleteLocalRef(supported_val); +} + +static void btavrcp_set_abs_vol_cmd_callback(bt_bdaddr_t *bd_addr, uint8_t abs_vol) { + jbyteArray addr; + + ALOGI("%s", __FUNCTION__); + + if (!checkCallbackThread()) { \ + ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__); \ + return; \ + } + + addr = sCallbackEnv->NewByteArray(sizeof(bt_bdaddr_t)); + if ((!addr)) { + ALOGE("Fail to get new array "); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + return; + } + + sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*)bd_addr); + sCallbackEnv->CallVoidMethod(mCallbacksObj, method_handleSetAbsVolume, addr, (jbyte)abs_vol); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + sCallbackEnv->DeleteLocalRef(addr); +} + +static void btavrcp_register_notification_absvol_callback(bt_bdaddr_t *bd_addr) { + jbyteArray addr; + + ALOGI("%s", __FUNCTION__); + + if (!checkCallbackThread()) { \ + ALOGE("Callback: '%s' is not called on the correct thread", __FUNCTION__); \ + return; \ + } + + addr = sCallbackEnv->NewByteArray(sizeof(bt_bdaddr_t)); + if ((!addr)) { + ALOGE("Fail to get new array "); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + return; + } + + sCallbackEnv->SetByteArrayRegion(addr, 0, sizeof(bt_bdaddr_t), (jbyte*)bd_addr); + sCallbackEnv->CallVoidMethod(mCallbacksObj, method_handleRegisterNotificationAbsVol, addr); + checkAndClearExceptionFromCallback(sCallbackEnv, __FUNCTION__); + sCallbackEnv->DeleteLocalRef(addr); +} static btrc_ctrl_callbacks_t sBluetoothAvrcpCallbacks = { sizeof(sBluetoothAvrcpCallbacks), btavrcp_passthrough_response_callback, - btavrcp_connection_state_callback + btavrcp_connection_state_callback, + btavrcp_get_rcfeatures_callback, + btavrcp_getcap_rsp_callback, + btavrcp_list_player_app_setting_attrib_rsp_callback, + btavrcp_list_player_app_setting_value_rsp_callback, + btavrcp_current_player_app_setting_rsp_callback, + btavrcp_set_player_app_setting_rsp_callback, + btavrcp_notification_rsp_callback, + btavrcp_get_element_attrib_rsp_callback, + btavrcp_get_playstatus_rsp_callback, + btavrcp_set_abs_vol_cmd_callback, + btavrcp_register_notification_absvol_callback }; static void classInitNative(JNIEnv* env, jclass clazz) { @@ -98,6 +424,39 @@ static void classInitNative(JNIEnv* env, jclass clazz) { method_onConnectionStateChanged = env->GetMethodID(clazz, "onConnectionStateChanged", "(Z[B)V"); + method_getRcFeatures = + env->GetMethodID(clazz, "getRcFeatures", "([BI)V"); + + method_handleGetCapabilitiesResponse = + env->GetMethodID(clazz, "handleGetCapabilitiesResponse", "([BI[IIB)V"); + + method_handleListPlayerApplicationSettingsAttrib = + env->GetMethodID(clazz, "handleListPlayerApplicationSettingsAttrib", "([B[BIB)V"); + + method_handleListPlayerApplicationSettingValue = + env->GetMethodID(clazz, "handleListPlayerApplicationSettingValue", "([B[BBB)V"); + + method_handleCurrentPlayerApplicationSettingsResponse = + env->GetMethodID(clazz, "handleCurrentPlayerApplicationSettingsResponse", "([B[B[BBB)V"); + + method_handleNotificationRsp = + env->GetMethodID(clazz, "handleNotificationRsp", "([BBI[B)V"); + + method_handleGetElementAttributes = + env->GetMethodID(clazz, "handleGetElementAttributes", "([BBI[BB)V"); + + method_handleGetPlayStatus = + env->GetMethodID(clazz, "handleGetPlayStatus", "([BI[BB)V"); + + method_handleSetAbsVolume = + env->GetMethodID(clazz, "handleSetAbsVolume", "([BB)V"); + + method_handleRegisterNotificationAbsVol = + env->GetMethodID(clazz, "handleRegisterNotificationAbsVol", "([B)V"); + + method_handleSetPlayerApplicationResponse = + env->GetMethodID(clazz, "handleSetPlayerApplicationResponse", "([BB)V"); + ALOGI("%s: succeeds", __FUNCTION__); } @@ -183,12 +542,205 @@ static jboolean sendPassThroughCommandNative(JNIEnv *env, jobject object, jbyteA return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE; } +static void getCapabilitiesNative(JNIEnv *env, jobject object,jint capability_id) { + bt_status_t status; + + if (!sBluetoothAvrcpInterface) return; + + ALOGI("%s: sBluetoothAvrcpInterface: %p", __FUNCTION__, sBluetoothAvrcpInterface); + if ((status = sBluetoothAvrcpInterface->getcapabilities_cmd((uint8_t)capability_id)) + != BT_STATUS_SUCCESS) { + ALOGE("Failed sending getCapabilitiesNative command, status: %d", status); + } +} + +static void listPlayerApplicationSettingAttributeNative(JNIEnv *env, jobject object) { + bt_status_t status; + + if (!sBluetoothAvrcpInterface) return; + + ALOGI("%s: sBluetoothAvrcpInterface: %p", __FUNCTION__, sBluetoothAvrcpInterface); + if ((status = sBluetoothAvrcpInterface->list_player_app_setting_attrib_cmd()) + != BT_STATUS_SUCCESS) { + ALOGE("Failed sending listPlAppSettAttribNative command,status: %d", status); + } +} + +static void listPlayerApplicationSettingValueNative(JNIEnv *env, jobject object, jbyte attrib_id) { + bt_status_t status; + + if (!sBluetoothAvrcpInterface) return; + + ALOGI("%s: sBluetoothAvrcpInterface: %p", __FUNCTION__, sBluetoothAvrcpInterface); + if ((status = sBluetoothAvrcpInterface->list_player_app_setting_value_cmd((uint8_t)attrib_id)) + != BT_STATUS_SUCCESS) { + ALOGE("Failed sending listPlAppSettValNative command, status: %d", status); + } +} + +static void getPlayerApplicationSettingValuesNative(JNIEnv *env, jobject object, jbyte num_attrib, jbyteArray attrib_ids) { + bt_status_t status; + uint8_t *pAttrs = NULL; + int i; + jbyte *attr; + + if (!sBluetoothAvrcpInterface) return; + + pAttrs = new uint8_t[num_attrib]; + if (!pAttrs) { + ALOGE("getPlayerApplicationSettingValuesNative: not have enough memeory"); + return; + } + attr = env->GetByteArrayElements(attrib_ids, NULL); + if (!attr) { + delete[] pAttrs; + jniThrowIOException(env, EINVAL); + return; + } + for (i = 0; i < num_attrib; ++i) { + pAttrs[i] = (uint8_t)attr[i]; + } + if (i < num_attrib) { + delete[] pAttrs; + env->ReleaseByteArrayElements(attrib_ids, attr, 0); + return; + } + + ALOGI("%s: sBluetoothAvrcpInterface: %p", __FUNCTION__, sBluetoothAvrcpInterface); + if ((status = sBluetoothAvrcpInterface->get_player_app_setting_cmd((uint8_t)num_attrib, pAttrs)) + != BT_STATUS_SUCCESS) { + ALOGE("Failed sending getPlAppSettValNative command, status: %d", status); + } + delete[] pAttrs; + env->ReleaseByteArrayElements(attrib_ids, attr, 0); +} + +static void setPlayerApplicationSettingValuesNative(JNIEnv *env, jobject object, jbyte num_attrib, jbyteArray attrib_ids, + jbyteArray attrib_val) { + bt_status_t status; + uint8_t *pAttrs = NULL; + uint8_t *pAttrsVal = NULL; + int i; + jbyte *attr; + jbyte *attr_val; + + if (!sBluetoothAvrcpInterface) return; + + pAttrs = new uint8_t[num_attrib]; + pAttrsVal = new uint8_t[num_attrib]; + if ((!pAttrs) ||(!pAttrsVal)) { + ALOGE("setPlayerApplicationSettingValuesNative: not have enough memeory"); + return; + } + attr = env->GetByteArrayElements(attrib_ids, NULL); + attr_val = env->GetByteArrayElements(attrib_val, NULL); + if ((!attr)||(!attr_val)) { + delete[] pAttrs; + delete[] pAttrsVal; + jniThrowIOException(env, EINVAL); + return; + } + for (i = 0; i < num_attrib; ++i) { + pAttrs[i] = (uint8_t)attr[i]; + pAttrsVal[i] = (uint8_t)attr_val[i]; + } + if (i < num_attrib) { + delete[] pAttrs; + delete[] pAttrsVal; + env->ReleaseByteArrayElements(attrib_ids, attr, 0); + env->ReleaseByteArrayElements(attrib_val, attr_val, 0); + return; + } + + ALOGI("%s: sBluetoothAvrcpInterface: %p", __FUNCTION__, sBluetoothAvrcpInterface); + if ((status = sBluetoothAvrcpInterface->set_player_app_setting_cmd((uint8_t)num_attrib, pAttrs, pAttrsVal)) + != BT_STATUS_SUCCESS) { + ALOGE("Failed sending setPlAppSettValNative command, status: %d", status); + } + delete[] pAttrs; + delete[] pAttrsVal; + env->ReleaseByteArrayElements(attrib_ids, attr, 0); + env->ReleaseByteArrayElements(attrib_val, attr_val, 0); +} + +static void registerNotificationNative(JNIEnv *env, jobject object, jbyte event_id, jint value ) { + bt_status_t status; + + if (!sBluetoothAvrcpInterface) return; + + ALOGI("%s: sBluetoothAvrcpInterface: %p", __FUNCTION__, sBluetoothAvrcpInterface); + if ((status = sBluetoothAvrcpInterface->register_notification_cmd((uint8_t)event_id,(uint32_t)value)) + != BT_STATUS_SUCCESS) { + ALOGE("Failed sending registerNotificationNative command, status: %d", status); + } +} + +static void getElementAttributeNative(JNIEnv *env, jobject object, jbyte num_attrib, jint attrib_id) { + bt_status_t status; + + if (!sBluetoothAvrcpInterface) return; + + ALOGI("%s: sBluetoothAvrcpInterface: %p", __FUNCTION__, sBluetoothAvrcpInterface); + if ((status = sBluetoothAvrcpInterface->get_element_attribute_cmd((uint8_t)num_attrib, + (uint32_t)attrib_id))!= BT_STATUS_SUCCESS) { + ALOGE("Failed sending getElementAttributeNative command, status: %d", status); + } +} + +static void getPlayStatusNative(JNIEnv *env, jobject object) { + bt_status_t status; + + if (!sBluetoothAvrcpInterface) return; + + ALOGI("%s: sBluetoothAvrcpInterface: %p", __FUNCTION__, sBluetoothAvrcpInterface); + if ((status = sBluetoothAvrcpInterface->get_play_status_cmd())!= BT_STATUS_SUCCESS) { + ALOGE("Failed sending getPlayStatusNative command, status: %d", status); + } +} + +static void sendAbsVolRspNative(JNIEnv *env, jobject object, jint abs_vol) { + bt_status_t status; + + if (!sBluetoothAvrcpInterface) return; + + ALOGI("%s: sBluetoothAvrcpInterface: %p", __FUNCTION__, sBluetoothAvrcpInterface); + if ((status = sBluetoothAvrcpInterface->send_abs_vol_rsp((uint8_t)abs_vol))!= BT_STATUS_SUCCESS) { + ALOGE("Failed sending sendAbsVolRspNative command, status: %d", status); + } +} + +static void sendRegisterAbsVolRspNative(JNIEnv *env, jobject object, jbyte rsp_type, jint abs_vol) { + bt_status_t status; + + if (!sBluetoothAvrcpInterface) return; + + ALOGI("%s: sBluetoothAvrcpInterface: %p", __FUNCTION__, sBluetoothAvrcpInterface); + if ((status = sBluetoothAvrcpInterface->send_register_abs_vol_rsp((uint8_t)rsp_type, + (uint8_t)abs_vol))!= BT_STATUS_SUCCESS) { + ALOGE("Failed sending sendRegisterAbsVolRspNative command, status: %d", status); + } +} + static JNINativeMethod sMethods[] = { {"classInitNative", "()V", (void *) classInitNative}, {"initNative", "()V", (void *) initNative}, {"cleanupNative", "()V", (void *) cleanupNative}, - {"sendPassThroughCommandNative", "([BII)Z", - (void *) sendPassThroughCommandNative}, + {"sendPassThroughCommandNative", "([BII)Z",(void *) sendPassThroughCommandNative}, + {"getCapabilitiesNative", "(I)V",(void *) getCapabilitiesNative}, + {"listPlayerApplicationSettingAttributeNative", "()V", + (void *) listPlayerApplicationSettingAttributeNative}, + {"listPlayerApplicationSettingValueNative", "(B)V", + (void *) listPlayerApplicationSettingValueNative}, + {"getPlayerApplicationSettingValuesNative", "(B[B)V", + (void *) getPlayerApplicationSettingValuesNative}, + {"setPlayerApplicationSettingValuesNative", "(B[B[B)V", + (void *) setPlayerApplicationSettingValuesNative}, + {"registerNotificationNative", "(BI)V", + (void *) registerNotificationNative}, + {"getElementAttributeNative", "(BI)V",(void *) getElementAttributeNative}, + {"getPlayStatusNative", "()V",(void *) getPlayStatusNative}, + {"sendAbsVolRspNative", "(I)V",(void *) sendAbsVolRspNative}, + {"sendRegisterAbsVolRspNative", "(BI)V",(void *) sendRegisterAbsVolRspNative}, }; int register_com_android_bluetooth_avrcp_controller(JNIEnv* env) diff --git a/res/values/strings.xml b/res/values/strings.xml index b933df728..19d54aef0 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -23,6 +23,13 @@ --> <string name="permlab_bluetoothShareManager">Access download manager.</string> <!-- + This is the short description of a permission associated with the + Bluetooth Avrcp Data Manager. It is displayed as part of the description of + any application that was granted that permission. This specific + permission controls access to the Bluetooth Avrcp Data Manager. + --> + <string name="permlab_bluetoothAvrcpDataManager">Access avrcp metadata.</string> + <!-- This is the long description of a permission associated with the Android Download Manager. It is displayed as part of the description of any application that was granted that permission. This specific @@ -31,6 +38,14 @@ --> <string name="permdesc_bluetoothShareManager">Allows the app to access the BluetoothShare manager and use it to transfer files. </string> + <!-- + This is the long description of a permission associated with the + Bluetooth Avrcp Data Manager. It is displayed as part of the description of + any application that was granted that permission. This specific + permission controls access to the Bluetooth Avrcp Data Manager. + --> + <string name="permdesc_bluetoothAvrcpDataManager">Allows the app to access the + BluetoothAvrcpData manager and use it to display on UI. </string> <string name="permlab_bluetoothWhitelist">Whitelist bluetooth device access.</string> diff --git a/src/com/android/bluetooth/a2dp/A2dpSinkStateMachine.java b/src/com/android/bluetooth/a2dp/A2dpSinkStateMachine.java index eac52bc0a..b99dd36c7 100644 --- a/src/com/android/bluetooth/a2dp/A2dpSinkStateMachine.java +++ b/src/com/android/bluetooth/a2dp/A2dpSinkStateMachine.java @@ -182,6 +182,10 @@ final class A2dpSinkStateMachine extends StateMachine { " mPlayingDevice " + mPlayingDevice + "mOutPortSpeaker" + mOutPortSpeaker); if((mA2dpSinkAudioPatch == null) && (mPlayingDevice != null) && (mOutPortSpeaker != null) && (mInPortA2dpSink != null)) { + if((mAudioConfigs == null)||(!mAudioConfigs.containsKey(mPlayingDevice))) { + log(" AudioConfigs not yet received, returning"); + return; + } int sampleRate = getAudioConfig(mPlayingDevice).getSampleRate(); int channelMask = getAudioConfig(mPlayingDevice).getChannelConfig(); int format = getAudioConfig(mPlayingDevice).getAudioFormat(); diff --git a/src/com/android/bluetooth/avrcp/AvrcpControllerService.java b/src/com/android/bluetooth/avrcp/AvrcpControllerService.java index ed426ecef..e880fdfa9 100644 --- a/src/com/android/bluetooth/avrcp/AvrcpControllerService.java +++ b/src/com/android/bluetooth/avrcp/AvrcpControllerService.java @@ -1,4 +1,6 @@ /* + * Copyright (c) 2015, The Linux Foundation. All rights reserved. + * Not a Contribution * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,23 +20,35 @@ package com.android.bluetooth.avrcp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAvrcpController; +import android.bluetooth.BluetoothAvrcpInfo; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.IBluetoothAvrcpController; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.content.ContentValues; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import android.util.Log; +import android.media.AudioManager; +import android.net.Uri; +import android.database.Cursor; +import com.android.bluetooth.avrcp.Avrcp.Metadata; +import com.android.bluetooth.a2dp.A2dpSinkService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.Utils; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.HashMap; - +import android.util.Log; +import java.nio.charset.Charset; +import java.nio.ByteBuffer; /** * Provides Bluetooth AVRCP Controller profile, as a service in the Bluetooth application. * @hide @@ -43,10 +57,279 @@ public class AvrcpControllerService extends ProfileService { private static final boolean DBG = false; private static final String TAG = "AvrcpControllerService"; + //private Context mContext; +/* + * Messages handled by mHandler + */ private static final int MESSAGE_SEND_PASS_THROUGH_CMD = 1; + private static final int MESSAGE_GET_SUPPORTED_COMPANY_ID = 2; + private static final int MESSAGE_GET_SUPPORTED_EVENTS = 3; + private static final int MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_ATTRIB = 4; + private static final int MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_VALUES = 5; + private static final int MESSAGE_GET_CURRENT_PLAYER_APPLICATION_SETTINGS = 6; + private static final int MESSAGE_SET_CURRENT_PLAYER_APPLICATION_SETTINGS = 7; + private static final int MESSAGE_GET_ELEMENT_ATTRIBUTE = 8; + private static final int MESSAGE_GET_PLAY_STATUS = 9; + private static final int MESSAGE_DEINIT_AVRCP_DATABASE = 10; + private static final int MESSAGE_REGISTER_NOTIFICATION = 11; + + private static final int MESSAGE_CMD_TIMEOUT = 100; + /* Timeout Defined as per Spec */ + private static final int TIMEOUT_MTP = 1000; + private static final int TIMEOUT_MTC = 200; + private static final int TIMEOUT_RCP = 100; + /* we add 1000ms extra */ + private static final int MSG_TIMEOUT_MTP = 1000 + TIMEOUT_MTP; + private static final int MSG_TIMEOUT_MTC = 1000 + TIMEOUT_MTC; + private static final int MSG_TIMEOUT_RCP = 1000 + TIMEOUT_RCP; + private static final int GET_PLAY_STATUS_INTERVAL = 5000; // every 5sec + + + private static final int MESSAGE_PROCESS_SUPPORTED_COMPANY_ID = 1002; + private static final int MESSAGE_PROCESS_SUPPORTED_EVENTS = 1003; + private static final int MESSAGE_PROCESS_PLAYER_APPLICATION_SETTINGS_ATTRIB = 1004; + private static final int MESSAGE_PROCESS_PLAYER_APPLICATION_SETTINGS_VALUES = 1005; + private static final int MESSAGE_PROCESS_CURRENT_PLAYER_APPLICATION_SETTINGS = 1006; + private static final int MESSAGE_PROCESS_ELEMENT_ATTRIBUTE = 1008; + private static final int MESSAGE_PROCESS_PLAY_STATUS = 1009; + private static final int MESSAGE_REGISTER_PLAYBACK_STATUS_CHANGED = 1010; + private static final int MESSAGE_REGISTER_TRACK_CHANGED = 1011; + private static final int MESSAGE_REGISTER_PLAYBACK_POS_CHANGED = 1012; + private static final int MESSAGE_REGISTER_PLAYER_APPLICATION_SETTINGS_CHANGED = 1013; + private static final int MESSAGE_REGISTER_PLAYER_APPLICATION_VOLUME_CHANGED = 1014; + private static final int MESSAGE_PROCESS_NOTIFICATION_RESPONSE = 1015; + private static final int MESSAGE_PROCESS_SET_ABS_VOL_CMD = 1016; + private static final int MESSAGE_PROCESS_REGISTER_ABS_VOL_REQUEST = 1017; + + private static final int MESSAGE_PROCESS_RC_FEATURES = 1100; + private static final int MESSAGE_PROCESS_CONNECTION_CHANGE = 1200; + private static final int ABORT_FETCH_ELEMENT_ATTRIBUTE = 4000 + MESSAGE_GET_ELEMENT_ATTRIBUTE; + + +/* + * Capability IDs for GetCapabilities + */ + private static final int COMPANY_ID = 2; + private static final int EVENTS_SUPPORTED = 3; + + private static final int NOTIFICATION_RSP_TYPE_INTERIM = 0x0f; + private static final int NOTIFICATION_RSP_TYPE_CHANGED = 0x0d; + + private static final int PLAYBACK_POS_INTERVAL = 1; // time in seconds +/* + * Constants for Events Supported + */ + private static final byte EVENT_NOTIFICAION_ID_NONE = 0x00; + private static final byte EVENT_PLAYBACK_STATUS_CHANGED = 0x01; + private static final byte EVENT_TRACK_CHANGED = 0x02; + private static final byte EVENT_PLAYBACK_POS_CHANGED = 0x05; + private static final byte EVENT_PLAYER_APPLICATION_SETTINGS_CHANGED = 0x08; + private static final byte EVENT_VOLUME_CHANGED = 0x0d; +/* + * Timeout values for Events Supported + */ + private static final int MESSAGE_TIMEOUT_PLAYBACK_STATUS_CHANGED = 2001; + private static final int MESSAGE_TIMEOUT_TRACK_CHANGED = 2002; + private static final int MESSAGE_TIMEOUT_PLAYBACK_POS_CHNAGED = 2005; + private static final int MESSAGE_TIMEOUT_APPL_SETTINGS_CHANGED = 2008; + private static final int MESSAGE_TIMEOUT_VOLUME_CHANGED = 2013; + + + private static final byte ATTRIB_EQUALIZER_STATUS = 0x01; + private static final byte ATTRIB_REPEAT_STATUS = 0x02; + private static final byte ATTRIB_SHUFFLE_STATUS = 0x03; + private static final byte ATTRIB_SCAN_STATUS = 0x04; + +/* + * EQUALIZER State Values + */ + private static final byte EQUALIZER_STATUS_OFF = 0x01; + private static final byte EQUALIZER_STATUS_ON = 0x02; + +/* + * REPEAT State Values + */ + private static final byte REPEAT_STATUS_OFF = 0x01; + private static final byte REPEAT_STATUS_SINGLE_TRACK_REPEAT = 0x02; + private static final byte REPEAT_STATUS_ALL_TRACK_REPEAT = 0x03; + private static final byte REPEAT_STATUS_GROUP_REPEAT = 0x04; +/* + * SHUFFLE State Values + */ + private static final byte SHUFFLE_STATUS_OFF = 0x01; + private static final byte SHUFFLE_STATUS_ALL_TRACK_SHUFFLE = 0x02; + private static final byte SHUFFLE_STATUS_GROUP_SHUFFLE = 0x03; + +/* + * Scan State Values + */ + private static final byte SCAN_STATUS_OFF = 0x01; + private static final byte SCAN_STATUS_ALL_TRACK_SCAN = 0x02; + private static final byte SCAN_STATUS_GROUP_SCAN = 0x03; + +/* + * Play State Values + */ + private static final byte PLAY_STATUS_STOPPED = 0x00; + private static final byte PLAY_STATUS_PLAYING = 0x01; + private static final byte PLAY_STATUS_PAUSED = 0x02; + private static final byte PLAY_STATUS_FWD_SEEK = 0x03; + private static final byte PLAY_STATUS_REV_SEEK = 0x04; +/* + * values possible for notify_state + */ + private static final byte NOTIFY_NOT_NOTIFIED = 0x01; + private static final byte NOTIFY_INTERIM_EXPECTED = 0x02; + private static final byte NOTIFY_CHANGED_EXPECTED = 0x03; + /* + * For Absolute volume we are in a TG role. + * Where we will receive notification and we have + * to send an interim/Chnaged response. So keeping + * those states seperate. + * Also we shld not send Chnaged response if change + * in volume is because of setabsvol command send from + * remote. + */ + private static final byte NOTIFY_NOT_REGISTERED = 0x01; + private static final byte NOTIFY_RSP_INTERIM_SENT = 0x02; + private static final byte NOTIFY_RSP_ABS_VOL_DEFERRED = 0x03; + +/* + * Constants for GetElement Attribute + */ + private static final int MEDIA_ATTRIBUTE_ALL = 0x00; + private static final int MEDIA_ATTRIBUTE_TITLE = 0x01; + private static final int MEDIA_ATTRIBUTE_ARTIST_NAME = 0x02; + private static final int MEDIA_ATTRIBUTE_ALBUM_NAME = 0x03; + private static final int MEDIA_ATTRIBUTE_TRACK_NUMBER = 0x04; + private static final int MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER = 0x05; + private static final int MEDIA_ATTRIBUTE_GENRE = 0x06; + private static final int MEDIA_ATTRIBUTE_PLAYING_TIME = 0x07; + + private static final int MEDIA_PLAYSTATUS_ALL = 0x08; + private static final int MEDIA_PLAYSTATUS_SONG_TOTAL_LEN = 0x09; + private static final int MEDIA_PLAYSTATUS_SONG_CUR_POS = 0x0a; + private static final int MEDIA_PLAYSTATUS_SONG_PLAY_STATUS = 0x0b; + private static final int MEDIA_PLAYER_APPLICAITON_SETTING = 0x0c; + + /* + * Timeout values for GetElement Attribute + */ + private static final int GET_ELEMENT_ATTR_TIMEOUT_BASE = 3000; + private static final int MESSAGE_TIMEOUT_ATTRIBUTE_TITLE = + GET_ELEMENT_ATTR_TIMEOUT_BASE + MEDIA_ATTRIBUTE_TITLE; + private static final int MESSAGE_TIMEOUT_ATTRIBUTE_ARTIST_NAME = + GET_ELEMENT_ATTR_TIMEOUT_BASE + MEDIA_ATTRIBUTE_ARTIST_NAME; + private static final int MESSAGE_TIMEOUT_ATTRIBUTE_ALBUM_NAME = + GET_ELEMENT_ATTR_TIMEOUT_BASE + MEDIA_ATTRIBUTE_ALBUM_NAME; + private static final int MESSAGE_TIMEOUT_ATTRIBUTE_TRACK_NUMBER = + GET_ELEMENT_ATTR_TIMEOUT_BASE + MEDIA_ATTRIBUTE_TRACK_NUMBER; + private static final int MESSAGE_TIMEOUT_ATTRIBUTE_TOTAL_TRACK_NUMBER = + GET_ELEMENT_ATTR_TIMEOUT_BASE + MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER; + private static final int MESSAGE_TIMEOUT_ATTRIBUTE_GENRE = + GET_ELEMENT_ATTR_TIMEOUT_BASE + MEDIA_ATTRIBUTE_GENRE; + private static final int MESSAGE_TIMEOUT_ATTRIBUTE_PLAYING_TIME = + GET_ELEMENT_ATTR_TIMEOUT_BASE + MEDIA_ATTRIBUTE_PLAYING_TIME; + + private static final int ATTRIBUTE_FETCH_CONTINUE = 0; + private static final int ATTRIBUTE_FETCH_FRESH = 1; // discard all if we are already fetching + private static final int ATTRIBUTE_FETCH_SKIP = 3;// discard only current one + + /* AVRCP Rsp Status */ + private static final int AVRC_RSP_NOT_IMPL = 8; + private static final int AVRC_RSP_ACCEPT = 9; + private static final int AVRC_RSP_REJ = 10; + private static final int AVRC_RSP_IN_TRANS = 11; + private static final int AVRC_RSP_IMPL_STBL = 12; + private static final int AVRC_RSP_CHANGED = 13; + private static final int AVRC_RSP_INTERIM = 15; + + /* + * AVRCP Key event + */ + public static final int AVRC_ID_PLAY = 0x44; + public static final int AVRC_ID_PAUSE = 0x46; + public static final int AVRC_ID_STOP = 0x45; + public static final int AVRC_ID_FF = 0x49; + public static final int AVRC_ID_REWIND = 0x48; + public static final int AVRC_ID_FORWARD = 0x4B; + public static final int AVRC_ID_BACKWARD = 0x4C; + public static final int AVRC_ID_VOL_UP = 0x41; + public static final int AVRC_ID_VOL_DOWN = 0x42; + + public static final int ABS_VOL_BASE = 127; +/* + * Constant Variables Defined + */ + private final int BTSIG_COMPANY_ID = 0x001958; + + public static final int BTRC_FEAT_METADATA = 0x01; + public static final int BTRC_FEAT_ABSOLUTE_VOLUME = 0x02; + public static final int BTRC_FEAT_BROWSE = 0x04; + + private class PlayerSettings + { + public byte attr_Id; + public byte attr_val; + public byte [] supported_values; + }; + private class NotifyEvents + { + public byte notify_event_id; // these values will be one of Supported Events + public byte notify_state; // Current State of Notification + }; + private class Metadata { + private String artist; + private String trackTitle; + private String albumTitle; + private String genre; + private long trackNum; + private long totalTrackNum; + private byte playStatus; + private long playTime; + private long totalTrackLen; + private int attributesFetchedId; + + public Metadata() { + resetMetaData(); + } + public void resetMetaData() { + artist = BluetoothAvrcpInfo.ARTIST_NAME_INVALID; + trackTitle = BluetoothAvrcpInfo.TITLE_INVALID; + albumTitle = BluetoothAvrcpInfo.ALBUM_NAME_INVALID; + genre = BluetoothAvrcpInfo.GENRE_INVALID; + trackNum = BluetoothAvrcpInfo.TRACK_NUM_INVALID; + totalTrackNum = BluetoothAvrcpInfo.TOTAL_TRACKS_INVALID; + playStatus = PLAY_STATUS_STOPPED; + playTime = BluetoothAvrcpInfo.PLAYING_TIME_INVALID; + totalTrackLen = BluetoothAvrcpInfo.TOTAL_TRACK_TIME_INVALID; + attributesFetchedId = -1; // id of the attribute being fetched, initialized by -1 + } + public String toString() { + return "Metadata [artist=" + artist + " trackTitle= " + trackTitle + " albumTitle= " + + albumTitle + " genre= " +genre+" trackNum= "+Long.toString(trackNum) + " cur_time: "+ + Long.toString(playTime) + " total_time = "+ Long.toString(totalTrackLen) + "]"; + } + }; + + private final class RemoteAvrcpData { + private ArrayList <Integer> mCompanyIDSupported; // company ID + private ArrayList <Byte> mEventsSupported; + private ArrayList <PlayerSettings> mSupportedApplicationSettingsAttribute; + private ArrayList <NotifyEvents> mNotifyEvent; + private Metadata mMetadata; + int mRemoteFeatures; + int absVolNotificationState; + int playerSettingAttribIdFetch; + }; + + RemoteAvrcpData mRemoteData; + int[] requestedElementAttribs; private AvrcpMessageHandler mHandler; private static AvrcpControllerService sAvrcpControllerService; + private static AudioManager mAudioManager; + private static boolean mDbInitialized = false; private final ArrayList<BluetoothDevice> mConnectedDevices = new ArrayList<BluetoothDevice>(); @@ -64,6 +347,7 @@ public class AvrcpControllerService extends ProfileService { } protected IProfileServiceBinder initBinder() { + Log.d(TAG," initBinder Called "); return new BluetoothAvrcpControllerBinder(this); } @@ -72,25 +356,83 @@ public class AvrcpControllerService extends ProfileService { thread.start(); Looper looper = thread.getLooper(); mHandler = new AvrcpMessageHandler(looper); - + mRemoteData = null; setAvrcpControllerService(this); + mAudioManager = (AudioManager)sAvrcpControllerService. + getSystemService(Context.AUDIO_SERVICE); + IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION); + registerReceiver(mBroadcastReceiver, filter); return true; } protected boolean stop() { + unregisterReceiver(mBroadcastReceiver); + try { + deinitDatabase(); + if (mRemoteData != null) { + mRemoteData.mCompanyIDSupported.clear(); + mRemoteData.mEventsSupported.clear(); + mRemoteData.mMetadata.resetMetaData(); + mRemoteData.mNotifyEvent.clear(); + mRemoteData.mSupportedApplicationSettingsAttribute.clear(); + mRemoteData.absVolNotificationState = NOTIFY_NOT_REGISTERED; + mRemoteData.mRemoteFeatures = 0; + Log.d(TAG," RC_features, STOP " + mRemoteData.mRemoteFeatures); + mRemoteData.playerSettingAttribIdFetch = 0; + mRemoteData = null; + } + } catch (Exception e) { + Log.e(TAG, "Cleanup failed", e); + } return true; } - + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) { + int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); + if (streamType == AudioManager.STREAM_MUSIC) { + int streamValue = intent + .getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1); + int streamPrevValue = intent.getIntExtra( + AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, -1); + if (streamValue != -1 && streamValue != streamPrevValue) { + if ((mRemoteData == null) + ||((mRemoteData.mRemoteFeatures & BTRC_FEAT_ABSOLUTE_VOLUME) == 0) + ||(mConnectedDevices.isEmpty())) + return; + if(mRemoteData.absVolNotificationState == NOTIFY_RSP_INTERIM_SENT) { + int maxVol = mAudioManager. + getStreamMaxVolume(AudioManager.STREAM_MUSIC); + int currIndex = mAudioManager. + getStreamVolume(AudioManager.STREAM_MUSIC); + int percentageVol = ((currIndex*ABS_VOL_BASE)/maxVol); + byte rspType = NOTIFICATION_RSP_TYPE_CHANGED; + Log.d(TAG," Abs Vol Notify Rsp Changed val = "+ percentageVol); + mRemoteData.absVolNotificationState = NOTIFY_NOT_REGISTERED; + sendRegisterAbsVolRspNative(rspType,percentageVol); + } + else if (mRemoteData.absVolNotificationState == NOTIFY_RSP_ABS_VOL_DEFERRED) { + Log.d(TAG," Don't Complete Notification Rsp. "); + mRemoteData.absVolNotificationState = NOTIFY_RSP_INTERIM_SENT; + } + } + } + } + } + }; protected boolean cleanup() { if (mHandler != null) { mHandler.removeCallbacksAndMessages(null); Looper looper = mHandler.getLooper(); if (looper != null) looper.quit(); + if (looper != null) { + looper.quit(); + } } - clearAvrcpControllerService(); cleanupNative(); - return true; } @@ -158,12 +500,351 @@ public class AvrcpControllerService extends ProfileService { if (device == null) { throw new NullPointerException("device == null"); } - enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - Message msg = mHandler.obtainMessage(MESSAGE_SEND_PASS_THROUGH_CMD, + if (!(mConnectedDevices.contains(device))) { + Log.d(TAG," Device does not match"); + return; + } + if ((mRemoteData == null)||(mRemoteData.mMetadata == null)) { + Log.d(TAG," Device connected but PlayState not present "); + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + Message msg = mHandler.obtainMessage(MESSAGE_SEND_PASS_THROUGH_CMD, + keyCode, keyState, device); + mHandler.sendMessage(msg); + return; + } + boolean sendCommand = false; + switch(keyCode) { + case AVRC_ID_PLAY: + sendCommand = (mRemoteData.mMetadata.playStatus == PLAY_STATUS_STOPPED)|| + (mRemoteData.mMetadata.playStatus == PLAY_STATUS_PAUSED); + break; + case AVRC_ID_PAUSE: + sendCommand = (mRemoteData.mMetadata.playStatus == PLAY_STATUS_PLAYING)|| + (mRemoteData.mMetadata.playStatus == PLAY_STATUS_FWD_SEEK)|| + (mRemoteData.mMetadata.playStatus == PLAY_STATUS_STOPPED)|| + (mRemoteData.mMetadata.playStatus == PLAY_STATUS_REV_SEEK); + break; + case AVRC_ID_STOP: + sendCommand = (mRemoteData.mMetadata.playStatus == PLAY_STATUS_PLAYING)|| + (mRemoteData.mMetadata.playStatus == PLAY_STATUS_FWD_SEEK)|| + (mRemoteData.mMetadata.playStatus == PLAY_STATUS_REV_SEEK)|| + (mRemoteData.mMetadata.playStatus == PLAY_STATUS_STOPPED)|| + (mRemoteData.mMetadata.playStatus == PLAY_STATUS_PAUSED); + break; + case AVRC_ID_VOL_DOWN: + case AVRC_ID_VOL_UP: + case AVRC_ID_BACKWARD: + case AVRC_ID_FORWARD: + case AVRC_ID_FF: + case AVRC_ID_REWIND: + sendCommand = true; // we can send this command in all states + break; + } + if (sendCommand) { + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + Message msg = mHandler.obtainMessage(MESSAGE_SEND_PASS_THROUGH_CMD, keyCode, keyState, device); + mHandler.sendMessage(msg); + } + else { + Log.e(TAG," Not in right state, don't send Pass Thru cmd "); + } + } + public void getMetaData(int[] attributeIds) { + Log.d(TAG, "num getMetaData = "+ attributeIds.length); + if (mRemoteData == null) { + return; + } + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + /* + * Check if GetElementAttribute is in progress + * Check if it already does not contain all Attributes + * If it is, append any missing attribute + */ + if ((mRemoteData.mMetadata.attributesFetchedId != -1) && + (requestedElementAttribs != null)&& + (requestedElementAttribs.length < 7)) { + + int currAttributeId = + requestedElementAttribs[mRemoteData.mMetadata.attributesFetchedId]; + if (mHandler.hasMessages(GET_ELEMENT_ATTR_TIMEOUT_BASE + currAttributeId)) { + mHandler.removeMessages(GET_ELEMENT_ATTR_TIMEOUT_BASE + currAttributeId); + Log.d(TAG," Timeout CMD dequeued ID " + currAttributeId); + } + mHandler.sendEmptyMessage(ABORT_FETCH_ELEMENT_ATTRIBUTE); + requestedElementAttribs = Arrays.copyOf(attributeIds, attributeIds.length); + return; + } + else if ((attributeIds.length >= 0)&&(attributeIds[0] != MEDIA_ATTRIBUTE_ALL)) { + /* Subset sent by App, use them */ + requestedElementAttribs = Arrays.copyOf(attributeIds, attributeIds.length); + } + else { + /* Use SuperSet */ + requestedElementAttribs = new int [7]; + for (int xx = 0; xx < 7; xx++) + requestedElementAttribs[xx] = xx+1; + } + Arrays.sort(requestedElementAttribs); + boolean mMetaDataPresent = true; + for (int attributeId: requestedElementAttribs) { + if (!isMetaDataPresent(attributeId)) { + mMetaDataPresent = false; + break; + } + } + Log.d(TAG," MetaDataPresent " + mMetaDataPresent); + if(mMetaDataPresent) + triggerNotification(); + else + mHandler.sendEmptyMessage(MESSAGE_GET_ELEMENT_ATTRIBUTE); + } + public void getPlayStatus(int[] playStatusIds) { + if (DBG) Log.d(TAG, "num getPlayStatus ID = "+ playStatusIds.length); + if (mRemoteData == null) { + return; + } + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + int[] getApRequestedPlayStatusAttrib; + if ((playStatusIds.length >= 0)&&(playStatusIds[0] != MEDIA_PLAYSTATUS_ALL)) { + /* Subset sent by App, use them */ + getApRequestedPlayStatusAttrib = Arrays.copyOf(playStatusIds, playStatusIds.length); + } + else { + /* Use SuperSet */ + getApRequestedPlayStatusAttrib = new int [3]; + for (int xx = 0; xx < 3; xx++) + getApRequestedPlayStatusAttrib[xx] = 9 + xx; + } + Arrays.sort(getApRequestedPlayStatusAttrib); + boolean mMetaDataPresent = true; + for (int attributeId: getApRequestedPlayStatusAttrib) { + if (!isMetaDataPresent(attributeId)) { + mMetaDataPresent = false; + break; + } + } + if(mMetaDataPresent) + triggerNotification(); + else { + Log.d(TAG," Metadata not present"); + mHandler.sendEmptyMessage(MESSAGE_GET_PLAY_STATUS); + } + } + public void getPlayerApplicationSetting() { + if (DBG) Log.d(TAG, "getPlayerApplicationSetting "); + if (mRemoteData == null) { + return; + } + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + if(isMetaDataPresent(MEDIA_PLAYER_APPLICAITON_SETTING)) { + triggerNotification(); + } + else { + Log.d(TAG," Metadata not present, fetch it"); + mHandler.sendEmptyMessage(MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_ATTRIB); + } + } + public void setPlayerApplicationSetting(int attributeId, int attributeVal) { + if (mRemoteData == null) { + return; + } + enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + Message msg = mHandler.obtainMessage(MESSAGE_SET_CURRENT_PLAYER_APPLICATION_SETTINGS, + attributeId, attributeVal); mHandler.sendMessage(msg); } + public BluetoothAvrcpInfo getSupportedPlayerAppSetting(BluetoothDevice device) { + if((mRemoteData == null)||(mRemoteData.mSupportedApplicationSettingsAttribute.isEmpty()) + ||(!mConnectedDevices.contains(device))) + return null; + byte[] attribIds = new byte[mRemoteData.mSupportedApplicationSettingsAttribute.size()]; + byte[] numAttribVals = new byte[mRemoteData.mSupportedApplicationSettingsAttribute.size()]; + ArrayList<Byte> supportedVals = new ArrayList<Byte>(); + int index = 0; + for (PlayerSettings plSetting: mRemoteData.mSupportedApplicationSettingsAttribute) { + attribIds[index] = plSetting.attr_Id; + numAttribVals[index] = Integer.valueOf(plSetting.supported_values.length).byteValue(); + for (int xx = 0; xx < numAttribVals[index]; xx++) + supportedVals.add(plSetting.supported_values[xx]); + index++; + } + byte[] supportedPlSettingsVals = new byte[supportedVals.size()]; + for (int zz = 0; zz < supportedVals.size(); zz++) + supportedPlSettingsVals[zz] = supportedVals.get(zz); + if ((attribIds == null)||(numAttribVals == null)||(supportedPlSettingsVals == null)|| + (attribIds.length == 0)||(numAttribVals.length == 0)) + return null; + BluetoothAvrcpInfo btAvrcpMetaData = new BluetoothAvrcpInfo(attribIds, + numAttribVals, supportedPlSettingsVals); + return btAvrcpMetaData; + } + public int getSupportedFeatures(BluetoothDevice device) { + if (!mConnectedDevices.contains(device)||(mRemoteData == null)) { + Log.e(TAG," req Device " + device + " Internal List " + mConnectedDevices.get(0)); + Log.e(TAG," remoteData " + mRemoteData); + if (mRemoteData != null) + Log.e(TAG," getSupportedFeatures returning from here "+ mRemoteData.mRemoteFeatures); + return 0; + } + Log.d(TAG," getSupportedFeatures returning " + mRemoteData.mRemoteFeatures); + return mRemoteData.mRemoteFeatures; + } + private void triggerNotification() { + Uri avrcpDataUri = BluetoothAvrcpInfo.CONTENT_URI; + sAvrcpControllerService.getContentResolver().notifyChange(avrcpDataUri, null); + } + private boolean isMetaDataPresent(int attributeId) { + Uri avrcpDataUri = BluetoothAvrcpInfo.CONTENT_URI; + Cursor cursor = sAvrcpControllerService.getContentResolver().query(avrcpDataUri, + null, null, null,BluetoothAvrcpInfo._ID); + if ((cursor == null) || (!cursor.moveToFirst())) { + Log.d(TAG," isMetaDataPresent cursor not valid, returing"); + return false; + } + int index = 0; + boolean metaDataPresent = false; + switch(attributeId) + { + case MEDIA_ATTRIBUTE_ALBUM_NAME: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.ALBUM_NAME); + if(cursor.getString(index) != + BluetoothAvrcpInfo.ALBUM_NAME_INVALID) + metaDataPresent = true; + break; + case MEDIA_ATTRIBUTE_ARTIST_NAME: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.ARTIST_NAME); + if(cursor.getString(index) != + BluetoothAvrcpInfo.ARTIST_NAME_INVALID) + metaDataPresent = true; + break; + case MEDIA_ATTRIBUTE_GENRE: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.GENRE); + if(cursor.getString(index) != + BluetoothAvrcpInfo.GENRE_INVALID) + metaDataPresent = true; + break; + case MEDIA_ATTRIBUTE_TITLE: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.TITLE); + if(cursor.getString(index) != + BluetoothAvrcpInfo.TITLE_INVALID) + metaDataPresent = true; + break; + case MEDIA_PLAYSTATUS_SONG_CUR_POS: + case MEDIA_ATTRIBUTE_PLAYING_TIME: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.PLAYING_TIME); + if(cursor.getInt(index) != + BluetoothAvrcpInfo.PLAYING_TIME_INVALID) + metaDataPresent = true; + break; + case MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.TOTAL_TRACKS); + if(cursor.getInt(index) != + BluetoothAvrcpInfo.TOTAL_TRACKS_INVALID) + metaDataPresent = true; + break; + case MEDIA_ATTRIBUTE_TRACK_NUMBER: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.TRACK_NUM); + if(cursor.getInt(index) != + BluetoothAvrcpInfo.TRACK_NUM_INVALID) + metaDataPresent = true; + break; + case MEDIA_ATTRIBUTE_ALL: // check all attribute + index = cursor.getColumnIndex(BluetoothAvrcpInfo.TRACK_NUM); + if(cursor.getInt(index) == + BluetoothAvrcpInfo.TRACK_NUM_INVALID) + break; + index = cursor.getColumnIndex(BluetoothAvrcpInfo.TOTAL_TRACKS); + if(cursor.getInt(index) == + BluetoothAvrcpInfo.TOTAL_TRACKS_INVALID) + break; + index = cursor.getColumnIndex(BluetoothAvrcpInfo.PLAYING_TIME); + if(cursor.getInt(index) == + BluetoothAvrcpInfo.PLAYING_TIME_INVALID) + break; + index = cursor.getColumnIndex(BluetoothAvrcpInfo.TITLE); + if(cursor.getString(index) == + BluetoothAvrcpInfo.TITLE_INVALID) + break; + index = cursor.getColumnIndex(BluetoothAvrcpInfo.GENRE); + if(cursor.getString(index) == + BluetoothAvrcpInfo.GENRE_INVALID) + break; + index = cursor.getColumnIndex(BluetoothAvrcpInfo.ARTIST_NAME); + if(cursor.getString(index) == + BluetoothAvrcpInfo.ARTIST_NAME_INVALID) + break; + index = cursor.getColumnIndex(BluetoothAvrcpInfo.ALBUM_NAME); + if(cursor.getString(index) == + BluetoothAvrcpInfo.ALBUM_NAME_INVALID) + break; + metaDataPresent = true; + break; + case MEDIA_PLAYSTATUS_SONG_PLAY_STATUS: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.PLAY_STATUS); + if(cursor.getString(index) != + BluetoothAvrcpInfo.PLAY_STATUS_INVALID) + metaDataPresent = true; + break; + case MEDIA_PLAYSTATUS_SONG_TOTAL_LEN: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.TOTAL_TRACK_TIME); + if(cursor.getInt(index) != + BluetoothAvrcpInfo.TOTAL_TRACK_TIME_INVALID) + metaDataPresent = true; + break; + case MEDIA_PLAYSTATUS_ALL: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.PLAY_STATUS); + if(cursor.getString(index) == + BluetoothAvrcpInfo.PLAY_STATUS_INVALID) + break; + index = cursor.getColumnIndex(BluetoothAvrcpInfo.TOTAL_TRACK_TIME); + if(cursor.getInt(index) == + BluetoothAvrcpInfo.TOTAL_TRACK_TIME_INVALID) + break; + + metaDataPresent = true; + break; + case MEDIA_PLAYER_APPLICAITON_SETTING: + boolean plSettingSupported = true; + for (PlayerSettings plSetting: mRemoteData.mSupportedApplicationSettingsAttribute) { + switch(plSetting.attr_Id) + { + case ATTRIB_REPEAT_STATUS: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.REPEAT_STATUS); + if(cursor.getString(index) == + BluetoothAvrcpInfo.REPEAT_STATUS_INVALID) + plSettingSupported = false; + break; + case ATTRIB_EQUALIZER_STATUS: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.EQUALIZER_STATUS); + if(cursor.getString(index) == + BluetoothAvrcpInfo.EQUALIZER_STATUS_INVALID) + plSettingSupported = false; + break; + case ATTRIB_SCAN_STATUS: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.SCAN_STATUS); + if(cursor.getString(index) == + BluetoothAvrcpInfo.SCAN_STATUS_INVALID) + plSettingSupported = false; + break; + case ATTRIB_SHUFFLE_STATUS: + index = cursor.getColumnIndex(BluetoothAvrcpInfo.SHUFFLE_STATUS); + if(cursor.getString(index) == + BluetoothAvrcpInfo.SHUFFLE_STATUS_INVALID) + plSettingSupported = false; + break; + } + if(!plSettingSupported) + break; + } + metaDataPresent = plSettingSupported; + break; + } + cursor.close(); + Log.d(TAG," returning " + metaDataPresent + "for attrib " + attributeId); + return metaDataPresent; + } //Binder object: Must be static class or memory leak may occur private static class BluetoothAvrcpControllerBinder extends IBluetoothAvrcpController.Stub implements IProfileServiceBinder { @@ -214,8 +895,733 @@ public class AvrcpControllerService extends ProfileService { if (service == null) return; service.sendPassThroughCmd(device, keyCode, keyState); } + + public void getMetaData(int[] attributeIds) { + Log.v(TAG,"Binder Call: num getMetaData ID = "+ attributeIds.length); + AvrcpControllerService service = getService(); + if (service == null) return; + service.getMetaData(attributeIds); + } + public void getPlayStatus(int[] playStatusIds) { + Log.v(TAG,"Binder Call: num getPlayStatus ID = "+ playStatusIds.length); + AvrcpControllerService service = getService(); + if (service == null) return; + service.getPlayStatus(playStatusIds); + } + public void getPlayerApplicationSetting() { + Log.v(TAG,"Binder Call: getPlayerApplicationSetting "); + AvrcpControllerService service = getService(); + if (service == null) return; + service.getPlayerApplicationSetting(); + } + public void setPlayerApplicationSetting(int attributeId, int attributeVal) { + Log.v(TAG,"Binder Call: setPlayerApplicationSetting ID = " + + attributeId +" attributeVal ="+ attributeVal); + AvrcpControllerService service = getService(); + if (service == null) return; + service.setPlayerApplicationSetting(attributeId, attributeVal); + } + public BluetoothAvrcpInfo getSupportedPlayerAppSetting(BluetoothDevice device) { + Log.v(TAG,"Binder Call: getSupportedPlayerAppSetting Dev = " + device); + AvrcpControllerService service = getService(); + if (service == null) return null; + return service.getSupportedPlayerAppSetting(device); + } + public int getSupportedFeatures(BluetoothDevice device) { + Log.v(TAG,"Binder Call: getSupportedFeatures Dev = " + device); + AvrcpControllerService service = getService(); + if (service == null) return 0; + return service.getSupportedFeatures(device); + } }; + private void getSupportedCapabilities(int capability_id) + { + Message msg = mHandler.obtainMessage(MESSAGE_CMD_TIMEOUT,0,0,capability_id); + mHandler.sendMessageDelayed(msg, MSG_TIMEOUT_MTP); + getCapabilitiesNative(capability_id); + } + private void getPlayerApplicationSettingsAttrib() + { + Message msg = mHandler.obtainMessage(MESSAGE_CMD_TIMEOUT, + 0,0,MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_ATTRIB); + mHandler.sendMessageDelayed(msg, MSG_TIMEOUT_MTP); + listPlayerApplicationSettingAttributeNative(); + } + private void getCurrentPlayerApplicationSettingsValues() + { + int count = 0; + if ((mRemoteData == null)|| + (mRemoteData.mSupportedApplicationSettingsAttribute == null)|| + (mRemoteData.mSupportedApplicationSettingsAttribute.size() == 0)) { + Log.w(TAG," PlayerAppSettings not supporterd, returning"); + return; + } + byte[] supported_attrib = + new byte[mRemoteData.mSupportedApplicationSettingsAttribute.size()]; + byte numAttrib = Byte.valueOf((Integer.valueOf + (mRemoteData.mSupportedApplicationSettingsAttribute.size())).byteValue()); + for (PlayerSettings plSetting: mRemoteData.mSupportedApplicationSettingsAttribute) + { + supported_attrib[count++] = plSetting.attr_Id; + } + Message msg = mHandler.obtainMessage(MESSAGE_CMD_TIMEOUT, + 0,0,MESSAGE_GET_CURRENT_PLAYER_APPLICATION_SETTINGS); + mHandler.sendMessageDelayed(msg, MSG_TIMEOUT_MTP); + getPlayerApplicationSettingValuesNative(numAttrib,supported_attrib); + } + private void setCurrentPlayerApplicationSettingsValues(int attribId, int attribVal) + { + Message msg = mHandler.obtainMessage(MESSAGE_CMD_TIMEOUT, + 0,0,MESSAGE_SET_CURRENT_PLAYER_APPLICATION_SETTINGS); + mHandler.sendMessageDelayed(msg, MSG_TIMEOUT_MTP); + byte numAttrib = 1; + byte[] attributeId = new byte[1]; + byte[] attributeVal = new byte[1]; + attributeId[0] = (byte)attribId; + attributeVal[0] = (byte)attribVal; + setPlayerApplicationSettingValuesNative(numAttrib, attributeId, attributeVal); + } + private void getFurtherElementAttribute(int operationId) + { + if ((requestedElementAttribs == null)||(requestedElementAttribs.length == 0)) + { + Log.d(TAG," Applicaiton has not yet requested element attributes"); + return; + } + Log.d(TAG," getFurtherElementAttribute op_Id = " + + operationId + " requestedIdLen = " + requestedElementAttribs.length); + byte numAttrib = 1; + if (operationId == ATTRIBUTE_FETCH_FRESH) + mRemoteData.mMetadata.attributesFetchedId = 0; // reset fetched_id + else if ((operationId == ATTRIBUTE_FETCH_SKIP)||(operationId == ATTRIBUTE_FETCH_CONTINUE)) + mRemoteData.mMetadata.attributesFetchedId += 1; // skip this one, fetch next + if (mRemoteData.mMetadata.attributesFetchedId >= requestedElementAttribs.length) + { + /* + * we reached to last attribute. Update database now + */ + mRemoteData.mMetadata.attributesFetchedId = -1; + updateElementAttribute(); + mHandler.sendEmptyMessage(MESSAGE_GET_PLAY_STATUS); + return; + } + Message msg = mHandler.obtainMessage(GET_ELEMENT_ATTR_TIMEOUT_BASE + + requestedElementAttribs[mRemoteData.mMetadata.attributesFetchedId]); + mHandler.sendMessageDelayed(msg, MSG_TIMEOUT_MTP); + Log.d(TAG," getElemAttrReq numAttr "+ numAttrib + " Id " + + requestedElementAttribs[mRemoteData.mMetadata.attributesFetchedId]); + getElementAttributeNative(numAttrib, + requestedElementAttribs[mRemoteData.mMetadata.attributesFetchedId]); + } + private void getFurtherPlayerSettingAttrib(int operationId) + { + Log.d(TAG," getFurtherPlayerSettingAttrib Id = " + operationId); + if (mRemoteData == null) + return; + if (operationId == ATTRIBUTE_FETCH_FRESH) + mRemoteData.playerSettingAttribIdFetch = 0; + else if(operationId == ATTRIBUTE_FETCH_SKIP) + mRemoteData.playerSettingAttribIdFetch ++; + int fetch_id = mRemoteData.playerSettingAttribIdFetch; + if (fetch_id >= mRemoteData.mSupportedApplicationSettingsAttribute.size()) + { + Log.d(TAG," All Attrib Fetched " + fetch_id); + mHandler.sendEmptyMessage(MESSAGE_GET_CURRENT_PLAYER_APPLICATION_SETTINGS); + return; + } + Log.d(TAG," fetching_id = " + fetch_id); + Message msg = mHandler.obtainMessage(MESSAGE_CMD_TIMEOUT, + 0,0,MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_VALUES); + mHandler.sendMessageDelayed(msg, MSG_TIMEOUT_MTP); + listPlayerApplicationSettingValueNative(mRemoteData. + mSupportedApplicationSettingsAttribute.get(fetch_id).attr_Id); + } + /* + * Api to register notification. + * exemptNotificaionId = notificaiton ID for which notification will not be sent. + * in case of we have other notifications to be sent we will do nothing for + * exemptedID. In case exemptedId is the only one left we will send notification + * request for this one. + */ + private void registerFurtherNotification(int exemptNotificaionId) + { + Log.d(TAG," ExemptedNotificationId " + exemptNotificaionId); + /* + * check through the list and send notification + * request if state is not waiting for changed + */ + boolean notificationSent = false; + Message msg; + for (NotifyEvents notifyEvent: mRemoteData.mNotifyEvent) + { + Log.d(TAG," ID = " + notifyEvent.notify_event_id + " state = " + + notifyEvent.notify_state); + if (notifyEvent.notify_event_id == exemptNotificaionId) + continue; + if (notifyEvent.notify_state == NOTIFY_NOT_NOTIFIED) + { + notificationSent = true; + notifyEvent.notify_state = NOTIFY_INTERIM_EXPECTED; + Log.d(TAG," queing notificaiton request id " + + (2000+notifyEvent.notify_event_id)); + msg = mHandler.obtainMessage(2000 + notifyEvent.notify_event_id); + mHandler.sendMessageDelayed(msg, MSG_TIMEOUT_MTP); + if (notifyEvent.notify_event_id == EVENT_PLAYBACK_POS_CHANGED) + registerNotificationNative(notifyEvent.notify_event_id, PLAYBACK_POS_INTERVAL); + else + registerNotificationNative(notifyEvent.notify_event_id, 0); + break; + } + } + if ((!notificationSent) && (exemptNotificaionId != EVENT_NOTIFICAION_ID_NONE)) + { + /* + * exempted value was valid and only this was not notified. + */ + for (NotifyEvents notifyEvent: mRemoteData.mNotifyEvent) + { + if (notifyEvent.notify_event_id == exemptNotificaionId) + { + notificationSent = true; + notifyEvent.notify_state = NOTIFY_INTERIM_EXPECTED; + msg = mHandler.obtainMessage(2000 + notifyEvent.notify_event_id); + mHandler.sendMessageDelayed(msg, MSG_TIMEOUT_MTP); + if (notifyEvent.notify_event_id == EVENT_PLAYBACK_POS_CHANGED) + registerNotificationNative(notifyEvent.notify_event_id, + PLAYBACK_POS_INTERVAL); + else + registerNotificationNative(notifyEvent.notify_event_id, 0); + break; + } + } + } + } + private void initializeDatabase() + { + if (mDbInitialized) + return; + ContentValues values = new ContentValues(); + values.put(BluetoothAvrcpInfo.TRACK_NUM, BluetoothAvrcpInfo.TRACK_NUM_INVALID); + values.put(BluetoothAvrcpInfo.TITLE, BluetoothAvrcpInfo.TITLE_INVALID); + values.put(BluetoothAvrcpInfo.ARTIST_NAME, BluetoothAvrcpInfo.ARTIST_NAME_INVALID); + values.put(BluetoothAvrcpInfo.ALBUM_NAME, BluetoothAvrcpInfo.ALBUM_NAME_INVALID); + values.put(BluetoothAvrcpInfo.TOTAL_TRACKS, BluetoothAvrcpInfo.TOTAL_TRACKS_INVALID); + values.put(BluetoothAvrcpInfo.GENRE, BluetoothAvrcpInfo.GENRE_INVALID); + values.put(BluetoothAvrcpInfo.PLAYING_TIME, BluetoothAvrcpInfo.PLAYING_TIME_INVALID); + values.put(BluetoothAvrcpInfo.TOTAL_TRACK_TIME, + BluetoothAvrcpInfo.TOTAL_TRACK_TIME_INVALID); + values.put(BluetoothAvrcpInfo.PLAY_STATUS, BluetoothAvrcpInfo.PLAY_STATUS_INVALID); + values.put(BluetoothAvrcpInfo.REPEAT_STATUS, BluetoothAvrcpInfo.REPEAT_STATUS_INVALID); + values.put(BluetoothAvrcpInfo.SHUFFLE_STATUS, BluetoothAvrcpInfo.SHUFFLE_STATUS_INVALID); + values.put(BluetoothAvrcpInfo.SCAN_STATUS, BluetoothAvrcpInfo.SCAN_STATUS_INVALID); + values.put(BluetoothAvrcpInfo.EQUALIZER_STATUS, + BluetoothAvrcpInfo.EQUALIZER_STATUS_INVALID); + Cursor cursor = getContentResolver().query(BluetoothAvrcpInfo.CONTENT_URI, null, null, null, + BluetoothAvrcpInfo._ID); + if((cursor != null)&&(cursor.getCount() > 0)) { + int rowsUpdated = sAvrcpControllerService.getContentResolver(). + update(BluetoothAvrcpInfo.CONTENT_URI, values, null, null); + Log.d(TAG," initializeDataBase num_rows_updated " + rowsUpdated); + mDbInitialized = true; + cursor.close(); + return; + } + Uri contentUri = sAvrcpControllerService.getContentResolver(). + insert(BluetoothAvrcpInfo.CONTENT_URI, values); + Log.d(TAG," InitializeDatabase uri " + contentUri); + mDbInitialized = true; + } + private void deinitDatabase() + { + mDbInitialized = false; + int rows_deleted = sAvrcpControllerService.getContentResolver(). + delete(BluetoothAvrcpInfo.CONTENT_URI, null, null); + Log.d(TAG, " DeinitDatabase rows_deleted "+ rows_deleted); + } + private String getPlayStatusString(byte playStatus) + { + switch(playStatus) + { + case PLAY_STATUS_STOPPED: + return "STOPPED"; + case PLAY_STATUS_PLAYING: + return "PLAYING"; + case PLAY_STATUS_PAUSED: + return "PAUSED"; + case PLAY_STATUS_FWD_SEEK: + return "FWD_SEEK"; + case PLAY_STATUS_REV_SEEK: + return "REV_SEEK"; + } + return BluetoothAvrcpInfo.PLAY_STATUS_INVALID; + } + private String getShuffleStatusString() + { + for (PlayerSettings plSettings: mRemoteData.mSupportedApplicationSettingsAttribute) + { + if (plSettings.attr_Id == ATTRIB_SHUFFLE_STATUS) + { + switch(plSettings.attr_val) + { + case SHUFFLE_STATUS_OFF: + return "SHUFFLE_OFF"; + case SHUFFLE_STATUS_GROUP_SHUFFLE: + return "SHUFFLE_GROUP_SHUFFLE"; + case SHUFFLE_STATUS_ALL_TRACK_SHUFFLE: + return "SHUFFLE_ALL_TRACK_SHUFFLE"; + } + } + } + return BluetoothAvrcpInfo.SHUFFLE_STATUS_INVALID; + } + private String getScanStatusString() + { + for (PlayerSettings plSettings: mRemoteData.mSupportedApplicationSettingsAttribute) + { + if (plSettings.attr_Id == ATTRIB_SCAN_STATUS) + { + switch(plSettings.attr_val) + { + case SCAN_STATUS_OFF: + return "SCAN_OFF"; + case SCAN_STATUS_GROUP_SCAN: + return "SCAN_GROUP_SCAN"; + case SCAN_STATUS_ALL_TRACK_SCAN: + return "SCAN_ALL_TRACK_SCAN"; + } + } + } + return BluetoothAvrcpInfo.SCAN_STATUS_INVALID; + } + private String getEqualizerStatusString() + { + for (PlayerSettings plSettings: mRemoteData.mSupportedApplicationSettingsAttribute) + { + if (plSettings.attr_Id == ATTRIB_EQUALIZER_STATUS) + { + switch(plSettings.attr_val) + { + case EQUALIZER_STATUS_OFF: + return "EQUALIZER_OFF"; + case EQUALIZER_STATUS_ON: + return "EQUALIZER_ON"; + } + } + } + return BluetoothAvrcpInfo.EQUALIZER_STATUS_INVALID; + } + private String getRepeatStatusString() + { + for (PlayerSettings plSettings: mRemoteData.mSupportedApplicationSettingsAttribute) + { + if (plSettings.attr_Id == ATTRIB_REPEAT_STATUS) + { + switch(plSettings.attr_val) + { + case REPEAT_STATUS_OFF: + return "REPEAT_OFF"; + case REPEAT_STATUS_SINGLE_TRACK_REPEAT: + return "REPEAT_SINGLE_TRACK_REPEAT"; + case REPEAT_STATUS_GROUP_REPEAT: + return "REPEAT_GROUP_REPEAT"; + case REPEAT_STATUS_ALL_TRACK_REPEAT: + return "REPEAT_ALL_TRACK_REPEAT"; + } + } + } + return BluetoothAvrcpInfo.REPEAT_STATUS_INVALID; + } + + private void updateElementAttribute() + { + Log.d(TAG," updateElementAttribute " + mRemoteData.mMetadata.toString()); + ContentValues values = new ContentValues(); + values.put(BluetoothAvrcpInfo.TITLE, mRemoteData.mMetadata.trackTitle); + values.put(BluetoothAvrcpInfo.ARTIST_NAME, mRemoteData.mMetadata.artist); + values.put(BluetoothAvrcpInfo.ALBUM_NAME, mRemoteData.mMetadata.albumTitle); + values.put(BluetoothAvrcpInfo.TRACK_NUM, mRemoteData.mMetadata.trackNum); + values.put(BluetoothAvrcpInfo.TOTAL_TRACKS, mRemoteData.mMetadata.totalTrackNum); + values.put(BluetoothAvrcpInfo.GENRE, mRemoteData.mMetadata.genre); + values.put(BluetoothAvrcpInfo.PLAYING_TIME, mRemoteData.mMetadata.playTime); + values.put(BluetoothAvrcpInfo.TOTAL_TRACK_TIME, mRemoteData.mMetadata.totalTrackLen); + int rowsUpdated = sAvrcpControllerService.getContentResolver(). + update(BluetoothAvrcpInfo.CONTENT_URI, values, null, null); + Log.d(TAG," updateElementAttribute num_rows_updated " + rowsUpdated); + } + private void updateTrackNum() + { + ContentValues values = new ContentValues(); + values.put(BluetoothAvrcpInfo.TRACK_NUM, mRemoteData.mMetadata.trackNum); + int rowsUpdated = sAvrcpControllerService.getContentResolver(). + update(BluetoothAvrcpInfo.CONTENT_URI, values, null, null); + Log.d(TAG," updateTrackNum num_rows_updated " + rowsUpdated); + } + private void updatePlayTime() + { + ContentValues values = new ContentValues(); + values.put(BluetoothAvrcpInfo.PLAYING_TIME, mRemoteData.mMetadata.playTime); + int rowsUpdated = sAvrcpControllerService.getContentResolver(). + update(BluetoothAvrcpInfo.CONTENT_URI, values, null, null); + Log.d(TAG," updatePlayTime num_rows_updated " + rowsUpdated); + } + private void updatePlayerApplicationSettings() + { + ContentValues values = new ContentValues(); + values.put(BluetoothAvrcpInfo.REPEAT_STATUS, getRepeatStatusString()); + values.put(BluetoothAvrcpInfo.SHUFFLE_STATUS, getShuffleStatusString()); + values.put(BluetoothAvrcpInfo.SCAN_STATUS, getScanStatusString()); + values.put(BluetoothAvrcpInfo.EQUALIZER_STATUS, getEqualizerStatusString()); + int rowsUpdated = sAvrcpControllerService.getContentResolver(). + update(BluetoothAvrcpInfo.CONTENT_URI,values, null, null); + Log.d(TAG," updatePlayerApplicationSettings num_rows_updated " + rowsUpdated); + } + private void updatePlayStatus() + { + ContentValues values = new ContentValues(); + Log.d(TAG," updatePlayStatus " + mRemoteData.mMetadata.toString()); + values.put(BluetoothAvrcpInfo.PLAY_STATUS, + getPlayStatusString(mRemoteData.mMetadata.playStatus)); + values.put(BluetoothAvrcpInfo.PLAYING_TIME, mRemoteData.mMetadata.playTime); + values.put(BluetoothAvrcpInfo.TOTAL_TRACK_TIME, mRemoteData.mMetadata.totalTrackLen); + int rowsUpdated = sAvrcpControllerService.getContentResolver(). + update(BluetoothAvrcpInfo.CONTENT_URI,values, null, null); + Log.d(TAG," updatePlayStatus num_rows_updated " + rowsUpdated); + } + private boolean isEventSupported(byte eventId) + { + if ((eventId == EVENT_PLAYBACK_STATUS_CHANGED)|| + (eventId == EVENT_PLAYBACK_POS_CHANGED)|| + (eventId == EVENT_PLAYER_APPLICATION_SETTINGS_CHANGED)|| + (eventId == EVENT_TRACK_CHANGED)) + return true; + else + return false; + } + private void handleNotificationTimeout(int cmd) + { + Log.d(TAG," handleNotificationTimeout cmd " + cmd); + int notificaitonId = cmd - 2000; + int index; + for (index = 0; index < mRemoteData.mNotifyEvent.size(); index++) { + if (notificaitonId == mRemoteData.mNotifyEvent.get(index).notify_event_id) + break; + } + if (index == mRemoteData.mNotifyEvent.size()) + return; + NotifyEvents notifyEvent = mRemoteData.mNotifyEvent.get(index); + if ((notifyEvent.notify_event_id == notificaitonId) && + (notifyEvent.notify_state == NOTIFY_INTERIM_EXPECTED)) + { + notifyEvent.notify_state = NOTIFY_NOT_NOTIFIED; + registerFurtherNotification(notificaitonId); + } + } + private void handleCmdTimeout(int cmd) + { + Log.d(TAG," CMD " + cmd + " Timeout Happened"); + switch(cmd) + { + case MESSAGE_GET_SUPPORTED_COMPANY_ID: + mHandler.sendEmptyMessage(MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_ATTRIB); + break; + case MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_ATTRIB: + case MESSAGE_GET_CURRENT_PLAYER_APPLICATION_SETTINGS: + mHandler.sendEmptyMessage(MESSAGE_GET_SUPPORTED_EVENTS); + break; + case MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_VALUES: + getFurtherPlayerSettingAttrib(ATTRIBUTE_FETCH_SKIP); + break; + case MESSAGE_GET_SUPPORTED_EVENTS: + /* + * If Timeout for these command happened, We would not do anything + * We will wait for Application to send request for + * ElementAttributes/PlayStatus. Only after that MetaData/Playstatus will resume + */ + break; + case MESSAGE_TIMEOUT_APPL_SETTINGS_CHANGED: + case MESSAGE_TIMEOUT_PLAYBACK_POS_CHNAGED: + case MESSAGE_TIMEOUT_PLAYBACK_STATUS_CHANGED: + case MESSAGE_TIMEOUT_TRACK_CHANGED: + case MESSAGE_TIMEOUT_VOLUME_CHANGED: + handleNotificationTimeout(cmd); + break; + case MESSAGE_TIMEOUT_ATTRIBUTE_TITLE: + case MESSAGE_TIMEOUT_ATTRIBUTE_ARTIST_NAME: + case MESSAGE_TIMEOUT_ATTRIBUTE_ALBUM_NAME: + case MESSAGE_TIMEOUT_ATTRIBUTE_GENRE: + case MESSAGE_TIMEOUT_ATTRIBUTE_PLAYING_TIME: + case MESSAGE_TIMEOUT_ATTRIBUTE_TOTAL_TRACK_NUMBER: + case MESSAGE_TIMEOUT_ATTRIBUTE_TRACK_NUMBER: + getFurtherElementAttribute(ATTRIBUTE_FETCH_SKIP); + break; + case MESSAGE_GET_PLAY_STATUS: + mHandler.sendEmptyMessage(MESSAGE_GET_PLAY_STATUS); // reque this command + break; + case MESSAGE_SET_CURRENT_PLAYER_APPLICATION_SETTINGS: + // do nothing here + break; + } + } + private String utf8ToString(byte[] input) + { + Charset UTF8_CHARSET = Charset.forName("UTF-8"); + return new String(input,UTF8_CHARSET); + } + private int asciiToInt(int len, byte[] array) + { + return Integer.parseInt(utf8ToString(array)); + } + private void resetElementAttribute(int attributeId, int asciiStringLen) { + if(asciiStringLen == 0) { + switch(attributeId) + { + case MEDIA_ATTRIBUTE_TITLE: + mRemoteData.mMetadata.trackTitle = BluetoothAvrcpInfo.TITLE_INVALID; + break; + case MEDIA_ATTRIBUTE_ARTIST_NAME: + mRemoteData.mMetadata.artist = BluetoothAvrcpInfo.ARTIST_NAME_INVALID; + break; + case MEDIA_ATTRIBUTE_ALBUM_NAME: + mRemoteData.mMetadata.albumTitle = BluetoothAvrcpInfo.ALBUM_NAME_INVALID; + break; + case MEDIA_ATTRIBUTE_GENRE: + mRemoteData.mMetadata.genre = BluetoothAvrcpInfo.GENRE_INVALID; + break; + case MEDIA_ATTRIBUTE_TRACK_NUMBER: + mRemoteData.mMetadata.trackNum = BluetoothAvrcpInfo.TRACK_NUM_INVALID; + break; + case MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER: + mRemoteData.mMetadata.totalTrackNum = BluetoothAvrcpInfo.TOTAL_TRACKS_INVALID; + break; + case MEDIA_ATTRIBUTE_PLAYING_TIME: + mRemoteData.mMetadata.totalTrackLen = BluetoothAvrcpInfo.TOTAL_TRACK_TIME_INVALID; + break; + } + } + } + private int parseElementAttributes(int currentIndex, int attributeId, ByteBuffer attribBuffer) + { + Log.d(TAG,"parseElementAttributes Id = " + attributeId); + currentIndex += 2; // for character set + int asciiStringLen = attribBuffer.getChar(currentIndex); + Log.d(TAG," asciiStringLen "+ asciiStringLen); + currentIndex += 2;// for string len + if((asciiStringLen <= 0) || ((currentIndex + asciiStringLen) > attribBuffer.capacity())) + { + Log.d(TAG," parseElementAttribute wrong buffer"); + resetElementAttribute(attributeId, asciiStringLen); + return currentIndex; + } + byte[] asciiString = new byte[asciiStringLen]; + attribBuffer.position(currentIndex); + attribBuffer.get(asciiString, 0, asciiStringLen); + currentIndex += asciiStringLen; + switch(attributeId) + { + case MEDIA_ATTRIBUTE_TITLE: + mRemoteData.mMetadata.trackTitle = utf8ToString(asciiString); + break; + case MEDIA_ATTRIBUTE_ARTIST_NAME: + mRemoteData.mMetadata.artist = utf8ToString(asciiString); + break; + case MEDIA_ATTRIBUTE_ALBUM_NAME: + mRemoteData.mMetadata.albumTitle = utf8ToString(asciiString); + break; + case MEDIA_ATTRIBUTE_GENRE: + mRemoteData.mMetadata.genre = utf8ToString(asciiString); + break; + case MEDIA_ATTRIBUTE_TRACK_NUMBER: + mRemoteData.mMetadata.trackNum = asciiToInt(asciiStringLen,asciiString); + break; + case MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER: + mRemoteData.mMetadata.totalTrackNum = asciiToInt(asciiStringLen,asciiString); + break; + case MEDIA_ATTRIBUTE_PLAYING_TIME: + mRemoteData.mMetadata.totalTrackLen = asciiToInt(asciiStringLen,asciiString); + break; + } + return currentIndex; + } + private void handleProcessGetElementAttribute(int numAttributes, ByteBuffer attribBuffer) + { + Log.d(TAG,"handleProcessGetElementAttribute numAttrib ="+ numAttributes); + int attributeId = 0; + int currentIndex = 0; + for (int count = 0; count < numAttributes; count++) + { + /* + * In case remote sends rsp for more attributes that we can accomodate. + * Basically to avoid erroneous conditions. + */ + if (mRemoteData.mMetadata.attributesFetchedId >= requestedElementAttribs.length) + continue; + attributeId = attribBuffer.getInt(currentIndex); + if (requestedElementAttribs[mRemoteData.mMetadata.attributesFetchedId] != attributeId) + { + Log.e(TAG," Received Rsp for attributeId "+ attributeId +" Requested ID = " + + requestedElementAttribs[mRemoteData.mMetadata.attributesFetchedId]); + break; + } + currentIndex += 4; // 4 bytes for Int + Log.d(TAG," attributeID = "+ attributeId); + /* + * remove timeout message if it is present already in que. + */ + if (mHandler.hasMessages(GET_ELEMENT_ATTR_TIMEOUT_BASE + attributeId)) + { + mHandler.removeMessages(GET_ELEMENT_ATTR_TIMEOUT_BASE + attributeId); + Log.d(TAG," Timeout CMD = " + attributeId + "dequed"); + } + currentIndex = parseElementAttributes(currentIndex,attributeId,attribBuffer); + } + getFurtherElementAttribute(ATTRIBUTE_FETCH_CONTINUE); + } + private void handleProcessNotificationResponse(int notificationId, int notificationType, + ByteBuffer notificationRsp) + { + Log.d(TAG,"handleProcessNotificationResponse id " + notificationId + + " type = " + notificationType); + /* + * First remove timeout if already queued + */ + byte oldState = NOTIFY_NOT_NOTIFIED; + if (mHandler.hasMessages(2000 + notificationId)) + { + mHandler.removeMessages(2000 + notificationId); + Log.d(TAG," Timeout Notification CMD dequeued "); + } + for (NotifyEvents notifyEvent: mRemoteData.mNotifyEvent) + { + if (notificationId != notifyEvent.notify_event_id) + continue; + oldState = notifyEvent.notify_state; + if ((oldState == NOTIFY_INTERIM_EXPECTED) && + (notificationType == NOTIFICATION_RSP_TYPE_INTERIM)) { + notifyEvent.notify_state = NOTIFY_CHANGED_EXPECTED; + } + else if ((oldState == NOTIFY_CHANGED_EXPECTED) && + (notificationType == NOTIFICATION_RSP_TYPE_CHANGED)) { + notifyEvent.notify_state = NOTIFY_NOT_NOTIFIED; + } + break; + } + switch(notificationId) + { + case EVENT_PLAYBACK_STATUS_CHANGED: + byte oldPlayStatus = mRemoteData.mMetadata.playStatus; + mRemoteData.mMetadata.playStatus = notificationRsp.get(1); + /* + * Check if there is a transition from Stopped/Paused to Playing + * and Remote does not support EVENT_PLAYBACK_POS + * We need to Que GetPlayBackStatus command. + */ + if (((oldPlayStatus == PLAY_STATUS_STOPPED)|| + (oldPlayStatus == PLAY_STATUS_PAUSED))&& + (mRemoteData.mMetadata.playStatus == PLAY_STATUS_PLAYING)) { + if (!(mRemoteData.mEventsSupported.contains(EVENT_PLAYBACK_POS_CHANGED))|| + !(mRemoteData.mEventsSupported.contains(EVENT_PLAYBACK_STATUS_CHANGED))) { + Log.d(TAG," State Transition Triggered, Que GetPlayStatus "); + mHandler.sendEmptyMessage(MESSAGE_GET_PLAY_STATUS); + } + } + updatePlayStatus(); + break; + case EVENT_PLAYBACK_POS_CHANGED: + mRemoteData.mMetadata.playTime = notificationRsp.getInt(1); + updatePlayTime(); + break; + case EVENT_PLAYER_APPLICATION_SETTINGS_CHANGED: + int numPlayerSettingAttribs = notificationRsp.get(1); + int attribIndex = 2; // first attribute will be at 2 index + for (int count = 0; count < numPlayerSettingAttribs; count ++) + { + Byte attributeId = notificationRsp.get(attribIndex); + for (PlayerSettings plSettings: + mRemoteData.mSupportedApplicationSettingsAttribute) + { + if (plSettings.attr_Id == attributeId) + { + plSettings.attr_val = notificationRsp.get(attribIndex+1); + } + } + attribIndex = attribIndex + 2; + } + updatePlayerApplicationSettings(); + if ((oldState == NOTIFY_CHANGED_EXPECTED) && + (notificationType == NOTIFICATION_RSP_TYPE_CHANGED)) + { + mHandler.sendEmptyMessage(MESSAGE_GET_CURRENT_PLAYER_APPLICATION_SETTINGS); + } + break; + case EVENT_TRACK_CHANGED: + if ((oldState == NOTIFY_CHANGED_EXPECTED) && + (notificationType == NOTIFICATION_RSP_TYPE_CHANGED)) + { + Log.d(TAG," Track change Happened, que GetElement, PlayerSetting "); + mHandler.sendEmptyMessage(MESSAGE_GET_ELEMENT_ATTRIBUTE); + mHandler.sendEmptyMessage(MESSAGE_GET_CURRENT_PLAYER_APPLICATION_SETTINGS); + } + break; + } + registerFurtherNotification(EVENT_NOTIFICAION_ID_NONE); + } + private void handleProcessPlayStatus(ByteBuffer bb) + { + int currentIndex = 0; + int bufSize = bb.capacity(); + mRemoteData.mMetadata.totalTrackLen = bb.getInt(currentIndex); + currentIndex += 4; + mRemoteData.mMetadata.playTime = bb.getInt(currentIndex); + currentIndex += 4; + mRemoteData.mMetadata.playStatus = bb.get(currentIndex); + updatePlayStatus(); + if (!(mRemoteData.mEventsSupported.contains(EVENT_PLAYBACK_POS_CHANGED))|| + !(mRemoteData.mEventsSupported.contains(EVENT_PLAYBACK_STATUS_CHANGED))) + { + if ((mRemoteData.mMetadata.playStatus != PLAY_STATUS_STOPPED)&& + (mRemoteData.mMetadata.playStatus != PLAY_STATUS_PAUSED)) { + Log.d(TAG," POS and Status changed not supported"); + mHandler.sendEmptyMessageDelayed(MESSAGE_GET_PLAY_STATUS, GET_PLAY_STATUS_INTERVAL); + } + } + } + private void setAbsVolume(int absVol) + { + int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); + int newIndex = (maxVolume*absVol)/ABS_VOL_BASE; + Log.d(TAG," setAbsVolume ="+absVol + " maxVol = " + maxVolume + " cur = " + currIndex + + " new = "+newIndex); + /* + * In some cases change in percentage is not sufficient enough to warrant + * change in index values which are in range of 0-15. For such cases + * no action is required + */ + if (newIndex != currIndex) { + if (mRemoteData.absVolNotificationState == NOTIFY_RSP_INTERIM_SENT) + mRemoteData.absVolNotificationState = NOTIFY_RSP_ABS_VOL_DEFERRED; + mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newIndex, 0); + } + sendAbsVolRspNative(absVol); + } + private void handleProcessAbsVolNotification() + { + Log.d(TAG," handleProcessAbsVolNotification "); + if(mRemoteData == null) + return; + if(mRemoteData.absVolNotificationState == NOTIFY_NOT_REGISTERED) + { + int maxVol = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); + int percentageVol = ((currIndex*ABS_VOL_BASE)/maxVol); + Log.d(TAG," maxVol ="+maxVol+" currentIndex ="+currIndex+ + " percentageVol ="+percentageVol); + byte rspType = NOTIFICATION_RSP_TYPE_INTERIM; + mRemoteData.absVolNotificationState = NOTIFY_RSP_INTERIM_SENT; + sendRegisterAbsVolRspNative(rspType,percentageVol); + } + } /** Handles Avrcp messages. */ private final class AvrcpMessageHandler extends Handler { private AvrcpMessageHandler(Looper looper) { @@ -224,12 +1630,199 @@ public class AvrcpControllerService extends ProfileService { @Override public void handleMessage(Message msg) { + Log.d(TAG," HandleMessage: "+ msg.what + + " Remote Connected " + !mConnectedDevices.isEmpty()); switch (msg.what) { case MESSAGE_SEND_PASS_THROUGH_CMD: - if (DBG) Log.v(TAG, "MESSAGE_SEND_PASS_THROUGH_CMD"); BluetoothDevice device = (BluetoothDevice)msg.obj; sendPassThroughCommandNative(getByteAddress(device), msg.arg1, msg.arg2); break; + case MESSAGE_GET_SUPPORTED_COMPANY_ID: //first command in AVRCP 1.3 that we send. + /* + * If Sink is not active we won't allow AVRCP MetaData + */ + A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService(); + if (a2dpSinkService == null) { + Log.e(TAG," A2DP Sink service not started, MetaData will not proceed"); + return; + } + initializeDatabase(); + getSupportedCapabilities(COMPANY_ID); + break; + case MESSAGE_GET_SUPPORTED_EVENTS: + if ((mRemoteData.mNotifyEvent != null)&&(mRemoteData.mNotifyEvent.isEmpty())) + getSupportedCapabilities(EVENTS_SUPPORTED); + break; + case MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_ATTRIB: + getPlayerApplicationSettingsAttrib(); + break; + case MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_VALUES: + getFurtherPlayerSettingAttrib(ATTRIBUTE_FETCH_FRESH); + break; + case MESSAGE_GET_CURRENT_PLAYER_APPLICATION_SETTINGS: + if (!(mHandler.hasMessages(MESSAGE_GET_CURRENT_PLAYER_APPLICATION_SETTINGS))) + getCurrentPlayerApplicationSettingsValues(); + break; + case MESSAGE_SET_CURRENT_PLAYER_APPLICATION_SETTINGS: + setCurrentPlayerApplicationSettingsValues(msg.arg1, msg.arg2); + break; + case MESSAGE_DEINIT_AVRCP_DATABASE: + deinitDatabase(); + break; + case MESSAGE_CMD_TIMEOUT: + int cmd = (Integer)msg.obj; + handleCmdTimeout(cmd); + break; + case MESSAGE_TIMEOUT_APPL_SETTINGS_CHANGED: + case MESSAGE_TIMEOUT_PLAYBACK_POS_CHNAGED: + case MESSAGE_TIMEOUT_PLAYBACK_STATUS_CHANGED: + case MESSAGE_TIMEOUT_TRACK_CHANGED: + case MESSAGE_TIMEOUT_VOLUME_CHANGED: + case MESSAGE_TIMEOUT_ATTRIBUTE_TITLE: + case MESSAGE_TIMEOUT_ATTRIBUTE_ARTIST_NAME: + case MESSAGE_TIMEOUT_ATTRIBUTE_ALBUM_NAME: + case MESSAGE_TIMEOUT_ATTRIBUTE_GENRE: + case MESSAGE_TIMEOUT_ATTRIBUTE_PLAYING_TIME: + case MESSAGE_TIMEOUT_ATTRIBUTE_TOTAL_TRACK_NUMBER: + case MESSAGE_TIMEOUT_ATTRIBUTE_TRACK_NUMBER: + handleCmdTimeout(msg.what); + break; + + case MESSAGE_GET_ELEMENT_ATTRIBUTE: + getFurtherElementAttribute(ATTRIBUTE_FETCH_FRESH); + break; + case ABORT_FETCH_ELEMENT_ATTRIBUTE: + if ((mRemoteData != null) && (mRemoteData.mMetadata != null)) + mRemoteData.mMetadata.attributesFetchedId = -1; // reset it to -1 again. + break; + case MESSAGE_GET_PLAY_STATUS: + if (mHandler.hasMessages(MESSAGE_GET_PLAY_STATUS)) { + Log.d(TAG," Get Play Status Already There, return "); + return; + } + Message messsag = mHandler.obtainMessage(MESSAGE_CMD_TIMEOUT, + 0,0,MESSAGE_GET_PLAY_STATUS); + mHandler.sendMessageDelayed(messsag, MSG_TIMEOUT_MTP); + getPlayStatusNative(); + break; + case MESSAGE_PROCESS_RC_FEATURES: + /* check if we have already initiated MetaData Procedure */ + if ((mRemoteData != null)&&(mRemoteData.mRemoteFeatures != 0)) + break; + /* this is first time */ + int remoteFeatures = msg.arg1; + BluetoothDevice remoteDevice = (BluetoothDevice)msg.obj; + if (mConnectedDevices.contains(remoteDevice)) { + mRemoteData.mRemoteFeatures = remoteFeatures; + if ((mRemoteData.mRemoteFeatures & BTRC_FEAT_METADATA) != 0) + mHandler.sendMessage(mHandler. + obtainMessage(MESSAGE_GET_SUPPORTED_COMPANY_ID,0, 0, remoteDevice)); + } + break; + case MESSAGE_PROCESS_CONNECTION_CHANGE: + int connected = msg.arg1; + BluetoothDevice rtDevice = (BluetoothDevice)msg.obj; + if (connected == 1) + { + /* + * Connection up but RC features not received yet. We will + * send get_company_ID later. + */ + if (mRemoteData == null) + mRemoteData = new RemoteAvrcpData(); + mRemoteData.mCompanyIDSupported = new ArrayList<Integer>(); + mRemoteData.mEventsSupported = new ArrayList<Byte>(); + mRemoteData.mMetadata = new Metadata(); + mRemoteData.mNotifyEvent = new ArrayList<NotifyEvents>(); + mRemoteData.mSupportedApplicationSettingsAttribute = + new ArrayList<PlayerSettings>(); + mRemoteData.absVolNotificationState = NOTIFY_NOT_REGISTERED; + mRemoteData.playerSettingAttribIdFetch = 0; + mRemoteData.mRemoteFeatures = 0; + } + else + { + mHandler.removeCallbacksAndMessages(null); + mHandler.sendEmptyMessage(MESSAGE_DEINIT_AVRCP_DATABASE); + if (mRemoteData != null) + { + mRemoteData.mCompanyIDSupported.clear(); + mRemoteData.mEventsSupported.clear(); + mRemoteData.mMetadata.resetMetaData(); + mRemoteData.mNotifyEvent.clear(); + mRemoteData.mSupportedApplicationSettingsAttribute.clear(); + mRemoteData.absVolNotificationState = NOTIFY_NOT_REGISTERED; + mRemoteData.mRemoteFeatures = 0; + Log.d(TAG," RC_features, conn_change down " + mRemoteData.mRemoteFeatures); + mRemoteData.playerSettingAttribIdFetch = 0; + mRemoteData = null; + } + } + break; + case MESSAGE_PROCESS_SUPPORTED_COMPANY_ID: + List<Integer> company_ids = (List<Integer>)msg.obj; + mRemoteData.mCompanyIDSupported.addAll(company_ids); + mHandler.sendEmptyMessage(MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_ATTRIB); + break; + case MESSAGE_PROCESS_SUPPORTED_EVENTS: + List<Byte> events_supported = (List<Byte>)msg.obj; + mRemoteData.mEventsSupported.addAll(events_supported); + for (Byte event: mRemoteData.mEventsSupported) + { + NotifyEvents notifyevent = new NotifyEvents(); + notifyevent.notify_event_id = event; + notifyevent.notify_state = NOTIFY_NOT_NOTIFIED; + mRemoteData.mNotifyEvent.add(notifyevent); + } + registerFurtherNotification(EVENT_NOTIFICAION_ID_NONE); + break; + case MESSAGE_PROCESS_PLAYER_APPLICATION_SETTINGS_ATTRIB: + List<PlayerSettings> player_settings_supported = (List<PlayerSettings>)msg.obj; + mRemoteData.mSupportedApplicationSettingsAttribute. + addAll(player_settings_supported); + mHandler.sendEmptyMessage(MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_VALUES); + break; + case MESSAGE_PROCESS_PLAYER_APPLICATION_SETTINGS_VALUES: + mRemoteData.playerSettingAttribIdFetch ++; + getFurtherPlayerSettingAttrib(msg.arg1); + break; + case MESSAGE_PROCESS_CURRENT_PLAYER_APPLICATION_SETTINGS: + updatePlayerApplicationSettings(); + if ((mRemoteData.mNotifyEvent != null)&&(mRemoteData.mNotifyEvent.isEmpty())) + mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_GET_SUPPORTED_EVENTS)); + break; + case MESSAGE_PROCESS_NOTIFICATION_RESPONSE: + int notificationId = msg.arg1; + int notificationType = msg.arg2; + ByteBuffer bb = (ByteBuffer)msg.obj; + handleProcessNotificationResponse(notificationId, notificationType, bb); + break; + case MESSAGE_PROCESS_ELEMENT_ATTRIBUTE: + if ((mRemoteData.mMetadata.attributesFetchedId == -1) || + (mHandler.hasMessages(ABORT_FETCH_ELEMENT_ATTRIBUTE))) { + Log.d(TAG," ID reset, Fetch from Fresh"); + mHandler.sendEmptyMessage(MESSAGE_GET_ELEMENT_ATTRIBUTE); + break; + } + int processMode = msg.arg2; + if (processMode == ATTRIBUTE_FETCH_CONTINUE) + { + int numAttributes = msg.arg1; + ByteBuffer bbRsp = (ByteBuffer)msg.obj; + handleProcessGetElementAttribute(numAttributes, bbRsp); + } + else if(processMode == ATTRIBUTE_FETCH_SKIP) + getFurtherElementAttribute(ATTRIBUTE_FETCH_SKIP); + break; + case MESSAGE_PROCESS_PLAY_STATUS: + ByteBuffer playStatusRsp = (ByteBuffer)msg.obj; + handleProcessPlayStatus(playStatusRsp); + break; + case MESSAGE_PROCESS_SET_ABS_VOL_CMD: + setAbsVolume(msg.arg1); + break; + case MESSAGE_PROCESS_REGISTER_ABS_VOL_REQUEST: + handleProcessAbsVolNotification(); } } } @@ -237,6 +1830,8 @@ public class AvrcpControllerService extends ProfileService { private void onConnectionStateChanged(boolean connected, byte[] address) { BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice (Utils.getAddressStringFromByte(address)); + if (device == null) + return; Log.d(TAG, "onConnectionStateChanged " + connected + " " + device); Intent intent = new Intent(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED); int oldState = (mConnectedDevices.contains(device) ? BluetoothProfile.STATE_CONNECTED @@ -248,18 +1843,121 @@ public class AvrcpControllerService extends ProfileService { intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); // intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); + if (connected && oldState == BluetoothProfile.STATE_DISCONNECTED) { mConnectedDevices.add(device); + Message msg = mHandler.obtainMessage(MESSAGE_PROCESS_CONNECTION_CHANGE, 1, 0, device); + mHandler.sendMessage(msg); } else if (!connected && oldState == BluetoothProfile.STATE_CONNECTED) { mConnectedDevices.remove(device); + Message msg = mHandler.obtainMessage(MESSAGE_PROCESS_CONNECTION_CHANGE, 0, 0, device); + mHandler.sendMessage(msg); } } + private void getRcFeatures(byte[] address, int features) { + BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice + (Utils.getAddressStringFromByte(address)); + Message msg = mHandler.obtainMessage(MESSAGE_PROCESS_RC_FEATURES, features, 0, + device); + mHandler.sendMessage(msg); + } + private void handlePassthroughRsp(int id, int keyState) { Log.d(TAG, "passthrough response received as: key: " + id + " state: " + keyState); } + private void handleGetCapabilitiesResponse(byte[] address, int capability_id, + int[] supported_values,int num_supported, byte rsp_type) + { + Log.d(TAG, "handleGetCapabilitiesResponse cap_id" + capability_id + " num_supported " + + num_supported+ "rsp_type " + rsp_type); + BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice + (Utils.getAddressStringFromByte(address)); + if (!mConnectedDevices.contains(device)) + return; + if (num_supported <= 0) { + /* + * supported values are not reliable. Drop further processing + * Remove Timeout Commands + */ + if (mHandler.hasMessages(MESSAGE_CMD_TIMEOUT,COMPANY_ID)) + mHandler.removeMessages(MESSAGE_CMD_TIMEOUT, COMPANY_ID); + else if (mHandler.hasMessages(MESSAGE_CMD_TIMEOUT,EVENTS_SUPPORTED)) + mHandler.removeMessages(MESSAGE_CMD_TIMEOUT, EVENTS_SUPPORTED); + return; + } + if (mHandler.hasMessages(MESSAGE_CMD_TIMEOUT, capability_id)) + { + mHandler.removeMessages(MESSAGE_CMD_TIMEOUT, capability_id); + Log.d(TAG," Timeout CMD dequeued "); + } + if (rsp_type != AVRC_RSP_IMPL_STBL) + { + if(capability_id == COMPANY_ID) + mHandler.sendEmptyMessage(MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_ATTRIB); + /* for events supported failure, we don't do anything */ + return; + } + Message msg; + switch(capability_id) + { + case COMPANY_ID: + ArrayList<Integer> supportedCompanyIds = new ArrayList<Integer>(); + for (int count = 0; count<num_supported; ++count) + supportedCompanyIds.add(supported_values[count]); + msg = mHandler. + obtainMessage(MESSAGE_PROCESS_SUPPORTED_COMPANY_ID,supportedCompanyIds); + mHandler.sendMessage(msg); + break; + case EVENTS_SUPPORTED: + ArrayList<Byte> supportedEvents = new ArrayList<Byte>(); + for (int count = 0; count<num_supported; ++count) + { + Byte supported_event = + Byte.valueOf((Integer.valueOf(supported_values[count])).byteValue()); + if (!isEventSupported(supported_event)) + continue; + supportedEvents.add(supported_event); + } + msg = mHandler.obtainMessage(MESSAGE_PROCESS_SUPPORTED_EVENTS,supportedEvents); + mHandler.sendMessage(msg); + break; + } + } + private void handleListPlayerApplicationSettingsAttrib(byte[] address, + byte[] supported_setting_attrib,int num_attrib, byte rsp_type) + { + Log.d(TAG, "handleListPlayerApplicationSettingsAttrib " + + num_attrib + " rsp_type " + rsp_type); + BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice + (Utils.getAddressStringFromByte(address)); + if (!mConnectedDevices.contains(device)) + return; + if (mHandler.hasMessages(MESSAGE_CMD_TIMEOUT, + MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_ATTRIB)) + { + mHandler.removeMessages(MESSAGE_CMD_TIMEOUT, + MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_ATTRIB); + Log.d(TAG," Timeout CMD dequeued "); + } + if ((rsp_type != AVRC_RSP_IMPL_STBL)||(num_attrib <= 0)) + { + mHandler.sendEmptyMessage(MESSAGE_GET_SUPPORTED_EVENTS); + return; + } + ArrayList<PlayerSettings> supported_attrib = new ArrayList<PlayerSettings>(); + for (int count = 0; count < num_attrib; count++) + { + PlayerSettings attrib = new PlayerSettings(); + attrib.attr_Id = supported_setting_attrib[count]; + attrib.supported_values = null; + supported_attrib.add(attrib); + } + mHandler.sendMessage(mHandler. + obtainMessage(MESSAGE_PROCESS_PLAYER_APPLICATION_SETTINGS_ATTRIB,supported_attrib)); + } private byte[] getByteAddress(BluetoothDevice device) { return Utils.getBytesFromAddress(device.getAddress()); } @@ -269,8 +1967,207 @@ public class AvrcpControllerService extends ProfileService { super.dump(sb); } + private void handleListPlayerApplicationSettingValue(byte[] address, byte[] supported_val, + byte num_supported_val, byte rsp_type) + { + Log.d(TAG,"handleListPlayerApplicationSettingValue num_supported " + num_supported_val + + " rsp_type " + rsp_type); + BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice + (Utils.getAddressStringFromByte(address)); + if (!mConnectedDevices.contains(device)) + return; + if (mHandler.hasMessages(MESSAGE_CMD_TIMEOUT, + MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_VALUES)) + { + mHandler.removeMessages(MESSAGE_CMD_TIMEOUT, + MESSAGE_GET_PLAYER_APPLICATION_SETTINGS_VALUES); + Log.d(TAG," Timeout CMD dequeued "); + } + if ((rsp_type != AVRC_RSP_IMPL_STBL)||(num_supported_val <= 0)) + { + mHandler.sendMessage(mHandler.obtainMessage( + MESSAGE_PROCESS_PLAYER_APPLICATION_SETTINGS_VALUES, ATTRIBUTE_FETCH_SKIP, 0)); + return; + } + int fetch_id = mRemoteData.playerSettingAttribIdFetch; + PlayerSettings plSetting = mRemoteData.mSupportedApplicationSettingsAttribute.get(fetch_id); + plSetting.supported_values = new byte [num_supported_val]; + for (int count = 0; count < num_supported_val; ++count) + plSetting.supported_values[count] = supported_val[count]; + mHandler.sendMessage(mHandler.obtainMessage( + MESSAGE_PROCESS_PLAYER_APPLICATION_SETTINGS_VALUES, ATTRIBUTE_FETCH_CONTINUE, 0)); + } + private void handleCurrentPlayerApplicationSettingsResponse(byte[] address, + byte[] ids, byte[] values,byte num_attrib, byte rsp_type) + { + Log.d(TAG,"handleCurrentPlayerApplicationSettingsResponse num_attrib =" + num_attrib); + BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice + (Utils.getAddressStringFromByte(address)); + if (!mConnectedDevices.contains(device)) + return; + if (mHandler.hasMessages(MESSAGE_CMD_TIMEOUT, + MESSAGE_GET_CURRENT_PLAYER_APPLICATION_SETTINGS)) + { + mHandler.removeMessages(MESSAGE_CMD_TIMEOUT, + MESSAGE_GET_CURRENT_PLAYER_APPLICATION_SETTINGS); + Log.d(TAG," Timeout CMD dequeued "); + } + if ((rsp_type != AVRC_RSP_IMPL_STBL)||(num_attrib <= 0)) + { + mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_GET_SUPPORTED_EVENTS)); + return; + } + for (int count = 0; count < num_attrib; count++) + { + byte attribute = ids[count]; // count increment to point to value of attrib_id. + for (PlayerSettings plSetting: mRemoteData.mSupportedApplicationSettingsAttribute) + { + if (plSetting.attr_Id == attribute) { + plSetting.attr_val = values[count]; + } + } + } + mHandler.sendEmptyMessage(MESSAGE_PROCESS_CURRENT_PLAYER_APPLICATION_SETTINGS); + } + /* + * response contains array from Event-ID ( first byte of the array ). + * rspLen is same as Parameter Length field of PDU. + * rspType - either Interim or Notify. + */ + private void handleNotificationRsp(byte[] address, byte rspType, int rspLen, byte[] response) + { + Log.d(TAG,"handleNotificationRsp "); + BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice + (Utils.getAddressStringFromByte(address)); + if ((!mConnectedDevices.contains(device))||(rspLen <= 0)) + return; + int notificationEventId = response[0]; + int notificaionRspType = rspType; + Log.d(TAG," rsp_type " + rspType +" notificationId " + notificationEventId); + if ((rspType != AVRC_RSP_INTERIM)&&(rspType != AVRC_RSP_CHANGED)) + { + if (mHandler.hasMessages(2000 + notificationEventId)) + { + mHandler.removeMessages(2000 + notificationEventId); + Log.d(TAG," Timeout CMD dequeued "); + } + handleNotificationTimeout(2000 + notificationEventId); + return; + } + ByteBuffer bb = ByteBuffer.wrap(response, 0, rspLen); + mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_PROCESS_NOTIFICATION_RESPONSE, + notificationEventId, notificaionRspType, bb)); + } + /* + * attribRsp contains array after Number of Attributes + * First byte would be attribute ID ( 4 octets )of first attribute. + * attribRspLen - total length of attribRsp + */ + private void handleGetElementAttributes(byte[] address, byte numAttributes, int attribRspLen, + byte[] attribRsp, byte rsp_type) + { + Log.d(TAG,"handleGetElementAttributes "); + BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice + (Utils.getAddressStringFromByte(address)); + if ((!mConnectedDevices.contains(device))||(attribRspLen <= 0)) + return; + + ByteBuffer bb = ByteBuffer.wrap(attribRsp, 0, attribRspLen); + int attributeId = bb.getInt(0); + Log.d(TAG," numAttrib " + numAttributes + " attribRspLen " + attribRspLen + + " rsp_type " + rsp_type + " attribId " + attributeId); + if (rsp_type != AVRC_RSP_IMPL_STBL) + { + if (mHandler.hasMessages(GET_ELEMENT_ATTR_TIMEOUT_BASE + attributeId)) + { + mHandler.removeMessages(GET_ELEMENT_ATTR_TIMEOUT_BASE + attributeId); + Log.d(TAG," Timeout CMD dequeued "); + } + mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_PROCESS_ELEMENT_ATTRIBUTE, + 0, ATTRIBUTE_FETCH_SKIP)); + return; + } + Message msg = mHandler.obtainMessage(MESSAGE_PROCESS_ELEMENT_ATTRIBUTE, numAttributes, + ATTRIBUTE_FETCH_CONTINUE, bb); + mHandler.sendMessage(msg); + } + /* + * playStatusRsp will be after Parameter Length Feild + * first byte will be from Total Song Length + */ + private void handleGetPlayStatus(byte[] address, int paramLen, + byte[] playStatusRsp, byte rsp_type) + { + Log.d(TAG,"handleGetPlayStatus "); + BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice + (Utils.getAddressStringFromByte(address)); + if (!mConnectedDevices.contains(device)) + return; + if (mHandler.hasMessages(MESSAGE_CMD_TIMEOUT, MESSAGE_GET_PLAY_STATUS)) + { + mHandler.removeMessages(MESSAGE_CMD_TIMEOUT, MESSAGE_GET_PLAY_STATUS); + Log.d(TAG," Timeout CMD dequeued "); + } + if ((rsp_type!= AVRC_RSP_IMPL_STBL)||(paramLen <= 0)) { + return; + } + ByteBuffer bb = ByteBuffer.wrap(playStatusRsp, 0, paramLen); + Message msg = mHandler.obtainMessage(MESSAGE_PROCESS_PLAY_STATUS, 0, 0, bb); + mHandler.sendMessage(msg); + } + private void handleSetAbsVolume(byte[] address, byte absVol) + { + Log.d(TAG,"handleSetAbsVolume "); + BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice + (Utils.getAddressStringFromByte(address)); + if (!mConnectedDevices.contains(device)) + return; + Message msg = mHandler.obtainMessage(MESSAGE_PROCESS_SET_ABS_VOL_CMD, absVol, 0); + mHandler.sendMessage(msg); + } + private void handleRegisterNotificationAbsVol(byte[] address) + { + Log.d(TAG,"handleRegisterNotificationAbsVol "); + BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice + (Utils.getAddressStringFromByte(address)); + if (!mConnectedDevices.contains(device)) + return; + mHandler.sendEmptyMessage(MESSAGE_PROCESS_REGISTER_ABS_VOL_REQUEST); + } + private void handleSetPlayerApplicationResponse(byte[] address, byte rsp_type) + { + Log.d(TAG,"handleSetPlayerApplicationResponse rsp_type = "+ rsp_type); + BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice + (Utils.getAddressStringFromByte(address)); + if (!mConnectedDevices.contains(device)) + return; + if (mHandler.hasMessages(MESSAGE_CMD_TIMEOUT, + MESSAGE_SET_CURRENT_PLAYER_APPLICATION_SETTINGS)) + { + mHandler.removeMessages(MESSAGE_CMD_TIMEOUT, + MESSAGE_SET_CURRENT_PLAYER_APPLICATION_SETTINGS); + Log.d(TAG," Timeout CMD dequeued "); + } + /* + * Send GetPlayerAppSetting if we do not support EVENT_PLAYER_APPLICATION_SETTINGS_CHANGED + * Some Devices don't reply with Changed. So we have to send this command, though its a + * overhead for other devices. + */ + mHandler.sendEmptyMessage(MESSAGE_GET_CURRENT_PLAYER_APPLICATION_SETTINGS); + } private native static void classInitNative(); private native void initNative(); private native void cleanupNative(); private native boolean sendPassThroughCommandNative(byte[] address, int keyCode, int keyState); + private native void getCapabilitiesNative(int capId); + private native void listPlayerApplicationSettingAttributeNative(); + private native void listPlayerApplicationSettingValueNative(byte attribId); + private native void getPlayerApplicationSettingValuesNative(byte numAttrib, byte[] attribIds); + private native void setPlayerApplicationSettingValuesNative(byte numAttrib, + byte[] atttibIds, byte[]attribVal); + private native void registerNotificationNative(byte eventId, int value); + private native void getElementAttributeNative(byte numAttributes, int attribId); + private native void getPlayStatusNative(); + private native void sendAbsVolRspNative(int absVol); + private native void sendRegisterAbsVolRspNative(byte rspType, int absVol); } diff --git a/src/com/android/bluetooth/avrcp/BluetoothAvrcpDataProvider.java b/src/com/android/bluetooth/avrcp/BluetoothAvrcpDataProvider.java new file mode 100644 index 000000000..7bee7c1e9 --- /dev/null +++ b/src/com/android/bluetooth/avrcp/BluetoothAvrcpDataProvider.java @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2008-2009, Motorola, Inc. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Motorola, Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.android.bluetooth.avrcp; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.SQLException; +import android.content.UriMatcher; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.util.Log; +import android.bluetooth.BluetoothAvrcpController; +import android.bluetooth.BluetoothAvrcpInfo; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + + +/** + * This content-provider implements AVRCP-MetaData database. + */ + +public final class BluetoothAvrcpDataProvider extends ContentProvider { + + private static final String TAG = "BluetoothAvrcpDataProvider"; + private static final String DB_NAME = "btavrcp_ct.db"; + private static final int DB_VERSION = 1; + private static final int DB_VERSION_NOP_UPGRADE_FROM = 0; + private static final int DB_VERSION_NOP_UPGRADE_TO = 1; + private static final String DB_TABLE = "btavrcp_ct"; + private static final String TRACK_LIST_TYPE = "vnd.android.cursor.dir/vnd.android.btavrcp_ct"; + private static final String TRACK_TYPE = "vnd.android.cursor.item/vnd.android.btavrcp_ct"; + private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); + /** For all TrackList */ + private static final int TRACKS = 1; + /** For a single Track ID */ + private static final int TRACK_ID = 2; + + static { + sURIMatcher.addURI("com.android.bluetooth.avrcp", "btavrcp_ct", TRACKS); + sURIMatcher.addURI("com.android.bluetooth.avrcp", "btavrcp_ct/#", TRACK_ID); + } + private SQLiteOpenHelper mOpenHelper = null; + + private final class DbHelper extends SQLiteOpenHelper { + + public DbHelper(final Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + @Override + public void onCreate(final SQLiteDatabase db) { + Log.v(TAG, "populating new database"); + createTable(db); + } + + @Override + public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) { + if (oldV == DB_VERSION_NOP_UPGRADE_FROM) { + if (newV == DB_VERSION_NOP_UPGRADE_TO) { // that's a no-op + return; + } + oldV = DB_VERSION_NOP_UPGRADE_TO; + } + Log.i(TAG, "Upgrading downloads database from version " + oldV + " to " + + newV + ", which will destroy all old data"); + dropTable(db); + createTable(db); + } + } + + private void createTable(SQLiteDatabase db) { + try { + db.execSQL("CREATE TABLE " + DB_TABLE + "(" + + BluetoothAvrcpInfo._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + BluetoothAvrcpInfo.TRACK_NUM + " INTEGER, " + + BluetoothAvrcpInfo.TITLE + " TEXT, " + + BluetoothAvrcpInfo.ARTIST_NAME + " TEXT, " + + BluetoothAvrcpInfo.ALBUM_NAME + " TEXT, " + + BluetoothAvrcpInfo.TOTAL_TRACKS + " INTEGER, " + + BluetoothAvrcpInfo.GENRE + " TEXT, " + + BluetoothAvrcpInfo.PLAYING_TIME + " INTEGER, " + + BluetoothAvrcpInfo.TOTAL_TRACK_TIME + " INTEGER, " + + BluetoothAvrcpInfo.PLAY_STATUS + " TEXT, " + + BluetoothAvrcpInfo.REPEAT_STATUS + " TEXT, " + + BluetoothAvrcpInfo.SHUFFLE_STATUS + " TEXT, " + + BluetoothAvrcpInfo.SCAN_STATUS + " TEXT, " + + BluetoothAvrcpInfo.EQUALIZER_STATUS + " TEXT); "); + } catch (SQLException ex) { + Log.e(TAG, "couldn't create table in downloads database"); + throw ex; + } + } + + private void dropTable(SQLiteDatabase db) { + try { + db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE); + } catch (SQLException ex) { + Log.e(TAG, "Couldn't Drop table"); + throw ex; + } + } + + @Override + public String getType(Uri uri) { + int match = sURIMatcher.match(uri); + switch (match) { + case TRACKS: { + return TRACK_LIST_TYPE; + } + case TRACK_ID: { + return TRACK_TYPE; + } + default: { + Log.d(TAG, "getType called on unknown URI: " + uri); + throw new IllegalArgumentException("Unknown URI: " + uri); + } + } + } + + private static final void copyString(String key, ContentValues from, ContentValues to) { + String s = from.getAsString(key); + if (s != null) { + to.put(key, s); + } + } + + private static final void copyInteger(String key, ContentValues from, ContentValues to) { + Integer i = from.getAsInteger(key); + if (i != null) { + to.put(key, i); + } + } + + private static final void copyLong(String key, ContentValues from, ContentValues to) { + Long i = from.getAsLong(key); + if (i != null) { + to.put(key, i); + } + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + + if (sURIMatcher.match(uri) != TRACKS) { + Log.d(TAG, "calling insert on an unknown/invalid URI: " + uri); + throw new IllegalArgumentException("Unknown/Invalid URI " + uri); + } + + ContentValues filteredValues = new ContentValues(); + + copyLong(BluetoothAvrcpInfo.TRACK_NUM, values, filteredValues); + copyString(BluetoothAvrcpInfo.TITLE, values, filteredValues); + copyString(BluetoothAvrcpInfo.ARTIST_NAME, values, filteredValues); + copyString(BluetoothAvrcpInfo.ALBUM_NAME, values, filteredValues); + copyLong(BluetoothAvrcpInfo.TOTAL_TRACKS, values, filteredValues); + copyString(BluetoothAvrcpInfo.GENRE, values, filteredValues); + copyLong(BluetoothAvrcpInfo.PLAYING_TIME, values, filteredValues); + copyLong(BluetoothAvrcpInfo.TOTAL_TRACK_TIME, values, filteredValues); + copyString(BluetoothAvrcpInfo.PLAY_STATUS, values, filteredValues); + copyString(BluetoothAvrcpInfo.REPEAT_STATUS, values, filteredValues); + copyString(BluetoothAvrcpInfo.SHUFFLE_STATUS, values, filteredValues); + copyString(BluetoothAvrcpInfo.SCAN_STATUS, values, filteredValues); + copyString(BluetoothAvrcpInfo.EQUALIZER_STATUS, values, filteredValues); + + long rowID = db.insert(DB_TABLE, null, filteredValues); + + Uri ret = null; + Context context = getContext(); + + if (rowID != -1) { + ret = Uri.parse(BluetoothAvrcpInfo.CONTENT_URI + "/" + rowID); + context.getContentResolver().notifyChange(uri, null); + } else { + Log.d(TAG, "couldn't insert into database"); + } + + return ret; + } + + @Override + public boolean onCreate() { + mOpenHelper = new DbHelper(getContext()); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + + int match = sURIMatcher.match(uri); + switch (match) { + case TRACKS: { + qb.setTables(DB_TABLE); + break; + } + case TRACK_ID: { + qb.setTables(DB_TABLE); + qb.appendWhere(BluetoothAvrcpInfo._ID + "="); + qb.appendWhere(uri.getPathSegments().get(1)); + break; + } + default: { + Log.d(TAG, "querying unknown URI: " + uri); + throw new IllegalArgumentException("Unknown URI: " + uri); + } + } + + { + java.lang.StringBuilder sb = new java.lang.StringBuilder(); + sb.append("starting query, database is "); + if (db != null) { + sb.append("not "); + } + sb.append("null; "); + if (projection == null) { + sb.append("projection is null; "); + } else if (projection.length == 0) { + sb.append("projection is empty; "); + } else { + for (int i = 0; i < projection.length; ++i) { + sb.append("projection["); + sb.append(i); + sb.append("] is "); + sb.append(projection[i]); + sb.append("; "); + } + } + sb.append("selection is "); + sb.append(selection); + sb.append("; "); + if (selectionArgs == null) { + sb.append("selectionArgs is null; "); + } else if (selectionArgs.length == 0) { + sb.append("selectionArgs is empty; "); + } else { + for (int i = 0; i < selectionArgs.length; ++i) { + sb.append("selectionArgs["); + sb.append(i); + sb.append("] is "); + sb.append(selectionArgs[i]); + sb.append("; "); + } + } + sb.append("sort is "); + sb.append(sortOrder); + sb.append("."); + Log.v(TAG, sb.toString()); + } + + Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); + Log.v(TAG," Query Done "); + if (ret != null) { + ret.setNotificationUri(getContext().getContentResolver(), uri); + Log.v(TAG, "created cursor " + ret + " on behalf of "); + } else { + Log.d(TAG, "query failed in downloads database"); + } + + return ret; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + + int count; + long rowId = 0; + + int match = sURIMatcher.match(uri); + switch (match) { + case TRACKS: + case TRACK_ID: { + String myWhere; + if (selection != null) { + if (match == TRACKS) { + myWhere = "( " + selection + " )"; + } else { + myWhere = "( " + selection + " ) AND "; + } + } else { + myWhere = ""; + } + if (match == TRACK_ID) { + String segment = uri.getPathSegments().get(1); + rowId = Long.parseLong(segment); + myWhere += " ( " + BluetoothAvrcpInfo._ID + " = " + rowId + " ) "; + } + + if (values.size() > 0) { + count = db.update(DB_TABLE, values, myWhere, selectionArgs); + } else { + count = 0; + } + break; + } + default: { + Log.d(TAG, "updating unknown/invalid URI: " + uri); + throw new UnsupportedOperationException("Cannot update URI: " + uri); + } + } + getContext().getContentResolver().notifyChange(uri, null); + + return count; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + int count; + int match = sURIMatcher.match(uri); + switch (match) { + case TRACKS: + case TRACK_ID: { + String myWhere; + if (selection != null) { + if (match == TRACKS) { + myWhere = "( " + selection + " )"; + } else { + myWhere = "( " + selection + " ) AND "; + } + } else { + myWhere = ""; + } + if (match == TRACK_ID) { + String segment = uri.getPathSegments().get(1); + long rowId = Long.parseLong(segment); + myWhere += " ( " + BluetoothAvrcpInfo._ID + " = " + rowId + " ) "; + } + + count = db.delete(DB_TABLE, myWhere, selectionArgs); + break; + } + default: { + Log.d(TAG, "deleting unknown/invalid URI: " + uri); + throw new UnsupportedOperationException("Cannot delete URI: " + uri); + } + } + getContext().getContentResolver().notifyChange(uri, null); + return count; + } +}
\ No newline at end of file |