DO NOT MERGE - Merge pie-platform-release (PPRL.190801.002) into master
Bug: 139369544
Change-Id: If9b192f2b6d33372c1a255bd9b67fec1ce4fd889
diff --git a/Android.mk b/Android.mk
index 5d3db9a..f4a0d5d 100644
--- a/Android.mk
+++ b/Android.mk
@@ -25,11 +25,60 @@
sap-api-java-static \
services.net \
libprotobuf-java-lite \
- bluetooth-protos-lite
+ bluetooth-protos-lite \
-LOCAL_STATIC_ANDROID_LIBRARIES := android-support-v4
+LOCAL_STATIC_ANDROID_LIBRARIES := \
+ androidx.core_core \
+ androidx.legacy_legacy-support-v4 \
+ androidx.lifecycle_lifecycle-livedata \
+ androidx.room_room-runtime \
+ bt-androidx-room-runtime-nodeps \
+
+LOCAL_ANNOTATION_PROCESSORS := \
+ bt-androidx-annotation-nodeps \
+ bt-androidx-room-common-nodeps \
+ bt-androidx-room-compiler-nodeps \
+ bt-androidx-room-migration-nodeps \
+ bt-antlr4-nodeps \
+ bt-apache-commons-codec-nodeps \
+ bt-auto-common-nodeps \
+ bt-javapoet-nodeps \
+ bt-kotlin-metadata-nodeps \
+ bt-sqlite-jdbc-nodeps \
+ bt-jetbrain-nodeps \
+ guava-21.0 \
+ kotlin-stdlib \
+ gson-prebuilt-jar
+
+LOCAL_JAVACFLAGS := \
+ -Aroom.schemaLocation=$(LOCAL_PATH)/tests/unit/src/com/android/bluetooth/btservice/storage/schemas
+
+LOCAL_ANNOTATION_PROCESSOR_CLASSES := \
+ androidx.room.RoomProcessor
+
LOCAL_REQUIRED_MODULES := libbluetooth
LOCAL_PROGUARD_ENABLED := disabled
include $(BUILD_PACKAGE)
include $(call all-makefiles-under,$(LOCAL_PATH))
+
+include $(CLEAR_VARS)
+
+COMMON_LIBS_PATH := ../../../../../prebuilts/tools/common/m2/repository
+ROOM_LIBS_PATH := ../../lib/room
+
+LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
+ bt-androidx-annotation-nodeps:$(ROOM_LIBS_PATH)/annotation-1.0.0-beta01.jar \
+ bt-androidx-room-common-nodeps:$(ROOM_LIBS_PATH)/room-common-2.0.0-beta01.jar \
+ bt-androidx-room-compiler-nodeps:$(ROOM_LIBS_PATH)/room-compiler-2.0.0-beta01.jar \
+ bt-androidx-room-migration-nodeps:$(ROOM_LIBS_PATH)/room-migration-2.0.0-beta01.jar \
+ bt-androidx-room-runtime-nodeps:$(ROOM_LIBS_PATH)/room-runtime-2.0.0-alpha1.aar \
+ bt-antlr4-nodeps:$(COMMON_LIBS_PATH)/org/antlr/antlr4/4.5.3/antlr4-4.5.3.jar \
+ bt-apache-commons-codec-nodeps:$(COMMON_LIBS_PATH)/org/eclipse/tycho/tycho-bundles-external/0.18.1/eclipse/plugins/org.apache.commons.codec_1.4.0.v201209201156.jar \
+ bt-auto-common-nodeps:$(COMMON_LIBS_PATH)/com/google/auto/auto-common/0.9/auto-common-0.9.jar \
+ bt-javapoet-nodeps:$(COMMON_LIBS_PATH)/com/squareup/javapoet/1.8.0/javapoet-1.8.0.jar \
+ bt-kotlin-metadata-nodeps:$(COMMON_LIBS_PATH)/me/eugeniomarletti/kotlin-metadata/1.2.1/kotlin-metadata-1.2.1.jar \
+ bt-sqlite-jdbc-nodeps:$(COMMON_LIBS_PATH)/org/xerial/sqlite-jdbc/3.20.1/sqlite-jdbc-3.20.1.jar \
+ bt-jetbrain-nodeps:../../../../../prebuilts/tools/common/m2/repository/org/jetbrains/annotations/13.0/annotations-13.0.jar
+
+include $(BUILD_HOST_PREBUILT)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b24064f..5add14e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -58,7 +58,6 @@
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.WRITE_SMS" />
- <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
<uses-permission android:name="android.permission.MANAGE_APP_OPS_MODES" />
@@ -69,6 +68,8 @@
<uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+ <uses-sdk android:minSdkVersion="14"/>
+
<!-- For PBAP Owner Vcard Info -->
<uses-permission android:name="android.permission.READ_PROFILE"/>
<application
@@ -309,7 +310,7 @@
</service>
<service
android:process="@string/process"
- android:name=".a2dpsink.mbs.A2dpMediaBrowserService"
+ android:name=".avrcpcontroller.BluetoothMediaBrowserService"
android:exported="true"
android:enabled="@bool/profile_supported_a2dp_sink"
android:label="@string/a2dp_sink_mbs_label">
@@ -351,14 +352,6 @@
</service>
<service
android:process="@string/process"
- android:name = ".hdp.HealthService"
- android:enabled="@bool/profile_supported_hdp">
- <intent-filter>
- <action android:name="android.bluetooth.IBluetoothHealth" />
- </intent-filter>
- </service>
- <service
- android:process="@string/process"
android:name = ".pan.PanService"
android:enabled="@bool/profile_supported_pan">
<intent-filter>
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..2c1bdc4
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1 @@
+include platform/system/bt:/OWNERS
diff --git a/jni/Android.bp b/jni/Android.bp
index c0fdd80..9005ae8 100644
--- a/jni/Android.bp
+++ b/jni/Android.bp
@@ -8,13 +8,11 @@
"com_android_bluetooth_hfpclient.cpp",
"com_android_bluetooth_a2dp.cpp",
"com_android_bluetooth_a2dp_sink.cpp",
- "com_android_bluetooth_avrcp.cpp",
"com_android_bluetooth_avrcp_controller.cpp",
"com_android_bluetooth_avrcp_target.cpp",
"com_android_bluetooth_hid_host.cpp",
"com_android_bluetooth_hid_device.cpp",
"com_android_bluetooth_hearing_aid.cpp",
- "com_android_bluetooth_hdp.cpp",
"com_android_bluetooth_pan.cpp",
"com_android_bluetooth_gatt.cpp",
"com_android_bluetooth_sdp.cpp",
@@ -23,20 +21,20 @@
],
header_libs: ["libbluetooth_headers"],
include_dirs: [
- "libnativehelper/include/nativehelper",
"system/bt/types",
],
shared_libs: [
"libandroid_runtime",
+ "libbase",
"libbinder",
"libbluetooth-binder",
"libchrome",
"libnativehelper",
"liblog",
+ "libutils",
],
static_libs: [
"libbluetooth-types",
- "libutils",
"libcutils",
],
cflags: [
diff --git a/jni/bluetooth_socket_manager.cc b/jni/bluetooth_socket_manager.cc
index a63beed..ba45aa5 100644
--- a/jni/bluetooth_socket_manager.cc
+++ b/jni/bluetooth_socket_manager.cc
@@ -20,7 +20,6 @@
#include <base/logging.h>
#include <binder/IPCThreadState.h>
-using ::android::OK;
using ::android::String8;
using ::android::binder::Status;
using ::android::os::ParcelFileDescriptor;
@@ -57,8 +56,8 @@
return Status::ok();
}
- _aidl_return->reset(new ParcelFileDescriptor());
- (*_aidl_return)->setFileDescriptor(socket_fd, true);
+ _aidl_return->reset(
+ new ParcelFileDescriptor(android::base::unique_fd(socket_fd)));
return Status::ok();
}
@@ -97,8 +96,8 @@
return Status::ok();
}
- _aidl_return->reset(new ParcelFileDescriptor());
- (*_aidl_return)->setFileDescriptor(socket_fd, true);
+ _aidl_return->reset(
+ new ParcelFileDescriptor(android::base::unique_fd(socket_fd)));
return Status::ok();
}
diff --git a/jni/com_android_bluetooth.h b/jni/com_android_bluetooth.h
index 9f136a0..c305c16 100644
--- a/jni/com_android_bluetooth.h
+++ b/jni/com_android_bluetooth.h
@@ -89,8 +89,6 @@
int register_com_android_bluetooth_hid_device(JNIEnv* env);
-int register_com_android_bluetooth_hdp(JNIEnv* env);
-
int register_com_android_bluetooth_pan(JNIEnv* env);
int register_com_android_bluetooth_gatt (JNIEnv* env);
diff --git a/jni/com_android_bluetooth_a2dp.cpp b/jni/com_android_bluetooth_a2dp.cpp
index 126405a3..af125bd 100644
--- a/jni/com_android_bluetooth_a2dp.cpp
+++ b/jni/com_android_bluetooth_a2dp.cpp
@@ -390,6 +390,33 @@
return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
}
+static jboolean setSilenceDeviceNative(JNIEnv* env, jobject object,
+ jbyteArray address, jboolean silence) {
+ ALOGI("%s: sBluetoothA2dpInterface: %p", __func__, sBluetoothA2dpInterface);
+ std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+ if (!sBluetoothA2dpInterface) {
+ ALOGE("%s: Failed to get the Bluetooth A2DP Interface", __func__);
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+
+ RawAddress bd_addr = RawAddress::kEmpty;
+ if (addr) {
+ bd_addr.FromOctets(reinterpret_cast<const uint8_t*>(addr));
+ }
+ if (bd_addr == RawAddress::kEmpty) {
+ return JNI_FALSE;
+ }
+ bt_status_t status =
+ sBluetoothA2dpInterface->set_silence_device(bd_addr, silence);
+ if (status != BT_STATUS_SUCCESS) {
+ ALOGE("%s: Failed A2DP set_silence_device, status: %d", __func__, status);
+ }
+ env->ReleaseByteArrayElements(address, addr, 0);
+ return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
+}
+
static jboolean setActiveDeviceNative(JNIEnv* env, jobject object,
jbyteArray address) {
ALOGI("%s: sBluetoothA2dpInterface: %p", __func__, sBluetoothA2dpInterface);
@@ -405,6 +432,9 @@
if (addr) {
bd_addr.FromOctets(reinterpret_cast<const uint8_t*>(addr));
}
+ if (bd_addr == RawAddress::kEmpty) {
+ return JNI_FALSE;
+ }
bt_status_t status = sBluetoothA2dpInterface->set_active_device(bd_addr);
if (status != BT_STATUS_SUCCESS) {
ALOGE("%s: Failed A2DP set_active_device, status: %d", __func__, status);
@@ -450,6 +480,7 @@
{"cleanupNative", "()V", (void*)cleanupNative},
{"connectA2dpNative", "([B)Z", (void*)connectA2dpNative},
{"disconnectA2dpNative", "([B)Z", (void*)disconnectA2dpNative},
+ {"setSilenceDeviceNative", "([BZ)Z", (void*)setSilenceDeviceNative},
{"setActiveDeviceNative", "([B)Z", (void*)setActiveDeviceNative},
{"setCodecConfigPreferenceNative",
"([B[Landroid/bluetooth/BluetoothCodecConfig;)Z",
diff --git a/jni/com_android_bluetooth_a2dp_sink.cpp b/jni/com_android_bluetooth_a2dp_sink.cpp
index 50c5087..91de5af 100644
--- a/jni/com_android_bluetooth_a2dp_sink.cpp
+++ b/jni/com_android_bluetooth_a2dp_sink.cpp
@@ -231,7 +231,7 @@
int register_com_android_bluetooth_a2dp_sink(JNIEnv* env) {
return jniRegisterNativeMethods(
- env, "com/android/bluetooth/a2dpsink/A2dpSinkStateMachine", sMethods,
+ env, "com/android/bluetooth/a2dpsink/A2dpSinkService", sMethods,
NELEM(sMethods));
}
}
diff --git a/jni/com_android_bluetooth_avrcp.cpp b/jni/com_android_bluetooth_avrcp.cpp
deleted file mode 100644
index 1eb3553..0000000
--- a/jni/com_android_bluetooth_avrcp.cpp
+++ /dev/null
@@ -1,1529 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define LOG_TAG "BluetoothAvrcpServiceJni"
-
-#define LOG_NDEBUG 0
-
-#include "android_runtime/AndroidRuntime.h"
-#include "com_android_bluetooth.h"
-#include "hardware/bt_rc.h"
-#include "utils/Log.h"
-
-#include <inttypes.h>
-#include <string.h>
-
-namespace android {
-static jmethodID method_getRcFeatures;
-static jmethodID method_getPlayStatus;
-static jmethodID method_getElementAttr;
-static jmethodID method_registerNotification;
-static jmethodID method_volumeChangeCallback;
-static jmethodID method_handlePassthroughCmd;
-static jmethodID method_getFolderItemsCallback;
-static jmethodID method_setAddressedPlayerCallback;
-
-static jmethodID method_setBrowsedPlayerCallback;
-static jmethodID method_changePathCallback;
-static jmethodID method_searchCallback;
-static jmethodID method_playItemCallback;
-static jmethodID method_getItemAttrCallback;
-static jmethodID method_addToPlayListCallback;
-static jmethodID method_getTotalNumOfItemsCallback;
-
-static const btrc_interface_t* sBluetoothAvrcpInterface = NULL;
-static jobject mCallbacksObj = NULL;
-
-/* Function declarations */
-static bool copy_item_attributes(JNIEnv* env, jobject object,
- btrc_folder_items_t* pitem,
- jint* p_attributesIds,
- jobjectArray attributesArray, int item_idx,
- int attribCopiedIndex);
-
-static bool copy_jstring(uint8_t* str, int maxBytes, jstring jstr, JNIEnv* env);
-
-static void cleanup_items(btrc_folder_items_t* p_items, int numItems);
-
-static void btavrcp_remote_features_callback(const RawAddress& bd_addr,
- btrc_remote_features_t features) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
-
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Unable to allocate byte array for bd_addr");
- return;
- }
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_getRcFeatures, addr.get(),
- (jint)features);
-}
-
-/** Callback for play status request */
-static void btavrcp_get_play_status_callback(const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
-
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for get_play_status command");
- return;
- }
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_getPlayStatus, addr.get());
-}
-
-static void btavrcp_get_element_attr_callback(uint8_t num_attr,
- btrc_media_attr_t* p_attrs,
- const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
-
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for get_element_attr command");
- return;
- }
-
- ScopedLocalRef<jintArray> attrs(
- sCallbackEnv.get(), (jintArray)sCallbackEnv->NewIntArray(num_attr));
- if (!attrs.get()) {
- ALOGE("Fail to new jintArray for attrs");
- return;
- }
-
- sCallbackEnv->SetIntArrayRegion(attrs.get(), 0, num_attr, (jint*)p_attrs);
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_getElementAttr, addr.get(),
- (jbyte)num_attr, attrs.get());
-}
-
-static void btavrcp_register_notification_callback(btrc_event_id_t event_id,
- uint32_t param,
- const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
-
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for register_notification command");
- return;
- }
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_registerNotification,
- addr.get(), (jint)event_id, (jint)param);
-}
-
-static void btavrcp_volume_change_callback(uint8_t volume, uint8_t ctype,
- const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
-
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for volume_change command");
- return;
- }
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
-
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_volumeChangeCallback,
- addr.get(), (jint)volume, (jint)ctype);
-}
-
-static void btavrcp_passthrough_command_callback(int id, int pressed,
- const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
-
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for passthrough_command command");
- return;
- }
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
-
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_handlePassthroughCmd,
- addr.get(), (jint)id, (jint)pressed);
-}
-
-static void btavrcp_set_addressed_player_callback(uint16_t player_id,
- const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
-
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for set_addressed_player command");
- return;
- }
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_setAddressedPlayerCallback,
- addr.get(), (jint)player_id);
-}
-
-static void btavrcp_set_browsed_player_callback(uint16_t player_id,
- const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for set_browsed_player command");
- return;
- }
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
-
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_setBrowsedPlayerCallback,
- addr.get(), (jint)player_id);
-}
-
-static void btavrcp_get_folder_items_callback(
- uint8_t scope, uint32_t start_item, uint32_t end_item, uint8_t num_attr,
- uint32_t* p_attr_ids, const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
-
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for get_folder_items command");
- return;
- }
-
- uint32_t* puiAttr = (uint32_t*)p_attr_ids;
- ScopedLocalRef<jintArray> attr_ids(sCallbackEnv.get(), NULL);
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
-
- /* check number of attributes requested by remote device */
- if ((num_attr != BTRC_NUM_ATTR_ALL) && (num_attr != BTRC_NUM_ATTR_NONE)) {
- /* allocate memory for attr_ids only if some attributes passed from below
- * layer */
- attr_ids.reset((jintArray)sCallbackEnv->NewIntArray(num_attr));
- if (!attr_ids.get()) {
- ALOGE("Fail to allocate new jintArray for attrs");
- return;
- }
- sCallbackEnv->SetIntArrayRegion(attr_ids.get(), 0, num_attr,
- (jint*)puiAttr);
- }
-
- sCallbackEnv->CallVoidMethod(
- mCallbacksObj, method_getFolderItemsCallback, addr.get(), (jbyte)scope,
- (jlong)start_item, (jlong)end_item, (jbyte)num_attr, attr_ids.get());
-}
-
-static void btavrcp_change_path_callback(uint8_t direction, uint8_t* folder_uid,
- const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
-
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> attrs(sCallbackEnv.get(),
- sCallbackEnv->NewByteArray(BTRC_UID_SIZE));
- if (!attrs.get()) {
- ALOGE("Fail to new jintArray for attrs");
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for change_path command");
- return;
- }
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
- sCallbackEnv->SetByteArrayRegion(
- attrs.get(), 0, sizeof(uint8_t) * BTRC_UID_SIZE, (jbyte*)folder_uid);
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_changePathCallback,
- addr.get(), (jbyte)direction, attrs.get());
-}
-
-static void btavrcp_get_item_attr_callback(uint8_t scope, uint8_t* uid,
- uint16_t uid_counter,
- uint8_t num_attr,
- btrc_media_attr_t* p_attrs,
- const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
-
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> attr_uid(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(BTRC_UID_SIZE));
- if (!attr_uid.get()) {
- ALOGE("Fail to new jintArray for attr_uid");
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for get_item_attr command");
- return;
- }
-
- ScopedLocalRef<jintArray> attrs(
- sCallbackEnv.get(), (jintArray)sCallbackEnv->NewIntArray(num_attr));
- if (!attrs.get()) {
- ALOGE("Fail to new jintArray for attrs");
- return;
- }
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
- sCallbackEnv->SetIntArrayRegion(attrs.get(), 0, num_attr, (jint*)p_attrs);
- sCallbackEnv->SetByteArrayRegion(
- attr_uid.get(), 0, sizeof(uint8_t) * BTRC_UID_SIZE, (jbyte*)uid);
-
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_getItemAttrCallback,
- addr.get(), (jbyte)scope, attr_uid.get(),
- (jint)uid_counter, (jbyte)num_attr, attrs.get());
-}
-
-static void btavrcp_play_item_callback(uint8_t scope, uint16_t uid_counter,
- uint8_t* uid,
- const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> attrs(sCallbackEnv.get(),
- sCallbackEnv->NewByteArray(BTRC_UID_SIZE));
- if (!attrs.get()) {
- ALOGE("%s: Fail to new jByteArray attrs for play_item command", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for play_item command");
- return;
- }
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
- sCallbackEnv->SetByteArrayRegion(
- attrs.get(), 0, sizeof(uint8_t) * BTRC_UID_SIZE, (jbyte*)uid);
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_playItemCallback,
- addr.get(), (jbyte)scope, (jint)uid_counter,
- attrs.get());
-}
-
-static void btavrcp_get_total_num_items_callback(uint8_t scope,
- const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for get_total_num_items command");
- return;
- }
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_getTotalNumOfItemsCallback,
- addr.get(), (jbyte)scope);
-}
-
-static void btavrcp_search_callback(uint16_t charset_id, uint16_t str_len,
- uint8_t* p_str, const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> attrs(sCallbackEnv.get(),
- sCallbackEnv->NewByteArray(str_len));
- if (!attrs.get()) {
- ALOGE("Fail to new jintArray for attrs");
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for search command");
- return;
- }
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
- sCallbackEnv->SetByteArrayRegion(attrs.get(), 0, str_len * sizeof(uint8_t),
- (jbyte*)p_str);
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_searchCallback, addr.get(),
- (jint)charset_id, attrs.get());
-}
-
-static void btavrcp_add_to_play_list_callback(uint8_t scope, uint8_t* uid,
- uint16_t uid_counter,
- const RawAddress& bd_addr) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
- if (!mCallbacksObj) {
- ALOGE("%s: mCallbacksObj is null", __func__);
- return;
- }
-
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for add_to_play_list command");
- return;
- }
-
- ScopedLocalRef<jbyteArray> attrs(sCallbackEnv.get(),
- sCallbackEnv->NewByteArray(BTRC_UID_SIZE));
- if (!attrs.get()) {
- ALOGE("Fail to new jByteArray for attrs");
- return;
- }
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr.address);
- sCallbackEnv->SetByteArrayRegion(
- attrs.get(), 0, sizeof(uint8_t) * BTRC_UID_SIZE, (jbyte*)uid);
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_addToPlayListCallback,
- addr.get(), (jbyte)scope, attrs.get(),
- (jint)uid_counter);
-}
-
-static btrc_callbacks_t sBluetoothAvrcpCallbacks = {
- sizeof(sBluetoothAvrcpCallbacks),
- btavrcp_remote_features_callback,
- btavrcp_get_play_status_callback,
- NULL,
- NULL,
- NULL,
- NULL,
- NULL,
- NULL,
- btavrcp_get_element_attr_callback,
- btavrcp_register_notification_callback,
- btavrcp_volume_change_callback,
- btavrcp_passthrough_command_callback,
- btavrcp_set_addressed_player_callback,
- btavrcp_set_browsed_player_callback,
- btavrcp_get_folder_items_callback,
- btavrcp_change_path_callback,
- btavrcp_get_item_attr_callback,
- btavrcp_play_item_callback,
- btavrcp_get_total_num_items_callback,
- btavrcp_search_callback,
- btavrcp_add_to_play_list_callback,
-};
-
-static void classInitNative(JNIEnv* env, jclass clazz) {
- method_getRcFeatures =
- env->GetMethodID(clazz, "getRcFeaturesRequestFromNative", "([BI)V");
- method_getPlayStatus =
- env->GetMethodID(clazz, "getPlayStatusRequestFromNative", "([B)V");
-
- method_getElementAttr =
- env->GetMethodID(clazz, "getElementAttrRequestFromNative", "([BB[I)V");
-
- method_registerNotification = env->GetMethodID(
- clazz, "registerNotificationRequestFromNative", "([BII)V");
-
- method_volumeChangeCallback =
- env->GetMethodID(clazz, "volumeChangeRequestFromNative", "([BII)V");
-
- method_handlePassthroughCmd = env->GetMethodID(
- clazz, "handlePassthroughCmdRequestFromNative", "([BII)V");
-
- method_setAddressedPlayerCallback =
- env->GetMethodID(clazz, "setAddressedPlayerRequestFromNative", "([BI)V");
-
- method_setBrowsedPlayerCallback =
- env->GetMethodID(clazz, "setBrowsedPlayerRequestFromNative", "([BI)V");
-
- method_getFolderItemsCallback =
- env->GetMethodID(clazz, "getFolderItemsRequestFromNative", "([BBJJB[I)V");
-
- method_changePathCallback =
- env->GetMethodID(clazz, "changePathRequestFromNative", "([BB[B)V");
-
- method_getItemAttrCallback =
- env->GetMethodID(clazz, "getItemAttrRequestFromNative", "([BB[BIB[I)V");
-
- method_playItemCallback =
- env->GetMethodID(clazz, "playItemRequestFromNative", "([BBI[B)V");
-
- method_getTotalNumOfItemsCallback =
- env->GetMethodID(clazz, "getTotalNumOfItemsRequestFromNative", "([BB)V");
-
- method_searchCallback =
- env->GetMethodID(clazz, "searchRequestFromNative", "([BI[B)V");
-
- method_addToPlayListCallback =
- env->GetMethodID(clazz, "addToPlayListRequestFromNative", "([BB[BI)V");
-
- ALOGI("%s: succeeds", __func__);
-}
-
-static void initNative(JNIEnv* env, jobject object) {
- const bt_interface_t* btInf = getBluetoothInterface();
- if (btInf == NULL) {
- ALOGE("Bluetooth module is not loaded");
- return;
- }
-
- if (sBluetoothAvrcpInterface != NULL) {
- ALOGW("Cleaning up Avrcp Interface before initializing...");
- sBluetoothAvrcpInterface->cleanup();
- sBluetoothAvrcpInterface = NULL;
- }
-
- if (mCallbacksObj != NULL) {
- ALOGW("Cleaning up Avrcp callback object");
- env->DeleteGlobalRef(mCallbacksObj);
- mCallbacksObj = NULL;
- }
-
- sBluetoothAvrcpInterface =
- (btrc_interface_t*)btInf->get_profile_interface(BT_PROFILE_AV_RC_ID);
- if (sBluetoothAvrcpInterface == NULL) {
- ALOGE("Failed to get Bluetooth Avrcp Interface");
- return;
- }
-
- bt_status_t status =
- sBluetoothAvrcpInterface->init(&sBluetoothAvrcpCallbacks);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed to initialize Bluetooth Avrcp, status: %d", status);
- sBluetoothAvrcpInterface = NULL;
- return;
- }
-
- mCallbacksObj = env->NewGlobalRef(object);
-}
-
-static void cleanupNative(JNIEnv* env, jobject object) {
- const bt_interface_t* btInf = getBluetoothInterface();
- if (btInf == NULL) {
- ALOGE("Bluetooth module is not loaded");
- return;
- }
-
- if (sBluetoothAvrcpInterface != NULL) {
- sBluetoothAvrcpInterface->cleanup();
- sBluetoothAvrcpInterface = NULL;
- }
-
- if (mCallbacksObj != NULL) {
- env->DeleteGlobalRef(mCallbacksObj);
- mCallbacksObj = NULL;
- }
-}
-
-static jboolean getPlayStatusRspNative(JNIEnv* env, jobject object,
- jbyteArray address, jint playStatus,
- jint songLen, jint songPos) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
- RawAddress rawAddress;
- rawAddress.FromOctets((uint8_t*)addr);
-
- bt_status_t status = sBluetoothAvrcpInterface->get_play_status_rsp(
- rawAddress, (btrc_play_status_t)playStatus, songLen, songPos);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed get_play_status_rsp, status: %d", status);
- }
- env->ReleaseByteArrayElements(address, addr, 0);
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean getElementAttrRspNative(JNIEnv* env, jobject object,
- jbyteArray address, jbyte numAttr,
- jintArray attrIds,
- jobjectArray textArray) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- if (numAttr > BTRC_MAX_ELEM_ATTR_SIZE) {
- ALOGE("get_element_attr_rsp: number of attributes exceed maximum");
- return JNI_FALSE;
- }
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- btrc_element_attr_val_t* pAttrs = new btrc_element_attr_val_t[numAttr];
- if (!pAttrs) {
- ALOGE("get_element_attr_rsp: not have enough memeory");
- env->ReleaseByteArrayElements(address, addr, 0);
- return JNI_FALSE;
- }
-
- jint* attr = env->GetIntArrayElements(attrIds, NULL);
- if (!attr) {
- delete[] pAttrs;
- jniThrowIOException(env, EINVAL);
- env->ReleaseByteArrayElements(address, addr, 0);
- return JNI_FALSE;
- }
-
- int attr_cnt;
- for (attr_cnt = 0; attr_cnt < numAttr; ++attr_cnt) {
- pAttrs[attr_cnt].attr_id = attr[attr_cnt];
- ScopedLocalRef<jstring> text(
- env, (jstring)env->GetObjectArrayElement(textArray, attr_cnt));
-
- if (!copy_jstring(pAttrs[attr_cnt].text, BTRC_MAX_ATTR_STR_LEN, text.get(),
- env)) {
- break;
- }
- }
-
- if (attr_cnt < numAttr) {
- delete[] pAttrs;
- env->ReleaseIntArrayElements(attrIds, attr, 0);
- ALOGE("%s: Failed to copy attributes", __func__);
- return JNI_FALSE;
- }
-
- RawAddress rawAddress;
- rawAddress.FromOctets((uint8_t*)addr);
- bt_status_t status = sBluetoothAvrcpInterface->get_element_attr_rsp(
- rawAddress, numAttr, pAttrs);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed get_element_attr_rsp, status: %d", status);
- }
-
- delete[] pAttrs;
- env->ReleaseIntArrayElements(attrIds, attr, 0);
- env->ReleaseByteArrayElements(address, addr, 0);
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean getItemAttrRspNative(JNIEnv* env, jobject object,
- jbyteArray address, jint rspStatus,
- jbyte numAttr, jintArray attrIds,
- jobjectArray textArray) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- if (numAttr > BTRC_MAX_ELEM_ATTR_SIZE) {
- ALOGE("get_element_attr_rsp: number of attributes exceed maximum");
- return JNI_FALSE;
- }
-
- btrc_element_attr_val_t* pAttrs = new btrc_element_attr_val_t[numAttr];
- if (!pAttrs) {
- ALOGE("%s: not have enough memory", __func__);
- env->ReleaseByteArrayElements(address, addr, 0);
- return JNI_FALSE;
- }
-
- jint* attr = NULL;
- if (attrIds != NULL) {
- attr = env->GetIntArrayElements(attrIds, NULL);
- if (!attr) {
- delete[] pAttrs;
- jniThrowIOException(env, EINVAL);
- env->ReleaseByteArrayElements(address, addr, 0);
- return JNI_FALSE;
- }
- }
-
- for (int attr_cnt = 0; attr_cnt < numAttr; ++attr_cnt) {
- pAttrs[attr_cnt].attr_id = attr[attr_cnt];
- ScopedLocalRef<jstring> text(
- env, (jstring)env->GetObjectArrayElement(textArray, attr_cnt));
-
- if (!copy_jstring(pAttrs[attr_cnt].text, BTRC_MAX_ATTR_STR_LEN, text.get(),
- env)) {
- rspStatus = BTRC_STS_INTERNAL_ERR;
- ALOGE("%s: Failed to copy attributes", __func__);
- break;
- }
- }
- RawAddress rawAddress;
- rawAddress.FromOctets((uint8_t*)addr);
-
- bt_status_t status = sBluetoothAvrcpInterface->get_item_attr_rsp(
- rawAddress, (btrc_status_t)rspStatus, numAttr, pAttrs);
- if (status != BT_STATUS_SUCCESS)
- ALOGE("Failed get_item_attr_rsp, status: %d", status);
-
- if (pAttrs) delete[] pAttrs;
- if (attr) env->ReleaseIntArrayElements(attrIds, attr, 0);
- env->ReleaseByteArrayElements(address, addr, 0);
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean registerNotificationRspPlayStatusNative(JNIEnv* env,
- jobject object,
- jint type,
- jint playStatus) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- btrc_register_notification_t param;
- param.play_status = (btrc_play_status_t)playStatus;
-
- bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
- BTRC_EVT_PLAY_STATUS_CHANGED, (btrc_notification_type_t)type, ¶m);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed register_notification_rsp play status, status: %d", status);
- }
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean registerNotificationRspTrackChangeNative(JNIEnv* env,
- jobject object,
- jint type,
- jbyteArray track) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- jbyte* trk = env->GetByteArrayElements(track, NULL);
- if (!trk) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- btrc_register_notification_t param;
- uint64_t uid = 0;
- for (int uid_idx = 0; uid_idx < BTRC_UID_SIZE; ++uid_idx) {
- param.track[uid_idx] = trk[uid_idx];
- uid = uid + (trk[uid_idx] << (BTRC_UID_SIZE - 1 - uid_idx));
- }
-
- ALOGV("%s: Sending track change notification: %d -> %" PRIu64, __func__, type,
- uid);
-
- bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
- BTRC_EVT_TRACK_CHANGE, (btrc_notification_type_t)type, ¶m);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed register_notification_rsp track change, status: %d", status);
- }
-
- env->ReleaseByteArrayElements(track, trk, 0);
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean registerNotificationRspPlayPosNative(JNIEnv* env,
- jobject object, jint type,
- jint playPos) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- btrc_register_notification_t param;
- param.song_pos = (uint32_t)playPos;
-
- bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
- BTRC_EVT_PLAY_POS_CHANGED, (btrc_notification_type_t)type, ¶m);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed register_notification_rsp play position, status: %d", status);
- }
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean registerNotificationRspNowPlayingChangedNative(JNIEnv* env,
- jobject object,
- jint type) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- btrc_register_notification_t param;
- bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
- BTRC_EVT_NOW_PLAYING_CONTENT_CHANGED, (btrc_notification_type_t)type,
- ¶m);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed register_notification_rsp, nowPlaying Content status: %d",
- status);
- }
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean registerNotificationRspUIDsChangedNative(JNIEnv* env,
- jobject object,
- jint type,
- jint uidCounter) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- btrc_register_notification_t param;
- param.uids_changed.uid_counter = (uint16_t)uidCounter;
-
- bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
- BTRC_EVT_UIDS_CHANGED, (btrc_notification_type_t)type, ¶m);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed register_notification_rsp, uids changed status: %d", status);
- }
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean registerNotificationRspAddrPlayerChangedNative(
- JNIEnv* env, jobject object, jint type, jint playerId, jint uidCounter) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- btrc_register_notification_t param;
- param.addr_player_changed.player_id = (uint16_t)playerId;
- param.addr_player_changed.uid_counter = (uint16_t)uidCounter;
-
- bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
- BTRC_EVT_ADDR_PLAYER_CHANGE, (btrc_notification_type_t)type, ¶m);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed register_notification_rsp address player changed status: %d",
- status);
- }
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean registerNotificationRspAvalPlayerChangedNative(JNIEnv* env,
- jobject object,
- jint type) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- btrc_register_notification_t param;
- bt_status_t status = sBluetoothAvrcpInterface->register_notification_rsp(
- BTRC_EVT_AVAL_PLAYER_CHANGE, (btrc_notification_type_t)type, ¶m);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE(
- "Failed register_notification_rsp available player changed status, "
- "status: %d",
- status);
- }
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean setVolumeNative(JNIEnv* env, jobject object, jint volume) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- bt_status_t status = sBluetoothAvrcpInterface->set_volume((uint8_t)volume);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed set_volume, status: %d", status);
- }
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-/* native response for scope as Media player */
-static jboolean mediaPlayerListRspNative(
- JNIEnv* env, jobject object, jbyteArray address, jint rspStatus,
- jint uidCounter, jbyte itemType, jint numItems, jintArray playerIds,
- jbyteArray playerTypes, jintArray playerSubtypes,
- jbyteArray playStatusValues, jshortArray featureBitmask,
- jobjectArray textArray) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- jbyte *p_playerTypes = NULL, *p_PlayStatusValues = NULL;
- jshort* p_FeatBitMaskValues = NULL;
- jint *p_playerIds = NULL, *p_playerSubTypes = NULL;
- btrc_folder_items_t* p_items = NULL;
- if (rspStatus == BTRC_STS_NO_ERROR) {
- /* allocate memory */
- p_playerIds = env->GetIntArrayElements(playerIds, NULL);
- p_playerTypes = env->GetByteArrayElements(playerTypes, NULL);
- p_playerSubTypes = env->GetIntArrayElements(playerSubtypes, NULL);
- p_PlayStatusValues = env->GetByteArrayElements(playStatusValues, NULL);
- p_FeatBitMaskValues = env->GetShortArrayElements(featureBitmask, NULL);
- p_items = new btrc_folder_items_t[numItems];
- /* deallocate memory and return if allocation failed */
- if (!p_playerIds || !p_playerTypes || !p_playerSubTypes ||
- !p_PlayStatusValues || !p_FeatBitMaskValues || !p_items) {
- if (p_playerIds) env->ReleaseIntArrayElements(playerIds, p_playerIds, 0);
- if (p_playerTypes)
- env->ReleaseByteArrayElements(playerTypes, p_playerTypes, 0);
- if (p_playerSubTypes)
- env->ReleaseIntArrayElements(playerSubtypes, p_playerSubTypes, 0);
- if (p_PlayStatusValues)
- env->ReleaseByteArrayElements(playStatusValues, p_PlayStatusValues, 0);
- if (p_FeatBitMaskValues)
- env->ReleaseShortArrayElements(featureBitmask, p_FeatBitMaskValues, 0);
- if (p_items) delete[] p_items;
-
- jniThrowIOException(env, EINVAL);
- ALOGE("%s: not have enough memory", __func__);
- return JNI_FALSE;
- }
-
- p_items->item_type = (uint8_t)itemType;
-
- /* copy list of media players along with other parameters */
- int itemIdx;
- for (itemIdx = 0; itemIdx < numItems; ++itemIdx) {
- p_items[itemIdx].player.player_id = p_playerIds[itemIdx];
- p_items[itemIdx].player.major_type = p_playerTypes[itemIdx];
- p_items[itemIdx].player.sub_type = p_playerSubTypes[itemIdx];
- p_items[itemIdx].player.play_status = p_PlayStatusValues[itemIdx];
- p_items[itemIdx].player.charset_id = BTRC_CHARSET_ID_UTF8;
-
- ScopedLocalRef<jstring> text(
- env, (jstring)env->GetObjectArrayElement(textArray, itemIdx));
- /* copy player name */
- if (!copy_jstring(p_items[itemIdx].player.name, BTRC_MAX_ATTR_STR_LEN,
- text.get(), env))
- break;
-
- /* Feature bit mask is 128-bit value each */
- for (int InnCnt = 0; InnCnt < 16; InnCnt++) {
- p_items[itemIdx].player.features[InnCnt] =
- (uint8_t)p_FeatBitMaskValues[(itemIdx * 16) + InnCnt];
- }
- }
-
- /* failed to copy list of media players */
- if (itemIdx < numItems) {
- rspStatus = BTRC_STS_INTERNAL_ERR;
- ALOGE("%s: Failed to copy Media player attributes", __func__);
- }
- }
-
- RawAddress* btAddr = (RawAddress*)addr;
- bt_status_t status = sBluetoothAvrcpInterface->get_folder_items_list_rsp(
- *btAddr, (btrc_status_t)rspStatus, uidCounter, numItems, p_items);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed get_folder_items_list_rsp, status: %d", status);
- }
-
- /* release allocated memory */
- if (p_items) delete[] p_items;
- if (p_playerTypes)
- env->ReleaseByteArrayElements(playerTypes, p_playerTypes, 0);
- if (p_playerSubTypes)
- env->ReleaseIntArrayElements(playerSubtypes, p_playerSubTypes, 0);
- if (p_PlayStatusValues)
- env->ReleaseByteArrayElements(playStatusValues, p_PlayStatusValues, 0);
- if (p_FeatBitMaskValues) {
- env->ReleaseShortArrayElements(featureBitmask, p_FeatBitMaskValues, 0);
- }
- env->ReleaseByteArrayElements(address, addr, 0);
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean getFolderItemsRspNative(
- JNIEnv* env, jobject object, jbyteArray address, jint rspStatus,
- jshort uidCounter, jbyte scope, jint numItems, jbyteArray folderType,
- jbyteArray playable, jbyteArray itemType, jbyteArray itemUidArray,
- jobjectArray displayNameArray, jintArray numAttrs, jintArray attributesIds,
- jobjectArray attributesArray) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- jbyte *p_playable = NULL, *p_item_uid = NULL;
- jbyte* p_item_types = NULL; /* Folder or Media Item */
- jint* p_attributesIds = NULL;
- jbyte* p_folder_types =
- NULL; /* Folder properties like Album/Genre/Artists etc */
- jint* p_num_attrs = NULL;
- btrc_folder_items_t* p_items = NULL;
- /* none of the parameters should be null when no error */
- if (rspStatus == BTRC_STS_NO_ERROR) {
- /* allocate memory to each rsp item */
- if (folderType != NULL)
- p_folder_types = env->GetByteArrayElements(folderType, NULL);
- if (playable != NULL)
- p_playable = env->GetByteArrayElements(playable, NULL);
- if (itemType != NULL)
- p_item_types = env->GetByteArrayElements(itemType, NULL);
- if (NULL != numAttrs)
- p_num_attrs = env->GetIntArrayElements(numAttrs, NULL);
- if (NULL != attributesIds)
- p_attributesIds = env->GetIntArrayElements(attributesIds, NULL);
- if (itemUidArray != NULL)
- p_item_uid = (jbyte*)env->GetByteArrayElements(itemUidArray, NULL);
-
- p_items = new btrc_folder_items_t[numItems];
-
- /* if memory alloc failed, release memory */
- if (p_items && p_folder_types && p_playable && p_item_types && p_item_uid &&
- /* attributes can be null if remote requests 0 attributes */
- ((numAttrs != NULL && p_num_attrs) || (!numAttrs && !p_num_attrs)) &&
- ((attributesIds != NULL && p_attributesIds) ||
- (!attributesIds && !p_attributesIds))) {
- memset(p_items, 0, sizeof(btrc_folder_items_t) * numItems);
- if (scope == BTRC_SCOPE_FILE_SYSTEM || scope == BTRC_SCOPE_SEARCH ||
- scope == BTRC_SCOPE_NOW_PLAYING) {
- int attribCopiedIndex = 0;
- for (int item_idx = 0; item_idx < numItems; item_idx++) {
- if (BTRC_ITEM_FOLDER == p_item_types[item_idx]) {
- btrc_folder_items_t* pitem = &p_items[item_idx];
-
- memcpy(pitem->folder.uid, p_item_uid + item_idx * BTRC_UID_SIZE,
- BTRC_UID_SIZE);
- pitem->item_type = (uint8_t)BTRC_ITEM_FOLDER;
- pitem->folder.charset_id = BTRC_CHARSET_ID_UTF8;
- pitem->folder.type = p_folder_types[item_idx];
- pitem->folder.playable = p_playable[item_idx];
-
- ScopedLocalRef<jstring> text(
- env, (jstring)env->GetObjectArrayElement(displayNameArray,
- item_idx));
- if (!copy_jstring(pitem->folder.name, BTRC_MAX_ATTR_STR_LEN,
- text.get(), env)) {
- rspStatus = BTRC_STS_INTERNAL_ERR;
- ALOGE("%s: failed to copy display name of folder item", __func__);
- break;
- }
- } else if (BTRC_ITEM_MEDIA == p_item_types[item_idx]) {
- btrc_folder_items_t* pitem = &p_items[item_idx];
- memcpy(pitem->media.uid, p_item_uid + item_idx * BTRC_UID_SIZE,
- BTRC_UID_SIZE);
-
- pitem->item_type = (uint8_t)BTRC_ITEM_MEDIA;
- pitem->media.charset_id = BTRC_CHARSET_ID_UTF8;
- pitem->media.type = BTRC_MEDIA_TYPE_AUDIO;
- pitem->media.num_attrs =
- (p_num_attrs != NULL) ? p_num_attrs[item_idx] : 0;
-
- ScopedLocalRef<jstring> text(
- env, (jstring)env->GetObjectArrayElement(displayNameArray,
- item_idx));
- if (!copy_jstring(pitem->media.name, BTRC_MAX_ATTR_STR_LEN,
- text.get(), env)) {
- rspStatus = BTRC_STS_INTERNAL_ERR;
- ALOGE("%s: failed to copy display name of media item", __func__);
- break;
- }
-
- /* copy item attributes */
- if (!copy_item_attributes(env, object, pitem, p_attributesIds,
- attributesArray, item_idx,
- attribCopiedIndex)) {
- ALOGE("%s: error in copying attributes of item = %s", __func__,
- pitem->media.name);
- rspStatus = BTRC_STS_INTERNAL_ERR;
- break;
- }
- attribCopiedIndex += pitem->media.num_attrs;
- }
- }
- }
- } else {
- rspStatus = BTRC_STS_INTERNAL_ERR;
- ALOGE("%s: unable to allocate memory", __func__);
- }
- }
-
- RawAddress* btAddr = (RawAddress*)addr;
- bt_status_t status = sBluetoothAvrcpInterface->get_folder_items_list_rsp(
- *btAddr, (btrc_status_t)rspStatus, uidCounter, numItems, p_items);
- if (status != BT_STATUS_SUCCESS)
- ALOGE("Failed get_folder_items_list_rsp, status: %d", status);
-
- /* Release allocated memory for all attributes in each media item */
- if (p_items) cleanup_items(p_items, numItems);
-
- /* Release allocated memory */
- if (p_folder_types)
- env->ReleaseByteArrayElements(folderType, p_folder_types, 0);
- if (p_playable) env->ReleaseByteArrayElements(playable, p_playable, 0);
- if (p_item_types) env->ReleaseByteArrayElements(itemType, p_item_types, 0);
- if (p_num_attrs) env->ReleaseIntArrayElements(numAttrs, p_num_attrs, 0);
- if (p_attributesIds)
- env->ReleaseIntArrayElements(attributesIds, p_attributesIds, 0);
- if (p_item_uid) env->ReleaseByteArrayElements(itemUidArray, p_item_uid, 0);
- if (p_items) delete[] p_items;
- env->ReleaseByteArrayElements(address, addr, 0);
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean setAddressedPlayerRspNative(JNIEnv* env, jobject object,
- jbyteArray address,
- jint rspStatus) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- RawAddress* btAddr = (RawAddress*)addr;
- bt_status_t status = sBluetoothAvrcpInterface->set_addressed_player_rsp(
- *btAddr, (btrc_status_t)rspStatus);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed set_addressed_player_rsp, status: %d", status);
- }
- env->ReleaseByteArrayElements(address, addr, 0);
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean setBrowsedPlayerRspNative(JNIEnv* env, jobject object,
- jbyteArray address, jint rspStatus,
- jbyte depth, jint numItems,
- jobjectArray textArray) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- btrc_br_folder_name_t* p_folders = NULL;
- if (rspStatus == BTRC_STS_NO_ERROR) {
- if (depth > 0) {
- p_folders = new btrc_br_folder_name_t[depth];
- }
-
- for (int folder_idx = 0; folder_idx < depth; folder_idx++) {
- /* copy folder names */
- ScopedLocalRef<jstring> text(
- env, (jstring)env->GetObjectArrayElement(textArray, folder_idx));
-
- if (!copy_jstring(p_folders[folder_idx].p_str, BTRC_MAX_ATTR_STR_LEN,
- text.get(), env)) {
- rspStatus = BTRC_STS_INTERNAL_ERR;
- delete[] p_folders;
- env->ReleaseByteArrayElements(address, addr, 0);
- ALOGE("%s: Failed to copy folder name", __func__);
- return JNI_FALSE;
- }
-
- p_folders[folder_idx].str_len =
- strlen((char*)p_folders[folder_idx].p_str);
- }
- }
-
- uint8_t folder_depth =
- depth; /* folder_depth is 0 if current folder is root */
- uint16_t charset_id = BTRC_CHARSET_ID_UTF8;
- RawAddress* btAddr = (RawAddress*)addr;
- bt_status_t status = sBluetoothAvrcpInterface->set_browsed_player_rsp(
- *btAddr, (btrc_status_t)rspStatus, numItems, charset_id, folder_depth,
- p_folders);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("%s: Failed set_browsed_player_rsp, status: %d", __func__, status);
- }
-
- if (depth > 0) {
- delete[] p_folders;
- }
-
- env->ReleaseByteArrayElements(address, addr, 0);
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean changePathRspNative(JNIEnv* env, jobject object,
- jbyteArray address, jint rspStatus,
- jint numItems) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- uint32_t nItems = (uint32_t)numItems;
- RawAddress* btAddr = (RawAddress*)addr;
- bt_status_t status = sBluetoothAvrcpInterface->change_path_rsp(
- *btAddr, (btrc_status_t)rspStatus, (uint32_t)nItems);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed change_path_rsp, status: %d", status);
- }
- env->ReleaseByteArrayElements(address, addr, 0);
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean searchRspNative(JNIEnv* env, jobject object, jbyteArray address,
- jint rspStatus, jint uidCounter,
- jint numItems) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- uint32_t nItems = (uint32_t)numItems;
- RawAddress* btAddr = (RawAddress*)addr;
- bt_status_t status = sBluetoothAvrcpInterface->search_rsp(
- *btAddr, (btrc_status_t)rspStatus, (uint32_t)uidCounter,
- (uint32_t)nItems);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed search_rsp, status: %d", status);
- }
-
- env->ReleaseByteArrayElements(address, addr, 0);
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean playItemRspNative(JNIEnv* env, jobject object,
- jbyteArray address, jint rspStatus) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- RawAddress* btAddr = (RawAddress*)addr;
- bt_status_t status = sBluetoothAvrcpInterface->play_item_rsp(
- *btAddr, (btrc_status_t)rspStatus);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed play_item_rsp, status: %d", status);
- }
- env->ReleaseByteArrayElements(address, addr, 0);
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean getTotalNumOfItemsRspNative(JNIEnv* env, jobject object,
- jbyteArray address, jint rspStatus,
- jint uidCounter, jint numItems) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- uint32_t nItems = (uint32_t)numItems;
- RawAddress* btAddr = (RawAddress*)addr;
- bt_status_t status = sBluetoothAvrcpInterface->get_total_num_of_items_rsp(
- *btAddr, (btrc_status_t)rspStatus, (uint32_t)uidCounter,
- (uint32_t)nItems);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed get_total_num_of_items_rsp, status: %d", status);
- }
- env->ReleaseByteArrayElements(address, addr, 0);
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jboolean addToNowPlayingRspNative(JNIEnv* env, jobject object,
- jbyteArray address, jint rspStatus) {
- if (!sBluetoothAvrcpInterface) {
- ALOGE("%s: sBluetoothAvrcpInterface is null", __func__);
- return JNI_FALSE;
- }
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- RawAddress* btAddr = (RawAddress*)addr;
- bt_status_t status = sBluetoothAvrcpInterface->add_to_now_playing_rsp(
- *btAddr, (btrc_status_t)rspStatus);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed add_to_now_playing_rsp, status: %d", status);
- }
- env->ReleaseByteArrayElements(address, addr, 0);
-
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static JNINativeMethod sMethods[] = {
- {"classInitNative", "()V", (void*)classInitNative},
- {"initNative", "()V", (void*)initNative},
- {"cleanupNative", "()V", (void*)cleanupNative},
- {"getPlayStatusRspNative", "([BIII)Z", (void*)getPlayStatusRspNative},
- {"getElementAttrRspNative", "([BB[I[Ljava/lang/String;)Z",
- (void*)getElementAttrRspNative},
- {"registerNotificationRspPlayStatusNative", "(II)Z",
- (void*)registerNotificationRspPlayStatusNative},
- {"registerNotificationRspTrackChangeNative", "(I[B)Z",
- (void*)registerNotificationRspTrackChangeNative},
- {"registerNotificationRspPlayPosNative", "(II)Z",
- (void*)registerNotificationRspPlayPosNative},
- {"setVolumeNative", "(I)Z", (void*)setVolumeNative},
-
- {"setAddressedPlayerRspNative", "([BI)Z",
- (void*)setAddressedPlayerRspNative},
-
- {"setBrowsedPlayerRspNative", "([BIBI[Ljava/lang/String;)Z",
- (void*)setBrowsedPlayerRspNative},
-
- {"mediaPlayerListRspNative", "([BIIBI[I[B[I[B[S[Ljava/lang/String;)Z",
- (void*)mediaPlayerListRspNative},
-
- {"getFolderItemsRspNative",
- "([BISBI[B[B[B[B[Ljava/lang/String;[I[I[Ljava/lang/String;)Z",
- (void*)getFolderItemsRspNative},
-
- {"changePathRspNative", "([BII)Z", (void*)changePathRspNative},
-
- {"getItemAttrRspNative", "([BIB[I[Ljava/lang/String;)Z",
- (void*)getItemAttrRspNative},
-
- {"playItemRspNative", "([BI)Z", (void*)playItemRspNative},
-
- {"getTotalNumOfItemsRspNative", "([BIII)Z",
- (void*)getTotalNumOfItemsRspNative},
-
- {"searchRspNative", "([BIII)Z", (void*)searchRspNative},
-
- {"addToNowPlayingRspNative", "([BI)Z", (void*)addToNowPlayingRspNative},
-
- {"registerNotificationRspAddrPlayerChangedNative", "(III)Z",
- (void*)registerNotificationRspAddrPlayerChangedNative},
-
- {"registerNotificationRspAvalPlayerChangedNative", "(I)Z",
- (void*)registerNotificationRspAvalPlayerChangedNative},
-
- {"registerNotificationRspUIDsChangedNative", "(II)Z",
- (void*)registerNotificationRspUIDsChangedNative},
-
- {"registerNotificationRspNowPlayingChangedNative", "(I)Z",
- (void*)registerNotificationRspNowPlayingChangedNative}};
-
-int register_com_android_bluetooth_avrcp(JNIEnv* env) {
- return jniRegisterNativeMethods(env, "com/android/bluetooth/avrcp/Avrcp",
- sMethods, NELEM(sMethods));
-}
-
-/* Helper function to copy attributes of item.
- * Assumes that all items in response have same number of attributes
- *
- * returns true on succes, false otherwise.
-*/
-static bool copy_item_attributes(JNIEnv* env, jobject object,
- btrc_folder_items_t* pitem,
- jint* p_attributesIds,
- jobjectArray attributesArray, int item_idx,
- int attribCopiedIndex) {
- bool success = true;
-
- /* copy attributes of the item */
- if (0 < pitem->media.num_attrs) {
- int num_attrs = pitem->media.num_attrs;
- ALOGI("%s num_attr = %d", __func__, num_attrs);
- pitem->media.p_attrs = new btrc_element_attr_val_t[num_attrs];
- if (!pitem->media.p_attrs) {
- return false;
- }
-
- for (int tempAtrCount = 0; tempAtrCount < pitem->media.num_attrs;
- ++tempAtrCount) {
- pitem->media.p_attrs[tempAtrCount].attr_id =
- p_attributesIds[attribCopiedIndex + tempAtrCount];
-
- ScopedLocalRef<jstring> text(
- env, (jstring)env->GetObjectArrayElement(
- attributesArray, attribCopiedIndex + tempAtrCount));
-
- if (!copy_jstring(pitem->media.p_attrs[tempAtrCount].text,
- BTRC_MAX_ATTR_STR_LEN, text.get(), env)) {
- success = false;
- ALOGE("%s: failed to copy attributes", __func__);
- break;
- }
- }
- }
- return success;
-}
-
-/* Helper function to copy String data from java to native
- *
- * returns true on succes, false otherwise
- */
-static bool copy_jstring(uint8_t* str, int maxBytes, jstring jstr,
- JNIEnv* env) {
- if (str == NULL || jstr == NULL || env == NULL) return false;
-
- memset(str, 0, maxBytes);
- const char* p_str = env->GetStringUTFChars(jstr, NULL);
- size_t len = strnlen(p_str, maxBytes - 1);
- memcpy(str, p_str, len);
-
- env->ReleaseStringUTFChars(jstr, p_str);
- return true;
-}
-
-/* Helper function to cleanup items */
-static void cleanup_items(btrc_folder_items_t* p_items, int numItems) {
- for (int item_idx = 0; item_idx < numItems; item_idx++) {
- /* release memory for attributes in case item is media item */
- if ((BTRC_ITEM_MEDIA == p_items[item_idx].item_type) &&
- p_items[item_idx].media.p_attrs != NULL)
- delete[] p_items[item_idx].media.p_attrs;
- }
-}
-}
diff --git a/jni/com_android_bluetooth_avrcp_controller.cpp b/jni/com_android_bluetooth_avrcp_controller.cpp
index 7710a91..2d9e87b 100644
--- a/jni/com_android_bluetooth_avrcp_controller.cpp
+++ b/jni/com_android_bluetooth_avrcp_controller.cpp
@@ -24,6 +24,7 @@
#include "utils/Log.h"
#include <string.h>
+#include <shared_mutex>
namespace android {
static jmethodID method_handlePassthroughRsp;
@@ -46,23 +47,31 @@
static jmethodID method_handleChangeFolderRsp;
static jmethodID method_handleSetBrowsedPlayerRsp;
static jmethodID method_handleSetAddressedPlayerRsp;
+static jmethodID method_handleAddressedPlayerChanged;
+static jmethodID method_handleNowPlayingContentChanged;
static jclass class_MediaBrowser_MediaItem;
static jclass class_AvrcpPlayer;
static const btrc_ctrl_interface_t* sBluetoothAvrcpInterface = NULL;
static jobject sCallbacksObj = NULL;
+static std::shared_timed_mutex sCallbacks_mutex;
static void btavrcp_passthrough_response_callback(const RawAddress& bd_addr,
int id, int pressed) {
ALOGI("%s: id: %d, pressed: %d", __func__, id, pressed);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
ScopedLocalRef<jbyteArray> addr(
sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for passthrough response");
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
return;
}
@@ -74,8 +83,13 @@
static void btavrcp_groupnavigation_response_callback(int id, int pressed) {
ALOGV("%s", __func__);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
sCallbackEnv->CallVoidMethod(sCallbacksObj, method_handleGroupNavigationRsp,
(jint)id, (jint)pressed);
@@ -84,13 +98,18 @@
static void btavrcp_connection_state_callback(bool rc_connect, bool br_connect,
const RawAddress& bd_addr) {
ALOGI("%s: conn state: rc: %d br: %d", __func__, rc_connect, br_connect);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
ScopedLocalRef<jbyteArray> addr(
sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for connection state");
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
return;
}
@@ -104,13 +123,18 @@
static void btavrcp_get_rcfeatures_callback(const RawAddress& bd_addr,
int features) {
ALOGV("%s", __func__);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
ScopedLocalRef<jbyteArray> addr(
sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr ");
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
return;
}
@@ -123,13 +147,18 @@
static void btavrcp_setplayerapplicationsetting_rsp_callback(
const RawAddress& bd_addr, uint8_t accepted) {
ALOGV("%s", __func__);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
ScopedLocalRef<jbyteArray> addr(
sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr ");
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
return;
}
@@ -144,13 +173,18 @@
btrc_player_app_attr_t* app_attrs, uint8_t num_ext_attr,
btrc_player_app_ext_attr_t* ext_attrs) {
ALOGI("%s", __func__);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
ScopedLocalRef<jbyteArray> addr(
sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr ");
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
return;
}
sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
@@ -168,7 +202,7 @@
ScopedLocalRef<jbyteArray> playerattribs(
sCallbackEnv.get(), sCallbackEnv->NewByteArray(arraylen));
if (!playerattribs.get()) {
- ALOGE("Fail to new jbyteArray playerattribs ");
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
return;
}
@@ -191,13 +225,18 @@
static void btavrcp_playerapplicationsetting_changed_callback(
const RawAddress& bd_addr, const btrc_player_settings_t& vals) {
ALOGI("%s", __func__);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
ScopedLocalRef<jbyteArray> addr(
sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
if (!addr.get()) {
- ALOGE("Fail to get new array ");
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
return;
}
sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
@@ -229,13 +268,18 @@
static void btavrcp_set_abs_vol_cmd_callback(const RawAddress& bd_addr,
uint8_t abs_vol, uint8_t label) {
ALOGI("%s", __func__);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
ScopedLocalRef<jbyteArray> addr(
sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
if (!addr.get()) {
- ALOGE("Fail to get new array ");
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
return;
}
@@ -248,13 +292,18 @@
static void btavrcp_register_notification_absvol_callback(
const RawAddress& bd_addr, uint8_t label) {
ALOGI("%s", __func__);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
ScopedLocalRef<jbyteArray> addr(
sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
if (!addr.get()) {
- ALOGE("Fail to get new array ");
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
return;
}
@@ -273,13 +322,18 @@
* Assuming text feild to be null terminated.
*/
ALOGI("%s", __func__);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
ScopedLocalRef<jbyteArray> addr(
sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
if (!addr.get()) {
- ALOGE("Fail to get new array ");
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
return;
}
@@ -323,13 +377,18 @@
uint32_t song_len,
uint32_t song_pos) {
ALOGI("%s", __func__);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
ScopedLocalRef<jbyteArray> addr(
sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
if (!addr.get()) {
- ALOGE("Fail to get new array ");
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
return;
}
sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
@@ -341,13 +400,18 @@
static void btavrcp_play_status_changed_callback(
const RawAddress& bd_addr, btrc_play_status_t play_status) {
ALOGI("%s", __func__);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
ScopedLocalRef<jbyteArray> addr(
sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
if (!addr.get()) {
- ALOGE("Fail to get new array ");
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
return;
}
sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
@@ -364,8 +428,23 @@
* counterparts by calling the java constructor for each of the items.
*/
ALOGV("%s count %d", __func__, count);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&bd_addr.address);
// Inspect if the first element is a folder/item or player listing. They are
// always exclusive.
@@ -399,17 +478,7 @@
return;
}
// Parse UID
- ScopedLocalRef<jbyteArray> uidByteArray(
- sCallbackEnv.get(),
- sCallbackEnv->NewByteArray(sizeof(uint8_t) * BTRC_UID_SIZE));
- if (!uidByteArray.get()) {
- ALOGE("%s can't allocate uid array!", __func__);
- return;
- }
- sCallbackEnv->SetByteArrayRegion(uidByteArray.get(), 0,
- BTRC_UID_SIZE * sizeof(uint8_t),
- (jbyte*)item->media.uid);
-
+ long long uid = *(long long*)item->media.uid;
// Parse Attrs
ScopedLocalRef<jintArray> attrIdArray(
sCallbackEnv.get(),
@@ -435,10 +504,6 @@
ScopedLocalRef<jstring> attrValStr(
sCallbackEnv.get(),
sCallbackEnv->NewStringUTF((char*)(item->media.p_attrs[j].text)));
- if (!uidByteArray.get()) {
- ALOGE("%s can't allocate uid array!", __func__);
- return;
- }
sCallbackEnv->SetObjectArrayElement(attrValArray.get(), j,
attrValStr.get());
}
@@ -446,9 +511,9 @@
ScopedLocalRef<jobject> mediaObj(
sCallbackEnv.get(),
(jobject)sCallbackEnv->CallObjectMethod(
- sCallbacksObj, method_createFromNativeMediaItem,
- uidByteArray.get(), (jint)item->media.type, mediaName.get(),
- attrIdArray.get(), attrValArray.get()));
+ sCallbacksObj, method_createFromNativeMediaItem, uid,
+ (jint)item->media.type, mediaName.get(), attrIdArray.get(),
+ attrValArray.get()));
if (!mediaObj.get()) {
ALOGE("%s failed to creae MediaItem for type ITEM_MEDIA", __func__);
return;
@@ -467,22 +532,12 @@
return;
}
// Parse UID
- ScopedLocalRef<jbyteArray> uidByteArray(
- sCallbackEnv.get(),
- sCallbackEnv->NewByteArray(sizeof(uint8_t) * BTRC_UID_SIZE));
- if (!uidByteArray.get()) {
- ALOGE("%s can't allocate uid array!", __func__);
- return;
- }
- sCallbackEnv->SetByteArrayRegion(uidByteArray.get(), 0,
- BTRC_UID_SIZE * sizeof(uint8_t),
- (jbyte*)item->folder.uid);
-
+ long long uid = *(long long*)item->folder.uid;
ScopedLocalRef<jobject> folderObj(
sCallbackEnv.get(),
(jobject)sCallbackEnv->CallObjectMethod(
- sCallbacksObj, method_createFromNativeFolderItem,
- uidByteArray.get(), (jint)item->folder.type, folderName.get(),
+ sCallbacksObj, method_createFromNativeFolderItem, uid,
+ (jint)item->folder.type, folderName.get(),
(jint)item->folder.playable));
if (!folderObj.get()) {
ALOGE("%s failed to create MediaItem for type ITEM_FOLDER", __func__);
@@ -540,43 +595,129 @@
if (isPlayerListing) {
sCallbackEnv->CallVoidMethod(sCallbacksObj, method_handleGetPlayerItemsRsp,
- itemArray.get());
+ addr.get(), itemArray.get());
} else {
sCallbackEnv->CallVoidMethod(sCallbacksObj, method_handleGetFolderItemsRsp,
- status, itemArray.get());
+ addr.get(), status, itemArray.get());
}
}
static void btavrcp_change_path_callback(const RawAddress& bd_addr,
uint32_t count) {
ALOGI("%s count %d", __func__, count);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&bd_addr.address);
sCallbackEnv->CallVoidMethod(sCallbacksObj, method_handleChangeFolderRsp,
- (jint)count);
+ addr.get(), (jint)count);
}
static void btavrcp_set_browsed_player_callback(const RawAddress& bd_addr,
uint8_t num_items,
uint8_t depth) {
ALOGI("%s items %d depth %d", __func__, num_items, depth);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&bd_addr.address);
sCallbackEnv->CallVoidMethod(sCallbacksObj, method_handleSetBrowsedPlayerRsp,
- (jint)num_items, (jint)depth);
+ addr.get(), (jint)num_items, (jint)depth);
}
static void btavrcp_set_addressed_player_callback(const RawAddress& bd_addr,
uint8_t status) {
ALOGI("%s status %d", __func__, status);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
+ CallbackEnv sCallbackEnv(__func__);
+ if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&bd_addr.address);
+
+ sCallbackEnv->CallVoidMethod(sCallbacksObj,
+ method_handleSetAddressedPlayerRsp, addr.get(),
+ (jint)status);
+}
+
+static void btavrcp_addressed_player_changed_callback(const RawAddress& bd_addr,
+ uint16_t id) {
+ ALOGI("%s status %d", __func__, id);
+ std::shared_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
+ CallbackEnv sCallbackEnv(__func__);
+ if (!sCallbackEnv.valid()) return;
+ if (!sCallbacksObj) {
+ ALOGE("%s: sCallbacksObj is null", __func__);
+ return;
+ }
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&bd_addr.address);
+
+ sCallbackEnv->CallVoidMethod(
+ sCallbacksObj, method_handleAddressedPlayerChanged, addr.get(), (jint)id);
+}
+
+static void btavrcp_now_playing_content_changed_callback(
+ const RawAddress& bd_addr) {
+ ALOGI("%s", __func__);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ ALOGE("%s: Failed to allocate a new byte array", __func__);
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&bd_addr.address);
sCallbackEnv->CallVoidMethod(
- sCallbacksObj, method_handleSetAddressedPlayerRsp, (jint)status);
+ sCallbacksObj, method_handleNowPlayingContentChanged, addr.get());
}
static btrc_ctrl_callbacks_t sBluetoothAvrcpCallbacks = {
@@ -596,7 +737,9 @@
btavrcp_get_folder_items_callback,
btavrcp_change_path_callback,
btavrcp_set_browsed_player_callback,
- btavrcp_set_addressed_player_callback};
+ btavrcp_set_addressed_player_callback,
+ btavrcp_addressed_player_changed_callback,
+ btavrcp_now_playing_content_changed_callback};
static void classInitNative(JNIEnv* env, jclass clazz) {
method_handlePassthroughRsp =
@@ -636,32 +779,39 @@
method_handleGetFolderItemsRsp =
env->GetMethodID(clazz, "handleGetFolderItemsRsp",
- "(I[Landroid/media/browse/MediaBrowser$MediaItem;)V");
+ "([BI[Landroid/media/browse/MediaBrowser$MediaItem;)V");
method_handleGetPlayerItemsRsp = env->GetMethodID(
clazz, "handleGetPlayerItemsRsp",
- "([Lcom/android/bluetooth/avrcpcontroller/AvrcpPlayer;)V");
+ "([B[Lcom/android/bluetooth/avrcpcontroller/AvrcpPlayer;)V");
method_createFromNativeMediaItem =
env->GetMethodID(clazz, "createFromNativeMediaItem",
- "([BILjava/lang/String;[I[Ljava/lang/String;)Landroid/"
+ "(JILjava/lang/String;[I[Ljava/lang/String;)Landroid/"
"media/browse/MediaBrowser$MediaItem;");
method_createFromNativeFolderItem = env->GetMethodID(
clazz, "createFromNativeFolderItem",
- "([BILjava/lang/String;I)Landroid/media/browse/MediaBrowser$MediaItem;");
+ "(JILjava/lang/String;I)Landroid/media/browse/MediaBrowser$MediaItem;");
method_createFromNativePlayerItem =
env->GetMethodID(clazz, "createFromNativePlayerItem",
"(ILjava/lang/String;[BII)Lcom/android/bluetooth/"
"avrcpcontroller/AvrcpPlayer;");
method_handleChangeFolderRsp =
- env->GetMethodID(clazz, "handleChangeFolderRsp", "(I)V");
+ env->GetMethodID(clazz, "handleChangeFolderRsp", "([BI)V");
method_handleSetBrowsedPlayerRsp =
- env->GetMethodID(clazz, "handleSetBrowsedPlayerRsp", "(II)V");
+ env->GetMethodID(clazz, "handleSetBrowsedPlayerRsp", "([BII)V");
method_handleSetAddressedPlayerRsp =
- env->GetMethodID(clazz, "handleSetAddressedPlayerRsp", "(I)V");
+ env->GetMethodID(clazz, "handleSetAddressedPlayerRsp", "([BI)V");
+ method_handleAddressedPlayerChanged =
+ env->GetMethodID(clazz, "handleAddressedPlayerChanged", "([BI)V");
+ method_handleNowPlayingContentChanged =
+ env->GetMethodID(clazz, "handleNowPlayingContentChanged", "([B)V");
+
ALOGI("%s: succeeds", __func__);
}
static void initNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
+
jclass tmpMediaItem =
env->FindClass("android/media/browse/MediaBrowser$MediaItem");
class_MediaBrowser_MediaItem = (jclass)env->NewGlobalRef(tmpMediaItem);
@@ -709,6 +859,8 @@
}
static void cleanupNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_timed_mutex> lock(sCallbacks_mutex);
+
const bt_interface_t* btInf = getBluetoothInterface();
if (btInf == NULL) {
ALOGE("Bluetooth module is not loaded");
@@ -961,7 +1113,7 @@
static void changeFolderPathNative(JNIEnv* env, jobject object,
jbyteArray address, jbyte direction,
- jbyteArray uidarr) {
+ jlong uid) {
if (!sBluetoothAvrcpInterface) return;
jbyte* addr = env->GetByteArrayElements(address, NULL);
if (!addr) {
@@ -969,22 +1121,22 @@
return;
}
- jbyte* uid = env->GetByteArrayElements(uidarr, NULL);
- if (!uid) {
- jniThrowIOException(env, EINVAL);
- return;
- }
+ // jbyte* uid = env->GetByteArrayElements(uidarr, NULL);
+ // if (!uid) {
+ // jniThrowIOException(env, EINVAL);
+ // return;
+ //}
ALOGI("%s: sBluetoothAvrcpInterface: %p", __func__, sBluetoothAvrcpInterface);
RawAddress rawAddress;
rawAddress.FromOctets((uint8_t*)addr);
bt_status_t status = sBluetoothAvrcpInterface->change_folder_path_cmd(
- rawAddress, (uint8_t)direction, (uint8_t*)uid);
+ rawAddress, (uint8_t)direction, (uint8_t*)&uid);
if (status != BT_STATUS_SUCCESS) {
ALOGE("Failed sending changeFolderPathNative command, status: %d", status);
}
- env->ReleaseByteArrayElements(address, addr, 0);
+ // env->ReleaseByteArrayElements(address, addr, 0);
}
static void setBrowsedPlayerNative(JNIEnv* env, jobject object,
@@ -1029,7 +1181,7 @@
}
static void playItemNative(JNIEnv* env, jobject object, jbyteArray address,
- jbyte scope, jbyteArray uidArr, jint uidCounter) {
+ jbyte scope, jlong uid, jint uidCounter) {
if (!sBluetoothAvrcpInterface) return;
jbyte* addr = env->GetByteArrayElements(address, NULL);
if (!addr) {
@@ -1037,17 +1189,17 @@
return;
}
- jbyte* uid = env->GetByteArrayElements(uidArr, NULL);
- if (!uid) {
- jniThrowIOException(env, EINVAL);
- return;
- }
+ // jbyte* uid = env->GetByteArrayElements(uidArr, NULL);
+ // if (!uid) {
+ // jniThrowIOException(env, EINVAL);
+ // return;
+ // }
RawAddress rawAddress;
rawAddress.FromOctets((uint8_t*)addr);
ALOGI("%s: sBluetoothAvrcpInterface: %p", __func__, sBluetoothAvrcpInterface);
bt_status_t status = sBluetoothAvrcpInterface->play_item_cmd(
- rawAddress, (uint8_t)scope, (uint8_t*)uid, (uint16_t)uidCounter);
+ rawAddress, (uint8_t)scope, (uint8_t*)&uid, (uint16_t)uidCounter);
if (status != BT_STATUS_SUCCESS) {
ALOGE("Failed sending playItemNative command, status: %d", status);
}
@@ -1071,8 +1223,8 @@
{"getNowPlayingListNative", "([BII)V", (void*)getNowPlayingListNative},
{"getFolderListNative", "([BII)V", (void*)getFolderListNative},
{"getPlayerListNative", "([BII)V", (void*)getPlayerListNative},
- {"changeFolderPathNative", "([BB[B)V", (void*)changeFolderPathNative},
- {"playItemNative", "([BB[BI)V", (void*)playItemNative},
+ {"changeFolderPathNative", "([BBJ)V", (void*)changeFolderPathNative},
+ {"playItemNative", "([BBJI)V", (void*)playItemNative},
{"setBrowsedPlayerNative", "([BI)V", (void*)setBrowsedPlayerNative},
{"setAddressedPlayerNative", "([BI)V", (void*)setAddressedPlayerNative},
};
diff --git a/jni/com_android_bluetooth_avrcp_target.cpp b/jni/com_android_bluetooth_avrcp_target.cpp
index 8ac04e4..a6c8d28 100644
--- a/jni/com_android_bluetooth_avrcp_target.cpp
+++ b/jni/com_android_bluetooth_avrcp_target.cpp
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-#define LOG_TAG "NewAvrcpTargetJni"
+#define LOG_TAG "AvrcpTargetJni"
#include <base/bind.h>
+#include <base/callback.h>
#include <map>
#include <mutex>
#include <shared_mutex>
@@ -784,10 +785,18 @@
j_bdaddr);
}
-static void sendVolumeChangedNative(JNIEnv* env, jobject object, jint volume) {
+static void sendVolumeChangedNative(JNIEnv* env, jobject object,
+ jstring address, jint volume) {
+ const char* tmp_addr = env->GetStringUTFChars(address, 0);
+ RawAddress bdaddr;
+ bool success = RawAddress::FromString(tmp_addr, bdaddr);
+ env->ReleaseStringUTFChars(address, tmp_addr);
+
+ if (!success) return;
+
ALOGD("%s", __func__);
- for (const auto& cb : volumeCallbackMap) {
- cb.second.Run(volume & 0x7F);
+ if (volumeCallbackMap.find(bdaddr) != volumeCallbackMap.end()) {
+ volumeCallbackMap.find(bdaddr)->second.Run(volume & 0x7F);
}
}
@@ -814,7 +823,8 @@
(void*)connectDeviceNative},
{"disconnectDeviceNative", "(Ljava/lang/String;)Z",
(void*)disconnectDeviceNative},
- {"sendVolumeChangedNative", "(I)V", (void*)sendVolumeChangedNative},
+ {"sendVolumeChangedNative", "(Ljava/lang/String;I)V",
+ (void*)sendVolumeChangedNative},
};
int register_com_android_bluetooth_avrcp_target(JNIEnv* env) {
diff --git a/jni/com_android_bluetooth_btservice_AdapterService.cpp b/jni/com_android_bluetooth_btservice_AdapterService.cpp
index 976e388..6b4867f 100644
--- a/jni/com_android_bluetooth_btservice_AdapterService.cpp
+++ b/jni/com_android_bluetooth_btservice_AdapterService.cpp
@@ -40,8 +40,6 @@
#include <hardware/bluetooth.h>
#include <mutex>
-using base::StringPrintf;
-using bluetooth::Uuid;
using android::bluetooth::BluetoothSocketManagerBinderServer;
namespace android {
@@ -1217,6 +1215,25 @@
env->ReleaseByteArrayElements(address, addr, 0);
}
+static jbyteArray obfuscateAddressNative(JNIEnv* env, jobject obj,
+ jbyteArray address) {
+ ALOGV("%s", __func__);
+ if (!sBluetoothInterface) return env->NewByteArray(0);
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+ if (addr == nullptr) {
+ jniThrowIOException(env, EINVAL);
+ return env->NewByteArray(0);
+ }
+ RawAddress addr_obj = {};
+ addr_obj.FromOctets((uint8_t*)addr);
+ std::string output = sBluetoothInterface->obfuscate_address(addr_obj);
+ jsize output_size = output.size() * sizeof(char);
+ jbyteArray output_bytes = env->NewByteArray(output_size);
+ env->SetByteArrayRegion(output_bytes, 0, output_size,
+ (const jbyte*)output.data());
+ return output_bytes;
+}
+
static JNINativeMethod sMethods[] = {
/* name, signature, funcPtr */
{"classInitNative", "()V", (void*)classInitNative},
@@ -1251,7 +1268,8 @@
{"dumpMetricsNative", "()[B", (void*)dumpMetricsNative},
{"factoryResetNative", "()Z", (void*)factoryResetNative},
{"interopDatabaseClearNative", "()V", (void*)interopDatabaseClearNative},
- {"interopDatabaseAddNative", "(I[BI)V", (void*)interopDatabaseAddNative}};
+ {"interopDatabaseAddNative", "(I[BI)V", (void*)interopDatabaseAddNative},
+ {"obfuscateAddressNative", "([B)[B", (void*)obfuscateAddressNative}};
int register_com_android_bluetooth_btservice_AdapterService(JNIEnv* env) {
return jniRegisterNativeMethods(
@@ -1306,12 +1324,6 @@
return JNI_ERR;
}
- status = android::register_com_android_bluetooth_avrcp(e);
- if (status < 0) {
- ALOGE("jni avrcp target registration failure: %d", status);
- return JNI_ERR;
- }
-
status = android::register_com_android_bluetooth_avrcp_target(e);
if (status < 0) {
ALOGE("jni new avrcp target registration failure: %d", status);
@@ -1335,12 +1347,6 @@
return JNI_ERR;
}
- status = android::register_com_android_bluetooth_hdp(e);
- if (status < 0) {
- ALOGE("jni hdp registration failure: %d", status);
- return JNI_ERR;
- }
-
status = android::register_com_android_bluetooth_pan(e);
if (status < 0) {
ALOGE("jni pan registration failure: %d", status);
diff --git a/jni/com_android_bluetooth_gatt.cpp b/jni/com_android_bluetooth_gatt.cpp
index 86e667b..4521515 100644
--- a/jni/com_android_bluetooth_gatt.cpp
+++ b/jni/com_android_bluetooth_gatt.cpp
@@ -24,6 +24,7 @@
#include "utils/Log.h"
#include <base/bind.h>
+#include <base/callback.h>
#include <string.h>
#include <array>
#include <memory>
diff --git a/jni/com_android_bluetooth_hdp.cpp b/jni/com_android_bluetooth_hdp.cpp
deleted file mode 100644
index 3abc243..0000000
--- a/jni/com_android_bluetooth_hdp.cpp
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define LOG_TAG "BluetoothHealthServiceJni"
-
-#define LOG_NDEBUG 0
-
-#include "android_runtime/AndroidRuntime.h"
-#include "com_android_bluetooth.h"
-#include "hardware/bt_hl.h"
-#include "utils/Log.h"
-
-#include <string.h>
-
-namespace android {
-
-static jmethodID method_onAppRegistrationState;
-static jmethodID method_onChannelStateChanged;
-
-static const bthl_interface_t* sBluetoothHdpInterface = NULL;
-static jobject mCallbacksObj = NULL;
-
-// Define callback functions
-static void app_registration_state_callback(int app_id,
- bthl_app_reg_state_t state) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onAppRegistrationState,
- app_id, (jint)state);
-}
-
-static void channel_state_callback(int app_id, RawAddress* bd_addr,
- int mdep_cfg_index, int channel_id,
- bthl_channel_state_t state, int fd) {
- CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
- ScopedLocalRef<jbyteArray> addr(
- sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
- if (!addr.get()) {
- ALOGE("Fail to new jbyteArray bd addr for channel state");
- return;
- }
-
- // TODO(BT) check if fd is only valid for BTHH_CONN_STATE_CONNECTED state
- jobject fileDescriptor = NULL;
- if (state == BTHL_CONN_STATE_CONNECTED) {
- fileDescriptor = jniCreateFileDescriptor(sCallbackEnv.get(), fd);
- if (!fileDescriptor) {
- ALOGE("Failed to convert file descriptor, fd: %d", fd);
- return;
- }
- }
-
- sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
- (jbyte*)bd_addr);
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onChannelStateChanged,
- app_id, addr.get(), mdep_cfg_index, channel_id,
- (jint)state, fileDescriptor);
-}
-
-static bthl_callbacks_t sBluetoothHdpCallbacks = {
- sizeof(sBluetoothHdpCallbacks), app_registration_state_callback,
- channel_state_callback};
-
-// Define native functions
-
-static void classInitNative(JNIEnv* env, jclass clazz) {
- method_onAppRegistrationState =
- env->GetMethodID(clazz, "onAppRegistrationState", "(II)V");
- method_onChannelStateChanged = env->GetMethodID(
- clazz, "onChannelStateChanged", "(I[BIIILjava/io/FileDescriptor;)V");
- ALOGI("%s: succeeds", __func__);
-}
-
-static void initializeNative(JNIEnv* env, jobject object) {
- const bt_interface_t* btInf = getBluetoothInterface();
- if (btInf == NULL) {
- ALOGE("Bluetooth module is not loaded");
- return;
- }
-
- if (sBluetoothHdpInterface != NULL) {
- ALOGW("Cleaning up Bluetooth Health Interface before initializing...");
- sBluetoothHdpInterface->cleanup();
- sBluetoothHdpInterface = NULL;
- }
-
- if (mCallbacksObj != NULL) {
- ALOGW("Cleaning up Bluetooth Health callback object");
- env->DeleteGlobalRef(mCallbacksObj);
- mCallbacksObj = NULL;
- }
-
- sBluetoothHdpInterface =
- (bthl_interface_t*)btInf->get_profile_interface(BT_PROFILE_HEALTH_ID);
- if (sBluetoothHdpInterface == NULL) {
- ALOGE("Failed to get Bluetooth Health Interface");
- return;
- }
-
- bt_status_t status = sBluetoothHdpInterface->init(&sBluetoothHdpCallbacks);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed to initialize Bluetooth HDP, status: %d", status);
- sBluetoothHdpInterface = NULL;
- return;
- }
-
- mCallbacksObj = env->NewGlobalRef(object);
-}
-
-static void cleanupNative(JNIEnv* env, jobject object) {
- const bt_interface_t* btInf = getBluetoothInterface();
-
- if (btInf == NULL) {
- ALOGE("Bluetooth module is not loaded");
- return;
- }
-
- if (sBluetoothHdpInterface != NULL) {
- ALOGW("Cleaning up Bluetooth Health Interface...");
- sBluetoothHdpInterface->cleanup();
- sBluetoothHdpInterface = NULL;
- }
-
- if (mCallbacksObj != NULL) {
- ALOGW("Cleaning up Bluetooth Health object");
- env->DeleteGlobalRef(mCallbacksObj);
- mCallbacksObj = NULL;
- }
-}
-
-static jint registerHealthAppNative(JNIEnv* env, jobject object, jint data_type,
- jint role, jstring name,
- jint channel_type) {
- if (!sBluetoothHdpInterface) {
- ALOGE(
- "Failed to register health app. No Bluetooth Health Interface "
- "available");
- return -1;
- }
-
- bthl_mdep_cfg_t mdep_cfg;
- mdep_cfg.mdep_role = (bthl_mdep_role_t)role;
- mdep_cfg.data_type = data_type;
- mdep_cfg.channel_type = (bthl_channel_type_t)channel_type;
- // TODO(BT) pass all the followings in from java instead of reuse name
- mdep_cfg.mdep_description = env->GetStringUTFChars(name, NULL);
-
- bthl_reg_param_t reg_param;
- reg_param.application_name = env->GetStringUTFChars(name, NULL);
- reg_param.provider_name = NULL;
- reg_param.srv_name = NULL;
- reg_param.srv_desp = NULL;
- reg_param.number_of_mdeps = 1;
- reg_param.mdep_cfg = &mdep_cfg;
-
- int app_id;
- bt_status_t status =
- sBluetoothHdpInterface->register_application(®_param, &app_id);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed register health app, status: %d", status);
- return -1;
- }
-
- env->ReleaseStringUTFChars(name, mdep_cfg.mdep_description);
- env->ReleaseStringUTFChars(name, reg_param.application_name);
- return app_id;
-}
-
-static jboolean unregisterHealthAppNative(JNIEnv* env, jobject object,
- int app_id) {
- if (!sBluetoothHdpInterface) return JNI_FALSE;
-
- bt_status_t status = sBluetoothHdpInterface->unregister_application(app_id);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed to unregister app %d, status: %d", app_id, status);
- }
- return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
-}
-
-static jint connectChannelNative(JNIEnv* env, jobject object,
- jbyteArray address, jint app_id) {
- if (!sBluetoothHdpInterface) return -1;
-
- jbyte* addr = env->GetByteArrayElements(address, NULL);
- if (!addr) {
- ALOGE("Bluetooth device address null");
- return -1;
- }
-
- jint chan_id;
- bt_status_t status = sBluetoothHdpInterface->connect_channel(
- app_id, (RawAddress*)addr, 0, &chan_id);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed HDP channel connection, status: %d", status);
- chan_id = -1;
- }
- env->ReleaseByteArrayElements(address, addr, 0);
-
- return chan_id;
-}
-
-static jboolean disconnectChannelNative(JNIEnv* env, jobject object,
- jint channel_id) {
- if (!sBluetoothHdpInterface) return JNI_FALSE;
-
- bt_status_t status = sBluetoothHdpInterface->destroy_channel(channel_id);
- if (status != BT_STATUS_SUCCESS) {
- ALOGE("Failed disconnect health channel, status: %d", status);
- return JNI_FALSE;
- }
- return JNI_TRUE;
-}
-
-static JNINativeMethod sMethods[] = {
- {"classInitNative", "()V", (void*)classInitNative},
- {"initializeNative", "()V", (void*)initializeNative},
- {"cleanupNative", "()V", (void*)cleanupNative},
- {"registerHealthAppNative", "(IILjava/lang/String;I)I",
- (void*)registerHealthAppNative},
- {"unregisterHealthAppNative", "(I)Z", (void*)unregisterHealthAppNative},
- {"connectChannelNative", "([BI)I", (void*)connectChannelNative},
- {"disconnectChannelNative", "(I)Z", (void*)disconnectChannelNative},
-};
-
-int register_com_android_bluetooth_hdp(JNIEnv* env) {
- return jniRegisterNativeMethods(env,
- "com/android/bluetooth/hdp/HealthService",
- sMethods, NELEM(sMethods));
-}
-}
diff --git a/jni/com_android_bluetooth_hearing_aid.cpp b/jni/com_android_bluetooth_hearing_aid.cpp
index 1602aac..f93a42a 100644
--- a/jni/com_android_bluetooth_hearing_aid.cpp
+++ b/jni/com_android_bluetooth_hearing_aid.cpp
@@ -198,7 +198,6 @@
jbyteArray address) {
std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
if (!sHearingAidInterface) return JNI_FALSE;
-
jbyte* addr = env->GetByteArrayElements(address, nullptr);
if (!addr) {
jniThrowIOException(env, EINVAL);
@@ -211,23 +210,6 @@
return JNI_TRUE;
}
-static jboolean removeFromWhiteListNative(JNIEnv* env, jobject object,
- jbyteArray address) {
- std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
- if (!sHearingAidInterface) return JNI_FALSE;
-
- jbyte* addr = env->GetByteArrayElements(address, nullptr);
- if (!addr) {
- jniThrowIOException(env, EINVAL);
- return JNI_FALSE;
- }
-
- RawAddress* tmpraw = (RawAddress*)addr;
- sHearingAidInterface->RemoveFromWhiteList(*tmpraw);
- env->ReleaseByteArrayElements(address, addr, 0);
- return JNI_TRUE;
-}
-
static void setVolumeNative(JNIEnv* env, jclass clazz, jint volume) {
if (!sHearingAidInterface) {
LOG(ERROR) << __func__
@@ -244,7 +226,6 @@
{"connectHearingAidNative", "([B)Z", (void*)connectHearingAidNative},
{"disconnectHearingAidNative", "([B)Z", (void*)disconnectHearingAidNative},
{"addToWhiteListNative", "([B)Z", (void*)addToWhiteListNative},
- {"removeFromWhiteListNative", "([B)Z", (void*)removeFromWhiteListNative},
{"setVolumeNative", "(I)V", (void*)setVolumeNative},
};
diff --git a/jni/com_android_bluetooth_hfp.cpp b/jni/com_android_bluetooth_hfp.cpp
index 8547aa6..f474c28 100644
--- a/jni/com_android_bluetooth_hfp.cpp
+++ b/jni/com_android_bluetooth_hfp.cpp
@@ -38,7 +38,7 @@
static jmethodID method_onVolumeChanged;
static jmethodID method_onDialCall;
static jmethodID method_onSendDtmf;
-static jmethodID method_onNoiceReductionEnable;
+static jmethodID method_onNoiseReductionEnable;
static jmethodID method_onWBS;
static jmethodID method_onAtChld;
static jmethodID method_onAtCnum;
@@ -215,7 +215,7 @@
ALOGE("Fail to new jbyteArray bd addr for audio state");
return;
}
- sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onNoiceReductionEnable,
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onNoiseReductionEnable,
nrec == bluetooth::headset::BTHF_NREC_START,
addr.get());
}
@@ -396,8 +396,8 @@
method_onDialCall =
env->GetMethodID(clazz, "onDialCall", "(Ljava/lang/String;[B)V");
method_onSendDtmf = env->GetMethodID(clazz, "onSendDtmf", "(I[B)V");
- method_onNoiceReductionEnable =
- env->GetMethodID(clazz, "onNoiceReductionEnable", "(Z[B)V");
+ method_onNoiseReductionEnable =
+ env->GetMethodID(clazz, "onNoiseReductionEnable", "(Z[B)V");
method_onWBS = env->GetMethodID(clazz, "onWBS", "(I[B)V");
method_onAtChld = env->GetMethodID(clazz, "onAtChld", "(I[B)V");
method_onAtCnum = env->GetMethodID(clazz, "onAtCnum", "([B)V");
@@ -446,6 +446,7 @@
if (!sBluetoothHfpInterface) {
ALOGW("%s: Failed to get Bluetooth Handsfree Interface", __func__);
jniThrowIOException(env, EINVAL);
+ return;
}
bt_status_t status =
sBluetoothHfpInterface->Init(JniHeadsetCallbacks::GetInstance(),
@@ -804,7 +805,8 @@
static jboolean phoneStateChangeNative(JNIEnv* env, jobject object,
jint num_active, jint num_held,
jint call_state, jstring number_str,
- jint type, jbyteArray address) {
+ jint type, jstring name_str,
+ jbyteArray address) {
std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
if (!sBluetoothHfpInterface) {
ALOGW("%s: sBluetoothHfpInterface is null", __func__);
@@ -817,14 +819,21 @@
return JNI_FALSE;
}
const char* number = env->GetStringUTFChars(number_str, nullptr);
+ const char* name = nullptr;
+ if (name_str != nullptr) {
+ name = env->GetStringUTFChars(name_str, nullptr);
+ }
bt_status_t status = sBluetoothHfpInterface->PhoneStateChange(
num_active, num_held, (bluetooth::headset::bthf_call_state_t)call_state,
- number, (bluetooth::headset::bthf_call_addrtype_t)type,
+ number, (bluetooth::headset::bthf_call_addrtype_t)type, name,
(RawAddress*)addr);
if (status != BT_STATUS_SUCCESS) {
ALOGE("Failed report phone state change, status: %d", status);
}
env->ReleaseStringUTFChars(number_str, number);
+ if (name != nullptr) {
+ env->ReleaseStringUTFChars(name_str, name);
+ }
env->ReleaseByteArrayElements(address, addr, 0);
return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
}
@@ -908,7 +917,7 @@
{"atResponseCodeNative", "(II[B)Z", (void*)atResponseCodeNative},
{"clccResponseNative", "(IIIIZLjava/lang/String;I[B)Z",
(void*)clccResponseNative},
- {"phoneStateChangeNative", "(IIILjava/lang/String;I[B)Z",
+ {"phoneStateChangeNative", "(IIILjava/lang/String;ILjava/lang/String;[B)Z",
(void*)phoneStateChangeNative},
{"setScoAllowedNative", "(Z)Z", (void*)setScoAllowedNative},
{"sendBsirNative", "(Z[B)Z", (void*)sendBsirNative},
diff --git a/jni/com_android_bluetooth_hfpclient.cpp b/jni/com_android_bluetooth_hfpclient.cpp
index 3dfc86e..af90922 100644
--- a/jni/com_android_bluetooth_hfpclient.cpp
+++ b/jni/com_android_bluetooth_hfpclient.cpp
@@ -49,6 +49,7 @@
static jmethodID method_onInBandRing;
static jmethodID method_onLastVoiceTagNumber;
static jmethodID method_onRingIndication;
+static jmethodID method_onUnknownEvent;
static jbyteArray marshall_bda(const RawAddress* bd_addr) {
CallbackEnv sCallbackEnv(__func__);
@@ -322,6 +323,20 @@
addr.get());
}
+static void unknown_event_cb(const RawAddress* bd_addr,
+ const char* eventString) {
+ CallbackEnv sCallbackEnv(__func__);
+ if (!sCallbackEnv.valid()) return;
+
+ ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
+ if (!addr.get()) return;
+
+ ScopedLocalRef<jstring> js_event(sCallbackEnv.get(),
+ sCallbackEnv->NewStringUTF(eventString));
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onUnknownEvent,
+ js_event.get(), addr.get());
+}
+
static bthf_client_callbacks_t sBluetoothHfpClientCallbacks = {
sizeof(sBluetoothHfpClientCallbacks),
connection_state_cb,
@@ -345,6 +360,7 @@
in_band_ring_cb,
last_voice_tag_number_cb,
ring_indication_cb,
+ unknown_event_cb,
};
static void classInitNative(JNIEnv* env, jclass clazz) {
@@ -377,6 +393,8 @@
method_onLastVoiceTagNumber =
env->GetMethodID(clazz, "onLastVoiceTagNumber", "(Ljava/lang/String;[B)V");
method_onRingIndication = env->GetMethodID(clazz, "onRingIndication", "([B)V");
+ method_onUnknownEvent =
+ env->GetMethodID(clazz, "onUnknownEvent", "(Ljava/lang/String;[B)V");
ALOGI("%s succeeds", __func__);
}
diff --git a/jni/com_android_bluetooth_hid_host.cpp b/jni/com_android_bluetooth_hid_host.cpp
index 7838ff6..b8f4d65 100644
--- a/jni/com_android_bluetooth_hid_host.cpp
+++ b/jni/com_android_bluetooth_hid_host.cpp
@@ -24,7 +24,7 @@
#include "utils/Log.h"
#include <string.h>
-
+#include <shared_mutex>
namespace android {
static jmethodID method_onConnectStateChanged;
@@ -36,6 +36,7 @@
static const bthh_interface_t* sBluetoothHidInterface = NULL;
static jobject mCallbacksObj = NULL;
+static std::shared_timed_mutex mCallbacks_mutex;
static jbyteArray marshall_bda(RawAddress* bd_addr) {
CallbackEnv sCallbackEnv(__func__);
@@ -53,6 +54,7 @@
static void connection_state_callback(RawAddress* bd_addr,
bthh_connection_state_t state) {
+ std::shared_lock<std::shared_timed_mutex> lock(mCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
if (!mCallbacksObj) {
@@ -72,6 +74,7 @@
static void get_protocol_mode_callback(RawAddress* bd_addr,
bthh_status_t hh_status,
bthh_protocol_mode_t mode) {
+ std::shared_lock<std::shared_timed_mutex> lock(mCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
if (!mCallbacksObj) {
@@ -95,6 +98,7 @@
static void get_report_callback(RawAddress* bd_addr, bthh_status_t hh_status,
uint8_t* rpt_data, int rpt_size) {
+ std::shared_lock<std::shared_timed_mutex> lock(mCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
if (!mCallbacksObj) {
@@ -126,6 +130,7 @@
static void virtual_unplug_callback(RawAddress* bd_addr,
bthh_status_t hh_status) {
ALOGV("call to virtual_unplug_callback");
+ std::shared_lock<std::shared_timed_mutex> lock(mCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
if (!mCallbacksObj) {
@@ -142,6 +147,7 @@
}
static void handshake_callback(RawAddress* bd_addr, bthh_status_t hh_status) {
+ std::shared_lock<std::shared_timed_mutex> lock(mCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
if (!mCallbacksObj) {
@@ -160,6 +166,7 @@
static void get_idle_time_callback(RawAddress* bd_addr, bthh_status_t hh_status,
int idle_time) {
+ std::shared_lock<std::shared_timed_mutex> lock(mCallbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -198,6 +205,7 @@
}
static void initializeNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_timed_mutex> lock(mCallbacks_mutex);
const bt_interface_t* btInf = getBluetoothInterface();
if (btInf == NULL) {
ALOGE("Bluetooth module is not loaded");
@@ -234,6 +242,7 @@
}
static void cleanupNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_timed_mutex> lock(mCallbacks_mutex);
const bt_interface_t* btInf = getBluetoothInterface();
if (btInf == NULL) {
diff --git a/jni/com_android_bluetooth_sdp.cpp b/jni/com_android_bluetooth_sdp.cpp
index c2eb5ce..827db71 100644
--- a/jni/com_android_bluetooth_sdp.cpp
+++ b/jni/com_android_bluetooth_sdp.cpp
@@ -307,6 +307,37 @@
return handle;
}
+static jint sdpCreatePbapPceRecordNative(JNIEnv* env, jobject obj,
+ jstring name_str, jint version) {
+ ALOGD("%s", __func__);
+ if (!sBluetoothSdpInterface) return -1;
+
+ bluetooth_sdp_record record = {}; // Must be zero initialized
+ record.pce.hdr.type = SDP_TYPE_PBAP_PCE;
+
+ const char* service_name = NULL;
+ if (name_str != NULL) {
+ service_name = env->GetStringUTFChars(name_str, NULL);
+ record.pce.hdr.service_name = (char*)service_name;
+ record.pce.hdr.service_name_length = strlen(service_name);
+ } else {
+ record.pce.hdr.service_name = NULL;
+ record.pce.hdr.service_name_length = 0;
+ }
+ record.pce.hdr.profile_version = version;
+
+ int handle = -1;
+ int ret = sBluetoothSdpInterface->create_sdp_record(&record, &handle);
+ if (ret != BT_STATUS_SUCCESS) {
+ ALOGE("SDP Create record failed: %d", ret);
+ } else {
+ ALOGD("SDP Create record success - handle: %d", handle);
+ }
+
+ if (service_name) env->ReleaseStringUTFChars(name_str, service_name);
+ return handle;
+}
+
static jint sdpCreatePbapPseRecordNative(JNIEnv* env, jobject obj,
jstring name_str, jint scn,
jint l2cap_psm, jint version,
@@ -474,6 +505,8 @@
(void*)sdpCreateMapMasRecordNative},
{"sdpCreateMapMnsRecordNative", "(Ljava/lang/String;IIII)I",
(void*)sdpCreateMapMnsRecordNative},
+ {"sdpCreatePbapPceRecordNative", "(Ljava/lang/String;I)I",
+ (void*)sdpCreatePbapPceRecordNative},
{"sdpCreatePbapPseRecordNative", "(Ljava/lang/String;IIIII)I",
(void*)sdpCreatePbapPseRecordNative},
{"sdpCreateOppOpsRecordNative", "(Ljava/lang/String;III[B)I",
diff --git a/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java b/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java
index b959858..5040bdc 100644
--- a/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java
+++ b/lib/mapapi/com/android/bluetooth/mapapi/BluetoothMapContract.java
@@ -301,12 +301,12 @@
* E.g. as a mapping for them such that the naming will match the underlying
* matching folder ID's.
*/
- public static final String FOLDER_NAME_INBOX = "INBOX";
- public static final String FOLDER_NAME_SENT = "SENT";
- public static final String FOLDER_NAME_OUTBOX = "OUTBOX";
- public static final String FOLDER_NAME_DRAFT = "DRAFT";
- public static final String FOLDER_NAME_DELETED = "DELETED";
- public static final String FOLDER_NAME_OTHER = "OTHER";
+ public static final String FOLDER_NAME_INBOX = "inbox";
+ public static final String FOLDER_NAME_SENT = "sent";
+ public static final String FOLDER_NAME_OUTBOX = "outbox";
+ public static final String FOLDER_NAME_DRAFT = "draft";
+ public static final String FOLDER_NAME_DELETED = "deleted";
+ public static final String FOLDER_NAME_OTHER = "other";
/**
* Folder IDs to be used with Instant Messaging virtual folders
diff --git a/lib/room/annotation-1.0.0-beta01.jar b/lib/room/annotation-1.0.0-beta01.jar
new file mode 100644
index 0000000..124f128
--- /dev/null
+++ b/lib/room/annotation-1.0.0-beta01.jar
Binary files differ
diff --git a/lib/room/room-common-2.0.0-beta01.jar b/lib/room/room-common-2.0.0-beta01.jar
new file mode 100644
index 0000000..931d4aa
--- /dev/null
+++ b/lib/room/room-common-2.0.0-beta01.jar
Binary files differ
diff --git a/lib/room/room-compiler-2.0.0-beta01.jar b/lib/room/room-compiler-2.0.0-beta01.jar
new file mode 100644
index 0000000..623a07b
--- /dev/null
+++ b/lib/room/room-compiler-2.0.0-beta01.jar
Binary files differ
diff --git a/lib/room/room-migration-2.0.0-beta01.jar b/lib/room/room-migration-2.0.0-beta01.jar
new file mode 100644
index 0000000..04bca6f
--- /dev/null
+++ b/lib/room/room-migration-2.0.0-beta01.jar
Binary files differ
diff --git a/lib/room/room-runtime-2.0.0-alpha1.aar b/lib/room/room-runtime-2.0.0-alpha1.aar
new file mode 100644
index 0000000..053188b
--- /dev/null
+++ b/lib/room/room-runtime-2.0.0-alpha1.aar
Binary files differ
diff --git a/lib/room/room-testing-2.0.0-alpha1.aar b/lib/room/room-testing-2.0.0-alpha1.aar
new file mode 100644
index 0000000..bb6e62a
--- /dev/null
+++ b/lib/room/room-testing-2.0.0-alpha1.aar
Binary files differ
diff --git a/res/layout/bt_enabling_progress.xml b/res/layout/bt_enabling_progress.xml
index 7f2fd72..0e4f870 100644
--- a/res/layout/bt_enabling_progress.xml
+++ b/res/layout/bt_enabling_progress.xml
@@ -27,7 +27,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <ProgressBar android:id="@+android:id/progress"
+ <ProgressBar android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
diff --git a/res/mipmap-anydpi/bt_share.xml b/res/mipmap-anydpi/bt_share.xml
new file mode 100644
index 0000000..c99e560
--- /dev/null
+++ b/res/mipmap-anydpi/bt_share.xml
@@ -0,0 +1,28 @@
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ -->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <foreground>
+ <inset
+ android:drawable="@*android:drawable/ic_bluetooth_share_icon"
+ android:insetTop="25%"
+ android:insetRight="25%"
+ android:insetBottom="25%"
+ android:insetLeft="25%" />
+ </foreground>
+ <background>
+ <color android:color="@android:color/white" />
+ </background>
+</adaptive-icon>
diff --git a/res/mipmap-hdpi/bt_share.png b/res/mipmap-hdpi/bt_share.png
deleted file mode 100644
index 6d16ebf..0000000
--- a/res/mipmap-hdpi/bt_share.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-mdpi/bt_share.png b/res/mipmap-mdpi/bt_share.png
deleted file mode 100644
index 1f514d2..0000000
--- a/res/mipmap-mdpi/bt_share.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-xhdpi/bt_share.png b/res/mipmap-xhdpi/bt_share.png
deleted file mode 100644
index 7bd0e9c..0000000
--- a/res/mipmap-xhdpi/bt_share.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-xxhdpi/bt_share.png b/res/mipmap-xxhdpi/bt_share.png
deleted file mode 100644
index 0fe218e..0000000
--- a/res/mipmap-xxhdpi/bt_share.png
+++ /dev/null
Binary files differ
diff --git a/res/mipmap-xxxhdpi/bt_share.png b/res/mipmap-xxxhdpi/bt_share.png
deleted file mode 100644
index 7bf313f..0000000
--- a/res/mipmap-xxxhdpi/bt_share.png
+++ /dev/null
Binary files differ
diff --git a/res/raw/silent.wav b/res/raw/silent.wav
new file mode 100755
index 0000000..d6d93ef
--- /dev/null
+++ b/res/raw/silent.wav
Binary files differ
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 4bb0f8a..eb419ed 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -26,7 +26,7 @@
<string name="airplane_error_title" msgid="2683839635115739939">"وضع الطائرة"</string>
<string name="airplane_error_msg" msgid="8698965595254137230">"لا يمكنك استخدام البلوتوث في وضع الطائرة."</string>
<string name="bt_enable_title" msgid="8657832550503456572"></string>
- <string name="bt_enable_line1" msgid="7203551583048149">"لإستخدام خدمات البلوتوث، يجب تشغيل البلوتوث أولاً."</string>
+ <string name="bt_enable_line1" msgid="7203551583048149">"إلى لاستخدم خدمات البلوتوث، يجب أولاً تشغيل البلوتوث."</string>
<string name="bt_enable_line2" msgid="4341936569415937994">"هل تريد تشغيل البلوتوث الآن؟"</string>
<string name="bt_enable_cancel" msgid="1988832367505151727">"إلغاء"</string>
<string name="bt_enable_ok" msgid="3432462749994538265">"تشغيل"</string>
@@ -100,7 +100,7 @@
<string name="status_connection_error" msgid="947681831523219891">"لم يتم الاتصال بنجاح."</string>
<string name="status_protocol_error" msgid="3245444473429269539">"لا يمكن معالجة الطلب بشكل صحيح."</string>
<string name="status_unknown_error" msgid="8156660554237824912">"خطأ غير معروف."</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"تم الاستلام عبر بلوتوث"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"تم استلام بلوتوث"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"مشاركة البلوتوث"</string>
<string name="download_success" msgid="7036160438766730871">"اكتمل استلام <xliff:g id="FILE_SIZE">%1$s</xliff:g>."</string>
<string name="upload_success" msgid="4014469387779648949">"اكتمل إرسال <xliff:g id="FILE_SIZE">%1$s</xliff:g>."</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index bfb3168..a67ceaa 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -16,7 +16,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="permlab_bluetoothShareManager" msgid="311492132450338925">"ডাউনল’ড মেনেজাৰ ব্যৱহাৰ কৰিব পাৰে।"</string>
+ <string name="permlab_bluetoothShareManager" msgid="311492132450338925">"ডাউনল\'ড মেনেজাৰ ব্যৱহাৰ কৰিব পাৰে।"</string>
<string name="permdesc_bluetoothShareManager" msgid="8930572979123190223">"এপটোক BluetoothShare মেনেজাৰ ব্যৱহাৰ কৰি ফাইল স্থানান্তৰ কৰিবলৈ অনুমতি দিয়ে।"</string>
<string name="permlab_bluetoothWhitelist" msgid="7091552898592306386">"ব্লুটুথ ডিভাইচ ব্যৱাহৰ কৰিবলৈ স্বীকৃতি দিয়ক।"</string>
<string name="permdesc_bluetoothWhitelist" msgid="5494513855192170109">"ব্যৱহাৰকাৰীৰ অনুমতি অবিহনে এই ডিভাইচক ফাইলবোৰ পঠিয়াবলৈ দি এপটোক ব্লুটুথ ডিভাইচক কিছু সময়ৰ বাবে স্বীকৃতি দিয়ে।"</string>
@@ -39,11 +39,11 @@
<string name="incoming_file_confirm_Notification_title" msgid="5573329005298936903">"অন্তৰ্গামী ফাইল"</string>
<string name="incoming_file_confirm_Notification_content" msgid="3359694069319644738">"<xliff:g id="SENDER">%1$s</xliff:g> <xliff:g id="FILE">%2$s</xliff:g> পঠাবলৈ সাজু"</string>
<string name="notification_receiving" msgid="4674648179652543984">"ব্লুটুথ শ্বেয়াৰ: <xliff:g id="FILE">%1$s</xliff:g> লাভ কৰি থকা হৈছে"</string>
- <string name="notification_received" msgid="3324588019186687985">"ব্লুটুথ শ্বেয়াৰ: <xliff:g id="FILE">%1$s</xliff:g> লাভ কৰা হ’ল"</string>
+ <string name="notification_received" msgid="3324588019186687985">"ব্লুটুথ শ্বেয়াৰ: <xliff:g id="FILE">%1$s</xliff:g> লাভ কৰা হ\'ল"</string>
<string name="notification_received_fail" msgid="3619350997285714746">"ব্লুটুথ শ্বেয়াৰ: ফাইল <xliff:g id="FILE">%1$s</xliff:g> লাভ কৰা নহ\'ল"</string>
<string name="notification_sending" msgid="3035748958534983833">"ব্লুটুথ শ্বেয়াৰ: <xliff:g id="FILE">%1$s</xliff:g> প্ৰেৰণ কৰি থকা হৈছে"</string>
- <string name="notification_sent" msgid="9218710861333027778">"ব্লুটুথ শ্বেয়াৰ: <xliff:g id="FILE">%1$s</xliff:g> প্ৰেৰণ কৰা হ’ল"</string>
- <string name="notification_sent_complete" msgid="302943281067557969">"১০০% সম্পূৰ্ণ হ’ল"</string>
+ <string name="notification_sent" msgid="9218710861333027778">"ব্লুটুথ শ্বেয়াৰ: <xliff:g id="FILE">%1$s</xliff:g> প্ৰেৰণ কৰা হ\'ল"</string>
+ <string name="notification_sent_complete" msgid="302943281067557969">"১০০% সম্পূৰ্ণ হ\'ল"</string>
<string name="notification_sent_fail" msgid="6696082233774569445">"ব্লুটুথ শ্বেয়াৰ: ফাইল <xliff:g id="FILE">%1$s</xliff:g> প্ৰেৰণ কৰা নহ\'ল"</string>
<string name="download_title" msgid="3353228219772092586">"ফাইল স্থানান্তৰণ"</string>
<string name="download_line1" msgid="4926604799202134144">"প্ৰেৰক: \"<xliff:g id="SENDER">%1$s</xliff:g>\""</string>
@@ -60,12 +60,12 @@
<string name="download_fail_line2" msgid="8950394574689971071">"ফাইল: <xliff:g id="FILE">%1$s</xliff:g>"</string>
<string name="download_fail_line3" msgid="3451040656154861722">"কাৰণ: <xliff:g id="REASON">%1$s</xliff:g>"</string>
<string name="download_fail_ok" msgid="1521733664438320300">"ঠিক"</string>
- <string name="download_succ_line5" msgid="4509944688281573595">"ফাইল লাভ কৰা হ’ল"</string>
+ <string name="download_succ_line5" msgid="4509944688281573595">"ফাইল লাভ কৰা হ\'ল"</string>
<string name="download_succ_ok" msgid="7053688246357050216">"খোলক"</string>
<string name="upload_line1" msgid="2055952074059709052">"প্ৰতি: \"<xliff:g id="RECIPIENT">%1$s</xliff:g>\""</string>
<string name="upload_line3" msgid="4920689672457037437">"ফাইলৰ প্ৰকাৰ: <xliff:g id="TYPE">%1$s</xliff:g> (<xliff:g id="SIZE">%2$s</xliff:g>)"</string>
<string name="upload_line5" msgid="7759322537674229752">"ফাইল প্ৰেৰণ কৰি থকা হৈছে…"</string>
- <string name="upload_succ_line5" msgid="5687317197463383601">"ফাইল প্ৰেৰণ কৰা হ’ল"</string>
+ <string name="upload_succ_line5" msgid="5687317197463383601">"ফাইল প্ৰেৰণ কৰা হ\'ল"</string>
<string name="upload_succ_ok" msgid="7705428476405478828">"ঠিক"</string>
<string name="upload_fail_line1" msgid="7899394672421491701">"\"<xliff:g id="RECIPIENT">%1$s</xliff:g>\"লৈ ফাইলটো পঠিয়াব পৰা নগ\'ল।"</string>
<string name="upload_fail_line1_2" msgid="2108129204050841798">"ফাইল: <xliff:g id="FILE">%1$s</xliff:g>"</string>
@@ -80,17 +80,17 @@
<string name="enabling_progress_content" msgid="4601542238119927904">"ব্লুটুথ অন কৰি থকা হৈছে…"</string>
<string name="bt_toast_1" msgid="972182708034353383">"ফাইলটো লাভ কৰা হ\'ব। জাননী পেনেলত অগ্ৰগতি নিৰীক্ষণ কৰক।"</string>
<string name="bt_toast_2" msgid="8602553334099066582">"ফাইলটো লাভ কৰিব নোৱাৰি।"</string>
- <string name="bt_toast_3" msgid="6707884165086862518">"\"<xliff:g id="SENDER">%1$s</xliff:g>\"ৰ ফাইল লাভ কৰা বন্ধ কৰা হ’ল"</string>
+ <string name="bt_toast_3" msgid="6707884165086862518">"\"<xliff:g id="SENDER">%1$s</xliff:g>\"ৰ ফাইল লাভ কৰা বন্ধ কৰা হ\'ল"</string>
<string name="bt_toast_4" msgid="4678812947604395649">"\"<xliff:g id="RECIPIENT">%1$s</xliff:g>\"লৈ ফাইল প্ৰেৰণ কৰি থকা হৈছে"</string>
<string name="bt_toast_5" msgid="2846870992823019494">"\"<xliff:g id="RECIPIENT">%2$s</xliff:g>\"লৈ <xliff:g id="NUMBER">%1$s</xliff:g>টা ফাইল পঠিয়াই থকা হৈছে"</string>
- <string name="bt_toast_6" msgid="1855266596936622458">"\"<xliff:g id="RECIPIENT">%1$s</xliff:g>\"লৈ ফাইল পঠিওৱা বন্ধ কৰা হ’ল"</string>
+ <string name="bt_toast_6" msgid="1855266596936622458">"\"<xliff:g id="RECIPIENT">%1$s</xliff:g>\"লৈ ফাইল পঠিওৱা বন্ধ কৰা হ\'ল"</string>
<string name="bt_sm_2_1" product="nosdcard" msgid="352165168004521000">"\"<xliff:g id="SENDER">%1$s</xliff:g>\"ৰ পৰা লাভ কৰা ফাইলটো ছেভ কৰিবলৈ ইউএছবি সঞ্চয়াগাৰত পৰ্যাপ্ত খালী ঠাই নাই"</string>
<string name="bt_sm_2_1" product="default" msgid="1989018443456803630">"\"<xliff:g id="SENDER">%1$s</xliff:g>\"ৰ পৰা লাভ কৰা ফাইলটো ছেভ কৰিবলৈ এছডি কাৰ্ডত পৰ্যাপ্ত খালী ঠাই নাই"</string>
<string name="bt_sm_2_2" msgid="2965243265852680543">"ইমান খালী ঠাইৰ দৰকাৰ: <xliff:g id="SIZE">%1$s</xliff:g>"</string>
<string name="ErrorTooManyRequests" msgid="8578277541472944529">"বহুত বেছি অনুৰোধৰ ওপৰত প্ৰক্ৰিয়া চলি আছে৷ পিছত আকৌ চেষ্টা কৰক৷"</string>
<string name="status_pending" msgid="2503691772030877944">"ফাইলৰ স্থানান্তৰণ এতিয়ালৈকে আৰম্ভ হোৱা নাই।"</string>
<string name="status_running" msgid="6562808920311008696">"ফাইলৰ স্থানান্তৰণ চলি আছে।"</string>
- <string name="status_success" msgid="239573225847565868">"ফাইলৰ স্থানান্তৰণৰ কাৰ্য সফলতাৰে সম্পন্ন কৰা হ’ল।"</string>
+ <string name="status_success" msgid="239573225847565868">"ফাইলৰ স্থানান্তৰণৰ কাৰ্য সফলতাৰে সম্পন্ন কৰা হ\'ল।"</string>
<string name="status_not_accept" msgid="1695082417193780738">"সমল সমৰ্থিত নহয়।"</string>
<string name="status_forbidden" msgid="613956401054050725">"নিৰ্দিষ্ট কৰা ডিভাইচটোৱে স্থানান্তৰণ নিষিদ্ধ কৰিছে।"</string>
<string name="status_canceled" msgid="6664490318773098285">"ব্যৱহাৰকাৰীয়ে স্থানান্তৰণ বাতিল কৰিছে।"</string>
@@ -100,10 +100,10 @@
<string name="status_connection_error" msgid="947681831523219891">"সংযোগ কৰিব পৰা নগ\'ল।"</string>
<string name="status_protocol_error" msgid="3245444473429269539">"অনুৰোধ সঠিকভাৱে পৰিচালনা কৰিব নোৱাৰি।"</string>
<string name="status_unknown_error" msgid="8156660554237824912">"অজ্ঞাত আসোঁৱাহ।"</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"ব্লুটুথ লাভ কৰা হ’ল"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"ব্লুটুথ লাভ কৰা হ\'ল"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"ব্লুটুথ শ্বেয়াৰ"</string>
- <string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> লাভ কৰা কাৰ্য সম্পূৰ্ণ হ’ল।"</string>
- <string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> প্ৰেৰণ কৰা কাম সম্পূৰ্ণ হ’ল"</string>
+ <string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> লাভ কৰা কাৰ্য সম্পূৰ্ণ হ\'ল।"</string>
+ <string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> প্ৰেৰণ কৰা কাম সম্পূৰ্ণ হ\'ল"</string>
<string name="inbound_history_title" msgid="6940914942271327563">"অন্তৰ্গামী স্থানান্তৰণসমূহ"</string>
<string name="outbound_history_title" msgid="4279418703178140526">"বহিৰ্গামী স্থানান্তৰণসমূহ"</string>
<string name="no_transfers" msgid="3482965619151865672">"স্থানান্তৰণৰ ইতিহাস খালী আছে।"</string>
@@ -111,12 +111,12 @@
<string name="outbound_noti_title" msgid="8051906709452260849">"ব্লুটুথ শ্বেয়াৰ: প্ৰেৰণ কৰা ফাইলসমূহ"</string>
<string name="inbound_noti_title" msgid="4143352641953027595">"ব্লুটুথ শ্বেয়াৰ: লাভ কৰা ফাইলসমূহ"</string>
<plurals name="noti_caption_unsuccessful" formatted="false" msgid="2020750076679526122">
- <item quantity="one"><xliff:g id="UNSUCCESSFUL_NUMBER_1">%1$d</xliff:g>টা অসফল হ’ল।</item>
- <item quantity="other"><xliff:g id="UNSUCCESSFUL_NUMBER_1">%1$d</xliff:g>টা অসফল হ’ল।</item>
+ <item quantity="one"><xliff:g id="UNSUCCESSFUL_NUMBER_1">%1$d</xliff:g>টা অসফল হ\'ল।</item>
+ <item quantity="other"><xliff:g id="UNSUCCESSFUL_NUMBER_1">%1$d</xliff:g>টা অসফল হ\'ল।</item>
</plurals>
<plurals name="noti_caption_success" formatted="false" msgid="1572472450257645181">
- <item quantity="one"><xliff:g id="SUCCESSFUL_NUMBER_1">%1$d</xliff:g>টা সফল হ’ল, %2$s</item>
- <item quantity="other"><xliff:g id="SUCCESSFUL_NUMBER_1">%1$d</xliff:g>টা সফল হ’ল, %2$s</item>
+ <item quantity="one"><xliff:g id="SUCCESSFUL_NUMBER_1">%1$d</xliff:g>টা সফল হ\'ল, %2$s</item>
+ <item quantity="other"><xliff:g id="SUCCESSFUL_NUMBER_1">%1$d</xliff:g>টা সফল হ\'ল, %2$s</item>
</plurals>
<string name="transfer_menu_clear_all" msgid="790017462957873132">"সূচী মচক"</string>
<string name="transfer_menu_open" msgid="3368984869083107200">"খোলক"</string>
@@ -129,8 +129,8 @@
<string name="bluetooth_map_settings_app_icon" msgid="7105805610929114707">"এপ্লিকেশ্বন আইকন"</string>
<string name="bluetooth_map_settings_title" msgid="7420332483392851321">"ব্লুটুথৰ জৰিয়তে বাৰ্তা শ্বেয়াৰ কৰাৰ ছেটিংসমূহ"</string>
<string name="bluetooth_map_settings_no_account_slots_left" msgid="1796029082612965251">"একাউণ্ট বাছনি কৰিব নোৱাৰি। ০টা শ্লটবোৰ বাকী আছে"</string>
- <string name="bluetooth_connected" msgid="6718623220072656906">"ব্লুটুথ অডিঅ’ সংযুক্ত কৰা হ’ল"</string>
- <string name="bluetooth_disconnected" msgid="3318303728981478873">"ব্লুটুথ অডিঅ\'ৰ সৈতে সংযোগ বিচ্ছিন্ন কৰা হ’ল"</string>
+ <string name="bluetooth_connected" msgid="6718623220072656906">"ব্লুটুথ অডিঅ’ সংযুক্ত কৰা হ\'ল"</string>
+ <string name="bluetooth_disconnected" msgid="3318303728981478873">"ব্লুটুথ অডিঅ\'ৰ সৈতে সংযোগ বিচ্ছিন্ন কৰা হ\'ল"</string>
<string name="a2dp_sink_mbs_label" msgid="7566075853395412558">"ব্লুটুথ অডিঅ\'"</string>
<string name="bluetooth_opp_file_limit_exceeded" msgid="8894450394309084519">"৪ জি. বি. তকৈ ডাঙৰ ফাইল স্থানান্তৰ কৰিব নোৱাৰি"</string>
</resources>
diff --git a/res/values-az/strings_map.xml b/res/values-az/strings_map.xml
deleted file mode 100644
index 0b8584e..0000000
--- a/res/values-az/strings_map.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="map_session_key_dialog_title" msgid="4357431314165953502">"%1$s üçün seans açarı daxil edin"</string>
- <string name="map_session_key_dialog_header" msgid="1858363785687455516">"Bluetooth seans açarı tələb olunur"</string>
- <string name="map_acceptance_timeout_message" msgid="2247454902690565876">"%1$s ilə bağlantı qəbul edərkən fasilə yarandı"</string>
- <string name="map_authentication_timeout_message" msgid="2151423397615327066">"%1$s ilə seans açarını daxil edərkən fasilə yarandı"</string>
- <string name="map_auth_notif_ticker" msgid="8840174807927327119">"Obex təsdiqləmə sorğusu"</string>
- <string name="map_auth_notif_title" msgid="301767019364765129">"Seans Açarı"</string>
- <string name="map_auth_notif_message" msgid="1510095655064214863">"%1$s üçün seans açarı daxil edin"</string>
- <string name="map_defaultname" msgid="3805234816465216759">"Maşın dəsti"</string>
- <string name="map_unknownName" msgid="4985352365863687360">"Naməlum ad"</string>
- <string name="map_localPhoneName" msgid="6264496105941797768">"Mənim adım"</string>
- <string name="map_defaultnumber" msgid="3838264444043503815">"000000"</string>
-</resources>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 3dc12a2..2f07816 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -100,7 +100,7 @@
<string name="status_connection_error" msgid="947681831523219891">"Povezivanje nije uspjelo."</string>
<string name="status_protocol_error" msgid="3245444473429269539">"Nije moguće pravilno obraditi zahtjev."</string>
<string name="status_unknown_error" msgid="8156660554237824912">"Nepoznata greška."</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"Primljeno putem Bluetootha"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"Primljeno preko Bluetootha"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"Dijeljenje putem Bluetootha"</string>
<string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> Primanje završeno."</string>
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> Slanje dovršeno."</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index f77534d..0418fbc 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -45,7 +45,7 @@
<string name="notification_sent" msgid="9218710861333027778">"Bluetooth: <xliff:g id="FILE">%1$s</xliff:g> enviat"</string>
<string name="notification_sent_complete" msgid="302943281067557969">"100% complet"</string>
<string name="notification_sent_fail" msgid="6696082233774569445">"Bluetooth: <xliff:g id="FILE">%1$s</xliff:g> no enviat"</string>
- <string name="download_title" msgid="3353228219772092586">"Transferència de fitxers"</string>
+ <string name="download_title" msgid="3353228219772092586">"Transferència del fitxer"</string>
<string name="download_line1" msgid="4926604799202134144">"De: \"<xliff:g id="SENDER">%1$s</xliff:g>\""</string>
<string name="download_line2" msgid="5876973543019417712">"Fitxer: <xliff:g id="FILE">%1$s</xliff:g>"</string>
<string name="download_line3" msgid="4384821622908676061">"Mida del fitxer: <xliff:g id="SIZE">%1$s</xliff:g>"</string>
diff --git a/res/values-da/strings_pbap.xml b/res/values-da/strings_pbap.xml
index 4837260..ae570c6 100644
--- a/res/values-da/strings_pbap.xml
+++ b/res/values-da/strings_pbap.xml
@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="pbap_session_key_dialog_title" msgid="3580996574333882561">"Angiv sessionsnøgle til %1$s"</string>
+ <string name="pbap_session_key_dialog_title" msgid="3580996574333882561">"Indtast sessionsnøgle til %1$s"</string>
<string name="pbap_session_key_dialog_header" msgid="2772472422782758981">"Bluetooth-sessionsnøgle kræves"</string>
<string name="pbap_acceptance_timeout_message" msgid="1107401415099814293">"Der var timeout ved forbindelsen med %1$s"</string>
<string name="pbap_authentication_timeout_message" msgid="4166979525521902687">"Der var timeout i indgangssessionnøgle med %1$s"</string>
<string name="auth_notif_ticker" msgid="1575825798053163744">"Anmodning om Obex-godkendelse"</string>
<string name="auth_notif_title" msgid="7599854855681573258">"Sessionstast"</string>
- <string name="auth_notif_message" msgid="6667218116427605038">"Angiv sessionsnøgle til %1$s"</string>
+ <string name="auth_notif_message" msgid="6667218116427605038">"Indtast sessionsnøgle til %1$s"</string>
<string name="defaultname" msgid="4821590500649090078">"Bilsæt"</string>
<string name="unknownName" msgid="2841414754740600042">"Ukendt navn"</string>
<string name="localPhoneName" msgid="2349001318925409159">"Mit navn"</string>
<string name="defaultnumber" msgid="8520116145890867338">"000000"</string>
- <string name="pbap_notification_group" msgid="8487669554703627168">"Deling af kontakter via Bluetooth"</string>
+ <string name="pbap_notification_group" msgid="8487669554703627168">"Deling af kontaktpersoner via Bluetooth"</string>
</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index c8b882e..df7233a 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -87,7 +87,7 @@
<string name="bt_sm_2_1" product="nosdcard" msgid="352165168004521000">"Der USB-Speicher verfügt nicht über genügend Speicherplatz, um die Datei von \"<xliff:g id="SENDER">%1$s</xliff:g>\" zu speichern."</string>
<string name="bt_sm_2_1" product="default" msgid="1989018443456803630">"Die SD-Karte verfügt über zu wenig Speicherplatz für die Datei von \"<xliff:g id="SENDER">%1$s</xliff:g>\"."</string>
<string name="bt_sm_2_2" msgid="2965243265852680543">"Erforderlicher Speicherplatz: <xliff:g id="SIZE">%1$s</xliff:g>"</string>
- <string name="ErrorTooManyRequests" msgid="8578277541472944529">"Es werden zurzeit zu viele Anfragen verarbeitet. Bitte versuche es später noch einmal."</string>
+ <string name="ErrorTooManyRequests" msgid="8578277541472944529">"Es werden zurzeit zu viele Anfragen verarbeitet. Versuche es später erneut."</string>
<string name="status_pending" msgid="2503691772030877944">"Die Dateiübertragung wurde noch nicht gestartet."</string>
<string name="status_running" msgid="6562808920311008696">"Dateiübertragung läuft."</string>
<string name="status_success" msgid="239573225847565868">"Die Dateiübertragung wurde abgeschlossen."</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index efd1453..ba19b9c 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -23,8 +23,8 @@
<string name="bt_share_picker_label" msgid="6268100924487046932">"Bluetooth"</string>
<string name="unknown_device" msgid="9221903979877041009">"Unknown device"</string>
<string name="unknownNumber" msgid="4994750948072751566">"Unknown"</string>
- <string name="airplane_error_title" msgid="2683839635115739939">"Airplane mode"</string>
- <string name="airplane_error_msg" msgid="8698965595254137230">"You can\'t use Bluetooth in Airplane mode."</string>
+ <string name="airplane_error_title" msgid="2683839635115739939">"Aeroplane mode"</string>
+ <string name="airplane_error_msg" msgid="8698965595254137230">"You can\'t use Bluetooth in Aeroplane mode."</string>
<string name="bt_enable_title" msgid="8657832550503456572"></string>
<string name="bt_enable_line1" msgid="7203551583048149">"To use Bluetooth services, you must first turn on Bluetooth."</string>
<string name="bt_enable_line2" msgid="4341936569415937994">"Turn on Bluetooth now?"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 1b142d4..ddbb688 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -100,7 +100,7 @@
<string name="status_connection_error" msgid="947681831523219891">"Conexión incorrecta"</string>
<string name="status_protocol_error" msgid="3245444473429269539">"No se puede procesar la solicitud correctamente."</string>
<string name="status_unknown_error" msgid="8156660554237824912">"Error desconocido"</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"Recibido por Bluetooth"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"Recibidos por Bluetooth"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"Compartir por Bluetooth"</string>
<string name="download_success" msgid="7036160438766730871">"Se recibieron <xliff:g id="FILE_SIZE">%1$s</xliff:g> completos."</string>
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> enviados por completo."</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 413a4e1..e87bd01 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -100,7 +100,7 @@
<string name="status_connection_error" msgid="947681831523219891">"Ühendus ebaõnnestus."</string>
<string name="status_protocol_error" msgid="3245444473429269539">"Taotlust ei saa õigesti käsitleda."</string>
<string name="status_unknown_error" msgid="8156660554237824912">"Tundmatu viga."</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"Bluetoothiga vastu võetud"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"Bluetooth vastu võetud"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"Jagamine Bluetoothiga"</string>
<string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> vastuvõtmine lõpetatud."</string>
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> saatmine lõpetatud."</string>
diff --git a/res/values-et/strings_map.xml b/res/values-et/strings_map.xml
deleted file mode 100644
index 8fe1642..0000000
--- a/res/values-et/strings_map.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="map_session_key_dialog_title" msgid="4357431314165953502">"Sisestage seansivõti seadmele %1$s"</string>
- <string name="map_session_key_dialog_header" msgid="1858363785687455516">"Vajalik Bluetoothi seansivõti"</string>
- <string name="map_acceptance_timeout_message" msgid="2247454902690565876">"Esines ajalõpp ühenduse aktsepteerimiseks seadmega %1$s"</string>
- <string name="map_authentication_timeout_message" msgid="2151423397615327066">"Esines ajalõpp seansivõtme sisestamiseks seadmele %1$s"</string>
- <string name="map_auth_notif_ticker" msgid="8840174807927327119">"Obexi autentimise taotlus"</string>
- <string name="map_auth_notif_title" msgid="301767019364765129">"Seansivõti"</string>
- <string name="map_auth_notif_message" msgid="1510095655064214863">"Sisestage seansivõti seadmele %1$s"</string>
- <string name="map_defaultname" msgid="3805234816465216759">"Autokomplekt"</string>
- <string name="map_unknownName" msgid="4985352365863687360">"Tundmatu nimi"</string>
- <string name="map_localPhoneName" msgid="6264496105941797768">"Minu nimi"</string>
- <string name="map_defaultnumber" msgid="3838264444043503815">"000000"</string>
-</resources>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index af1fc3f..b975c56 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -100,7 +100,7 @@
<string name="status_connection_error" msgid="947681831523219891">"Ezin izan da konektatu."</string>
<string name="status_protocol_error" msgid="3245444473429269539">"Ezin da eskaera behar bezala kudeatu."</string>
<string name="status_unknown_error" msgid="8156660554237824912">"Errore ezezaguna."</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"Bluetooth bidez jasotakoak"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"Bluetooth bidezko elementua jaso da"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"Bluetooth bidez partekatzea"</string>
<string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> jaso dira osorik."</string>
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> osorik bidali da."</string>
diff --git a/res/values-fa/strings_pbap.xml b/res/values-fa/strings_pbap.xml
index 348156a..8282443 100644
--- a/res/values-fa/strings_pbap.xml
+++ b/res/values-fa/strings_pbap.xml
@@ -3,8 +3,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="pbap_session_key_dialog_title" msgid="3580996574333882561">"تایپ کلید جلسه برای %1$s"</string>
<string name="pbap_session_key_dialog_header" msgid="2772472422782758981">"به کلید جلسه بلوتوث نیاز است"</string>
- <string name="pbap_acceptance_timeout_message" msgid="1107401415099814293">"پذیرش اتصال با %1$s خیلی زمان برد"</string>
- <string name="pbap_authentication_timeout_message" msgid="4166979525521902687">"وارد کردن کلید جلسه با %1$s خیلی زمان برد"</string>
+ <string name="pbap_acceptance_timeout_message" msgid="1107401415099814293">"هنگام پذیرش اتصال به %1$s وقفه زمانی ایجاد شده است"</string>
+ <string name="pbap_authentication_timeout_message" msgid="4166979525521902687">"هنگام ورود کلید جلسه با %1$s وقفه زمانی ایجاد شده است"</string>
<string name="auth_notif_ticker" msgid="1575825798053163744">"درخواست احراز هویت Obex"</string>
<string name="auth_notif_title" msgid="7599854855681573258">"کلید جلسه"</string>
<string name="auth_notif_message" msgid="6667218116427605038">"تایپ کلید جلسه برای %1$s"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 6860f15..8460d36 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -100,13 +100,13 @@
<string name="status_connection_error" msgid="947681831523219891">"કનેક્શન અસફળ."</string>
<string name="status_protocol_error" msgid="3245444473429269539">"વિનંતી યોગ્ય રીતે હેન્ડલ કરી શકાતી નથી."</string>
<string name="status_unknown_error" msgid="8156660554237824912">"અજાણી ભૂલ."</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"બ્લૂટૂથથી મળેલી ફાઇલો"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"બ્લૂટૂથ પ્રાપ્ત"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"બ્લૂટૂથ શેર"</string>
<string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> પ્રાપ્તિ પૂર્ણ"</string>
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> મોકલવું પૂર્ણ."</string>
<string name="inbound_history_title" msgid="6940914942271327563">"ઇન્બાઉન્ડ સ્થાનાંતરણો"</string>
<string name="outbound_history_title" msgid="4279418703178140526">"આઉટબાઉન્ડ સ્થાનાંતરણ"</string>
- <string name="no_transfers" msgid="3482965619151865672">"કંઈપણ ટ્રાન્સફર કરવામાં આવ્યું નથી."</string>
+ <string name="no_transfers" msgid="3482965619151865672">"સ્થાનાંતરણ ઇતિહાસ ખાલી છે."</string>
<string name="transfer_clear_dlg_msg" msgid="1712376797268438075">"સૂચિમાંથી તમામ આઇટમ્સ સાફ કરવામાં આવશે."</string>
<string name="outbound_noti_title" msgid="8051906709452260849">"બ્લૂટૂથ શેર: મોકલેલી ફાઇલો"</string>
<string name="inbound_noti_title" msgid="4143352641953027595">"બ્લૂટૂથ શેર: પ્રાપ્ત ફાઇલો"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 9e66d3b..4ed2477 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -100,13 +100,13 @@
<string name="status_connection_error" msgid="947681831523219891">"कनेक्शन विफल."</string>
<string name="status_protocol_error" msgid="3245444473429269539">"अनुरोध को सही तरह से प्रबंधित नहीं किया जा सकता."</string>
<string name="status_unknown_error" msgid="8156660554237824912">"अज्ञात गड़बड़ी."</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"ब्लूटूथ से मिली फ़ाइलें"</string>
- <string name="opp_notification_group" msgid="3486303082135789982">"ब्लूटूथ के ज़रिए शेयर"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"ब्लूटूथ प्राप्त"</string>
+ <string name="opp_notification_group" msgid="3486303082135789982">"ब्लूटूथ के ज़रिए साझा"</string>
<string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> प्राप्ति पूर्ण."</string>
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> भेजना पूर्ण."</string>
<string name="inbound_history_title" msgid="6940914942271327563">"इनबाउंड स्थानांतरण"</string>
<string name="outbound_history_title" msgid="4279418703178140526">"आउटबाउंड स्थानांतरण"</string>
- <string name="no_transfers" msgid="3482965619151865672">"कुछ भी ट्रांसफ़र नहीं किया गया."</string>
+ <string name="no_transfers" msgid="3482965619151865672">"स्थानांतरण इतिहास खाली है."</string>
<string name="transfer_clear_dlg_msg" msgid="1712376797268438075">"सूची से सभी आइटम साफ़ कर दिए जाएंगे."</string>
<string name="outbound_noti_title" msgid="8051906709452260849">"ब्लूटूथ शेयर: भेजी गई फ़ाइलें"</string>
<string name="inbound_noti_title" msgid="4143352641953027595">"ब्लूटूथ शेयर: प्राप्त फ़ाइलें"</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 54396de..60dfe89 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -100,7 +100,7 @@
<string name="status_connection_error" msgid="947681831523219891">"Միացումը անհաջող էր:"</string>
<string name="status_protocol_error" msgid="3245444473429269539">"Հարցումը հնարավոր չէ ճշգրտորեն մշակել:"</string>
<string name="status_unknown_error" msgid="8156660554237824912">"Անհայտ սխալ:"</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"Ստացված է Bluetooth-ով"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"Bluetooth-ը ստացված է"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"Bluetooth համօգտագործում"</string>
<string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> ստացումն ավարտված է:"</string>
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> ուղարկումն ավարտված է:"</string>
diff --git a/res/values-hy/strings_map.xml b/res/values-hy/strings_map.xml
deleted file mode 100644
index 91f2078..0000000
--- a/res/values-hy/strings_map.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="map_session_key_dialog_title" msgid="4357431314165953502">"Մուտքագրեք աշխատաշրջանի բանալին %1$s-ի համար"</string>
- <string name="map_session_key_dialog_header" msgid="1858363785687455516">"Պահանջվում է Bluetooth-ի աշխատաշրջանի բանալի"</string>
- <string name="map_acceptance_timeout_message" msgid="2247454902690565876">"%1$s-ի հետ կապի ընդունման ժամանակը սպառվեց"</string>
- <string name="map_authentication_timeout_message" msgid="2151423397615327066">"%1$s-ով աշխատաշրջանի բանալու մուտքագրման ժամանակը սպառվեց"</string>
- <string name="map_auth_notif_ticker" msgid="8840174807927327119">"Կասեցնել նույնականացման հարցումը"</string>
- <string name="map_auth_notif_title" msgid="301767019364765129">"Աշխատաշրջանի բանալի"</string>
- <string name="map_auth_notif_message" msgid="1510095655064214863">"Մուտքագրեք աշխատաշրջանի բանալին %1$s-ի համար"</string>
- <string name="map_defaultname" msgid="3805234816465216759">"Carkit"</string>
- <string name="map_unknownName" msgid="4985352365863687360">"Անհայտ անուն"</string>
- <string name="map_localPhoneName" msgid="6264496105941797768">"Իմ անունը"</string>
- <string name="map_defaultnumber" msgid="3838264444043503815">"000000"</string>
-</resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 602b60e..8a6396a 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -78,7 +78,7 @@
<string name="not_exist_file_desc" msgid="4059531573790529229">"הקובץ לא קיים. \n"</string>
<string name="enabling_progress_title" msgid="436157952334723406">"המתן..."</string>
<string name="enabling_progress_content" msgid="4601542238119927904">"מפעיל Bluetooth…"</string>
- <string name="bt_toast_1" msgid="972182708034353383">"הקובץ יתקבל. יש לבדוק את ההתקדמות בלוח ההתראות."</string>
+ <string name="bt_toast_1" msgid="972182708034353383">"הקובץ יתקבל. בדוק את ההתקדמות בחלונית \'הודעות\'."</string>
<string name="bt_toast_2" msgid="8602553334099066582">"לא ניתן לקבל את הקובץ."</string>
<string name="bt_toast_3" msgid="6707884165086862518">"הופסקה קבלת קובץ מאת \"<xliff:g id="SENDER">%1$s</xliff:g>\""</string>
<string name="bt_toast_4" msgid="4678812947604395649">"שולח קובץ אל \"<xliff:g id="RECIPIENT">%1$s</xliff:g>\""</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index e4bff90..8b91ce0 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -100,7 +100,7 @@
<string name="status_connection_error" msgid="947681831523219891">"接続できませんでした。"</string>
<string name="status_protocol_error" msgid="3245444473429269539">"リクエストを正しく処理できません。"</string>
<string name="status_unknown_error" msgid="8156660554237824912">"不明なエラーです。"</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"Bluetooth で受信"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"Bluetoothで受信"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"Bluetooth 共有"</string>
<string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g>の受信が完了しました。"</string>
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g>の送信が完了しました。"</string>
diff --git a/res/values-ka/strings_map.xml b/res/values-ka/strings_map.xml
deleted file mode 100644
index 8e645c4..0000000
--- a/res/values-ka/strings_map.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="map_session_key_dialog_title" msgid="4357431314165953502">"სესიის გასაღები %1$s-ისთვის"</string>
- <string name="map_session_key_dialog_header" msgid="1858363785687455516">"აუცილებელია Bluetooth სესიის გასაღები"</string>
- <string name="map_acceptance_timeout_message" msgid="2247454902690565876">"%1$s-თან კავშირის მიღების დრო ამოიწურა"</string>
- <string name="map_authentication_timeout_message" msgid="2151423397615327066">"%1$s-თან სესიის გასაღების შეყვანის დრო ამოიწურა"</string>
- <string name="map_auth_notif_ticker" msgid="8840174807927327119">"Obex ავთენტიფიკაციის მოთხოვნა"</string>
- <string name="map_auth_notif_title" msgid="301767019364765129">"სესიის გასაღები"</string>
- <string name="map_auth_notif_message" msgid="1510095655064214863">"სესიის გასაღები %1$s-ისთვის"</string>
- <string name="map_defaultname" msgid="3805234816465216759">"მანქანის ნაკრები"</string>
- <string name="map_unknownName" msgid="4985352365863687360">"უცნობი სახელი"</string>
- <string name="map_localPhoneName" msgid="6264496105941797768">"ჩემი სახელი"</string>
- <string name="map_defaultnumber" msgid="3838264444043503815">"000000"</string>
-</resources>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index bb91d3e..aaefa04 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -100,7 +100,7 @@
<string name="status_connection_error" msgid="947681831523219891">"Байланыс сәтсіз болды."</string>
<string name="status_protocol_error" msgid="3245444473429269539">"Өтінішті дұрыс орындау мүмкін емес."</string>
<string name="status_unknown_error" msgid="8156660554237824912">"Белгісіз қателік."</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"Bluetooth арқылы алынғандар"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"Bluetooth қабылданды"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"Bluetooth бөлісу"</string>
<string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> Толығымен қабылданды."</string>
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> Жіберіліп болды."</string>
diff --git a/res/values-km/strings_map.xml b/res/values-km/strings_map.xml
deleted file mode 100644
index 8118669..0000000
--- a/res/values-km/strings_map.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="map_session_key_dialog_title" msgid="4357431314165953502">"បញ្ចូលសោសម័យសម្រាប់ %1$s"</string>
- <string name="map_session_key_dialog_header" msgid="1858363785687455516">"បានទាមទារសោសម័យប៊្លូធូស"</string>
- <string name="map_acceptance_timeout_message" msgid="2247454902690565876">"អស់ពេលវេលាក្នុងការទទួលយកការតភ្ជាប់ជាមួយ %1$s"</string>
- <string name="map_authentication_timeout_message" msgid="2151423397615327066">"អស់ពេលដើម្បីបញ្ចូលសោសម័យជាមួយ %1$s"</string>
- <string name="map_auth_notif_ticker" msgid="8840174807927327119">"សំណើការផ្ទៀងផ្ទាត់ Obex"</string>
- <string name="map_auth_notif_title" msgid="301767019364765129">"សោសម័យ"</string>
- <string name="map_auth_notif_message" msgid="1510095655064214863">"បញ្ចូលសោសម័យសម្រាប់ %1$s"</string>
- <string name="map_defaultname" msgid="3805234816465216759">"Carkit"</string>
- <string name="map_unknownName" msgid="4985352365863687360">"មិនស្គាល់ឈ្មោះ"</string>
- <string name="map_localPhoneName" msgid="6264496105941797768">"ឈ្មោះរបស់ខ្ញុំ"</string>
- <string name="map_defaultnumber" msgid="3838264444043503815">"000000"</string>
-</resources>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 64c1ede..0d9521e 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -100,13 +100,13 @@
<string name="status_connection_error" msgid="947681831523219891">"Туташуу ийгиликсиз."</string>
<string name="status_protocol_error" msgid="3245444473429269539">"Сурамды туура иштетүү мүмкүн эмес."</string>
<string name="status_unknown_error" msgid="8156660554237824912">"Белгисиз ката."</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"Bluetooth аркылуу алынгандар"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"Bluetooth кабыл алгандар"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"Bluetooth аркылуу бөлүшүү"</string>
<string name="download_success" msgid="7036160438766730871">"Бардыгы кабыл алынды: <xliff:g id="FILE_SIZE">%1$s</xliff:g>."</string>
<string name="upload_success" msgid="4014469387779648949">"Бардыгы жөнөтүлдү: <xliff:g id="FILE_SIZE">%1$s</xliff:g>."</string>
<string name="inbound_history_title" msgid="6940914942271327563">"Кириш өткөрүүлөр"</string>
<string name="outbound_history_title" msgid="4279418703178140526">"Чыгуучу өткөрүүлөр"</string>
- <string name="no_transfers" msgid="3482965619151865672">"Эч нерсе алына элек"</string>
+ <string name="no_transfers" msgid="3482965619151865672">"Өткөрүү тарыхы бош."</string>
<string name="transfer_clear_dlg_msg" msgid="1712376797268438075">"Тизмек толугу менен тазаланат."</string>
<string name="outbound_noti_title" msgid="8051906709452260849">"Bluetooth бөлүшүү: Файлдар жөнөтүлдү"</string>
<string name="inbound_noti_title" msgid="4143352641953027595">"Bluetooth бөлүшүү: Алынган файлдар"</string>
@@ -128,7 +128,7 @@
<string name="bluetooth_map_settings_count" msgid="4557473074937024833">"Калган көзөнөктөр:"</string>
<string name="bluetooth_map_settings_app_icon" msgid="7105805610929114707">"Колдонмонун сөлөкөтү"</string>
<string name="bluetooth_map_settings_title" msgid="7420332483392851321">"Bluetooth билдирүү бөлүшүү жөндөөлөрү"</string>
- <string name="bluetooth_map_settings_no_account_slots_left" msgid="1796029082612965251">"Аккаунт тандалбай жатат: 0 орун калды"</string>
+ <string name="bluetooth_map_settings_no_account_slots_left" msgid="1796029082612965251">"Каттоо эсебин тандоо мүмкүн эмес. 0 көзөнөк калды"</string>
<string name="bluetooth_connected" msgid="6718623220072656906">"Bluetooth аудио туташты"</string>
<string name="bluetooth_disconnected" msgid="3318303728981478873">"Bluetooth аудио ажыратылды"</string>
<string name="a2dp_sink_mbs_label" msgid="7566075853395412558">"Bluetooth аудио"</string>
diff --git a/res/values-lo/strings_map.xml b/res/values-lo/strings_map.xml
deleted file mode 100644
index dd3a288..0000000
--- a/res/values-lo/strings_map.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="map_session_key_dialog_title" msgid="4357431314165953502">"ພິມລະຫັດເຊສຊັນສຳລັບ %1$s"</string>
- <string name="map_session_key_dialog_header" msgid="1858363785687455516">"ຕ້ອງມີ Bluetooth ລະຫັດເຊສຊັນ"</string>
- <string name="map_acceptance_timeout_message" msgid="2247454902690565876">"ໝົດເວລາທີ່ຈະຮັບການເຊື່ອມຕໍ່ກັບ %1$s"</string>
- <string name="map_authentication_timeout_message" msgid="2151423397615327066">"ເກີດໝົດເວລາກ່ອນທີ່ຈະໃສ່ລະຫັດເຊສຊັນກັບ %1$s"</string>
- <string name="map_auth_notif_ticker" msgid="8840174807927327119">"ການຮ້ອງຂໍພິສູດຢືນຢັນໂຕ Obex"</string>
- <string name="map_auth_notif_title" msgid="301767019364765129">"ກະແຈເຊສຊັສ"</string>
- <string name="map_auth_notif_message" msgid="1510095655064214863">"ພິມລະຫັດເຊສຊັນສຳລັບ %1$s"</string>
- <string name="map_defaultname" msgid="3805234816465216759">"Carkit"</string>
- <string name="map_unknownName" msgid="4985352365863687360">"ບໍ່ຮູ້ຊື່"</string>
- <string name="map_localPhoneName" msgid="6264496105941797768">"ຊື່ຂອງຂ້ອຍ"</string>
- <string name="map_defaultnumber" msgid="3838264444043503815">"000000"</string>
-</resources>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index c6ec941..6936fd7 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -100,7 +100,7 @@
<string name="status_connection_error" msgid="947681831523219891">"Врската е неуспешна."</string>
<string name="status_protocol_error" msgid="3245444473429269539">"Барањето не може да се обработи правилно."</string>
<string name="status_unknown_error" msgid="8156660554237824912">"Непозната грешка."</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"Прием од Bluetooth"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"Примен Bluetooth"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"Споделено преку Bluetooth"</string>
<string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> Примањето е завршено."</string>
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> Праќањето е завршено."</string>
diff --git a/res/values-mn/strings_map.xml b/res/values-mn/strings_map.xml
deleted file mode 100644
index 45fc07d..0000000
--- a/res/values-mn/strings_map.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="map_session_key_dialog_title" msgid="4357431314165953502">"%1$s-н горимын түлхүүрийг оруулна уу"</string>
- <string name="map_session_key_dialog_header" msgid="1858363785687455516">"Блютүүт горимын түлхүүр шаардлагатай"</string>
- <string name="map_acceptance_timeout_message" msgid="2247454902690565876">"%1$s-тай холболт хийхийг зөвшөөрөх явцад хугацаа хэтэрсэн байна"</string>
- <string name="map_authentication_timeout_message" msgid="2151423397615327066">"%1$s-д горимын түлхүүр оруулах явцад хугацаа хэтэрсэн байна"</string>
- <string name="map_auth_notif_ticker" msgid="8840174807927327119">"Obex гэрчлэлтийн хүсэлт"</string>
- <string name="map_auth_notif_title" msgid="301767019364765129">"Горимын Түлхүүр"</string>
- <string name="map_auth_notif_message" msgid="1510095655064214863">"%1$s-н горимын түлхүүрийг оруулна уу"</string>
- <string name="map_defaultname" msgid="3805234816465216759">"Carkit"</string>
- <string name="map_unknownName" msgid="4985352365863687360">"Тодорхойгүй нэр"</string>
- <string name="map_localPhoneName" msgid="6264496105941797768">"Миний нэр"</string>
- <string name="map_defaultnumber" msgid="3838264444043503815">"000000"</string>
-</resources>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index 345f44f..a5b2b39 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -26,7 +26,7 @@
<string name="airplane_error_title" msgid="2683839635115739939">"विमान मोड"</string>
<string name="airplane_error_msg" msgid="8698965595254137230">"तुम्ही विमान मोड मध्ये ब्लूटूथ वापरू शकत नाही."</string>
<string name="bt_enable_title" msgid="8657832550503456572"></string>
- <string name="bt_enable_line1" msgid="7203551583048149">"ब्लूटूथ सेवांचा वापर करण्यासाठी, तुम्ही प्रथम ब्लूटूथ चालू करा."</string>
+ <string name="bt_enable_line1" msgid="7203551583048149">"ब्लूटुथ सेवांचा वापर करण्यासाठी, आपण प्रथम ब्लूटुथ चालू करा."</string>
<string name="bt_enable_line2" msgid="4341936569415937994">"आता ब्लूटूथ चालू करायचे?"</string>
<string name="bt_enable_cancel" msgid="1988832367505151727">"रद्द करा"</string>
<string name="bt_enable_ok" msgid="3432462749994538265">"चालू करा"</string>
@@ -73,7 +73,7 @@
<string name="upload_fail_cancel" msgid="9118496285835687125">"बंद करा"</string>
<string name="bt_error_btn_ok" msgid="5965151173011534240">"ठीक"</string>
<string name="unknown_file" msgid="6092727753965095366">"अज्ञात फाइल"</string>
- <string name="unknown_file_desc" msgid="480434281415453287">"या प्रकारची फाइल हाताळण्यासाठी कोणताही अॅप नाही. \n"</string>
+ <string name="unknown_file_desc" msgid="480434281415453287">"या प्रकारची फाइल हाताळण्यासाठी कोणताही अॅप नाही. \n"</string>
<string name="not_exist_file" msgid="3489434189599716133">"फाइल नाही"</string>
<string name="not_exist_file_desc" msgid="4059531573790529229">"फाइल अस्तित्वात नाही. \n"</string>
<string name="enabling_progress_title" msgid="436157952334723406">"कृपया प्रतीक्षा करा..."</string>
@@ -84,8 +84,8 @@
<string name="bt_toast_4" msgid="4678812947604395649">"\"<xliff:g id="RECIPIENT">%1$s</xliff:g>\" ना फाइल पाठवित आहे"</string>
<string name="bt_toast_5" msgid="2846870992823019494">"\"<xliff:g id="RECIPIENT">%2$s</xliff:g>\" ना <xliff:g id="NUMBER">%1$s</xliff:g> फायली पाठवित आहे"</string>
<string name="bt_toast_6" msgid="1855266596936622458">"\"<xliff:g id="RECIPIENT">%1$s</xliff:g>\" ना फाइल पाठविणे थांबविले"</string>
- <string name="bt_sm_2_1" product="nosdcard" msgid="352165168004521000">"\"<xliff:g id="SENDER">%1$s</xliff:g>\" कडील फाइल सेव्ह करण्यासाठी USB संचयनात पुरेसे स्थान नाही"</string>
- <string name="bt_sm_2_1" product="default" msgid="1989018443456803630">"\"<xliff:g id="SENDER">%1$s</xliff:g>\" कडील फाइल सेव्ह करण्यासाठी SD कार्डवर पुरेसे स्थान नाही"</string>
+ <string name="bt_sm_2_1" product="nosdcard" msgid="352165168004521000">"\"<xliff:g id="SENDER">%1$s</xliff:g>\" कडील फाइल जतन करण्यासाठी USB संचयनात पुरेसे स्थान नाही"</string>
+ <string name="bt_sm_2_1" product="default" msgid="1989018443456803630">"\"<xliff:g id="SENDER">%1$s</xliff:g>\" कडील फाइल जतन करण्यासाठी SD कार्डवर पुरेसे स्थान नाही"</string>
<string name="bt_sm_2_2" msgid="2965243265852680543">"स्थान आवश्यक: <xliff:g id="SIZE">%1$s</xliff:g>"</string>
<string name="ErrorTooManyRequests" msgid="8578277541472944529">"बर्याच विनंत्यांवर प्रक्रिया होत आहे. नंतर पुन्हा प्रयत्न करा."</string>
<string name="status_pending" msgid="2503691772030877944">"फाइल स्थानांतरणाचा अद्याप प्रारंभ झाला नाही."</string>
@@ -96,7 +96,7 @@
<string name="status_canceled" msgid="6664490318773098285">"वापरकर्त्याने स्थानांतरण रद्द केले."</string>
<string name="status_file_error" msgid="3671917770630165299">"संचयन समस्या."</string>
<string name="status_no_sd_card" product="nosdcard" msgid="1112125377088421469">"USB स्टोरेज नाही."</string>
- <string name="status_no_sd_card" product="default" msgid="5760944071743325592">"SD कार्ड नाही. स्थानांतरित केलेल्या फायली सेव्ह करण्यासाठी SD कार्ड घाला."</string>
+ <string name="status_no_sd_card" product="default" msgid="5760944071743325592">"SD कार्ड नाही. स्थानांतरित केलेल्या फायली जतन करण्यासाठी SD कार्ड घाला."</string>
<string name="status_connection_error" msgid="947681831523219891">"कनेक्शन अयशस्वी."</string>
<string name="status_protocol_error" msgid="3245444473429269539">"विनंती योग्य रीतीने हाताळली जाऊ शकत नाही."</string>
<string name="status_unknown_error" msgid="8156660554237824912">"अज्ञात एरर"</string>
@@ -106,7 +106,7 @@
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> पाठविणे पूर्ण."</string>
<string name="inbound_history_title" msgid="6940914942271327563">"इनबाउंड स्थानांतरणे"</string>
<string name="outbound_history_title" msgid="4279418703178140526">"आउटबाउंड स्थानांतरणे"</string>
- <string name="no_transfers" msgid="3482965619151865672">"ट्रांसफर इतिहास रिक्त आहे."</string>
+ <string name="no_transfers" msgid="3482965619151865672">"स्थानांतरण इतिहास रिक्त आहे."</string>
<string name="transfer_clear_dlg_msg" msgid="1712376797268438075">"सूचीमधून सर्व आयटम साफ केले जातील."</string>
<string name="outbound_noti_title" msgid="8051906709452260849">"ब्लूटूथ शेअर: पाठवलेल्या फायली"</string>
<string name="inbound_noti_title" msgid="4143352641953027595">"ब्लूटूथ शेअर: मिळवलेल्या फायली"</string>
@@ -124,10 +124,10 @@
<string name="transfer_clear_dlg_title" msgid="2953444575556460386">"साफ करा"</string>
<string name="bluetooth_map_settings_save" msgid="7635491847388074606">"सेव्ह करा"</string>
<string name="bluetooth_map_settings_cancel" msgid="9205350798049865699">"रद्द करा"</string>
- <string name="bluetooth_map_settings_intro" msgid="6482369468223987562">"तुम्ही ब्लूटूथद्वारे शेअर करू इच्छित असलेली खाती निवडा. कनेक्ट करताना अद्याप तुम्ही खात्यांमधील कोणताही अॅक्सेस स्वीकारण्याची आवश्यकता आहे."</string>
+ <string name="bluetooth_map_settings_intro" msgid="6482369468223987562">"तुम्ही ब्लूटूथद्वारे सामायिक करू इच्छित असलेली खाती निवडा. कनेक्ट करताना अद्याप तुम्ही खात्यांमधील कोणताही अॅक्सेस स्वीकारण्याची आवश्यकता आहे."</string>
<string name="bluetooth_map_settings_count" msgid="4557473074937024833">"स्लॉट शिल्लक:"</string>
<string name="bluetooth_map_settings_app_icon" msgid="7105805610929114707">"अॅप्लिकेशन आयकन"</string>
- <string name="bluetooth_map_settings_title" msgid="7420332483392851321">"ब्लूटूथ मेसेज सामायिकरण सेटिंग्ज"</string>
+ <string name="bluetooth_map_settings_title" msgid="7420332483392851321">"ब्लूटूथ संदेश सामायिकरण सेटिंग्ज"</string>
<string name="bluetooth_map_settings_no_account_slots_left" msgid="1796029082612965251">"खाते निवडू शकत नाही. 0 स्लॉट शिल्लक"</string>
<string name="bluetooth_connected" msgid="6718623220072656906">"ब्लूटूथ ऑडिओ कनेक्ट केला"</string>
<string name="bluetooth_disconnected" msgid="3318303728981478873">"ब्लूटूथ ऑडिओ डिस्कनेक्ट केला"</string>
diff --git a/res/values-ms/strings_map.xml b/res/values-ms/strings_map.xml
deleted file mode 100644
index afc98d7..0000000
--- a/res/values-ms/strings_map.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="map_session_key_dialog_title" msgid="4357431314165953502">"Taipkan kunci sesi untuk %1$s"</string>
- <string name="map_session_key_dialog_header" msgid="1858363785687455516">"Kunci sesi Bluetooth diperlukan"</string>
- <string name="map_acceptance_timeout_message" msgid="2247454902690565876">"Berlaku tamat masa semasa menerima sambungan dengan %1$s"</string>
- <string name="map_authentication_timeout_message" msgid="2151423397615327066">"Tamat masa berlaku semasa memasukkan kunci sesi dengan %1$s"</string>
- <string name="map_auth_notif_ticker" msgid="8840174807927327119">"Permintaan pengesahan Obex"</string>
- <string name="map_auth_notif_title" msgid="301767019364765129">"Kunci Sesi"</string>
- <string name="map_auth_notif_message" msgid="1510095655064214863">"Taipkan kunci sesi untuk %1$s"</string>
- <string name="map_defaultname" msgid="3805234816465216759">"Kit kereta"</string>
- <string name="map_unknownName" msgid="4985352365863687360">"Nama tidak diketahui"</string>
- <string name="map_localPhoneName" msgid="6264496105941797768">"Nama saya"</string>
- <string name="map_defaultnumber" msgid="3838264444043503815">"000000"</string>
-</resources>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index cfdea66..5865087 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -22,7 +22,7 @@
<string name="permdesc_bluetoothWhitelist" msgid="5494513855192170109">"အက်ပ်အား ဘလူးတုသ်စက်ကို ယာယီခွင့်ပြုစာရင်းထဲ ထည့်ရန်ခွင့်ပြုကာ အသုံးပြုသူ၏ အတည်ပြုချက်မရယူပဲ ဖိုင်များကို စက်ထဲသို့ ပို့ခွင့်ပြုမည်"</string>
<string name="bt_share_picker_label" msgid="6268100924487046932">"ဘလူးတုသ်"</string>
<string name="unknown_device" msgid="9221903979877041009">"မသိသော စက်"</string>
- <string name="unknownNumber" msgid="4994750948072751566">"မသိပါ"</string>
+ <string name="unknownNumber" msgid="4994750948072751566">"အကြောင်းအရာ မသိရှိ"</string>
<string name="airplane_error_title" msgid="2683839635115739939">"လေယာဉ်ပျံမုဒ်"</string>
<string name="airplane_error_msg" msgid="8698965595254137230">"လေယာဥ်ပျံပေါ်သုံးစနစ်တွင် ဘလူးတုသ်အသုံးပြုမရပါ"</string>
<string name="bt_enable_title" msgid="8657832550503456572"></string>
@@ -96,7 +96,7 @@
<string name="status_canceled" msgid="6664490318773098285">"အသုံးပြုသူမှ လွှဲပြောင်းခြင်းကို ဖယ်ဖျက်ရန်"</string>
<string name="status_file_error" msgid="3671917770630165299">"သိုလှောင်မှု အချက်"</string>
<string name="status_no_sd_card" product="nosdcard" msgid="1112125377088421469">"USBသိုလှောင်ရာမရှိပါ"</string>
- <string name="status_no_sd_card" product="default" msgid="5760944071743325592">"SDကဒ်မရှိပါ၊ လွှဲပြောင်းထားသော ဖိုင်များကို သိမ်းရန် SDကဒ်ထည့်ပါ"</string>
+ <string name="status_no_sd_card" product="default" msgid="5760944071743325592">"SDကဒ်မရှိပါ၊ လွှဲပြောင်းထားသော ဖိုင်များကို သိမ်းဆည်းရန် SDကဒ်ထည့်ပါ"</string>
<string name="status_connection_error" msgid="947681831523219891">"ချိတ်ဆက်ခြင်း မအောင်မြင်ပါ"</string>
<string name="status_protocol_error" msgid="3245444473429269539">"တောင်းခံခြင်းကို မှန်ကန်စွာကိုင်တွယ်မရပါ"</string>
<string name="status_unknown_error" msgid="8156660554237824912">"အမည်မသိသော မှားယွင်းမှု"</string>
@@ -106,7 +106,7 @@
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> ပို့ခြင်း ပြီးဆုံးပြီး"</string>
<string name="inbound_history_title" msgid="6940914942271327563">"ပြန်လည်ရောက်လာသော လွှဲမှုများ"</string>
<string name="outbound_history_title" msgid="4279418703178140526">"ပြန်လည်ထွက်မည့် လွှဲမှုများ"</string>
- <string name="no_transfers" msgid="3482965619151865672">"လွဲပြောင်းခြင်း မှတ်တမ်းတွင် ဘာမျှမရှိပါ"</string>
+ <string name="no_transfers" msgid="3482965619151865672">"လွဲပြောင်းခြင်း မှတ်တမ်းတွင် ဘာမှမရှိပါ"</string>
<string name="transfer_clear_dlg_msg" msgid="1712376797268438075">"အရာများအားလုံး စာရင်းမှ ရှင်းလင်းပစ်လိမ့်မည်"</string>
<string name="outbound_noti_title" msgid="8051906709452260849">"ဘလူးတုသ် ဝေမျှမှု - ဖိုင်ပို့ပြီး"</string>
<string name="inbound_noti_title" msgid="4143352641953027595">"ဘလူးတုသ် ဝေမျှမှု - ရရှိပြီးဖိုင်များ"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 24c8a16..996c0b7 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -34,7 +34,7 @@
<string name="incoming_file_confirm_content" msgid="2752605552743148036">"आगमन फाइल स्वीकार गर्नुहुन्छ?"</string>
<string name="incoming_file_confirm_cancel" msgid="2973321832477704805">"अस्वीकार गर्नुहोस्"</string>
<string name="incoming_file_confirm_ok" msgid="281462442932231475">"स्वीकार्नुहोस्"</string>
- <string name="incoming_file_confirm_timeout_ok" msgid="1414676773249857278">"ठिक छ"</string>
+ <string name="incoming_file_confirm_timeout_ok" msgid="1414676773249857278">"ठीक छ"</string>
<string name="incoming_file_confirm_timeout_content" msgid="172779756093975981">"\"<xliff:g id="SENDER">%1$s</xliff:g>\" बाट आगमन फाइल स्वीकार्दा समय सकिएको थियो"</string>
<string name="incoming_file_confirm_Notification_title" msgid="5573329005298936903">"आगमन फाइल"</string>
<string name="incoming_file_confirm_Notification_content" msgid="3359694069319644738">"<xliff:g id="SENDER">%1$s</xliff:g> ले <xliff:g id="FILE">%2$s</xliff:g> पठाउन तयार छ"</string>
@@ -59,19 +59,19 @@
<string name="download_fail_line1" msgid="3846450148862894552">"फाइल प्राप्त भएन"</string>
<string name="download_fail_line2" msgid="8950394574689971071">"फाइल: <xliff:g id="FILE">%1$s</xliff:g>"</string>
<string name="download_fail_line3" msgid="3451040656154861722">"कारण: <xliff:g id="REASON">%1$s</xliff:g>"</string>
- <string name="download_fail_ok" msgid="1521733664438320300">"ठिक छ"</string>
+ <string name="download_fail_ok" msgid="1521733664438320300">"ठीक छ"</string>
<string name="download_succ_line5" msgid="4509944688281573595">"फाइल प्राप्त भयो"</string>
<string name="download_succ_ok" msgid="7053688246357050216">"खोल्नुहोस्"</string>
<string name="upload_line1" msgid="2055952074059709052">"लाई:\"<xliff:g id="RECIPIENT">%1$s</xliff:g> \""</string>
<string name="upload_line3" msgid="4920689672457037437">"फाइल प्रकार: <xliff:g id="TYPE">%1$s</xliff:g> ( <xliff:g id="SIZE">%2$s</xliff:g> )"</string>
<string name="upload_line5" msgid="7759322537674229752">"फाइल पठाउँदै..."</string>
<string name="upload_succ_line5" msgid="5687317197463383601">"फाइल पठाइयो"</string>
- <string name="upload_succ_ok" msgid="7705428476405478828">"ठिक छ"</string>
+ <string name="upload_succ_ok" msgid="7705428476405478828">"ठीक छ"</string>
<string name="upload_fail_line1" msgid="7899394672421491701">"फाइल\"<xliff:g id="RECIPIENT">%1$s</xliff:g>\" लाई पठाइएन।"</string>
<string name="upload_fail_line1_2" msgid="2108129204050841798">"फाइल: <xliff:g id="FILE">%1$s</xliff:g>"</string>
<string name="upload_fail_ok" msgid="5807702461606714296">"फेरि प्रयास गर्नुहोस्"</string>
<string name="upload_fail_cancel" msgid="9118496285835687125">"बन्द गर्नुहोस्"</string>
- <string name="bt_error_btn_ok" msgid="5965151173011534240">"ठिक छ"</string>
+ <string name="bt_error_btn_ok" msgid="5965151173011534240">"ठीक छ"</string>
<string name="unknown_file" msgid="6092727753965095366">"अज्ञात फाइल"</string>
<string name="unknown_file_desc" msgid="480434281415453287">"यस प्रकारको फाइल सम्हालन कुनै अनुप्रयोग छैन।\n"</string>
<string name="not_exist_file" msgid="3489434189599716133">"फाइल छैन"</string>
diff --git a/res/values-ne/strings_map.xml b/res/values-ne/strings_map.xml
deleted file mode 100644
index 7584c1e..0000000
--- a/res/values-ne/strings_map.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="map_session_key_dialog_title" msgid="4357431314165953502">"%1$s का लागि सत्र कुञ्जी टाइप गर्नुहोस्"</string>
- <string name="map_session_key_dialog_header" msgid="1858363785687455516">"ब्लूटूथ सत्र कुञ्जी आवश्यक"</string>
- <string name="map_acceptance_timeout_message" msgid="2247454902690565876">"%1$s सँग जडान स्वीकार गर्न समय सकियो"</string>
- <string name="map_authentication_timeout_message" msgid="2151423397615327066">"%1$s का साथ सत्र कुञ्जी इनपुट गर्ने समय सकियो"</string>
- <string name="map_auth_notif_ticker" msgid="8840174807927327119">"Obex प्रमाणिकरण अनुरोध"</string>
- <string name="map_auth_notif_title" msgid="301767019364765129">"सत्र कुञ्जी"</string>
- <string name="map_auth_notif_message" msgid="1510095655064214863">"%1$s का लागि सत्र कुञ्जी टाइप गर्नुहोस्"</string>
- <string name="map_defaultname" msgid="3805234816465216759">"Carkit"</string>
- <string name="map_unknownName" msgid="4985352365863687360">"अज्ञात नाम"</string>
- <string name="map_localPhoneName" msgid="6264496105941797768">"मेरो नाम"</string>
- <string name="map_defaultnumber" msgid="3838264444043503815">"००००००"</string>
-</resources>
diff --git a/res/values-ne/test_strings.xml b/res/values-ne/test_strings.xml
index df5a45f..ce2bc40 100644
--- a/res/values-ne/test_strings.xml
+++ b/res/values-ne/test_strings.xml
@@ -6,7 +6,7 @@
<string name="update_record" msgid="2480425402384910635">"रेकर्ड निश्चित गर्नुहोस्"</string>
<string name="ack_record" msgid="6716152390978472184">"Ack रेकर्ड"</string>
<string name="deleteAll_record" msgid="4383349788485210582">"सबै रेकर्ड मेटाउनुहोस्"</string>
- <string name="ok_button" msgid="6519033415223065454">"ठिक छ"</string>
+ <string name="ok_button" msgid="6519033415223065454">"ठीक छ"</string>
<string name="delete_record" msgid="4645040331967533724">"रेकर्ड मेटाउनुहोस्"</string>
<string name="start_server" msgid="9034821924409165795">"TCP सर्भर सुरु गर्नुहोस्"</string>
<string name="notify_server" msgid="4369106744022969655">"TCP सर्भर सूचित गर्नुहोस्"</string>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 39e300d..24a48e8 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -37,7 +37,7 @@
<string name="incoming_file_confirm_timeout_ok" msgid="1414676773249857278">"OK"</string>
<string name="incoming_file_confirm_timeout_content" msgid="172779756093975981">"Er is een time-out opgetreden bij het accepteren van een inkomend bestand van \'<xliff:g id="SENDER">%1$s</xliff:g>\'"</string>
<string name="incoming_file_confirm_Notification_title" msgid="5573329005298936903">"Inkomend bestand"</string>
- <string name="incoming_file_confirm_Notification_content" msgid="3359694069319644738">"<xliff:g id="SENDER">%1$s</xliff:g> is klaar om <xliff:g id="FILE">%2$s</xliff:g> te verzenden"</string>
+ <string name="incoming_file_confirm_Notification_content" msgid="3359694069319644738">"<xliff:g id="SENDER">%1$s</xliff:g> is gereed om <xliff:g id="FILE">%2$s</xliff:g> te verzenden"</string>
<string name="notification_receiving" msgid="4674648179652543984">"Delen via Bluetooth: <xliff:g id="FILE">%1$s</xliff:g> ontvangen"</string>
<string name="notification_received" msgid="3324588019186687985">"Delen via Bluetooth: <xliff:g id="FILE">%1$s</xliff:g> ontvangen"</string>
<string name="notification_received_fail" msgid="3619350997285714746">"Delen via Bluetooth: bestand <xliff:g id="FILE">%1$s</xliff:g> niet ontvangen"</string>
diff --git a/res/values-or/config.xml b/res/values-or/config.xml
deleted file mode 100644
index 4f96544..0000000
--- a/res/values-or/config.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Copyright (C) 2009-2012 Broadcom Corporation
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="pairing_ui_package" msgid="6399948348712579121">"com.android.settings"</string>
-</resources>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
deleted file mode 100644
index b93a78d..0000000
--- a/res/values-or/strings.xml
+++ /dev/null
@@ -1,136 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Copyright (C) 2007 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="permlab_bluetoothShareManager" msgid="311492132450338925">"ଡାଉନଲୋଡ୍ ମ୍ୟାନେଜର୍କୁ ଆକ୍ସେସ୍ କରନ୍ତୁ।"</string>
- <string name="permdesc_bluetoothShareManager" msgid="8930572979123190223">"BluetoothShare ମ୍ୟାନେଜର୍ ଆକ୍ସେସ୍ କରି ଫାଇଲ୍ଗୁଡ଼ିକ ଟ୍ରାନ୍ସଫର୍ କରିବାକୁ ବ୍ୟବହାର କରିବା ପାଇଁ ଆପ୍କୁ ଅନୁମତି ଦିଅନ୍ତୁ।"</string>
- <string name="permlab_bluetoothWhitelist" msgid="7091552898592306386">"ବ୍ଲୁ-ଟୂଥ୍ ଡିଭାଇସ୍ ଆକ୍ସେସ୍କୁ ସ୍ୱୀକୃତି ଦିଅନ୍ତୁ।"</string>
- <string name="permdesc_bluetoothWhitelist" msgid="5494513855192170109">"ୟୁଜର୍ଙ୍କ ସୁନିଶ୍ଚିତତା ବିନା ଏହି ଡିଭାଇସ୍କୁ ଫାଇଲ୍ ପଠାଇବା ନିମନ୍ତେ ଗୋଟିଏ ବ୍ଲୁ-ଟୂଥ୍ ଡିଭାଇସ୍କୁ ଅନୁମତି ଦେଇ କିଛି ସମୟ ପାଇଁ ଆପ୍କୁ ସ୍ୱୀକୃତି ଦେଇଥାଏ।"</string>
- <string name="bt_share_picker_label" msgid="6268100924487046932">"ବ୍ଲୁ-ଟୂଥ୍"</string>
- <string name="unknown_device" msgid="9221903979877041009">"ଅଜଣା ଡିଭାଇସ୍"</string>
- <string name="unknownNumber" msgid="4994750948072751566">"ଅଜଣା"</string>
- <string name="airplane_error_title" msgid="2683839635115739939">"ଏରୋପ୍ଲେନ୍ ମୋଡ୍"</string>
- <string name="airplane_error_msg" msgid="8698965595254137230">"ଆପଣ, ଏୟାରପ୍ଲେନ୍ ମୋଡ୍ରେ ବ୍ଲୁ-ଟୂଥ୍ ବ୍ୟବହାର କରିପାରିବେ ନାହିଁ।"</string>
- <string name="bt_enable_title" msgid="8657832550503456572"></string>
- <string name="bt_enable_line1" msgid="7203551583048149">"ବ୍ଲୁ-ଟୂଥ୍ ସେବା ବ୍ୟବହାର କରିବା ପାଇଁ, ଆପଣଙ୍କୁ ପ୍ରଥମେ ବ୍ଲୁ-ଟୂଥ୍ ଅନ୍ କରିବାକୁ ପଡ଼ିବ।"</string>
- <string name="bt_enable_line2" msgid="4341936569415937994">"ବ୍ଲୁ-ଟୁଥ୍କୁ ଏବେ ଅନ୍ କରିବେ?"</string>
- <string name="bt_enable_cancel" msgid="1988832367505151727">"କ୍ୟାନ୍ସଲ୍"</string>
- <string name="bt_enable_ok" msgid="3432462749994538265">"ଅନ୍ କରନ୍ତୁ"</string>
- <string name="incoming_file_confirm_title" msgid="8139874248612182627">"ଫାଇଲ୍ ଟ୍ରାନ୍ସଫର୍ କରନ୍ତୁ"</string>
- <string name="incoming_file_confirm_content" msgid="2752605552743148036">"ଆସୁଥିବା ଫାଇଲ୍କୁ ସ୍ୱୀକାର କରିବେ?"</string>
- <string name="incoming_file_confirm_cancel" msgid="2973321832477704805">"ଅସ୍ୱୀକାର"</string>
- <string name="incoming_file_confirm_ok" msgid="281462442932231475">"ସ୍ୱୀକାର କରନ୍ତୁ"</string>
- <string name="incoming_file_confirm_timeout_ok" msgid="1414676773249857278">"ଠିକ୍ ଅଛି"</string>
- <string name="incoming_file_confirm_timeout_content" msgid="172779756093975981">"\"<xliff:g id="SENDER">%1$s</xliff:g>\"ଙ୍କଠାରୁ ଆସୁଥିବା ଫାଇଲ୍ ସ୍ୱୀକାର କରୁଥିବାବେଳେ ସମୟ ସମାପ୍ତ ହୋଇଗଲା"</string>
- <string name="incoming_file_confirm_Notification_title" msgid="5573329005298936903">"ଆସୁଥିବା ଫାଇଲ୍"</string>
- <string name="incoming_file_confirm_Notification_content" msgid="3359694069319644738">"<xliff:g id="FILE">%2$s</xliff:g> ପଠାଇବା ପାଇଁ <xliff:g id="SENDER">%1$s</xliff:g> ପ୍ରସ୍ତୁତ"</string>
- <string name="notification_receiving" msgid="4674648179652543984">"ବ୍ଲୁ-ଟୂଥ୍ ଶେୟାର୍: <xliff:g id="FILE">%1$s</xliff:g> ପ୍ରାପ୍ତ କରୁଛି"</string>
- <string name="notification_received" msgid="3324588019186687985">"ବ୍ଲୁ-ଟୂଥ୍ ଶେୟାର୍: <xliff:g id="FILE">%1$s</xliff:g> ପ୍ରାପ୍ତ କରାଯାଇଛି"</string>
- <string name="notification_received_fail" msgid="3619350997285714746">"ବ୍ଲୁ-ଟୂଥ୍ ଶେୟାର୍: <xliff:g id="FILE">%1$s</xliff:g> ଫାଇଲ୍ ପ୍ରାପ୍ତ କରାଯାଇନାହିଁ"</string>
- <string name="notification_sending" msgid="3035748958534983833">"ବ୍ଲୁ-ଟୂଥ୍ ଶେୟାର୍: <xliff:g id="FILE">%1$s</xliff:g> ପଠାଉଛି"</string>
- <string name="notification_sent" msgid="9218710861333027778">"ବ୍ଲୁ-ଟୂଥ୍ ଶେୟାର୍: <xliff:g id="FILE">%1$s</xliff:g> ପଠାଗଲା"</string>
- <string name="notification_sent_complete" msgid="302943281067557969">"100% ସମ୍ପୂର୍ଣ୍ଣ"</string>
- <string name="notification_sent_fail" msgid="6696082233774569445">"ବ୍ଲୁ-ଟୂଥ୍ ଶେୟାର୍: <xliff:g id="FILE">%1$s</xliff:g> ଫାଇଲ୍ ପଠାଯାଇନାହିଁ"</string>
- <string name="download_title" msgid="3353228219772092586">"ଫାଇଲ୍ ଟ୍ରାନ୍ସଫର୍ କରନ୍ତୁ"</string>
- <string name="download_line1" msgid="4926604799202134144">"ପ୍ରେରକ: \"<xliff:g id="SENDER">%1$s</xliff:g>\""</string>
- <string name="download_line2" msgid="5876973543019417712">"ଫାଇଲ୍: <xliff:g id="FILE">%1$s</xliff:g>"</string>
- <string name="download_line3" msgid="4384821622908676061">"ଫାଇଲ୍ ଆକାର: <xliff:g id="SIZE">%1$s</xliff:g>"</string>
- <string name="download_line4" msgid="8535996869722666525"></string>
- <string name="download_line5" msgid="3069560415845295386">"ଫାଇଲ୍ ପ୍ରାପ୍ତ କରୁଛି…"</string>
- <string name="download_cancel" msgid="9177305996747500768">"ବନ୍ଦ କରନ୍ତୁ"</string>
- <string name="download_ok" msgid="5000360731674466039">"ଲୁଚାନ୍ତୁ"</string>
- <string name="incoming_line1" msgid="2127419875681087545">"ପ୍ରେରକ"</string>
- <string name="incoming_line2" msgid="3348994249285315873">"ଫାଇଲ୍ ନାମ"</string>
- <string name="incoming_line3" msgid="7954237069667474024">"ଆକାର"</string>
- <string name="download_fail_line1" msgid="3846450148862894552">"ଫାଇଲ୍ ପ୍ରାପ୍ତ ହେଲା ନାହିଁ"</string>
- <string name="download_fail_line2" msgid="8950394574689971071">"ଫାଇଲ୍: <xliff:g id="FILE">%1$s</xliff:g>"</string>
- <string name="download_fail_line3" msgid="3451040656154861722">"କାରଣ: <xliff:g id="REASON">%1$s</xliff:g>"</string>
- <string name="download_fail_ok" msgid="1521733664438320300">"ଠିକ୍ ଅଛି"</string>
- <string name="download_succ_line5" msgid="4509944688281573595">"ଫାଇଲ୍ ପ୍ରାପ୍ତ ହେଲା"</string>
- <string name="download_succ_ok" msgid="7053688246357050216">"ଖୋଲନ୍ତୁ"</string>
- <string name="upload_line1" msgid="2055952074059709052">"ପ୍ରାପ୍ତକର୍ତ୍ତା: \"<xliff:g id="RECIPIENT">%1$s</xliff:g>\""</string>
- <string name="upload_line3" msgid="4920689672457037437">"ଫାଇଲ୍ ପ୍ରକାର: <xliff:g id="TYPE">%1$s</xliff:g> (<xliff:g id="SIZE">%2$s</xliff:g>)"</string>
- <string name="upload_line5" msgid="7759322537674229752">"ଫାଇଲ୍ ପଠାଯାଉଛି…"</string>
- <string name="upload_succ_line5" msgid="5687317197463383601">"ଫାଇଲ୍ ପଠାଗଲା"</string>
- <string name="upload_succ_ok" msgid="7705428476405478828">"ଠିକ୍ ଅଛି"</string>
- <string name="upload_fail_line1" msgid="7899394672421491701">"ଫାଇଲ୍ \"<xliff:g id="RECIPIENT">%1$s</xliff:g>\"ଙ୍କୁ ପଠାଯାଇନଥିଲା।"</string>
- <string name="upload_fail_line1_2" msgid="2108129204050841798">"ଫାଇଲ୍: <xliff:g id="FILE">%1$s</xliff:g>"</string>
- <string name="upload_fail_ok" msgid="5807702461606714296">"ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ"</string>
- <string name="upload_fail_cancel" msgid="9118496285835687125">"ବନ୍ଦ କରନ୍ତୁ"</string>
- <string name="bt_error_btn_ok" msgid="5965151173011534240">"ଠିକ୍ ଅଛି"</string>
- <string name="unknown_file" msgid="6092727753965095366">"ଅଜଣା ଫାଇଲ୍"</string>
- <string name="unknown_file_desc" msgid="480434281415453287">"ଏହିଭଳି ଫାଇଲ୍କୁ ସମ୍ଭାଳିବା ପାଇଁ କୌଣସି ଆପ୍ ନାହିଁ। \n"</string>
- <string name="not_exist_file" msgid="3489434189599716133">"କୌଣସି ଫାଇଲ୍ ନାହିଁ"</string>
- <string name="not_exist_file_desc" msgid="4059531573790529229">"ଏଭଳି କୌଣସି ଫାଇଲ୍ ନାହିଁ। \n"</string>
- <string name="enabling_progress_title" msgid="436157952334723406">"ଦୟାକରି ଅପେକ୍ଷା କରନ୍ତୁ…"</string>
- <string name="enabling_progress_content" msgid="4601542238119927904">"ବ୍ଲୁ-ଟୂଥ୍ ଅନ୍ କରୁଛି…"</string>
- <string name="bt_toast_1" msgid="972182708034353383">"ଫାଇଲ୍କୁ ଗ୍ରହଣ କରାଯିବ। ବିଜ୍ଞପ୍ତି ପ୍ୟାନେଲ୍ରେ ପ୍ରଗତିକୁ ଦେଖନ୍ତୁ।"</string>
- <string name="bt_toast_2" msgid="8602553334099066582">"ଫାଇଲ୍କୁ ଗ୍ରହଣ କରାଯାଇପାରିବ ନାହିଁ।"</string>
- <string name="bt_toast_3" msgid="6707884165086862518">"\"<xliff:g id="SENDER">%1$s</xliff:g>\"ଙ୍କଠାରୁ ଗ୍ରହଣ କରିବା ବନ୍ଦ ହୋଇଗଲା"</string>
- <string name="bt_toast_4" msgid="4678812947604395649">"\"<xliff:g id="RECIPIENT">%1$s</xliff:g>\"ଙ୍କୁ ଫାଇଲ୍ ପଠାଯାଉଛି"</string>
- <string name="bt_toast_5" msgid="2846870992823019494">"\"<xliff:g id="RECIPIENT">%2$s</xliff:g>\"ଙ୍କୁ <xliff:g id="NUMBER">%1$s</xliff:g>ଟି ଫାଇଲ୍ ପଠାଯାଉଛି"</string>
- <string name="bt_toast_6" msgid="1855266596936622458">"\"<xliff:g id="RECIPIENT">%1$s</xliff:g>\"ଙ୍କୁ ଫାଇଲ୍ ପଠାଇବା ବନ୍ଦ ହୋଇଗଲା"</string>
- <string name="bt_sm_2_1" product="nosdcard" msgid="352165168004521000">"\"<xliff:g id="SENDER">%1$s</xliff:g>\"ଙ୍କଠାରୁ ଫାଇଲ୍ ସେଭ୍ କରିବା ପାଇଁ USB ଷ୍ଟୋରେଜ୍ରେ ଯଥେଷ୍ଟ ସ୍ପେସ୍ ନାହିଁ"</string>
- <string name="bt_sm_2_1" product="default" msgid="1989018443456803630">"\"<xliff:g id="SENDER">%1$s</xliff:g>\"ଙ୍କଠାରୁ ଫାଇଲ୍ ସେଭ୍ କରିବା ପାଇଁ SD କାର୍ଡରେ ଯଥେଷ୍ଟ ସ୍ପେସ୍ ନାହିଁ"</string>
- <string name="bt_sm_2_2" msgid="2965243265852680543">"ସ୍ପେସ୍ ଆବଶ୍ୟକ: <xliff:g id="SIZE">%1$s</xliff:g>"</string>
- <string name="ErrorTooManyRequests" msgid="8578277541472944529">"ଅନେକ ଅନୁରୋଧ ଉପରେ କାମ ଚାଲୁଛି। ପରେ ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ।"</string>
- <string name="status_pending" msgid="2503691772030877944">"ଏପର୍ଯ୍ୟନ୍ତ ଫାଇଲ୍ ଟ୍ରାନ୍ସଫର୍ ଆରମ୍ଭ ହୋଇନାହିଁ।"</string>
- <string name="status_running" msgid="6562808920311008696">"ଫାଇଲ୍ ଟ୍ରାନ୍ସଫର୍ ଚାଲୁଛି।"</string>
- <string name="status_success" msgid="239573225847565868">"ଫାଇଲ୍ ଟ୍ରାନ୍ସଫର୍ ସଫଳତାପୂର୍ବକ ସମ୍ପୂର୍ଣ୍ଣ ହେଲା।"</string>
- <string name="status_not_accept" msgid="1695082417193780738">"କଣ୍ଟେଣ୍ଟ ସପୋର୍ଟ କରୁନାହିଁ।"</string>
- <string name="status_forbidden" msgid="613956401054050725">"ଟାର୍ଗେଟ୍ ଡିଭାଇସ୍ ଦ୍ୱାରା ଟ୍ରାନ୍ସଫର୍ ପ୍ରତିବନ୍ଧିତ କରାଯାଇଛି।"</string>
- <string name="status_canceled" msgid="6664490318773098285">"ୟୁଜର୍ଙ୍କ ଦ୍ୱାରା ଟ୍ରାନ୍ସଫର୍ କ୍ୟାନ୍ସଲ୍ କରାଗଲା।"</string>
- <string name="status_file_error" msgid="3671917770630165299">"ଷ୍ଟୋରେଜ୍ ସମସ୍ୟା।"</string>
- <string name="status_no_sd_card" product="nosdcard" msgid="1112125377088421469">"କୌଣସି USB ଷ୍ଟୋରେଜ୍ ନାହିଁ।"</string>
- <string name="status_no_sd_card" product="default" msgid="5760944071743325592">"କୌଣସି SD କାର୍ଡ ନାହିଁ। ଟ୍ରାନ୍ସଫର୍ କରାଯାଇଥିବା ଫାଇଲ୍ଗୁଡ଼ିକୁ ସେଭ୍ କରିବା ପାଇଁ ଗୋଟିଏ SD କାର୍ଡ ଭର୍ତ୍ତି କରନ୍ତୁ।"</string>
- <string name="status_connection_error" msgid="947681831523219891">"ସଂଯୋଗ ବିଫଳ ହେଲା।"</string>
- <string name="status_protocol_error" msgid="3245444473429269539">"ଅନୁରୋଧକୁ ଠିକ୍ ଭାବେ ସମ୍ଭାଳି ହେବନାହିଁ।"</string>
- <string name="status_unknown_error" msgid="8156660554237824912">"ଅଜଣା ତୃଟି।"</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"ବ୍ଲୁ-ଟୂଥ୍ ପ୍ରାପ୍ତ ହେଲା"</string>
- <string name="opp_notification_group" msgid="3486303082135789982">"ବ୍ଲୁ-ଟୂଥ୍ ଶେୟାର୍"</string>
- <string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> ପ୍ରାପ୍ତ କରିବା ସମ୍ପୂର୍ଣ୍ଣ ହେଲା।"</string>
- <string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> ପଠାଇବା ସମ୍ପୂର୍ଣ୍ଣ ହେଲା।"</string>
- <string name="inbound_history_title" msgid="6940914942271327563">"ଇନ୍ବାଉଣ୍ଡ ଟ୍ରାନ୍ସଫର୍"</string>
- <string name="outbound_history_title" msgid="4279418703178140526">"ଆଉଟ୍ବାଉଣ୍ଡ ଟ୍ରାନ୍ସଫର୍"</string>
- <string name="no_transfers" msgid="3482965619151865672">"ଟ୍ରାନ୍ସଫର୍ ହିଷ୍ଟୋରୀ ଖାଲି ଅଛି।"</string>
- <string name="transfer_clear_dlg_msg" msgid="1712376797268438075">"ତାଲିକାରୁ ସମସ୍ତ ଆଇଟମ୍କୁ ଖାଲି କରିଦିଆଯିବ।"</string>
- <string name="outbound_noti_title" msgid="8051906709452260849">"ବ୍ଲୁ-ଟୂଥ୍ ଶେୟାର୍: ପଠାଯାଇଥିବା ଫାଇଲ୍"</string>
- <string name="inbound_noti_title" msgid="4143352641953027595">"ବ୍ଲୁ-ଟୂଥ୍ ଶେୟାର୍: ପ୍ରାପ୍ତ କରାଯାଇଥିବା ଫାଇଲ୍"</string>
- <plurals name="noti_caption_unsuccessful" formatted="false" msgid="2020750076679526122">
- <item quantity="other"><xliff:g id="UNSUCCESSFUL_NUMBER_1">%1$d</xliff:g> ବିଫଳ.</item>
- <item quantity="one"><xliff:g id="UNSUCCESSFUL_NUMBER_0">%1$d</xliff:g> ବିଫଳ.</item>
- </plurals>
- <plurals name="noti_caption_success" formatted="false" msgid="1572472450257645181">
- <item quantity="other"><xliff:g id="SUCCESSFUL_NUMBER_1">%1$d</xliff:g> ସଫଳ, %2$s</item>
- <item quantity="one"><xliff:g id="SUCCESSFUL_NUMBER_0">%1$d</xliff:g> ସଫଳ, %2$s</item>
- </plurals>
- <string name="transfer_menu_clear_all" msgid="790017462957873132">"ତାଲିକା ଖାଲି କରନ୍ତୁ"</string>
- <string name="transfer_menu_open" msgid="3368984869083107200">"ଖୋଲନ୍ତୁ"</string>
- <string name="transfer_menu_clear" msgid="5854038118831427492">"ତାଲିକାରୁ ଖାଲି କରନ୍ତୁ"</string>
- <string name="transfer_clear_dlg_title" msgid="2953444575556460386">"ଖାଲି କରନ୍ତୁ"</string>
- <string name="bluetooth_map_settings_save" msgid="7635491847388074606">"ସେଭ୍ କରନ୍ତୁ"</string>
- <string name="bluetooth_map_settings_cancel" msgid="9205350798049865699">"କ୍ୟାନ୍ସଲ୍"</string>
- <string name="bluetooth_map_settings_intro" msgid="6482369468223987562">"ବ୍ଲୁ-ଟୂଥ୍ ମାଧ୍ୟମରେ ଶେୟାର୍ କରିବାକୁ ଚାହୁଁଥିବା ଆକାଉଣ୍ଟଗୁଡ଼ିକୁ ଚୟନ କରନ୍ତୁ। ଆପଣଙ୍କୁ ତଥାପି ସଂଯୋଗ କରୁଥିବା ସମୟରେ ଆକାଉଣ୍ଟଗୁଡ଼ିକ ପ୍ରତି ଯେକୌଣସି ଆକ୍ସେସ୍କୁ ସ୍ୱୀକାର କରିବାକୁ ପଡ଼ିବ।"</string>
- <string name="bluetooth_map_settings_count" msgid="4557473074937024833">"ବଳକା ସ୍ଲଟ୍:"</string>
- <string name="bluetooth_map_settings_app_icon" msgid="7105805610929114707">"ଆପ୍ଲିକେଶନ୍ ଆଇକନ୍"</string>
- <string name="bluetooth_map_settings_title" msgid="7420332483392851321">"ବ୍ଲୁ-ଟୂଥ୍ ମେସେଜ୍ ଶେୟାରିଙ୍ଗ ସେଟିଙ୍ଗ"</string>
- <string name="bluetooth_map_settings_no_account_slots_left" msgid="1796029082612965251">"ଆକାଉଣ୍ଟ ଚୟନ କରାଯାଇପାରିବ ନାହିଁ। 0 ସ୍ଲଟ୍ ବଳକା ରହିଲା"</string>
- <string name="bluetooth_connected" msgid="6718623220072656906">"ବ୍ଲୁ-ଟୂଥ୍ ଅଡିଓ ସଂଯୁକ୍ତ କରାଗଲା"</string>
- <string name="bluetooth_disconnected" msgid="3318303728981478873">"ବ୍ଲୁ-ଟୂଥ୍ ଅଡିଓ ବିଚ୍ଛିନ୍ନ କରାଗଲା"</string>
- <string name="a2dp_sink_mbs_label" msgid="7566075853395412558">"ବ୍ଲୁ-ଟୂଥ୍ ଅଡିଓ"</string>
- <string name="bluetooth_opp_file_limit_exceeded" msgid="8894450394309084519">"4GBରୁ ବଡ଼ ଫାଇଲ୍ଗୁଡ଼ିକୁ ଟ୍ରାନ୍ସଫର୍ କରାଯାଇପାରିବ ନାହିଁ"</string>
-</resources>
diff --git a/res/values-or/strings_pbap.xml b/res/values-or/strings_pbap.xml
deleted file mode 100644
index 804f4a2..0000000
--- a/res/values-or/strings_pbap.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="pbap_session_key_dialog_title" msgid="3580996574333882561">"%1$s ପାଇଁ ସେସନ୍ କୀ’ ଟାଇପ୍ କରନ୍ତୁ"</string>
- <string name="pbap_session_key_dialog_header" msgid="2772472422782758981">"ବ୍ଲୁ-ଟୂଥ୍ ସେସନ୍ କୀ ଆବଶ୍ୟକ"</string>
- <string name="pbap_acceptance_timeout_message" msgid="1107401415099814293">"%1$s ସହ ଯୋଡ଼ି ହେବାର ସମୟ ସମାପ୍ତ ହୋଇଯାଇଛି"</string>
- <string name="pbap_authentication_timeout_message" msgid="4166979525521902687">"%1$s ସହ ସେସନ୍ କୀ’ ଲେଖିବାର ସମୟ ସମାପ୍ତ ହୋଇଯାଇଛି"</string>
- <string name="auth_notif_ticker" msgid="1575825798053163744">"Obex ପ୍ରମାଣୀକରଣ ଅନୁରୋଧ"</string>
- <string name="auth_notif_title" msgid="7599854855681573258">"ସେସନ୍ କୀ’"</string>
- <string name="auth_notif_message" msgid="6667218116427605038">"%1$s ପାଇଁ ସେସନ୍ କୀ’ ଟାଇପ୍ କରନ୍ତୁ"</string>
- <string name="defaultname" msgid="4821590500649090078">"କାର୍ କିଟ୍"</string>
- <string name="unknownName" msgid="2841414754740600042">"ଅଜଣା ନାମ"</string>
- <string name="localPhoneName" msgid="2349001318925409159">"ମୋର ନାମ"</string>
- <string name="defaultnumber" msgid="8520116145890867338">"000000"</string>
- <string name="pbap_notification_group" msgid="8487669554703627168">"ବ୍ଲୁ-ଟୂଥ୍ ଯୋଗାଯୋଗ ଶେୟାର୍ କରନ୍ତୁ"</string>
-</resources>
diff --git a/res/values-or/strings_pbap_client.xml b/res/values-or/strings_pbap_client.xml
deleted file mode 100644
index 186b23a..0000000
--- a/res/values-or/strings_pbap_client.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="pbap_account_type" msgid="6257077123906049322">"com.android.bluetooth.pbapsink"</string>
-</resources>
diff --git a/res/values-or/strings_sap.xml b/res/values-or/strings_sap.xml
deleted file mode 100644
index f52d36f..0000000
--- a/res/values-or/strings_sap.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="bluetooth_sap_notif_title" msgid="6877860822993195074">"ବ୍ଲୁ-ଟୂଥ୍ SIM ଆକ୍ସେସ୍"</string>
- <string name="bluetooth_sap_notif_ticker" msgid="6807778527893726699">"ବ୍ଲୁ-ଟୂଥ୍ SIM ଆକ୍ସେସ୍"</string>
- <string name="bluetooth_sap_notif_message" msgid="7138657801087500690">"ବିଚ୍ଛିନ୍ନ କରିବା ପାଇଁ କ୍ଲାଏଣ୍ଟଙ୍କୁ ଅନୁରୋଧ କରିବେ?"</string>
- <string name="bluetooth_sap_notif_disconnecting" msgid="819150843490233288">"ବିଚ୍ଛିନ୍ନ କରିବା ପାଇଁ କ୍ଲାଏଣ୍ଟଙ୍କ ଅପେକ୍ଷା କରାଯାଉଛି"</string>
- <string name="bluetooth_sap_notif_disconnect_button" msgid="3678476872583356919">"ବିଚ୍ଛିନ୍ନ କରନ୍ତୁ"</string>
- <string name="bluetooth_sap_notif_force_disconnect_button" msgid="8144086340185532030">"ବିଚ୍ଛିନ୍ନ କରିବା ପାଇଁ ବାଧ୍ୟ କରନ୍ତୁ"</string>
-</resources>
diff --git a/res/values-or/test_strings.xml b/res/values-or/test_strings.xml
deleted file mode 100644
index 693cd65..0000000
--- a/res/values-or/test_strings.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="app_name" msgid="6006644116867509664">"ବ୍ଲୁ-ଟୂଥ୍"</string>
- <string name="insert_record" msgid="1450997173838378132">"ରେକର୍ଡ ଭର୍ତ୍ତି କରନ୍ତୁ"</string>
- <string name="update_record" msgid="2480425402384910635">"ରେକର୍ଡ ସୁନିଶ୍ଚିତ କରନ୍ତୁ"</string>
- <string name="ack_record" msgid="6716152390978472184">"ରେକର୍ଡ ସ୍ୱୀକୃତ କରନ୍ତୁ"</string>
- <string name="deleteAll_record" msgid="4383349788485210582">"ସମସ୍ତ ରେକର୍ଡ ଡିଲିଟ୍ କରନ୍ତୁ"</string>
- <string name="ok_button" msgid="6519033415223065454">"ଠିକ୍ ଅଛି"</string>
- <string name="delete_record" msgid="4645040331967533724">"ରେକର୍ଡ ଡିଲିଟ୍ କରନ୍ତୁ"</string>
- <string name="start_server" msgid="9034821924409165795">"TCP ସର୍ଭର୍ ଆରମ୍ଭ କରନ୍ତୁ"</string>
- <string name="notify_server" msgid="4369106744022969655">"TCP ସର୍ଭର୍କୁ ସୂଚିତ କରନ୍ତୁ"</string>
-</resources>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 0e6a0f1..f0b52bf 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -100,13 +100,13 @@
<string name="status_connection_error" msgid="947681831523219891">"ਕਨੈਕਸ਼ਨ ਅਸਫਲ।"</string>
<string name="status_protocol_error" msgid="3245444473429269539">"ਬੇਨਤੀ ਨੂੰ ਸਹੀ ਢੰਗ ਨਾਲ ਸੰਭਾਲਿਆ ਨਹੀਂ ਜਾ ਸਕਦਾ।"</string>
<string name="status_unknown_error" msgid="8156660554237824912">"ਅਗਿਆਤ ਅਸ਼ੁੱਧੀ।"</string>
- <string name="btopp_live_folder" msgid="7967791481444474554">"ਬਲੂਟੁੱਥ ਰਾਹੀਂ ਪ੍ਰਾਪਤ"</string>
+ <string name="btopp_live_folder" msgid="7967791481444474554">"Bluetooth ਪ੍ਰਾਪਤ ਕੀਤੀ"</string>
<string name="opp_notification_group" msgid="3486303082135789982">"ਬਲੂਟੁੱਥ ਸਾਂਝਾਕਰਨ"</string>
<string name="download_success" msgid="7036160438766730871">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> ਪੂਰਾ ਪ੍ਰਾਪਤ ਕੀਤਾ।"</string>
<string name="upload_success" msgid="4014469387779648949">"<xliff:g id="FILE_SIZE">%1$s</xliff:g> ਪੂਰਾ ਭੇਜਿਆ ਗਿਆ।"</string>
<string name="inbound_history_title" msgid="6940914942271327563">"ਇਨਬਾਊਂਡ ਟ੍ਰਾਂਸਫਰ"</string>
<string name="outbound_history_title" msgid="4279418703178140526">"ਆਊਟਬਾਊਂਡ ਟ੍ਰਾਂਸਫਰ"</string>
- <string name="no_transfers" msgid="3482965619151865672">"ਟ੍ਰਾਂਸਫ਼ਰ ਇਤਿਹਾਸ ਵਿੱਚ ਕੁਝ ਵੀ ਨਹੀਂ ਹੈ।"</string>
+ <string name="no_transfers" msgid="3482965619151865672">"ਟ੍ਰਾਂਸਫਰ ਇਤਿਹਾਸ ਖਾਲੀ ਹੈ।"</string>
<string name="transfer_clear_dlg_msg" msgid="1712376797268438075">"ਸਾਰੀਆਂ ਆਈਟਮਾਂ ਸੂਚੀ ਵਿੱਚੋਂ ਹਟਾਈਆਂ ਜਾਣਗੀਆਂ।"</string>
<string name="outbound_noti_title" msgid="8051906709452260849">"Bluetooth ਸ਼ੇਅਰ: ਭੇਜੀਆਂ ਗਈਆਂ ਫਾਈਲਾਂ"</string>
<string name="inbound_noti_title" msgid="4143352641953027595">"Bluetooth ਸ਼ੇਅਰ: ਪ੍ਰਾਪਤ ਕੀਤੀਆਂ ਫਾਈਲਾਂ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index ee09650..f1b4575 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -78,7 +78,7 @@
<string name="not_exist_file_desc" msgid="4059531573790529229">"Plik nie istnieje. \n"</string>
<string name="enabling_progress_title" msgid="436157952334723406">"Czekaj…"</string>
<string name="enabling_progress_content" msgid="4601542238119927904">"Włączanie Bluetooth…"</string>
- <string name="bt_toast_1" msgid="972182708034353383">"Plik zostanie odebrany. Sprawdzaj postęp w panelu powiadomień."</string>
+ <string name="bt_toast_1" msgid="972182708034353383">"Plik zostanie odebrany. Sprawdzaj postęp na panelu powiadomień."</string>
<string name="bt_toast_2" msgid="8602553334099066582">"Nie można odebrać pliku."</string>
<string name="bt_toast_3" msgid="6707884165086862518">"Zatrzymano odbiór pliku z urządzenia „<xliff:g id="SENDER">%1$s</xliff:g>”"</string>
<string name="bt_toast_4" msgid="4678812947604395649">"Wysyłanie pliku do urządzenia „<xliff:g id="RECIPIENT">%1$s</xliff:g>”"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index d024a42..9b4d14f 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -124,7 +124,7 @@
<string name="transfer_clear_dlg_title" msgid="2953444575556460386">"Limpar"</string>
<string name="bluetooth_map_settings_save" msgid="7635491847388074606">"Salvar"</string>
<string name="bluetooth_map_settings_cancel" msgid="9205350798049865699">"Cancelar"</string>
- <string name="bluetooth_map_settings_intro" msgid="6482369468223987562">"Selecione as contas que você quer compartilhar via Bluetooth. Você ainda precisa aceitar qualquer acesso às contas ao se conectar."</string>
+ <string name="bluetooth_map_settings_intro" msgid="6482369468223987562">"Selecione as contas que você deseja compartilhar via Bluetooth. Você ainda precisa aceitar qualquer acesso às contas ao se conectar."</string>
<string name="bluetooth_map_settings_count" msgid="4557473074937024833">"Espaços disponíveis:"</string>
<string name="bluetooth_map_settings_app_icon" msgid="7105805610929114707">"Ícone do app"</string>
<string name="bluetooth_map_settings_title" msgid="7420332483392851321">"Configurações de compartilhamento de mensagens Bluetooth"</string>
diff --git a/res/values-si/strings_map.xml b/res/values-si/strings_map.xml
deleted file mode 100644
index 31c2feb..0000000
--- a/res/values-si/strings_map.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="map_session_key_dialog_title" msgid="4357431314165953502">"%1$s සඳහා සැසි යතුර ටයිප් කරන්න"</string>
- <string name="map_session_key_dialog_header" msgid="1858363785687455516">"බ්ලූටූත් සැසි යතුර අවශ්යයි"</string>
- <string name="map_acceptance_timeout_message" msgid="2247454902690565876">"%1$s හා සම්බන්ධය පිළිගැනීමට කාල නිමාවක් විය"</string>
- <string name="map_authentication_timeout_message" msgid="2151423397615327066">"%1$s හා සැසි යතුර ආදානය කිරීමට කාල නිමාවක් විය"</string>
- <string name="map_auth_notif_ticker" msgid="8840174807927327119">"Obex සත්යාපන ඉල්ලීම"</string>
- <string name="map_auth_notif_title" msgid="301767019364765129">"සැසි යතුර"</string>
- <string name="map_auth_notif_message" msgid="1510095655064214863">"%1$s සඳහා සැසි යතුර ටයිප් කරන්න"</string>
- <string name="map_defaultname" msgid="3805234816465216759">"Carkit"</string>
- <string name="map_unknownName" msgid="4985352365863687360">"නොදන්නා නමකි"</string>
- <string name="map_localPhoneName" msgid="6264496105941797768">"මගේ නම"</string>
- <string name="map_defaultnumber" msgid="3838264444043503815">"000000"</string>
-</resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 705984a..3631b3a 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -106,7 +106,7 @@
<string name="upload_success" msgid="4014469387779648949">"Kutuma kwa <xliff:g id="FILE_SIZE">%1$s</xliff:g> kumekamilika."</string>
<string name="inbound_history_title" msgid="6940914942271327563">"Mahamisho yanayoingia"</string>
<string name="outbound_history_title" msgid="4279418703178140526">"Mahamisho yanayotoka"</string>
- <string name="no_transfers" msgid="3482965619151865672">"Historia ya uhamishaji haina chochote."</string>
+ <string name="no_transfers" msgid="3482965619151865672">"Historia ya uhamishaji ni tupu."</string>
<string name="transfer_clear_dlg_msg" msgid="1712376797268438075">"Vipengee vyote vitafutwa kutoka kwenye orodha."</string>
<string name="outbound_noti_title" msgid="8051906709452260849">"Kushiriki kwa bluetooth: Faili zilizotumwa"</string>
<string name="inbound_noti_title" msgid="4143352641953027595">"Kushiriki kwa bluetooth: Faili zilizopokelewa"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index 88535c5..4022ba6 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -52,7 +52,7 @@
<string name="download_line4" msgid="8535996869722666525"></string>
<string name="download_line5" msgid="3069560415845295386">"Fayl qabul qilinmoqda…"</string>
<string name="download_cancel" msgid="9177305996747500768">"To‘xtatish"</string>
- <string name="download_ok" msgid="5000360731674466039">"Berkitish"</string>
+ <string name="download_ok" msgid="5000360731674466039">"Yashirish"</string>
<string name="incoming_line1" msgid="2127419875681087545">"Kimdan"</string>
<string name="incoming_line2" msgid="3348994249285315873">"Fayl nomi"</string>
<string name="incoming_line3" msgid="7954237069667474024">"Hajmi"</string>
@@ -106,7 +106,7 @@
<string name="upload_success" msgid="4014469387779648949">"To‘liq yuborildi: <xliff:g id="FILE_SIZE">%1$s</xliff:g>"</string>
<string name="inbound_history_title" msgid="6940914942271327563">"Kiruvchi o‘tkazmalar"</string>
<string name="outbound_history_title" msgid="4279418703178140526">"Chiquvchi o‘tkazmalar"</string>
- <string name="no_transfers" msgid="3482965619151865672">"Hech narsa topilmadi."</string>
+ <string name="no_transfers" msgid="3482965619151865672">"O‘tkazmalar tarixi bo‘m-bo‘sh."</string>
<string name="transfer_clear_dlg_msg" msgid="1712376797268438075">"Barcha qaydlar ro‘yxatdan o‘chirib tashlanadi."</string>
<string name="outbound_noti_title" msgid="8051906709452260849">"Bluetooth orqali yuborildi"</string>
<string name="inbound_noti_title" msgid="4143352641953027595">"Bluetooth orqali olindi"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 0be8dea..ea1587a 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -106,7 +106,7 @@
<string name="upload_success" msgid="4014469387779648949">"Hoàn tất gửi <xliff:g id="FILE_SIZE">%1$s</xliff:g>."</string>
<string name="inbound_history_title" msgid="6940914942271327563">"Nội dung chuyển đến"</string>
<string name="outbound_history_title" msgid="4279418703178140526">"Nội dung chuyển đi"</string>
- <string name="no_transfers" msgid="3482965619151865672">"Không có nội dung trong lịch sử truyền."</string>
+ <string name="no_transfers" msgid="3482965619151865672">"Lịch sử chuyển trống."</string>
<string name="transfer_clear_dlg_msg" msgid="1712376797268438075">"Tất cả các mục sẽ bị xóa khỏi danh sách."</string>
<string name="outbound_noti_title" msgid="8051906709452260849">"Chia sẻ qua Bluetooth: Tệp đã gửi"</string>
<string name="inbound_noti_title" msgid="4143352641953027595">"Chia sẻ qua Bluetooth: Tệp đã nhận"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 4220c84..54bd000 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -106,7 +106,7 @@
<string name="upload_success" msgid="4014469387779648949">"已完成 <xliff:g id="FILE_SIZE">%1$s</xliff:g> 的傳送作業。"</string>
<string name="inbound_history_title" msgid="6940914942271327563">"傳入"</string>
<string name="outbound_history_title" msgid="4279418703178140526">"傳出"</string>
- <string name="no_transfers" msgid="3482965619151865672">"沒有傳輸記錄。"</string>
+ <string name="no_transfers" msgid="3482965619151865672">"傳輸紀錄是空的。"</string>
<string name="transfer_clear_dlg_msg" msgid="1712376797268438075">"所有項目都會從清單中清除。"</string>
<string name="outbound_noti_title" msgid="8051906709452260849">"藍牙分享:傳送的檔案"</string>
<string name="inbound_noti_title" msgid="4143352641953027595">"藍牙分享:接收的檔案"</string>
diff --git a/res/values-zh-rTW/test_strings.xml b/res/values-zh-rTW/test_strings.xml
index dd2369a..2d2d57b 100644
--- a/res/values-zh-rTW/test_strings.xml
+++ b/res/values-zh-rTW/test_strings.xml
@@ -5,7 +5,7 @@
<string name="insert_record" msgid="1450997173838378132">"插入記錄"</string>
<string name="update_record" msgid="2480425402384910635">"確認記錄"</string>
<string name="ack_record" msgid="6716152390978472184">"Ack 記錄"</string>
- <string name="deleteAll_record" msgid="4383349788485210582">"刪除所有記錄"</string>
+ <string name="deleteAll_record" msgid="4383349788485210582">"刪除所有紀錄"</string>
<string name="ok_button" msgid="6519033415223065454">"確定"</string>
<string name="delete_record" msgid="4645040331967533724">"刪除記錄"</string>
<string name="start_server" msgid="9034821924409165795">"啟動 TCP 伺服器"</string>
diff --git a/res/values/config.xml b/res/values/config.xml
index 9301e9d..711993e 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -15,7 +15,6 @@
<resources>
<bool name="profile_supported_a2dp">true</bool>
<bool name="profile_supported_a2dp_sink">false</bool>
- <bool name="profile_supported_hdp">true</bool>
<bool name="profile_supported_hs_hfp">true</bool>
<bool name="profile_supported_hfpclient">false</bool>
<bool name="profile_supported_hid_host">true</bool>
@@ -32,7 +31,6 @@
<bool name="profile_supported_pbapclient">false</bool>
<bool name="profile_supported_mapmce">false</bool>
<bool name="profile_supported_hid_device">true</bool>
- <bool name="profile_supported_hearing_aid">false</bool>
<!-- If true, we will require location to be enabled on the device to
fire Bluetooth LE scan result callbacks in addition to having one
@@ -71,10 +69,15 @@
<!-- For A2DP sink ducking volume feature. -->
<integer name="a2dp_sink_duck_percent">25</integer>
+ <!-- If true, device requests audio focus and start avrcp updates on source start or play -->
+ <bool name="a2dp_sink_automatically_request_audio_focus">false</bool>
<!-- For enabling the hfp client connection service -->
<bool name="hfp_client_connection_service_enabled">false</bool>
+ <!-- For supporting emergency call through the hfp client connection service -->
+ <bool name="hfp_client_connection_service_support_emergency_call">false</bool>
+
<!-- Enabling autoconnect over pan -->
<bool name="config_bluetooth_pan_enable_autoconnect">true</bool>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 17023e4..1389e0d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -178,9 +178,9 @@
<string name="bt_toast_6">Stopped sending file to \u0022<xliff:g id="recipient">%1$s</xliff:g>\u0022</string>
<!-- Bluetooth System Messages [CHAR LIMIT=NONE] -->
- <string name="bt_sm_2_1" product="nosdcard">There isn\'t enough space in USB storage to save the file from \u0022<xliff:g id="sender">%1$s</xliff:g>\u0022</string>
+ <string name="bt_sm_2_1_nosdcard">There isn\'t enough space in USB storage to save the file from \u0022<xliff:g id="sender">%1$s</xliff:g>\u0022</string>
<!-- Bluetooth System Messages -->
- <string name="bt_sm_2_1" product="default">There isn\'t enough space on the SD card to save the file from \u0022<xliff:g id="sender">%1$s</xliff:g>\u0022</string>
+ <string name="bt_sm_2_1_default">There isn\'t enough space on the SD card to save the file from \u0022<xliff:g id="sender">%1$s</xliff:g>\u0022</string>
<string name="bt_sm_2_2">Space needed: <xliff:g id="size">%1$s</xliff:g></string>
<string name="ErrorTooManyRequests">Too many requests are being processed. Try again later.</string>
@@ -194,8 +194,8 @@
<string name="status_canceled">Transfer canceled by user.</string>
<string name="status_file_error">Storage issue.</string>
<!-- Shown when USB storage cannot be found. [CHAR LIMIT=NONE] -->
- <string name="status_no_sd_card" product="nosdcard">No USB storage.</string>
- <string name="status_no_sd_card" product="default">No SD card. Insert an SD card to save transferred files.</string>
+ <string name="status_no_sd_card_nosdcard">No USB storage.</string>
+ <string name="status_no_sd_card_default">No SD card. Insert an SD card to save transferred files.</string>
<string name="status_connection_error">Connection unsuccessful.</string>
<string name="status_protocol_error">Request can\'t be handled correctly.</string>
<string name="status_unknown_error">Unknown error.</string>
@@ -237,6 +237,7 @@
<string name="process" translate="false"><xliff:g id="x" /></string>
+ <string name="bluetooth_a2dp_sink_queue_name">Now Playing</string>
<string name="bluetooth_map_settings_save">Save</string>
<string name="bluetooth_map_settings_cancel">Cancel</string>
<string name="bluetooth_map_settings_intro">Select the accounts you want to share through Bluetooth. You still have to accept any access to the accounts when connecting.</string>
diff --git a/src/com/android/bluetooth/Utils.java b/src/com/android/bluetooth/Utils.java
index 457b053..4432067 100644
--- a/src/com/android/bluetooth/Utils.java
+++ b/src/com/android/bluetooth/Utils.java
@@ -24,6 +24,7 @@
import android.content.ContextWrapper;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
+import android.location.LocationManager;
import android.os.Binder;
import android.os.Build;
import android.os.ParcelUuid;
@@ -38,6 +39,8 @@
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@@ -109,6 +112,24 @@
return sb.toString();
}
+ /**
+ * A parser to transfer a byte array to a UTF8 string
+ *
+ * @param valueBuf the byte array to transfer
+ * @return the transferred UTF8 string
+ */
+ public static String byteArrayToUtf8String(byte[] valueBuf) {
+ CharsetDecoder decoder = Charset.forName("UTF8").newDecoder();
+ ByteBuffer byteBuffer = ByteBuffer.wrap(valueBuf);
+ String valueStr = "";
+ try {
+ valueStr = decoder.decode(byteBuffer).toString();
+ } catch (Exception ex) {
+ Log.e(TAG, "Error when parsing byte array to UTF8 String. " + ex);
+ }
+ return valueStr;
+ }
+
public static byte[] intToByteArray(int value) {
ByteBuffer converter = ByteBuffer.allocate(4);
converter.order(ByteOrder.nativeOrder());
@@ -277,73 +298,117 @@
}
/**
- * Checks that calling process has android.Manifest.permission.ACCESS_COARSE_LOCATION or
- * android.Manifest.permission.ACCESS_FINE_LOCATION and a corresponding app op is allowed
+ * Checks whether location is off and must be on for us to perform some operation
*/
- public static boolean checkCallerHasLocationPermission(Context context, AppOpsManager appOps,
- String callingPackage) {
- if (context.checkCallingOrSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
- == PackageManager.PERMISSION_GRANTED && isAppOppAllowed(
- appOps, AppOpsManager.OP_FINE_LOCATION, callingPackage)) {
+ public static boolean blockedByLocationOff(Context context, UserHandle userHandle) {
+ return !context.getSystemService(LocationManager.class)
+ .isLocationEnabledForUser(userHandle);
+ }
+
+ /**
+ * Checks that calling process has android.Manifest.permission.ACCESS_COARSE_LOCATION and
+ * OP_COARSE_LOCATION is allowed
+ */
+ public static boolean checkCallerHasCoarseLocation(Context context, AppOpsManager appOps,
+ String callingPackage, UserHandle userHandle) {
+ if (blockedByLocationOff(context, userHandle)) {
+ Log.e(TAG, "Permission denial: Location is off.");
+ return false;
+ }
+
+ // Check coarse, but note fine
+ if (context.checkCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_COARSE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED
+ && isAppOppAllowed(appOps, AppOpsManager.OP_FINE_LOCATION, callingPackage)) {
return true;
}
- if (context.checkCallingOrSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
- == PackageManager.PERMISSION_GRANTED && isAppOppAllowed(
- appOps, AppOpsManager.OP_COARSE_LOCATION, callingPackage)) {
- return true;
- }
- // Enforce location permission for apps targeting M and later versions
- if (isMApp(context, callingPackage)) {
- // PEERS_MAC_ADDRESS is another way to get scan results without
- // requiring location permissions, so only throw an exception here
- // if PEERS_MAC_ADDRESS permission is missing as well
- if (!checkCallerHasPeersMacAddressPermission(context)) {
- throw new SecurityException("Need ACCESS_COARSE_LOCATION or "
- + "ACCESS_FINE_LOCATION permission to get scan results");
- }
- } else {
- // Pre-M apps running in the foreground should continue getting scan results
- if (isForegroundApp(context, callingPackage)) {
- return true;
- }
- Log.e(TAG, "Permission denial: Need ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION "
- + "permission to get scan results");
- }
+ Log.e(TAG, "Permission denial: Need ACCESS_COARSE_LOCATION "
+ + "permission to get scan results");
return false;
}
/**
- * Returns true if the caller holds PEERS_MAC_ADDRESS.
+ * Checks that calling process has android.Manifest.permission.ACCESS_COARSE_LOCATION and
+ * OP_COARSE_LOCATION is allowed or android.Manifest.permission.ACCESS_FINE_LOCATION and
+ * OP_FINE_LOCATION is allowed
*/
- public static boolean checkCallerHasPeersMacAddressPermission(Context context) {
- return context.checkCallingOrSelfPermission(android.Manifest.permission.PEERS_MAC_ADDRESS)
- == PackageManager.PERMISSION_GRANTED;
- }
-
- public static boolean isLegacyForegroundApp(Context context, String pkgName) {
- return !isMApp(context, pkgName) && isForegroundApp(context, pkgName);
- }
-
- private static boolean isMApp(Context context, String pkgName) {
- try {
- return context.getPackageManager().getApplicationInfo(pkgName, 0).targetSdkVersion
- >= Build.VERSION_CODES.M;
- } catch (PackageManager.NameNotFoundException e) {
- // In case of exception, assume M app
+ public static boolean checkCallerHasCoarseOrFineLocation(Context context, AppOpsManager appOps,
+ String callingPackage, UserHandle userHandle) {
+ if (blockedByLocationOff(context, userHandle)) {
+ Log.e(TAG, "Permission denial: Location is off.");
+ return false;
}
- return true;
+
+ if (context.checkCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_FINE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED
+ && isAppOppAllowed(appOps, AppOpsManager.OP_FINE_LOCATION, callingPackage)) {
+ return true;
+ }
+
+ // Check coarse, but note fine
+ if (context.checkCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_COARSE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED
+ && isAppOppAllowed(appOps, AppOpsManager.OP_FINE_LOCATION, callingPackage)) {
+ return true;
+ }
+
+ Log.e(TAG, "Permission denial: Need ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION"
+ + "permission to get scan results");
+ return false;
}
/**
- * Return true if the specified package name is a foreground app.
- *
- * @param pkgName application package name.
+ * Checks that calling process has android.Manifest.permission.ACCESS_FINE_LOCATION and
+ * OP_FINE_LOCATION is allowed
*/
- private static boolean isForegroundApp(Context context, String pkgName) {
- ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
- List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(1);
- return !tasks.isEmpty() && pkgName.equals(tasks.get(0).topActivity.getPackageName());
+ public static boolean checkCallerHasFineLocation(Context context, AppOpsManager appOps,
+ String callingPackage, UserHandle userHandle) {
+ if (blockedByLocationOff(context, userHandle)) {
+ Log.e(TAG, "Permission denial: Location is off.");
+ return false;
+ }
+
+ if (context.checkCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_FINE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED
+ && isAppOppAllowed(appOps, AppOpsManager.OP_FINE_LOCATION, callingPackage)) {
+ return true;
+ }
+
+ Log.e(TAG, "Permission denial: Need ACCESS_FINE_LOCATION "
+ + "permission to get scan results");
+ return false;
+ }
+
+ /**
+ * Returns true if the caller holds NETWORK_SETTINGS
+ */
+ public static boolean checkCallerHasNetworkSettingsPermission(Context context) {
+ return context.checkCallingOrSelfPermission(android.Manifest.permission.NETWORK_SETTINGS)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * Returns true if the caller holds NETWORK_SETUP_WIZARD
+ */
+ public static boolean checkCallerHasNetworkSetupWizardPermission(Context context) {
+ return context.checkCallingOrSelfPermission(
+ android.Manifest.permission.NETWORK_SETUP_WIZARD)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ public static boolean isQApp(Context context, String pkgName) {
+ try {
+ return context.getPackageManager().getApplicationInfo(pkgName, 0).targetSdkVersion
+ >= Build.VERSION_CODES.Q;
+ } catch (PackageManager.NameNotFoundException e) {
+ // In case of exception, assume Q app
+ }
+ return true;
}
private static boolean isAppOppAllowed(AppOpsManager appOps, int op, String callingPackage) {
diff --git a/src/com/android/bluetooth/a2dp/A2dpCodecConfig.java b/src/com/android/bluetooth/a2dp/A2dpCodecConfig.java
index 4de89df..8127e76 100644
--- a/src/com/android/bluetooth/a2dp/A2dpCodecConfig.java
+++ b/src/com/android/bluetooth/a2dp/A2dpCodecConfig.java
@@ -17,13 +17,17 @@
package com.android.bluetooth.a2dp;
import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecStatus;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
+import android.util.Log;
import com.android.bluetooth.R;
+import java.util.Arrays;
+import java.util.Objects;
/*
* A2DP Codec Configuration setup.
*/
@@ -52,13 +56,49 @@
}
void setCodecConfigPreference(BluetoothDevice device,
- BluetoothCodecConfig codecConfig) {
+ BluetoothCodecStatus codecStatus,
+ BluetoothCodecConfig newCodecConfig) {
+ Objects.requireNonNull(codecStatus);
+
+ // Check whether the codecConfig is selectable for this Bluetooth device.
+ BluetoothCodecConfig[] selectableCodecs = codecStatus.getCodecsSelectableCapabilities();
+ if (!Arrays.asList(selectableCodecs).stream().anyMatch(codec ->
+ codec.isMandatoryCodec())) {
+ // Do not set codec preference to native if the selectableCodecs not contain mandatory
+ // codec. The reason could be remote codec negotiation is not completed yet.
+ Log.w(TAG, "setCodecConfigPreference: must have mandatory codec before changing.");
+ return;
+ }
+ if (!codecStatus.isCodecConfigSelectable(newCodecConfig)) {
+ Log.w(TAG, "setCodecConfigPreference: invalid codec "
+ + Objects.toString(newCodecConfig));
+ return;
+ }
+
+ // Check whether the codecConfig would change current codec config.
+ int prioritizedCodecType = getPrioitizedCodecType(newCodecConfig, selectableCodecs);
+ BluetoothCodecConfig currentCodecConfig = codecStatus.getCodecConfig();
+ if (prioritizedCodecType == currentCodecConfig.getCodecType()
+ && (prioritizedCodecType != newCodecConfig.getCodecType()
+ || (currentCodecConfig.similarCodecFeedingParameters(newCodecConfig)
+ && currentCodecConfig.sameCodecSpecificParameters(newCodecConfig)))) {
+ // Same codec with same parameters, no need to send this request to native.
+ Log.w(TAG, "setCodecConfigPreference: codec not changed.");
+ return;
+ }
+
BluetoothCodecConfig[] codecConfigArray = new BluetoothCodecConfig[1];
- codecConfigArray[0] = codecConfig;
+ codecConfigArray[0] = newCodecConfig;
mA2dpNativeInterface.setCodecConfigPreference(device, codecConfigArray);
}
- void enableOptionalCodecs(BluetoothDevice device) {
+ void enableOptionalCodecs(BluetoothDevice device, BluetoothCodecConfig currentCodecConfig) {
+ if (currentCodecConfig != null && !currentCodecConfig.isMandatoryCodec()) {
+ Log.i(TAG, "enableOptionalCodecs: already using optional codec "
+ + currentCodecConfig.getCodecName());
+ return;
+ }
+
BluetoothCodecConfig[] codecConfigArray = assignCodecConfigPriorities();
if (codecConfigArray == null) {
return;
@@ -75,7 +115,12 @@
mA2dpNativeInterface.setCodecConfigPreference(device, codecConfigArray);
}
- void disableOptionalCodecs(BluetoothDevice device) {
+ void disableOptionalCodecs(BluetoothDevice device, BluetoothCodecConfig currentCodecConfig) {
+ if (currentCodecConfig != null && currentCodecConfig.isMandatoryCodec()) {
+ Log.i(TAG, "disableOptionalCodecs: already using mandatory codec.");
+ return;
+ }
+
BluetoothCodecConfig[] codecConfigArray = assignCodecConfigPriorities();
if (codecConfigArray == null) {
return;
@@ -92,6 +137,21 @@
mA2dpNativeInterface.setCodecConfigPreference(device, codecConfigArray);
}
+ // Get the codec type of the highest priority of selectableCodecs and codecConfig.
+ private int getPrioitizedCodecType(BluetoothCodecConfig codecConfig,
+ BluetoothCodecConfig[] selectableCodecs) {
+ BluetoothCodecConfig prioritizedCodecConfig = codecConfig;
+ for (BluetoothCodecConfig config : selectableCodecs) {
+ if (prioritizedCodecConfig == null) {
+ prioritizedCodecConfig = config;
+ }
+ if (config.getCodecPriority() > prioritizedCodecConfig.getCodecPriority()) {
+ prioritizedCodecConfig = config;
+ }
+ }
+ return prioritizedCodecConfig.getCodecType();
+ }
+
// Assign the A2DP Source codec config priorities
private BluetoothCodecConfig[] assignCodecConfigPriorities() {
Resources resources = mContext.getResources();
diff --git a/src/com/android/bluetooth/a2dp/A2dpNativeInterface.java b/src/com/android/bluetooth/a2dp/A2dpNativeInterface.java
index e01b325..cbdc28a 100644
--- a/src/com/android/bluetooth/a2dp/A2dpNativeInterface.java
+++ b/src/com/android/bluetooth/a2dp/A2dpNativeInterface.java
@@ -25,11 +25,11 @@
import android.bluetooth.BluetoothCodecConfig;
import android.bluetooth.BluetoothCodecStatus;
import android.bluetooth.BluetoothDevice;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.bluetooth.Utils;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
/**
* A2DP Native Interface to/from JNI.
@@ -107,6 +107,16 @@
}
/**
+ * Sets a connected A2DP remote device to silence mode.
+ *
+ * @param device the remote device
+ * @return true on success, otherwise false.
+ */
+ public boolean setSilenceDevice(BluetoothDevice device, boolean silence) {
+ return setSilenceDeviceNative(getByteAddress(device), silence);
+ }
+
+ /**
* Sets a connected A2DP remote device as active.
*
* @param device the remote device
@@ -199,6 +209,7 @@
private native void cleanupNative();
private native boolean connectA2dpNative(byte[] address);
private native boolean disconnectA2dpNative(byte[] address);
+ private native boolean setSilenceDeviceNative(byte[] address, boolean silence);
private native boolean setActiveDeviceNative(byte[] address);
private native boolean setCodecConfigPreferenceNative(byte[] address,
BluetoothCodecConfig[] codecConfigArray);
diff --git a/src/com/android/bluetooth/a2dp/A2dpService.java b/src/com/android/bluetooth/a2dp/A2dpService.java
index 0548cdd..cebf767 100644
--- a/src/com/android/bluetooth/a2dp/A2dpService.java
+++ b/src/com/android/bluetooth/a2dp/A2dpService.java
@@ -17,7 +17,6 @@
package com.android.bluetooth.a2dp;
import android.bluetooth.BluetoothA2dp;
-import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCodecConfig;
import android.bluetooth.BluetoothCodecStatus;
import android.bluetooth.BluetoothDevice;
@@ -30,23 +29,21 @@
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.HandlerThread;
-import android.provider.Settings;
-import android.support.annotation.GuardedBy;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
+import android.util.StatsLog;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.Utils;
-import com.android.bluetooth.avrcp.Avrcp;
-import com.android.bluetooth.avrcp.AvrcpTargetService;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.bluetooth.btservice.ServiceFactory;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -60,13 +57,13 @@
private static A2dpService sA2dpService;
- private BluetoothAdapter mAdapter;
private AdapterService mAdapterService;
private HandlerThread mStateMachinesThread;
- private Avrcp mAvrcp;
@VisibleForTesting
A2dpNativeInterface mA2dpNativeInterface;
+ @VisibleForTesting
+ ServiceFactory mFactory = new ServiceFactory();
private AudioManager mAudioManager;
private A2dpCodecConfig mA2dpCodecConfig;
@@ -102,10 +99,8 @@
throw new IllegalStateException("start() called twice");
}
- // Step 1: Get BluetoothAdapter, AdapterService, A2dpNativeInterface, AudioManager.
+ // Step 1: Get AdapterService, A2dpNativeInterface, AudioManager.
// None of them can be null.
- mAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter(),
- "BluetoothAdapter cannot be null when A2dpService starts");
mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),
"AdapterService cannot be null when A2dpService starts");
mA2dpNativeInterface = Objects.requireNonNull(A2dpNativeInterface.getInstance(),
@@ -118,28 +113,25 @@
mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices();
Log.i(TAG, "Max connected audio devices set to " + mMaxConnectedAudioDevices);
- // Step 3: Setup AVRCP
- mAvrcp = Avrcp.make(this);
-
- // Step 4: Start handler thread for state machines
+ // Step 3: Start handler thread for state machines
mStateMachines.clear();
mStateMachinesThread = new HandlerThread("A2dpService.StateMachines");
mStateMachinesThread.start();
- // Step 5: Setup codec config
+ // Step 4: Setup codec config
mA2dpCodecConfig = new A2dpCodecConfig(this, mA2dpNativeInterface);
- // Step 6: Initialize native interface
+ // Step 5: Initialize native interface
mA2dpNativeInterface.init(mMaxConnectedAudioDevices,
mA2dpCodecConfig.codecConfigPriorities());
- // Step 7: Check if A2DP is in offload mode
+ // Step 6: Check if A2DP is in offload mode
mA2dpOffloadEnabled = mAdapterService.isA2dpOffloadEnabled();
if (DBG) {
Log.d(TAG, "A2DP offload flag set to " + mA2dpOffloadEnabled);
}
- // Step 8: Setup broadcast receivers
+ // Step 7: Setup broadcast receivers
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
mBondStateChangedReceiver = new BondStateChangedReceiver();
@@ -149,10 +141,10 @@
mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver();
registerReceiver(mConnectionStateChangedReceiver, filter);
- // Step 9: Mark service as started
+ // Step 8: Mark service as started
setA2dpService(this);
- // Step 10: Clear active device
+ // Step 9: Clear active device
setActiveDevice(null);
return true;
@@ -166,11 +158,6 @@
return true;
}
- // Step 10: Store volume if there is an active device
- if (mActiveDevice != null && AvrcpTargetService.get() != null) {
- AvrcpTargetService.get().storeVolumeForDevice(mActiveDevice);
- }
-
// Step 9: Clear active device and stop playing audio
removeActiveDevice(true);
@@ -201,19 +188,13 @@
mStateMachinesThread.quitSafely();
mStateMachinesThread = null;
- // Step 3: Cleanup AVRCP
- mAvrcp.doQuit();
- mAvrcp.cleanup();
- mAvrcp = null;
-
// Step 2: Reset maximum number of connected audio devices
mMaxConnectedAudioDevices = 1;
- // Step 1: Clear BluetoothAdapter, AdapterService, A2dpNativeInterface, AudioManager
+ // Step 1: Clear AdapterService, A2dpNativeInterface, AudioManager
mAudioManager = null;
mA2dpNativeInterface = null;
mAdapterService = null;
- mAdapter = null;
return true;
}
@@ -260,8 +241,23 @@
synchronized (mStateMachines) {
if (!connectionAllowedCheckMaxDevices(device)) {
- Log.e(TAG, "Cannot connect to " + device + " : too many connected devices");
- return false;
+ // when mMaxConnectedAudioDevices is one, disconnect current device first.
+ if (mMaxConnectedAudioDevices == 1) {
+ List<BluetoothDevice> sinks = getDevicesMatchingConnectionStates(
+ new int[] {BluetoothProfile.STATE_CONNECTED,
+ BluetoothProfile.STATE_CONNECTING,
+ BluetoothProfile.STATE_DISCONNECTING});
+ for (BluetoothDevice sink : sinks) {
+ if (sink.equals(device)) {
+ Log.w(TAG, "Connecting to device " + device + " : disconnect skipped");
+ continue;
+ }
+ disconnect(sink);
+ }
+ } else {
+ Log.e(TAG, "Cannot connect to " + device + " : too many connected devices");
+ return false;
+ }
}
A2dpStateMachine smConnect = getOrCreateStateMachine(device);
if (smConnect == null) {
@@ -340,7 +336,7 @@
* request, otherwise is for incoming connection request
* @return true if connection is allowed, otherwise false
*/
- @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public boolean okToConnect(BluetoothDevice device, boolean isOutgoingRequest) {
Log.i(TAG, "okToConnect: device " + device + " isOutgoingRequest: " + isOutgoingRequest);
// Check if this is an incoming connection in Quiet mode.
@@ -375,7 +371,13 @@
List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
List<BluetoothDevice> devices = new ArrayList<>();
- Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
+ if (states == null) {
+ return devices;
+ }
+ final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
+ if (bondedDevices == null) {
+ return devices;
+ }
synchronized (mStateMachines) {
for (BluetoothDevice device : bondedDevices) {
if (!BluetoothUuid.isUuidPresent(mAdapterService.getRemoteUuids(device),
@@ -387,9 +389,10 @@
if (sm != null) {
connectionState = sm.getConnectionState();
}
- for (int i = 0; i < states.length; i++) {
- if (connectionState == states[i]) {
+ for (int state : states) {
+ if (connectionState == state) {
devices.add(device);
+ break;
}
}
}
@@ -424,14 +427,22 @@
}
}
+ private void storeActiveDeviceVolume() {
+ // Make sure volume has been stored before been removed from active.
+ if (mFactory.getAvrcpTargetService() != null && mActiveDevice != null) {
+ mFactory.getAvrcpTargetService().storeVolumeForDevice(mActiveDevice);
+ }
+ }
+
private void removeActiveDevice(boolean forceStopPlayingAudio) {
BluetoothDevice previousActiveDevice = mActiveDevice;
synchronized (mStateMachines) {
- // Clear the active device
- mActiveDevice = null;
+ // Make sure volume has been store before device been remove from active.
+ storeActiveDeviceVolume();
+
// This needs to happen before we inform the audio manager that the device
- // disconnected. Please see comment in broadcastActiveDevice() for why.
- broadcastActiveDevice(null);
+ // disconnected. Please see comment in updateAndBroadcastActiveDevice() for why.
+ updateAndBroadcastActiveDevice(null);
if (previousActiveDevice == null) {
return;
@@ -458,6 +469,47 @@
}
/**
+ * Process a change in the silence mode for a {@link BluetoothDevice}.
+ *
+ * @param device the device to change silence mode
+ * @param silence true to enable silence mode, false to disable.
+ * @return true on success, false on error
+ */
+ @VisibleForTesting
+ public boolean setSilenceMode(BluetoothDevice device, boolean silence) {
+ if (DBG) {
+ Log.d(TAG, "setSilenceMode(" + device + "): " + silence);
+ }
+ if (silence && Objects.equals(mActiveDevice, device)) {
+ removeActiveDevice(true);
+ } else if (!silence && mActiveDevice == null) {
+ // Set the device as the active device if currently no active device.
+ setActiveDevice(device);
+ }
+ if (!mA2dpNativeInterface.setSilenceDevice(device, silence)) {
+ Log.e(TAG, "Cannot set " + device + " silence mode " + silence + " in native layer");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Early notification that Hearing Aids will be the active device. This allows the A2DP to save
+ * its volume before the Audio Service starts changing its media stream.
+ */
+ public void earlyNotifyHearingAidActive() {
+ enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
+
+ synchronized (mStateMachines) {
+ // Switch active device from A2DP to Hearing Aids.
+ if (DBG) {
+ Log.d(TAG, "earlyNotifyHearingAidActive: Save volume for " + mActiveDevice);
+ }
+ storeActiveDeviceVolume();
+ }
+ }
+
+ /**
* Set the active device.
*
* @param device the active device
@@ -471,10 +523,6 @@
Log.d(TAG, "setActiveDevice(" + device + "): previous is " + previousActiveDevice);
}
- if (previousActiveDevice != null && AvrcpTargetService.get() != null) {
- AvrcpTargetService.get().storeVolumeForDevice(previousActiveDevice);
- }
-
if (device == null) {
// Remove active device and continue playing audio only if necessary.
removeActiveDevice(false);
@@ -500,10 +548,17 @@
codecStatus = sm.getCodecStatus();
boolean deviceChanged = !Objects.equals(device, mActiveDevice);
- mActiveDevice = device;
+ if (deviceChanged) {
+ // Switch from one A2DP to another A2DP device
+ if (DBG) {
+ Log.d(TAG, "Switch A2DP devices to " + device + " from " + mActiveDevice);
+ }
+ storeActiveDeviceVolume();
+ }
+
// This needs to happen before we inform the audio manager that the device
- // disconnected. Please see comment in broadcastActiveDevice() for why.
- broadcastActiveDevice(mActiveDevice);
+ // disconnected. Please see comment in updateAndBroadcastActiveDevice() for why.
+ updateAndBroadcastActiveDevice(device);
if (deviceChanged) {
// Send an intent with the active device codec config
if (codecStatus != null) {
@@ -516,7 +571,7 @@
if (previousActiveDevice != null) {
if (!mAudioManager.isStreamMute(AudioManager.STREAM_MUSIC)) {
mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
- AudioManager.ADJUST_MUTE, 0);
+ AudioManager.ADJUST_MUTE, AudioManager.FLAG_BLUETOOTH_ABS_VOLUME);
wasMuted = true;
}
mAudioManager.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
@@ -525,10 +580,8 @@
}
int rememberedVolume = -1;
- if (AvrcpTargetService.get() != null) {
- AvrcpTargetService.get().volumeDeviceSwitched(mActiveDevice);
-
- rememberedVolume = AvrcpTargetService.get()
+ if (mFactory.getAvrcpTargetService() != null) {
+ rememberedVolume = mFactory.getAvrcpTargetService()
.getRememberedVolumeForDevice(mActiveDevice);
}
@@ -542,7 +595,7 @@
mAudioManager.handleBluetoothA2dpDeviceConfigChange(mActiveDevice);
if (wasMuted) {
mAudioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
- AudioManager.ADJUST_UNMUTE, 0);
+ AudioManager.ADJUST_UNMUTE, AudioManager.FLAG_BLUETOOTH_ABS_VOLUME);
}
}
}
@@ -569,46 +622,33 @@
public boolean setPriority(BluetoothDevice device, int priority) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothA2dpSinkPriorityKey(device.getAddress()), priority);
if (DBG) {
Log.d(TAG, "Saved priority " + device + " = " + priority);
}
+ mAdapterService.getDatabase()
+ .setProfilePriority(device, BluetoothProfile.A2DP, priority);
return true;
}
public int getPriority(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- int priority = Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothA2dpSinkPriorityKey(device.getAddress()),
- BluetoothProfile.PRIORITY_UNDEFINED);
- return priority;
+ return mAdapterService.getDatabase()
+ .getProfilePriority(device, BluetoothProfile.A2DP);
}
- /* Absolute volume implementation */
public boolean isAvrcpAbsoluteVolumeSupported() {
- return mAvrcp.isAbsoluteVolumeSupported();
+ // TODO (apanicke): Add a hook here for the AvrcpTargetService.
+ return false;
}
+
public void setAvrcpAbsoluteVolume(int volume) {
// TODO (apanicke): Instead of using A2DP as a middleman for volume changes, add a binder
// service to the new AVRCP Profile and have the audio manager use that instead.
- if (AvrcpTargetService.get() != null) {
- AvrcpTargetService.get().sendVolumeChanged(volume);
+ if (mFactory.getAvrcpTargetService() != null) {
+ mFactory.getAvrcpTargetService().sendVolumeChanged(volume);
return;
}
-
- mAvrcp.setAbsoluteVolume(volume);
- }
-
- public void setAvrcpAudioState(int state) {
- mAvrcp.setA2dpAudioState(state);
- }
-
- public void resetAvrcpBlacklist(BluetoothDevice device) {
- if (mAvrcp != null) {
- mAvrcp.resetBlackList(device.getAddress());
- }
}
boolean isA2dpPlaying(BluetoothDevice device) {
@@ -672,10 +712,19 @@
device = mActiveDevice;
}
if (device == null) {
- Log.e(TAG, "Cannot set codec config preference: no active A2DP device");
+ Log.e(TAG, "setCodecConfigPreference: Invalid device");
return;
}
- mA2dpCodecConfig.setCodecConfigPreference(device, codecConfig);
+ if (codecConfig == null) {
+ Log.e(TAG, "setCodecConfigPreference: Codec config can't be null");
+ return;
+ }
+ BluetoothCodecStatus codecStatus = getCodecStatus(device);
+ if (codecStatus == null) {
+ Log.e(TAG, "setCodecConfigPreference: Codec status is null");
+ return;
+ }
+ mA2dpCodecConfig.setCodecConfigPreference(device, codecStatus, codecConfig);
}
/**
@@ -694,10 +743,19 @@
device = mActiveDevice;
}
if (device == null) {
- Log.e(TAG, "Cannot enable optional codecs: no active A2DP device");
+ Log.e(TAG, "enableOptionalCodecs: Invalid device");
return;
}
- mA2dpCodecConfig.enableOptionalCodecs(device);
+ if (getSupportsOptionalCodecs(device) != BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED) {
+ Log.e(TAG, "enableOptionalCodecs: No optional codecs");
+ return;
+ }
+ BluetoothCodecStatus codecStatus = getCodecStatus(device);
+ if (codecStatus == null) {
+ Log.e(TAG, "enableOptionalCodecs: Codec status is null");
+ return;
+ }
+ mA2dpCodecConfig.enableOptionalCodecs(device, codecStatus.getCodecConfig());
}
/**
@@ -716,34 +774,36 @@
device = mActiveDevice;
}
if (device == null) {
- Log.e(TAG, "Cannot disable optional codecs: no active A2DP device");
+ Log.e(TAG, "disableOptionalCodecs: Invalid device");
return;
}
- mA2dpCodecConfig.disableOptionalCodecs(device);
+ if (getSupportsOptionalCodecs(device) != BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED) {
+ Log.e(TAG, "disableOptionalCodecs: No optional codecs");
+ return;
+ }
+ BluetoothCodecStatus codecStatus = getCodecStatus(device);
+ if (codecStatus == null) {
+ Log.e(TAG, "disableOptionalCodecs: Codec status is null");
+ return;
+ }
+ mA2dpCodecConfig.disableOptionalCodecs(device, codecStatus.getCodecConfig());
}
public int getSupportsOptionalCodecs(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- int support = Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothA2dpSupportsOptionalCodecsKey(device.getAddress()),
- BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN);
- return support;
+ return mAdapterService.getDatabase().getA2dpSupportsOptionalCodecs(device);
}
public void setSupportsOptionalCodecs(BluetoothDevice device, boolean doesSupport) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
int value = doesSupport ? BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED
: BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED;
- Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothA2dpSupportsOptionalCodecsKey(device.getAddress()),
- value);
+ mAdapterService.getDatabase().setA2dpSupportsOptionalCodecs(device, value);
}
public int getOptionalCodecsEnabled(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- return Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothA2dpOptionalCodecsEnabledKey(device.getAddress()),
- BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN);
+ return mAdapterService.getDatabase().getA2dpOptionalCodecsEnabled(device);
}
public void setOptionalCodecsEnabled(BluetoothDevice device, int value) {
@@ -754,9 +814,7 @@
Log.w(TAG, "Unexpected value passed to setOptionalCodecsEnabled:" + value);
return;
}
- Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothA2dpOptionalCodecsEnabledKey(device.getAddress()),
- value);
+ mAdapterService.getDatabase().setA2dpOptionalCodecsEnabled(device, value);
}
// Handle messages from native (JNI) to Java
@@ -800,8 +858,27 @@
* @param sameAudioFeedingParameters if true the audio feeding parameters
* haven't been changed
*/
- void codecConfigUpdated(BluetoothDevice device, BluetoothCodecStatus codecStatus,
+ @VisibleForTesting
+ public void codecConfigUpdated(BluetoothDevice device, BluetoothCodecStatus codecStatus,
boolean sameAudioFeedingParameters) {
+ // Log codec config and capability metrics
+ BluetoothCodecConfig codecConfig = codecStatus.getCodecConfig();
+ StatsLog.write(StatsLog.BLUETOOTH_A2DP_CODEC_CONFIG_CHANGED,
+ mAdapterService.obfuscateAddress(device), codecConfig.getCodecType(),
+ codecConfig.getCodecPriority(), codecConfig.getSampleRate(),
+ codecConfig.getBitsPerSample(), codecConfig.getChannelMode(),
+ codecConfig.getCodecSpecific1(), codecConfig.getCodecSpecific2(),
+ codecConfig.getCodecSpecific3(), codecConfig.getCodecSpecific4());
+ BluetoothCodecConfig[] codecCapabilities = codecStatus.getCodecsSelectableCapabilities();
+ for (BluetoothCodecConfig codecCapability : codecCapabilities) {
+ StatsLog.write(StatsLog.BLUETOOTH_A2DP_CODEC_CAPABILITY_CHANGED,
+ mAdapterService.obfuscateAddress(device), codecCapability.getCodecType(),
+ codecCapability.getCodecPriority(), codecCapability.getSampleRate(),
+ codecCapability.getBitsPerSample(), codecCapability.getChannelMode(),
+ codecConfig.getCodecSpecific1(), codecConfig.getCodecSpecific2(),
+ codecConfig.getCodecSpecific3(), codecConfig.getCodecSpecific4());
+ }
+
broadcastCodecConfig(device, codecStatus);
// Inform the Audio Service about the codec configuration change,
@@ -838,11 +915,24 @@
}
}
- private void broadcastActiveDevice(BluetoothDevice device) {
+ // This needs to run before any of the Audio Manager connection functions since
+ // AVRCP needs to be aware that the audio device is changed before the Audio Manager
+ // changes the volume of the output devices.
+ private void updateAndBroadcastActiveDevice(BluetoothDevice device) {
if (DBG) {
- Log.d(TAG, "broadcastActiveDevice(" + device + ")");
+ Log.d(TAG, "updateAndBroadcastActiveDevice(" + device + ")");
}
+ synchronized (mStateMachines) {
+ if (mFactory.getAvrcpTargetService() != null) {
+ mFactory.getAvrcpTargetService().volumeDeviceSwitched(device);
+ }
+
+ mActiveDevice = device;
+ }
+
+ StatsLog.write(StatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED, BluetoothProfile.A2DP,
+ mAdapterService.obfuscateAddress(device));
Intent intent = new Intent(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
@@ -902,6 +992,10 @@
if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) {
return;
}
+ if (mFactory.getAvrcpTargetService() != null) {
+ mFactory.getAvrcpTargetService().removeStoredVolumeForDevice(device);
+ }
+
removeStateMachine(device);
}
}
@@ -921,9 +1015,17 @@
}
}
- private void updateOptionalCodecsSupport(BluetoothDevice device) {
+
+ /**
+ * Update and initiate optional codec status change to native.
+ *
+ * @param device the device to change optional codec status
+ */
+ @VisibleForTesting
+ public void updateOptionalCodecsSupport(BluetoothDevice device) {
int previousSupport = getSupportsOptionalCodecs(device);
boolean supportsOptional = false;
+ boolean hasMandatoryCodec = false;
synchronized (mStateMachines) {
A2dpStateMachine sm = mStateMachines.get(device);
@@ -933,13 +1035,22 @@
BluetoothCodecStatus codecStatus = sm.getCodecStatus();
if (codecStatus != null) {
for (BluetoothCodecConfig config : codecStatus.getCodecsSelectableCapabilities()) {
- if (!config.isMandatoryCodec()) {
+ if (config.isMandatoryCodec()) {
+ hasMandatoryCodec = true;
+ } else {
supportsOptional = true;
- break;
}
}
}
}
+ if (!hasMandatoryCodec) {
+ // Mandatory codec(SBC) is not selectable. It could be caused by the remote device
+ // select codec before native finish get codec capabilities. Stop use this codec
+ // status as the reference to support/enable optional codecs.
+ Log.i(TAG, "updateOptionalCodecsSupport: Mandatory codec is not selectable.");
+ return;
+ }
+
if (previousSupport == BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN
|| supportsOptional != (previousSupport
== BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED)) {
@@ -947,10 +1058,17 @@
}
if (supportsOptional) {
int enabled = getOptionalCodecsEnabled(device);
- if (enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED) {
- enableOptionalCodecs(device);
- } else if (enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED) {
- disableOptionalCodecs(device);
+ switch (enabled) {
+ case BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN:
+ // Enable optional codec by default.
+ setOptionalCodecsEnabled(device, BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED);
+ // Fall through intended
+ case BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED:
+ enableOptionalCodecs(device);
+ break;
+ case BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED:
+ disableOptionalCodecs(device);
+ break;
}
}
}
@@ -961,10 +1079,6 @@
}
synchronized (mStateMachines) {
if (toState == BluetoothProfile.STATE_CONNECTED) {
- // Each time a device connects, we want to re-check if it supports optional
- // codecs (perhaps it's had a firmware update, etc.) and save that state if
- // it differs from what we had saved before.
- updateOptionalCodecsSupport(device);
MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.A2DP);
}
// Set the active device if only one connected device is supported and it was connected
@@ -979,6 +1093,10 @@
if (toState == BluetoothProfile.STATE_DISCONNECTED) {
int bondState = mAdapterService.getBondState(device);
if (bondState == BluetoothDevice.BOND_NONE) {
+ if (mFactory.getAvrcpTargetService() != null) {
+ mFactory.getAvrcpTargetService().removeStoredVolumeForDevice(device);
+ }
+
removeStateMachine(device);
}
}
@@ -1212,8 +1330,5 @@
for (A2dpStateMachine sm : mStateMachines.values()) {
sm.dump(sb);
}
- if (mAvrcp != null) {
- mAvrcp.dump(sb);
- }
}
}
diff --git a/src/com/android/bluetooth/a2dp/A2dpStateMachine.java b/src/com/android/bluetooth/a2dp/A2dpStateMachine.java
index 3ab26e1..e0df8b2 100644
--- a/src/com/android/bluetooth/a2dp/A2dpStateMachine.java
+++ b/src/com/android/bluetooth/a2dp/A2dpStateMachine.java
@@ -53,10 +53,11 @@
import android.content.Intent;
import android.os.Looper;
import android.os.Message;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
+import android.util.StatsLog;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
@@ -88,7 +89,8 @@
private A2dpService mA2dpService;
private A2dpNativeInterface mA2dpNativeInterface;
- private boolean mA2dpOffloadEnabled = false;
+ @VisibleForTesting
+ boolean mA2dpOffloadEnabled = false;
private final BluetoothDevice mDevice;
private boolean mIsPlaying = false;
private BluetoothCodecStatus mCodecStatus;
@@ -130,7 +132,6 @@
// Stop if auido is still playing
log("doQuit: stopped playing " + mDevice);
mIsPlaying = false;
- mA2dpService.setAvrcpAudioState(BluetoothA2dp.STATE_NOT_PLAYING);
broadcastAudioState(BluetoothA2dp.STATE_NOT_PLAYING,
BluetoothA2dp.STATE_PLAYING);
}
@@ -158,7 +159,6 @@
if (mIsPlaying) {
Log.i(TAG, "Disconnected: stopped playing: " + mDevice);
mIsPlaying = false;
- mA2dpService.setAvrcpAudioState(BluetoothA2dp.STATE_NOT_PLAYING);
broadcastAudioState(BluetoothA2dp.STATE_NOT_PLAYING,
BluetoothA2dp.STATE_PLAYING);
}
@@ -468,6 +468,10 @@
removeDeferredMessages(CONNECT);
+ // Each time a device connects, we want to re-check if it supports optional
+ // codecs (perhaps it's had a firmware update, etc.) and save that state if
+ // it differs from what we had saved before.
+ mA2dpService.updateOptionalCodecsSupport(mDevice);
broadcastConnectionState(mConnectionState, mLastConnectionState);
// Upon connected, the audio starts out as stopped
broadcastAudioState(BluetoothA2dp.STATE_NOT_PLAYING,
@@ -559,7 +563,6 @@
if (!mIsPlaying) {
Log.i(TAG, "Connected: started playing: " + mDevice);
mIsPlaying = true;
- mA2dpService.setAvrcpAudioState(BluetoothA2dp.STATE_PLAYING);
broadcastAudioState(BluetoothA2dp.STATE_PLAYING,
BluetoothA2dp.STATE_NOT_PLAYING);
}
@@ -571,7 +574,6 @@
if (mIsPlaying) {
Log.i(TAG, "Connected: stopped playing: " + mDevice);
mIsPlaying = false;
- mA2dpService.setAvrcpAudioState(BluetoothA2dp.STATE_NOT_PLAYING);
broadcastAudioState(BluetoothA2dp.STATE_NOT_PLAYING,
BluetoothA2dp.STATE_PLAYING);
}
@@ -611,8 +613,11 @@
}
// NOTE: This event is processed in any state
- private void processCodecConfigEvent(BluetoothCodecStatus newCodecStatus) {
+ @VisibleForTesting
+ void processCodecConfigEvent(BluetoothCodecStatus newCodecStatus) {
BluetoothCodecConfig prevCodecConfig = null;
+ BluetoothCodecStatus prevCodecStatus = mCodecStatus;
+
synchronized (this) {
if (mCodecStatus != null) {
prevCodecConfig = mCodecStatus.getCodecConfig();
@@ -633,6 +638,12 @@
}
}
+ if (isConnected() && !sameSelectableCodec(prevCodecStatus, mCodecStatus)) {
+ // Remote selectable codec could be changed if codec config changed
+ // in connected state, we need to re-check optional codec status
+ // for this codec change event.
+ mA2dpService.updateOptionalCodecsSupport(mDevice);
+ }
if (mA2dpOffloadEnabled) {
boolean update = false;
BluetoothCodecConfig newCodecConfig = mCodecStatus.getCodecConfig();
@@ -676,7 +687,7 @@
private void broadcastAudioState(int newState, int prevState) {
log("A2DP Playing state : device: " + mDevice + " State:" + audioStateToString(prevState)
+ "->" + audioStateToString(newState));
-
+ StatsLog.write(StatsLog.BLUETOOTH_A2DP_PLAYBACK_STATE_CHANGED, newState);
Intent intent = new Intent(BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
@@ -699,6 +710,16 @@
return builder.toString();
}
+ private static boolean sameSelectableCodec(BluetoothCodecStatus prevCodecStatus,
+ BluetoothCodecStatus newCodecStatus) {
+ if (prevCodecStatus == null) {
+ return false;
+ }
+ return BluetoothCodecStatus.sameCapabilities(
+ prevCodecStatus.getCodecsSelectableCapabilities(),
+ newCodecStatus.getCodecsSelectableCapabilities());
+ }
+
private static String messageWhatToString(int what) {
switch (what) {
case CONNECT:
@@ -752,7 +773,6 @@
ProfileService.println(sb, " mCodecConfig: " + mCodecStatus.getCodecConfig());
}
}
- ProfileService.println(sb, " StateMachine: " + this);
// Dump the state machine logs
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
index 17c8885..e41def0 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
@@ -13,224 +13,94 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.android.bluetooth.a2dpsink;
+import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAudioConfig;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.IBluetoothA2dpSink;
-import android.content.Intent;
-import android.provider.Settings;
import android.util.Log;
import com.android.bluetooth.Utils;
-import com.android.bluetooth.a2dpsink.mbs.A2dpMediaBrowserService;
-import com.android.bluetooth.avrcpcontroller.AvrcpControllerService;
+import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
/**
* Provides Bluetooth A2DP Sink profile, as a service in the Bluetooth application.
* @hide
*/
public class A2dpSinkService extends ProfileService {
- private static final boolean DBG = true;
private static final String TAG = "A2dpSinkService";
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ static final int MAXIMUM_CONNECTED_DEVICES = 1;
- private A2dpSinkStateMachine mStateMachine;
- private static A2dpSinkService sA2dpSinkService;
+ private final BluetoothAdapter mAdapter;
+ protected Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap =
+ new ConcurrentHashMap<>(1);
- @Override
- protected IProfileServiceBinder initBinder() {
- return new BluetoothA2dpSinkBinder(this);
+ private A2dpSinkStreamHandler mA2dpSinkStreamHandler;
+ private static A2dpSinkService sService;
+
+ static {
+ classInitNative();
}
@Override
protected boolean start() {
- if (DBG) {
- Log.d(TAG, "start()");
- }
- // Start the media browser service.
- Intent startIntent = new Intent(this, A2dpMediaBrowserService.class);
- startService(startIntent);
- mStateMachine = A2dpSinkStateMachine.make(this, this);
- setA2dpSinkService(this);
+ initNative();
+ sService = this;
+ mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, this);
return true;
}
@Override
protected boolean stop() {
- if (DBG) {
- Log.d(TAG, "stop()");
+ for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) {
+ stateMachine.quitNow();
}
- setA2dpSinkService(null);
- if (mStateMachine != null) {
- mStateMachine.doQuit();
- }
- Intent stopIntent = new Intent(this, A2dpMediaBrowserService.class);
- stopService(stopIntent);
+ sService = null;
return true;
}
+ public static A2dpSinkService getA2dpSinkService() {
+ return sService;
+ }
+
+ public A2dpSinkService() {
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ }
+
+ protected A2dpSinkStateMachine newStateMachine(BluetoothDevice device) {
+ return new A2dpSinkStateMachine(device, this);
+ }
+
+ protected synchronized A2dpSinkStateMachine getStateMachine(BluetoothDevice device) {
+ return mDeviceStateMap.get(device);
+ }
+
+ /**
+ * Request audio focus such that the designated device can stream audio
+ */
+ public void requestAudioFocus(BluetoothDevice device, boolean request) {
+ mA2dpSinkStreamHandler.requestAudioFocus(request);
+ }
+
@Override
- protected void cleanup() {
- if (mStateMachine != null) {
- mStateMachine.cleanup();
- }
- }
-
- //API Methods
-
- public static synchronized A2dpSinkService getA2dpSinkService() {
- if (sA2dpSinkService == null) {
- Log.w(TAG, "getA2dpSinkService(): service is null");
- return null;
- }
- if (!sA2dpSinkService.isAvailable()) {
- Log.w(TAG, "getA2dpSinkService(): service is not available ");
- return null;
- }
- return sA2dpSinkService;
- }
-
- private static synchronized void setA2dpSinkService(A2dpSinkService instance) {
- if (DBG) {
- Log.d(TAG, "setA2dpSinkService(): set to: " + instance);
- }
- sA2dpSinkService = instance;
- }
-
- public boolean connect(BluetoothDevice device) {
- enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
-
- int connectionState = mStateMachine.getConnectionState(device);
- if (connectionState == BluetoothProfile.STATE_CONNECTED
- || connectionState == BluetoothProfile.STATE_CONNECTING) {
- return false;
- }
-
- if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) {
- return false;
- }
-
- mStateMachine.sendMessage(A2dpSinkStateMachine.CONNECT, device);
- return true;
- }
-
- boolean disconnect(BluetoothDevice device) {
- enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
- int connectionState = mStateMachine.getConnectionState(device);
- if (connectionState != BluetoothProfile.STATE_CONNECTED
- && connectionState != BluetoothProfile.STATE_CONNECTING) {
- return false;
- }
-
- mStateMachine.sendMessage(A2dpSinkStateMachine.DISCONNECT, device);
- return true;
- }
-
- public List<BluetoothDevice> getConnectedDevices() {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- return mStateMachine.getConnectedDevices();
- }
-
- List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- return mStateMachine.getDevicesMatchingConnectionStates(states);
- }
-
- int getConnectionState(BluetoothDevice device) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- return mStateMachine.getConnectionState(device);
- }
-
- public boolean setPriority(BluetoothDevice device, int priority) {
- enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothA2dpSrcPriorityKey(device.getAddress()), priority);
- if (DBG) {
- Log.d(TAG, "Saved priority " + device + " = " + priority);
- }
- return true;
- }
-
- public int getPriority(BluetoothDevice device) {
- enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- int priority = Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothA2dpSrcPriorityKey(device.getAddress()),
- BluetoothProfile.PRIORITY_UNDEFINED);
- return priority;
- }
-
- /**
- * Called by AVRCP controller to provide information about the last user intent on CT.
- *
- * If the user has pressed play in the last attempt then A2DP Sink component will grant focus to
- * any incoming sound from the phone (and also retain focus for a few seconds before
- * relinquishing. On the other hand if the user has pressed pause/stop then the A2DP sink
- * component will take the focus away but also notify the stack to throw away incoming data.
- */
- public void informAvrcpPassThroughCmd(BluetoothDevice device, int keyCode, int keyState) {
- if (mStateMachine != null) {
- if (keyCode == AvrcpControllerService.PASS_THRU_CMD_ID_PLAY
- && keyState == AvrcpControllerService.KEY_STATE_RELEASED) {
- mStateMachine.sendMessage(A2dpSinkStateMachine.EVENT_AVRCP_CT_PLAY);
- } else if ((keyCode == AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE
- || keyCode == AvrcpControllerService.PASS_THRU_CMD_ID_STOP)
- && keyState == AvrcpControllerService.KEY_STATE_RELEASED) {
- mStateMachine.sendMessage(A2dpSinkStateMachine.EVENT_AVRCP_CT_PAUSE);
- }
- }
- }
-
- /**
- * Called by AVRCP controller to provide information about the last user intent on TG.
- *
- * Tf the user has pressed pause on the TG then we can preempt streaming music. This is opposed
- * to when the streaming stops abruptly (jitter) in which case we will wait for sometime before
- * stopping playback.
- */
- public void informTGStatePlaying(BluetoothDevice device, boolean isPlaying) {
- if (mStateMachine != null) {
- if (!isPlaying) {
- mStateMachine.sendMessage(A2dpSinkStateMachine.EVENT_AVRCP_TG_PAUSE);
- } else {
- mStateMachine.sendMessage(A2dpSinkStateMachine.EVENT_AVRCP_TG_PLAY);
- }
- }
- }
-
- /**
- * Called by AVRCP controller to establish audio focus.
- *
- * In order to perform streaming the A2DP sink must have audio focus. This interface allows the
- * associated MediaSession to inform the sink of intent to play and then allows streaming to be
- * started from either the source or the sink endpoint.
- */
- public void requestAudioFocus(BluetoothDevice device, boolean request) {
- if (mStateMachine != null) {
- mStateMachine.sendMessage(A2dpSinkStateMachine.EVENT_REQUEST_FOCUS);
- }
- }
-
- synchronized boolean isA2dpPlaying(BluetoothDevice device) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- if (DBG) {
- Log.d(TAG, "isA2dpPlaying(" + device + ")");
- }
- return mStateMachine.isPlaying(device);
- }
-
- BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- return mStateMachine.getAudioConfig(device);
+ protected IProfileServiceBinder initBinder() {
+ return new A2dpSinkServiceBinder(this);
}
//Binder object: Must be static class or memory leak may occur
- private static class BluetoothA2dpSinkBinder extends IBluetoothA2dpSink.Stub
+ private static class A2dpSinkServiceBinder extends IBluetoothA2dpSink.Stub
implements IProfileServiceBinder {
private A2dpSinkService mService;
@@ -240,13 +110,13 @@
return null;
}
- if (mService != null && mService.isAvailable()) {
+ if (mService != null) {
return mService;
}
return null;
}
- BluetoothA2dpSinkBinder(A2dpSinkService svc) {
+ A2dpSinkServiceBinder(A2dpSinkService svc) {
mService = svc;
}
@@ -301,15 +171,6 @@
}
@Override
- public boolean isA2dpPlaying(BluetoothDevice device) {
- A2dpSinkService service = getService();
- if (service == null) {
- return false;
- }
- return service.isA2dpPlaying(device);
- }
-
- @Override
public boolean setPriority(BluetoothDevice device, int priority) {
A2dpSinkService service = getService();
if (service == null) {
@@ -328,6 +189,15 @@
}
@Override
+ public boolean isA2dpPlaying(BluetoothDevice device) {
+ A2dpSinkService service = getService();
+ if (service == null) {
+ return false;
+ }
+ return service.isA2dpPlaying(device);
+ }
+
+ @Override
public BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
A2dpSinkService service = getService();
if (service == null) {
@@ -337,13 +207,222 @@
}
}
- ;
+ /* Generic Profile Code */
+
+ /**
+ * Connect the given Bluetooth device.
+ *
+ * @return true if connection is successful, false otherwise.
+ */
+ public synchronized boolean connect(BluetoothDevice device) {
+ if (device == null) {
+ throw new IllegalArgumentException("Null device");
+ }
+ if (DBG) {
+ StringBuilder sb = new StringBuilder();
+ dump(sb);
+ Log.d(TAG, " connect device: " + device
+ + ", InstanceMap start state: " + sb.toString());
+ }
+ if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) {
+ Log.w(TAG, "Connection not allowed: <" + device.getAddress() + "> is PRIORITY_OFF");
+ return false;
+ }
+ A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
+ if (stateMachine != null) {
+ stateMachine.connect();
+ return true;
+ } else {
+ // a state machine instance doesn't exist yet, and the max has been reached.
+ Log.e(TAG, "Maxed out on the number of allowed MAP connections. "
+ + "Connect request rejected on " + device);
+ return false;
+ }
+ }
+
+ /**
+ * Disconnect the given Bluetooth device.
+ *
+ * @return true if disconnect is successful, false otherwise.
+ */
+ public synchronized boolean disconnect(BluetoothDevice device) {
+ if (DBG) {
+ StringBuilder sb = new StringBuilder();
+ dump(sb);
+ Log.d(TAG, "A2DP disconnect device: " + device
+ + ", InstanceMap start state: " + sb.toString());
+ }
+ A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
+ // a state machine instance doesn't exist. maybe it is already gone?
+ if (stateMachine == null) {
+ return false;
+ }
+ int connectionState = stateMachine.getState();
+ if (connectionState == BluetoothProfile.STATE_DISCONNECTED
+ || connectionState == BluetoothProfile.STATE_DISCONNECTING) {
+ return false;
+ }
+ // upon completion of disconnect, the state machine will remove itself from the available
+ // devices map
+ stateMachine.disconnect();
+ return true;
+ }
+
+ void removeStateMachine(A2dpSinkStateMachine stateMachine) {
+ mDeviceStateMap.remove(stateMachine.getDevice());
+ }
+
+ public List<BluetoothDevice> getConnectedDevices() {
+ return getDevicesMatchingConnectionStates(new int[]{BluetoothAdapter.STATE_CONNECTED});
+ }
+
+ protected A2dpSinkStateMachine getOrCreateStateMachine(BluetoothDevice device) {
+ A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
+ if (stateMachine == null) {
+ stateMachine = newStateMachine(device);
+ mDeviceStateMap.put(device, stateMachine);
+ stateMachine.start();
+ }
+ return stateMachine;
+ }
+
+ List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
+ List<BluetoothDevice> deviceList = new ArrayList<>();
+ Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
+ int connectionState;
+ for (BluetoothDevice device : bondedDevices) {
+ connectionState = getConnectionState(device);
+ if (DBG) Log.d(TAG, "Device: " + device + "State: " + connectionState);
+ for (int i = 0; i < states.length; i++) {
+ if (connectionState == states[i]) {
+ deviceList.add(device);
+ }
+ }
+ }
+ if (DBG) Log.d(TAG, deviceList.toString());
+ Log.d(TAG, "GetDevicesDone");
+ return deviceList;
+ }
+
+ synchronized int getConnectionState(BluetoothDevice device) {
+ A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
+ return (stateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED
+ : stateMachine.getState();
+ }
+
+ /**
+ * Set the priority of the profile.
+ *
+ * @param device the remote device
+ * @param priority the priority of the profile
+ * @return true on success, otherwise false
+ */
+ public boolean setPriority(BluetoothDevice device, int priority) {
+ enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
+ if (DBG) {
+ Log.d(TAG, "Saved priority " + device + " = " + priority);
+ }
+ AdapterService.getAdapterService().getDatabase()
+ .setProfilePriority(device, BluetoothProfile.A2DP_SINK, priority);
+ return true;
+ }
+
+ /**
+ * Get the priority of the profile.
+ *
+ * @param device the remote device
+ * @return priority of the specified device
+ */
+ public int getPriority(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
+ return AdapterService.getAdapterService().getDatabase()
+ .getProfilePriority(device, BluetoothProfile.A2DP_SINK);
+ }
+
@Override
public void dump(StringBuilder sb) {
super.dump(sb);
- if (mStateMachine != null) {
- mStateMachine.dump(sb);
+ ProfileService.println(sb, "Devices Tracked = " + mDeviceStateMap.size());
+ for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) {
+ ProfileService.println(sb,
+ "==== StateMachine for " + stateMachine.getDevice() + " ====");
+ stateMachine.dump(sb);
}
}
+
+ /**
+ * Get the current Bluetooth Audio focus state
+ *
+ * @return focus
+ */
+ public static int getFocusState() {
+ return sService.mA2dpSinkStreamHandler.getFocusState();
+ }
+
+ boolean isA2dpPlaying(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ return mA2dpSinkStreamHandler.isPlaying();
+ }
+
+ BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
+ A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
+ // a state machine instance doesn't exist. maybe it is already gone?
+ if (stateMachine == null) {
+ return null;
+ }
+ return stateMachine.getAudioConfig();
+ }
+
+ /* JNI interfaces*/
+
+ private static native void classInitNative();
+
+ private native void initNative();
+
+ private native void cleanupNative();
+
+ native boolean connectA2dpNative(byte[] address);
+
+ native boolean disconnectA2dpNative(byte[] address);
+
+ /**
+ * inform A2DP decoder of the current audio focus
+ *
+ * @param focusGranted
+ */
+ @VisibleForTesting
+ public native void informAudioFocusStateNative(int focusGranted);
+
+ /**
+ * inform A2DP decoder the desired audio gain
+ *
+ * @param gain
+ */
+ @VisibleForTesting
+ public native void informAudioTrackGainNative(float gain);
+
+ private void onConnectionStateChanged(byte[] address, int state) {
+ StackEvent event = StackEvent.connectionStateChanged(getDevice(address), state);
+ A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice);
+ stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
+ }
+
+ private void onAudioStateChanged(byte[] address, int state) {
+ if (state == StackEvent.AUDIO_STATE_STARTED) {
+ mA2dpSinkStreamHandler.obtainMessage(
+ A2dpSinkStreamHandler.SRC_STR_START).sendToTarget();
+ } else if (state == StackEvent.AUDIO_STATE_STOPPED) {
+ mA2dpSinkStreamHandler.obtainMessage(
+ A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget();
+ }
+ }
+
+ private void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) {
+ StackEvent event = StackEvent.audioConfigChanged(getDevice(address), sampleRate,
+ channelCount);
+ A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice);
+ stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
+ }
}
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
index fb64318..19ed87f 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
@@ -13,874 +13,288 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-/**
- * Bluetooth A2dp Sink StateMachine
- * (Disconnected)
- * | ^
- * CONNECT | | DISCONNECTED
- * V |
- * (Pending)
- * | ^
- * CONNECTED | | CONNECT
- * V |
- * (Connected -- See A2dpSinkStreamHandler)
- */
package com.android.bluetooth.a2dpsink;
import android.bluetooth.BluetoothA2dpSink;
-import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAudioConfig;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
-import android.bluetooth.BluetoothUuid;
-import android.content.Context;
import android.content.Intent;
import android.media.AudioFormat;
-import android.media.AudioManager;
-import android.os.Handler;
import android.os.Message;
-import android.os.ParcelUuid;
-import android.os.PowerManager;
import android.util.Log;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.Utils;
-import com.android.bluetooth.avrcpcontroller.AvrcpControllerService;
-import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
-import com.android.internal.util.IState;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Set;
public class A2dpSinkStateMachine extends StateMachine {
- private static final boolean DBG = false;
+ static final String TAG = "A2DPSinkStateMachine";
+ static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
- static final int CONNECT = 1;
- static final int DISCONNECT = 2;
- private static final int STACK_EVENT = 101;
- private static final int CONNECT_TIMEOUT = 201;
- public static final int EVENT_AVRCP_CT_PLAY = 301;
- public static final int EVENT_AVRCP_CT_PAUSE = 302;
- public static final int EVENT_AVRCP_TG_PLAY = 303;
- public static final int EVENT_AVRCP_TG_PAUSE = 304;
- public static final int EVENT_REQUEST_FOCUS = 305;
+ //0->99 Events from Outside
+ public static final int CONNECT = 1;
+ public static final int DISCONNECT = 2;
- private static final int IS_INVALID_DEVICE = 0;
- private static final int IS_VALID_DEVICE = 1;
- public static final int AVRC_ID_PLAY = 0x44;
- public static final int AVRC_ID_PAUSE = 0x46;
- public static final int KEY_STATE_PRESSED = 0;
- public static final int KEY_STATE_RELEASED = 1;
+ //100->199 Internal Events
+ protected static final int CLEANUP = 100;
+ private static final int CONNECT_TIMEOUT = 101;
- // Connection states.
- // 1. Disconnected: The connection does not exist.
- // 2. Pending: The connection is being established.
- // 3. Connected: The connection is established. The audio connection is in Idle state.
- private Disconnected mDisconnected;
- private Pending mPending;
- private Connected mConnected;
+ //200->299 Events from Native
+ static final int STACK_EVENT = 200;
- private A2dpSinkService mService;
- private Context mContext;
- private BluetoothAdapter mAdapter;
- private IntentBroadcastHandler mIntentBroadcastHandler;
+ static final int CONNECT_TIMEOUT_MS = 5000;
- private static final int MSG_CONNECTION_STATE_CHANGED = 0;
+ protected final BluetoothDevice mDevice;
+ protected final byte[] mDeviceAddress;
+ protected final A2dpSinkService mService;
+ protected final Disconnected mDisconnected;
+ protected final Connecting mConnecting;
+ protected final Connected mConnected;
+ protected final Disconnecting mDisconnecting;
- private final Object mLockForPatch = new Object();
+ protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
+ protected BluetoothAudioConfig mAudioConfig = null;
- // mCurrentDevice is the device connected before the state changes
- // mTargetDevice is the device to be connected
- // mIncomingDevice is the device connecting to us, valid only in Pending state
- // when mIncomingDevice is not null, both mCurrentDevice
- // and mTargetDevice are null
- // when either mCurrentDevice or mTargetDevice is not null,
- // mIncomingDevice is null
- // Stable states
- // No connection, Disconnected state
- // both mCurrentDevice and mTargetDevice are null
- // Connected, Connected state
- // mCurrentDevice is not null, mTargetDevice is null
- // Interim states
- // Connecting to a device, Pending
- // mCurrentDevice is null, mTargetDevice is not null
- // Disconnecting device, Connecting to new device
- // Pending
- // Both mCurrentDevice and mTargetDevice are not null
- // Disconnecting device Pending
- // mCurrentDevice is not null, mTargetDevice is null
- // Incoming connections Pending
- // Both mCurrentDevice and mTargetDevice are null
- private BluetoothDevice mCurrentDevice = null;
- private BluetoothDevice mTargetDevice = null;
- private BluetoothDevice mIncomingDevice = null;
- private BluetoothDevice mPlayingDevice = null;
- private A2dpSinkStreamHandler mStreaming = null;
-
- private final HashMap<BluetoothDevice, BluetoothAudioConfig> mAudioConfigs =
- new HashMap<BluetoothDevice, BluetoothAudioConfig>();
-
- static {
- classInitNative();
- }
-
- private A2dpSinkStateMachine(A2dpSinkService svc, Context context) {
- super("A2dpSinkStateMachine");
- mService = svc;
- mContext = context;
- mAdapter = BluetoothAdapter.getDefaultAdapter();
-
- initNative();
+ A2dpSinkStateMachine(BluetoothDevice device, A2dpSinkService service) {
+ super(TAG);
+ mDevice = device;
+ mDeviceAddress = Utils.getByteAddress(mDevice);
+ mService = service;
+ if (DBG) Log.d(TAG, device.toString());
mDisconnected = new Disconnected();
- mPending = new Pending();
+ mConnecting = new Connecting();
mConnected = new Connected();
+ mDisconnecting = new Disconnecting();
addState(mDisconnected);
- addState(mPending);
+ addState(mConnecting);
addState(mConnected);
+ addState(mDisconnecting);
setInitialState(mDisconnected);
-
- PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
-
- mIntentBroadcastHandler = new IntentBroadcastHandler();
}
- static A2dpSinkStateMachine make(A2dpSinkService svc, Context context) {
- Log.d("A2dpSinkStateMachine", "make");
- A2dpSinkStateMachine a2dpSm = new A2dpSinkStateMachine(svc, context);
- a2dpSm.start();
- return a2dpSm;
+ protected String getConnectionStateChangedIntent() {
+ return BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED;
}
- public void doQuit() {
- if (DBG) {
- Log.d("A2dpSinkStateMachine", "Quit");
- }
- synchronized (A2dpSinkStateMachine.this) {
- mStreaming = null;
- }
- quitNow();
+ /**
+ * Get the current connection state
+ *
+ * @return current State
+ */
+ public int getState() {
+ return mMostRecentState;
}
- public void cleanup() {
- cleanupNative();
- mAudioConfigs.clear();
+ /**
+ * get current audio config
+ */
+ BluetoothAudioConfig getAudioConfig() {
+ return mAudioConfig;
}
+ /**
+ * Get the underlying device tracked by this state machine
+ *
+ * @return device in focus
+ */
+ public synchronized BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * send the Connect command asynchronously
+ */
+ public final void connect() {
+ sendMessage(CONNECT);
+ }
+
+ /**
+ * send the Disconnect command asynchronously
+ */
+ public final void disconnect() {
+ sendMessage(DISCONNECT);
+ }
+
+ /**
+ * Dump the current State Machine to the string builder.
+ * @param sb output string
+ */
public void dump(StringBuilder sb) {
- ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice);
- ProfileService.println(sb, "mTargetDevice: " + mTargetDevice);
- ProfileService.println(sb, "mIncomingDevice: " + mIncomingDevice);
- ProfileService.println(sb, "StateMachine: " + this.toString());
+ ProfileService.println(sb, "mDevice: " + mDevice.getAddress() + "("
+ + mDevice.getName() + ") " + this.toString());
}
- private class Disconnected extends State {
+ @Override
+ protected void unhandledMessage(Message msg) {
+ Log.w(TAG, "unhandledMessage in state " + getCurrentState() + "msg.what=" + msg.what);
+ }
+
+ class Disconnected extends State {
@Override
public void enter() {
- log("Enter Disconnected: " + getCurrentMessage().what);
+ if (DBG) Log.d(TAG, "Enter Disconnected");
+ if (mMostRecentState != BluetoothProfile.STATE_DISCONNECTED) {
+ sendMessage(CLEANUP);
+ }
+ onConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTED);
}
@Override
public boolean processMessage(Message message) {
- log("Disconnected process message: " + message.what);
- if (mCurrentDevice != null || mTargetDevice != null || mIncomingDevice != null) {
- loge("ERROR: current, target, or mIncomingDevice not null in Disconnected");
- return NOT_HANDLED;
- }
-
- boolean retValue = HANDLED;
switch (message.what) {
- case CONNECT:
- BluetoothDevice device = (BluetoothDevice) message.obj;
- broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING,
- BluetoothProfile.STATE_DISCONNECTED);
-
- if (!connectA2dpNative(getByteAddress(device))) {
- broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
- BluetoothProfile.STATE_CONNECTING);
- break;
- }
-
- synchronized (A2dpSinkStateMachine.this) {
- mTargetDevice = device;
- transitionTo(mPending);
- }
- // TODO(BT) remove CONNECT_TIMEOUT when the stack
- // sends back events consistently
- sendMessageDelayed(CONNECT_TIMEOUT, 30000);
- break;
- case DISCONNECT:
- // ignore
- break;
case STACK_EVENT:
- StackEvent event = (StackEvent) message.obj;
- switch (event.type) {
- case EVENT_TYPE_CONNECTION_STATE_CHANGED:
- processConnectionEvent(event.device, event.valueInt);
- break;
- case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
- processAudioConfigEvent(event.device, event.audioConfig);
- break;
- default:
- loge("Unexpected stack event: " + event.type);
- break;
- }
- break;
- default:
- return NOT_HANDLED;
- }
- return retValue;
- }
-
- @Override
- public void exit() {
- log("Exit Disconnected: " + getCurrentMessage().what);
- }
-
- // in Disconnected state
- private void processConnectionEvent(BluetoothDevice device, int state) {
- switch (state) {
- case CONNECTION_STATE_DISCONNECTED:
- logw("Ignore A2DP DISCONNECTED event, device: " + device);
- break;
- case CONNECTION_STATE_CONNECTING:
- if (okToConnect(device)) {
- logi("Incoming A2DP accepted");
- broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING,
- BluetoothProfile.STATE_DISCONNECTED);
- synchronized (A2dpSinkStateMachine.this) {
- mIncomingDevice = device;
- transitionTo(mPending);
- }
- } else {
- //reject the connection and stay in Disconnected state itself
- logi("Incoming A2DP rejected");
- disconnectA2dpNative(getByteAddress(device));
- }
- break;
- case CONNECTION_STATE_CONNECTED:
- logw("A2DP Connected from Disconnected state");
- if (okToConnect(device)) {
- logi("Incoming A2DP accepted");
- broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED,
- BluetoothProfile.STATE_DISCONNECTED);
- synchronized (A2dpSinkStateMachine.this) {
- mCurrentDevice = device;
- transitionTo(mConnected);
- }
- } else {
- //reject the connection and stay in Disconnected state itself
- logi("Incoming A2DP rejected");
- disconnectA2dpNative(getByteAddress(device));
- }
- break;
- case CONNECTION_STATE_DISCONNECTING:
- logw("Ignore HF DISCONNECTING event, device: " + device);
- break;
- default:
- loge("Incorrect state: " + state);
- break;
- }
- }
- }
-
- private class Pending extends State {
- @Override
- public void enter() {
- log("Enter Pending: " + getCurrentMessage().what);
- }
-
- @Override
- public boolean processMessage(Message message) {
- log("Pending process message: " + message.what);
-
- boolean retValue = HANDLED;
- switch (message.what) {
+ processStackEvent((StackEvent) message.obj);
+ return true;
case CONNECT:
- logd("Disconnect before connecting to another target");
- break;
- case CONNECT_TIMEOUT:
- onConnectionStateChanged(getByteAddress(mTargetDevice),
- CONNECTION_STATE_DISCONNECTED);
- break;
- case DISCONNECT:
- BluetoothDevice device = (BluetoothDevice) message.obj;
- if (mCurrentDevice != null && mTargetDevice != null && mTargetDevice.equals(
- device)) {
- // cancel connection to the mTargetDevice
- broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
- BluetoothProfile.STATE_CONNECTING);
- synchronized (A2dpSinkStateMachine.this) {
- mTargetDevice = null;
- }
- }
- break;
- case STACK_EVENT:
- StackEvent event = (StackEvent) message.obj;
- log("STACK_EVENT " + event.type);
- switch (event.type) {
- case EVENT_TYPE_CONNECTION_STATE_CHANGED:
- removeMessages(CONNECT_TIMEOUT);
- processConnectionEvent(event.device, event.valueInt);
- break;
- case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
- processAudioConfigEvent(event.device, event.audioConfig);
- break;
- default:
- loge("Unexpected stack event: " + event.type);
- break;
- }
- break;
- default:
- return NOT_HANDLED;
+ if (DBG) Log.d(TAG, "Connect");
+ transitionTo(mConnecting);
+ return true;
+ case CLEANUP:
+ mService.removeStateMachine(A2dpSinkStateMachine.this);
+ return true;
}
- return retValue;
- }
-
- // in Pending state
- private void processConnectionEvent(BluetoothDevice device, int state) {
- log("processConnectionEvent state " + state);
- log("Devices curr: " + mCurrentDevice + " target: " + mTargetDevice + " incoming: "
- + mIncomingDevice + " device: " + device);
- switch (state) {
- case CONNECTION_STATE_DISCONNECTED:
- mAudioConfigs.remove(device);
- if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
- broadcastConnectionState(mCurrentDevice,
- BluetoothProfile.STATE_DISCONNECTED,
- BluetoothProfile.STATE_DISCONNECTING);
- synchronized (A2dpSinkStateMachine.this) {
- mCurrentDevice = null;
- }
-
- if (mTargetDevice != null) {
- if (!connectA2dpNative(getByteAddress(mTargetDevice))) {
- broadcastConnectionState(mTargetDevice,
- BluetoothProfile.STATE_DISCONNECTED,
- BluetoothProfile.STATE_CONNECTING);
- synchronized (A2dpSinkStateMachine.this) {
- mTargetDevice = null;
- transitionTo(mDisconnected);
- }
- }
- } else {
- synchronized (A2dpSinkStateMachine.this) {
- mIncomingDevice = null;
- transitionTo(mDisconnected);
- }
- }
- } else if (mTargetDevice != null && mTargetDevice.equals(device)) {
- // outgoing connection failed
- broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED,
- BluetoothProfile.STATE_CONNECTING);
- synchronized (A2dpSinkStateMachine.this) {
- mTargetDevice = null;
- transitionTo(mDisconnected);
- }
- } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
- broadcastConnectionState(mIncomingDevice,
- BluetoothProfile.STATE_DISCONNECTED,
- BluetoothProfile.STATE_CONNECTING);
- synchronized (A2dpSinkStateMachine.this) {
- mIncomingDevice = null;
- transitionTo(mDisconnected);
- }
- } else {
- loge("Unknown device Disconnected: " + device);
- }
- break;
- case CONNECTION_STATE_CONNECTED:
- if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
- loge("current device is not null");
- // disconnection failed
- broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTED,
- BluetoothProfile.STATE_DISCONNECTING);
- if (mTargetDevice != null) {
- broadcastConnectionState(mTargetDevice,
- BluetoothProfile.STATE_DISCONNECTED,
- BluetoothProfile.STATE_CONNECTING);
- }
- synchronized (A2dpSinkStateMachine.this) {
- mTargetDevice = null;
- transitionTo(mConnected);
- }
- } else if (mTargetDevice != null && mTargetDevice.equals(device)) {
- loge("target device is not null");
- broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_CONNECTED,
- BluetoothProfile.STATE_CONNECTING);
- synchronized (A2dpSinkStateMachine.this) {
- mCurrentDevice = mTargetDevice;
- mTargetDevice = null;
- transitionTo(mConnected);
- }
- } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
- loge("incoming device is not null");
- broadcastConnectionState(mIncomingDevice, BluetoothProfile.STATE_CONNECTED,
- BluetoothProfile.STATE_CONNECTING);
- synchronized (A2dpSinkStateMachine.this) {
- mCurrentDevice = mIncomingDevice;
- mIncomingDevice = null;
- transitionTo(mConnected);
- }
- } else {
- loge("Unknown device Connected: " + device);
- // something is wrong here, but sync our state with stack by connecting to
- // the new device and disconnect from previous device.
- broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED,
- BluetoothProfile.STATE_DISCONNECTED);
- broadcastConnectionState(mCurrentDevice,
- BluetoothProfile.STATE_DISCONNECTED,
- BluetoothProfile.STATE_CONNECTING);
- synchronized (A2dpSinkStateMachine.this) {
- mCurrentDevice = device;
- mTargetDevice = null;
- mIncomingDevice = null;
- transitionTo(mConnected);
- }
- }
- break;
- case CONNECTION_STATE_CONNECTING:
- if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
- log("current device tries to connect back");
- // TODO(BT) ignore or reject
- } else if (mTargetDevice != null && mTargetDevice.equals(device)) {
- // The stack is connecting to target device or
- // there is an incoming connection from the target device at the same time
- // we already broadcasted the intent, doing nothing here
- log("Stack and target device are connecting");
- } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
- loge("Another connecting event on the incoming device");
- } else {
- // We get an incoming connecting request while Pending
- // TODO(BT) is stack handing this case? let's ignore it for now
- log("Incoming connection while pending, ignore");
- }
- break;
- case CONNECTION_STATE_DISCONNECTING:
- if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
- // we already broadcasted the intent, doing nothing here
- if (DBG) {
- log("stack is disconnecting mCurrentDevice");
- }
- } else if (mTargetDevice != null && mTargetDevice.equals(device)) {
- loge("TargetDevice is getting disconnected");
- } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) {
- loge("IncomingDevice is getting disconnected");
- } else {
- loge("Disconnecting unknown device: " + device);
- }
- break;
- default:
- loge("Incorrect state: " + state);
- break;
- }
- }
-
- }
-
- private class Connected extends State {
- @Override
- public void enter() {
- log("Enter Connected: " + getCurrentMessage().what);
- // Upon connected, the audio starts out as stopped
- broadcastAudioState(mCurrentDevice, BluetoothA2dpSink.STATE_NOT_PLAYING,
- BluetoothA2dpSink.STATE_PLAYING);
- synchronized (A2dpSinkStateMachine.this) {
- if (mStreaming == null) {
- if (DBG) {
- log("Creating New A2dpSinkStreamHandler");
- }
- mStreaming = new A2dpSinkStreamHandler(A2dpSinkStateMachine.this, mContext);
- }
- }
- if (mStreaming.getAudioFocus() == AudioManager.AUDIOFOCUS_NONE) {
- informAudioFocusStateNative(0);
- }
- }
-
- @Override
- public boolean processMessage(Message message) {
- log("Connected process message: " + message.what);
- if (mCurrentDevice == null) {
- loge("ERROR: mCurrentDevice is null in Connected");
- return NOT_HANDLED;
- }
-
- switch (message.what) {
- case CONNECT:
- logd("Disconnect before connecting to another target");
- break;
-
- case DISCONNECT: {
- BluetoothDevice device = (BluetoothDevice) message.obj;
- if (!mCurrentDevice.equals(device)) {
- break;
- }
- broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTING,
- BluetoothProfile.STATE_CONNECTED);
- if (!disconnectA2dpNative(getByteAddress(device))) {
- broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED,
- BluetoothProfile.STATE_DISCONNECTED);
- break;
- }
- mPlayingDevice = null;
- mStreaming.obtainMessage(A2dpSinkStreamHandler.DISCONNECT).sendToTarget();
- transitionTo(mPending);
- }
- break;
-
- case STACK_EVENT:
- StackEvent event = (StackEvent) message.obj;
- switch (event.type) {
- case EVENT_TYPE_CONNECTION_STATE_CHANGED:
- processConnectionEvent(event.device, event.valueInt);
- break;
- case EVENT_TYPE_AUDIO_STATE_CHANGED:
- processAudioStateEvent(event.device, event.valueInt);
- break;
- case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
- processAudioConfigEvent(event.device, event.audioConfig);
- break;
- default:
- loge("Unexpected stack event: " + event.type);
- break;
- }
- break;
-
- case EVENT_AVRCP_CT_PLAY:
- mStreaming.obtainMessage(A2dpSinkStreamHandler.SNK_PLAY).sendToTarget();
- break;
-
- case EVENT_AVRCP_TG_PLAY:
- mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_PLAY).sendToTarget();
- break;
-
- case EVENT_AVRCP_CT_PAUSE:
- mStreaming.obtainMessage(A2dpSinkStreamHandler.SNK_PAUSE).sendToTarget();
- break;
-
- case EVENT_AVRCP_TG_PAUSE:
- mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_PAUSE).sendToTarget();
- break;
-
- case EVENT_REQUEST_FOCUS:
- mStreaming.obtainMessage(A2dpSinkStreamHandler.REQUEST_FOCUS).sendToTarget();
- break;
-
- default:
- return NOT_HANDLED;
- }
- return HANDLED;
- }
-
- // in Connected state
- private void processConnectionEvent(BluetoothDevice device, int state) {
- switch (state) {
- case CONNECTION_STATE_DISCONNECTED:
- mAudioConfigs.remove(device);
- if ((mPlayingDevice != null) && (device.equals(mPlayingDevice))) {
- mPlayingDevice = null;
- }
- if (mCurrentDevice.equals(device)) {
- broadcastConnectionState(mCurrentDevice,
- BluetoothProfile.STATE_DISCONNECTED,
- BluetoothProfile.STATE_CONNECTED);
- synchronized (A2dpSinkStateMachine.this) {
- // Take care of existing audio focus in the streaming state machine.
- mStreaming.obtainMessage(A2dpSinkStreamHandler.DISCONNECT)
- .sendToTarget();
- mCurrentDevice = null;
- transitionTo(mDisconnected);
- }
- } else {
- loge("Disconnected from unknown device: " + device);
- }
- break;
- default:
- loge("Connection State Device: " + device + " bad state: " + state);
- break;
- }
- }
-
- private void processAudioStateEvent(BluetoothDevice device, int state) {
- if (!mCurrentDevice.equals(device)) {
- loge("Audio State Device:" + device + "is different from ConnectedDevice:"
- + mCurrentDevice);
- return;
- }
- log(" processAudioStateEvent in state " + state);
- switch (state) {
- case AUDIO_STATE_STARTED:
- mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_STR_START).sendToTarget();
- break;
- case AUDIO_STATE_REMOTE_SUSPEND:
- case AUDIO_STATE_STOPPED:
- mStreaming.obtainMessage(A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget();
- break;
- default:
- loge("Audio State Device: " + device + " bad state: " + state);
- break;
- }
- }
- }
-
- private void processAudioConfigEvent(BluetoothDevice device, BluetoothAudioConfig audioConfig) {
- log("processAudioConfigEvent: " + device);
- mAudioConfigs.put(device, audioConfig);
- broadcastAudioConfig(device, audioConfig);
- }
-
- int getConnectionState(BluetoothDevice device) {
- if (getCurrentState() == mDisconnected) {
- return BluetoothProfile.STATE_DISCONNECTED;
- }
-
- synchronized (this) {
- IState currentState = getCurrentState();
- if (currentState == mPending) {
- if ((mTargetDevice != null) && mTargetDevice.equals(device)) {
- return BluetoothProfile.STATE_CONNECTING;
- }
- if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) {
- return BluetoothProfile.STATE_DISCONNECTING;
- }
- if ((mIncomingDevice != null) && mIncomingDevice.equals(device)) {
- return BluetoothProfile.STATE_CONNECTING; // incoming connection
- }
- return BluetoothProfile.STATE_DISCONNECTED;
- }
-
- if (currentState == mConnected) {
- if (mCurrentDevice.equals(device)) {
- return BluetoothProfile.STATE_CONNECTED;
- }
- return BluetoothProfile.STATE_DISCONNECTED;
- } else {
- loge("Bad currentState: " + currentState);
- return BluetoothProfile.STATE_DISCONNECTED;
- }
- }
- }
-
- BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
- return mAudioConfigs.get(device);
- }
-
- List<BluetoothDevice> getConnectedDevices() {
- List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
- synchronized (this) {
- if (getCurrentState() == mConnected) {
- devices.add(mCurrentDevice);
- }
- }
- return devices;
- }
-
- boolean isPlaying(BluetoothDevice device) {
- synchronized (this) {
- if ((mPlayingDevice != null) && (device.equals(mPlayingDevice))) {
- return true;
- }
- }
- return false;
- }
-
- // Utility Functions
- boolean okToConnect(BluetoothDevice device) {
- AdapterService adapterService = AdapterService.getAdapterService();
- int priority = mService.getPriority(device);
-
- // check priority and accept or reject the connection. if priority is undefined
- // it is likely that our SDP has not completed and peer is initiating the
- // connection. Allow this connection, provided the device is bonded
- if ((BluetoothProfile.PRIORITY_OFF < priority) || (
- (BluetoothProfile.PRIORITY_UNDEFINED == priority) && (device.getBondState()
- != BluetoothDevice.BOND_NONE))) {
- return true;
- }
- logw("okToConnect not OK to connect " + device);
- return false;
- }
-
- synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
- List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>();
- Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
- int connectionState;
-
- for (BluetoothDevice device : bondedDevices) {
- ParcelUuid[] featureUuids = device.getUuids();
- if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.AudioSource)) {
- continue;
- }
- connectionState = getConnectionState(device);
- for (int i = 0; i < states.length; i++) {
- if (connectionState == states[i]) {
- deviceList.add(device);
- }
- }
- }
- return deviceList;
- }
-
-
- // This method does not check for error conditon (newState == prevState)
- private void broadcastConnectionState(BluetoothDevice device, int newState, int prevState) {
-
- int delay = 0;
- mIntentBroadcastHandler.sendMessageDelayed(
- mIntentBroadcastHandler.obtainMessage(MSG_CONNECTION_STATE_CHANGED, prevState,
- newState, device), delay);
- }
-
- private void broadcastAudioState(BluetoothDevice device, int state, int prevState) {
- Intent intent = new Intent(BluetoothA2dpSink.ACTION_PLAYING_STATE_CHANGED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
- intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
- intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
-//FIXME intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
-
- log("A2DP Playing state : device: " + device + " State:" + prevState + "->" + state);
- }
-
- private void broadcastAudioConfig(BluetoothDevice device, BluetoothAudioConfig audioConfig) {
- Intent intent = new Intent(BluetoothA2dpSink.ACTION_AUDIO_CONFIG_CHANGED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
- intent.putExtra(BluetoothA2dpSink.EXTRA_AUDIO_CONFIG, audioConfig);
-//FIXME intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
-
- log("A2DP Audio Config : device: " + device + " config: " + audioConfig);
- }
-
- private byte[] getByteAddress(BluetoothDevice device) {
- return Utils.getBytesFromAddress(device.getAddress());
- }
-
- private void onConnectionStateChanged(byte[] address, int state) {
- StackEvent event = new StackEvent(EVENT_TYPE_CONNECTION_STATE_CHANGED);
- event.device = getDevice(address);
- event.valueInt = state;
- sendMessage(STACK_EVENT, event);
- }
-
- private void onAudioStateChanged(byte[] address, int state) {
- StackEvent event = new StackEvent(EVENT_TYPE_AUDIO_STATE_CHANGED);
- event.device = getDevice(address);
- event.valueInt = state;
- sendMessage(STACK_EVENT, event);
- }
-
- private void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) {
- StackEvent event = new StackEvent(EVENT_TYPE_AUDIO_CONFIG_CHANGED);
- event.device = getDevice(address);
- int channelConfig =
- (channelCount == 1 ? AudioFormat.CHANNEL_IN_MONO : AudioFormat.CHANNEL_IN_STEREO);
- event.audioConfig =
- new BluetoothAudioConfig(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);
- sendMessage(STACK_EVENT, event);
- }
-
- private BluetoothDevice getDevice(byte[] address) {
- return mAdapter.getRemoteDevice(Utils.getAddressStringFromByte(address));
- }
-
- private class StackEvent {
- public int type = EVENT_TYPE_NONE;
- public BluetoothDevice device = null;
- public int valueInt = 0;
- public BluetoothAudioConfig audioConfig = null;
-
- private StackEvent(int type) {
- this.type = type;
- }
- }
-
- /** Handles A2DP connection state change intent broadcasts. */
- private class IntentBroadcastHandler extends Handler {
-
- private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) {
- if (prevState != state && state == BluetoothProfile.STATE_CONNECTED) {
- MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.A2DP_SINK);
- }
- Intent intent = new Intent(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
- intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
- intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
-//FIXME intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
- log("Connection state " + device + ": " + prevState + "->" + state);
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_CONNECTION_STATE_CHANGED:
- onConnectionStateChanged((BluetoothDevice) msg.obj, msg.arg1, msg.arg2);
- break;
- }
- }
- }
-
- public boolean sendPassThruPlay(BluetoothDevice mDevice) {
- log("sendPassThruPlay + ");
- AvrcpControllerService avrcpCtrlService =
- AvrcpControllerService.getAvrcpControllerService();
- if ((avrcpCtrlService != null) && (mDevice != null)
- && (avrcpCtrlService.getConnectedDevices().contains(mDevice))) {
- avrcpCtrlService.sendPassThroughCmd(mDevice,
- AvrcpControllerService.PASS_THRU_CMD_ID_PLAY,
- AvrcpControllerService.KEY_STATE_PRESSED);
- avrcpCtrlService.sendPassThroughCmd(mDevice,
- AvrcpControllerService.PASS_THRU_CMD_ID_PLAY,
- AvrcpControllerService.KEY_STATE_RELEASED);
- log(" sendPassThruPlay command sent - ");
- return true;
- } else {
- log("passthru command not sent, connection unavailable");
return false;
}
+
+ void processStackEvent(StackEvent event) {
+ switch (event.mType) {
+ case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+ switch (event.mState) {
+ case StackEvent.CONNECTION_STATE_CONNECTED:
+ transitionTo(mConnected);
+ break;
+ case StackEvent.CONNECTION_STATE_DISCONNECTED:
+ sendMessage(CLEANUP);
+ break;
+ }
+ }
+ }
}
- // Event types for STACK_EVENT message
- private static final int EVENT_TYPE_NONE = 0;
- private static final int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1;
- private static final int EVENT_TYPE_AUDIO_STATE_CHANGED = 2;
- private static final int EVENT_TYPE_AUDIO_CONFIG_CHANGED = 3;
+ class Connecting extends State {
+ boolean mIncommingConnection = false;
- // Do not modify without updating the HAL bt_av.h files.
+ @Override
+ public void enter() {
+ if (DBG) Log.d(TAG, "Enter Connecting");
+ onConnectionStateChanged(BluetoothProfile.STATE_CONNECTING);
+ sendMessageDelayed(CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS);
- // match up with btav_connection_state_t enum of bt_av.h
- static final int CONNECTION_STATE_DISCONNECTED = 0;
- static final int CONNECTION_STATE_CONNECTING = 1;
- static final int CONNECTION_STATE_CONNECTED = 2;
- static final int CONNECTION_STATE_DISCONNECTING = 3;
+ if (!mIncommingConnection) {
+ mService.connectA2dpNative(mDeviceAddress);
+ }
- // match up with btav_audio_state_t enum of bt_av.h
- static final int AUDIO_STATE_REMOTE_SUSPEND = 0;
- static final int AUDIO_STATE_STOPPED = 1;
- static final int AUDIO_STATE_STARTED = 2;
+ super.enter();
+ }
- private static native void classInitNative();
+ @Override
+ public boolean processMessage(Message message) {
+ switch (message.what) {
+ case STACK_EVENT:
+ processStackEvent((StackEvent) message.obj);
+ return true;
+ case CONNECT_TIMEOUT:
+ transitionTo(mDisconnected);
+ return true;
+ }
+ return false;
+ }
- private native void initNative();
+ void processStackEvent(StackEvent event) {
+ switch (event.mType) {
+ case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+ switch (event.mState) {
+ case StackEvent.CONNECTION_STATE_CONNECTED:
+ transitionTo(mConnected);
+ break;
+ case StackEvent.CONNECTION_STATE_DISCONNECTED:
+ transitionTo(mDisconnected);
+ break;
+ }
+ }
+ }
+ @Override
+ public void exit() {
+ removeMessages(CONNECT_TIMEOUT);
+ }
- private native void cleanupNative();
+ }
- private native boolean connectA2dpNative(byte[] address);
+ class Connected extends State {
+ @Override
+ public void enter() {
+ if (DBG) Log.d(TAG, "Enter Connected");
+ onConnectionStateChanged(BluetoothProfile.STATE_CONNECTED);
+ }
- private native boolean disconnectA2dpNative(byte[] address);
+ @Override
+ public boolean processMessage(Message message) {
+ switch (message.what) {
+ case DISCONNECT:
+ transitionTo(mDisconnecting);
+ mService.disconnectA2dpNative(mDeviceAddress);
+ return true;
+ case STACK_EVENT:
+ processStackEvent((StackEvent) message.obj);
+ return true;
+ }
+ return false;
+ }
- public native void informAudioFocusStateNative(int focusGranted);
+ void processStackEvent(StackEvent event) {
+ switch (event.mType) {
+ case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+ switch (event.mState) {
+ case StackEvent.CONNECTION_STATE_DISCONNECTING:
+ transitionTo(mDisconnecting);
+ break;
+ case StackEvent.CONNECTION_STATE_DISCONNECTED:
+ transitionTo(mDisconnected);
+ break;
+ }
+ break;
+ case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED:
+ mAudioConfig = new BluetoothAudioConfig(event.mSampleRate, event.mChannelCount,
+ AudioFormat.ENCODING_PCM_16BIT);
+ break;
+ }
+ }
+ }
- public native void informAudioTrackGainNative(float focusGranted);
+ protected class Disconnecting extends State {
+ @Override
+ public void enter() {
+ if (DBG) Log.d(TAG, "Enter Disconnecting");
+ onConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTING);
+ transitionTo(mDisconnected);
+ }
+ }
+
+ protected void onConnectionStateChanged(int currentState) {
+ if (mMostRecentState == currentState) {
+ return;
+ }
+ if (currentState == BluetoothProfile.STATE_CONNECTED) {
+ MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.A2DP_SINK);
+ }
+ if (DBG) {
+ Log.d(TAG, "Connection state " + mDevice + ": " + mMostRecentState + "->"
+ + currentState);
+ }
+ Intent intent = new Intent(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
+ intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, mMostRecentState);
+ intent.putExtra(BluetoothProfile.EXTRA_STATE, currentState);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ mMostRecentState = currentState;
+ mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
+ }
}
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
index f442c49..c24eca9 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
@@ -23,12 +23,14 @@
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.media.MediaPlayer;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import com.android.bluetooth.R;
-import com.android.bluetooth.avrcpcontroller.AvrcpControllerService;
+import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService;
+import com.android.bluetooth.hfpclient.HeadsetClientService;
import java.util.List;
@@ -51,12 +53,12 @@
* restored.
*/
public class A2dpSinkStreamHandler extends Handler {
- private static final boolean DBG = false;
private static final String TAG = "A2dpSinkStreamHandler";
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
// Configuration Variables
private static final int DEFAULT_DUCK_PERCENT = 25;
- private static final int SETTLE_TIMEOUT = 1000;
+ private static final int SETTLE_TIMEOUT = 400;
// Incoming events.
public static final int SRC_STR_START = 0; // Audio stream from remote device started
@@ -68,7 +70,7 @@
public static final int DISCONNECT = 6; // Remote device was disconnected
public static final int AUDIO_FOCUS_CHANGE = 7; // Audio focus callback with associated change
public static final int REQUEST_FOCUS = 8; // Request focus when the media service is active
- public static final int DELAYED_RESUME = 9; // If a call just ended allow stack time to settle
+ public static final int DELAYED_PAUSE = 9; // If a call just started allow stack time to settle
// Used to indicate focus lost
private static final int STATE_FOCUS_LOST = 0;
@@ -76,7 +78,7 @@
private static final int STATE_FOCUS_GRANTED = 1;
// Private variables.
- private A2dpSinkStateMachine mA2dpSinkSm;
+ private A2dpSinkService mA2dpSinkService;
private Context mContext;
private AudioManager mAudioManager;
// Keep track if the remote device is providing audio
@@ -85,6 +87,16 @@
// Keep track of the relevant audio focus (None, Transient, Gain)
private int mAudioFocus = AudioManager.AUDIOFOCUS_NONE;
+ // In order for Bluetooth to be considered as an audio source capable of receiving media key
+ // events (In the eyes of MediaSessionService), we need an active MediaPlayer in addition to a
+ // MediaSession. Because of this, the media player below plays an incredibly short, silent audio
+ // sample so that MediaSessionService and AudioPlaybackStateMonitor will believe that we're the
+ // current active player and send the Bluetooth process media events. This allows AVRCP
+ // controller to create a MediaSession and handle the events if it would like. The player and
+ // session requirement is a restriction currently imposed by the media framework code and could
+ // be reconsidered in the future.
+ private MediaPlayer mMediaPlayer = null;
+
// Focus changes when we are currently holding focus.
private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
@Override
@@ -97,12 +109,26 @@
}
};
- public A2dpSinkStreamHandler(A2dpSinkStateMachine a2dpSinkSm, Context context) {
- mA2dpSinkSm = a2dpSinkSm;
+ public A2dpSinkStreamHandler(A2dpSinkService a2dpSinkService, Context context) {
+ mA2dpSinkService = a2dpSinkService;
mContext = context;
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
+ void requestAudioFocus(boolean request) {
+ obtainMessage(REQUEST_FOCUS, request).sendToTarget();
+ }
+
+ int getFocusState() {
+ return mAudioFocus;
+ }
+
+ boolean isPlaying() {
+ return (mStreamAvailable
+ && (mAudioFocus == AudioManager.AUDIOFOCUS_GAIN
+ || mAudioFocus == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK));
+ }
+
@Override
public void handleMessage(Message message) {
if (DBG) {
@@ -112,71 +138,45 @@
switch (message.what) {
case SRC_STR_START:
mStreamAvailable = true;
- // Always request audio focus if on TV.
- if (isTvDevice()) {
- if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
- requestAudioFocus();
- }
- }
- // Audio stream has started, stop it if we don't have focus.
- if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
- sendAvrcpPause();
- } else {
- startAvrcpUpdates();
+ if (isTvDevice() || shouldRequestFocus()) {
+ requestAudioFocusIfNone();
}
break;
case SRC_STR_STOP:
// Audio stream has stopped, maintain focus but stop avrcp updates.
- mStreamAvailable = false;
- stopAvrcpUpdates();
break;
case SNK_PLAY:
// Local play command, gain focus and start avrcp updates.
- if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
- requestAudioFocus();
- }
- startAvrcpUpdates();
+ requestAudioFocusIfNone();
break;
case SNK_PAUSE:
+ mStreamAvailable = false;
// Local pause command, maintain focus but stop avrcp updates.
- stopAvrcpUpdates();
break;
case SRC_PLAY:
+ mStreamAvailable = true;
// Remote play command.
- // If is an iot device gain focus and start avrcp updates.
- if (isIotDevice() || isTvDevice()) {
- if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
- requestAudioFocus();
- }
- startAvrcpUpdates();
+ if (isIotDevice() || isTvDevice() || shouldRequestFocus()) {
+ requestAudioFocusIfNone();
break;
}
- // Otherwise, pause if we don't have focus
- if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
- sendAvrcpPause();
- } else {
- startAvrcpUpdates();
- }
break;
case SRC_PAUSE:
+ mStreamAvailable = false;
// Remote pause command, stop avrcp updates.
- stopAvrcpUpdates();
break;
case REQUEST_FOCUS:
- if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
- requestAudioFocus();
- }
+ requestAudioFocusIfNone();
break;
case DISCONNECT:
// Remote device has disconnected, restore everything to default state.
- stopAvrcpUpdates();
mSentPause = false;
break;
@@ -184,11 +184,12 @@
// message.obj is the newly granted audio focus.
switch ((int) message.obj) {
case AudioManager.AUDIOFOCUS_GAIN:
+ removeMessages(DELAYED_PAUSE);
// Begin playing audio, if we paused the remote, send a play now.
- startAvrcpUpdates();
startFluorideStreaming();
if (mSentPause) {
- sendMessageDelayed(obtainMessage(DELAYED_RESUME), SETTLE_TIMEOUT);
+ sendAvrcpPlay();
+ mSentPause = false;
}
break;
@@ -210,11 +211,8 @@
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// Temporary loss of focus, if we are actively streaming pause the remote
// and make sure we resume playback when we regain focus.
- if (mStreamAvailable) {
- sendAvrcpPause();
- mSentPause = true;
- }
- stopFluorideStreaming();
+ sendMessageDelayed(obtainMessage(DELAYED_PAUSE), SETTLE_TIMEOUT);
+ setFluorideAudioTrackGain(0);
break;
case AudioManager.AUDIOFOCUS_LOSS:
@@ -226,13 +224,14 @@
}
break;
- case DELAYED_RESUME:
- // Resume playback after source and sink states settle.
- sendAvrcpPlay();
- mSentPause = false;
+ case DELAYED_PAUSE:
+ if (mStreamAvailable && !inCallFromStreamingDevice()) {
+ sendAvrcpPause();
+ mSentPause = true;
+ mStreamAvailable = false;
+ }
break;
-
default:
Log.w(TAG, "Received unexpected event: " + message.what);
}
@@ -241,129 +240,128 @@
/**
* Utility functions.
*/
+ private void requestAudioFocusIfNone() {
+ if (DBG) Log.d(TAG, "requestAudioFocusIfNone()");
+ if (mAudioFocus == AudioManager.AUDIOFOCUS_NONE) {
+ requestAudioFocus();
+ }
+ // On the off change mMediaPlayer errors out and dies, we want to make sure we retry this.
+ // This function immediately exits if we have a MediaPlayer object.
+ requestMediaKeyFocus();
+ }
+
private synchronized int requestAudioFocus() {
+ if (DBG) Log.d(TAG, "requestAudioFocus()");
// Bluetooth A2DP may carry Music, Audio Books, Navigation, or other sounds so mark content
// type unknown.
AudioAttributes streamAttributes =
new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
.build();
- // Bluetooth ducking is handled at the native layer so tell the Audio Manger to notify the
- // focus change listener via .setWillPauseWhenDucked().
+ // Bluetooth ducking is handled at the native layer at the request of AudioManager.
AudioFocusRequest focusRequest =
new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).setAudioAttributes(
streamAttributes)
- .setWillPauseWhenDucked(true)
.setOnAudioFocusChangeListener(mAudioFocusListener, this)
.build();
int focusRequestStatus = mAudioManager.requestAudioFocus(focusRequest);
// If the request is granted begin streaming immediately and schedule an upgrade.
if (focusRequestStatus == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- startAvrcpUpdates();
startFluorideStreaming();
mAudioFocus = AudioManager.AUDIOFOCUS_GAIN;
}
return focusRequestStatus;
}
+ /**
+ * Creates a MediaPlayer that plays a silent audio sample so that MediaSessionService will be
+ * aware of the fact that Bluetooth is playing audio.
+ *
+ * This allows the MediaSession in AVRCP Controller to be routed media key events, if we've
+ * chosen to use it.
+ */
+ private synchronized void requestMediaKeyFocus() {
+ if (DBG) Log.d(TAG, "requestMediaKeyFocus()");
+
+ if (mMediaPlayer != null) return;
+
+ AudioAttributes attrs = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+
+ mMediaPlayer = MediaPlayer.create(mContext, R.raw.silent, attrs,
+ mAudioManager.generateAudioSessionId());
+ if (mMediaPlayer == null) {
+ Log.e(TAG, "Failed to initialize media player. You may not get media key events");
+ return;
+ }
+
+ mMediaPlayer.setLooping(false);
+ mMediaPlayer.setOnErrorListener((mp, what, extra) -> {
+ Log.e(TAG, "Silent media player error: " + what + ", " + extra);
+ releaseMediaKeyFocus();
+ return false;
+ });
+
+ mMediaPlayer.start();
+ BluetoothMediaBrowserService.setActive(true);
+ }
private synchronized void abandonAudioFocus() {
+ if (DBG) Log.d(TAG, "abandonAudioFocus()");
stopFluorideStreaming();
+ releaseMediaKeyFocus();
mAudioManager.abandonAudioFocus(mAudioFocusListener);
mAudioFocus = AudioManager.AUDIOFOCUS_NONE;
}
+ /**
+ * Destroys the silent audio sample MediaPlayer, notifying MediaSessionService of the fact
+ * we're no longer playing audio.
+ */
+ private synchronized void releaseMediaKeyFocus() {
+ if (DBG) Log.d(TAG, "releaseMediaKeyFocus()");
+ if (mMediaPlayer == null) {
+ return;
+ }
+ BluetoothMediaBrowserService.setActive(false);
+ mMediaPlayer.stop();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+
private void startFluorideStreaming() {
- mA2dpSinkSm.informAudioFocusStateNative(STATE_FOCUS_GRANTED);
- mA2dpSinkSm.informAudioTrackGainNative(1.0f);
+ mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_GRANTED);
+ mA2dpSinkService.informAudioTrackGainNative(1.0f);
}
private void stopFluorideStreaming() {
- mA2dpSinkSm.informAudioFocusStateNative(STATE_FOCUS_LOST);
+ mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_LOST);
}
private void setFluorideAudioTrackGain(float gain) {
- mA2dpSinkSm.informAudioTrackGainNative(gain);
- }
-
- private void startAvrcpUpdates() {
- // Since AVRCP gets started after A2DP we may need to request it later in cycle.
- AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService();
-
- if (DBG) {
- Log.d(TAG, "startAvrcpUpdates");
- }
- if (avrcpService != null && avrcpService.getConnectedDevices().size() == 1) {
- avrcpService.startAvrcpUpdates();
- } else {
- Log.e(TAG, "startAvrcpUpdates failed because of connection.");
- }
- }
-
- private void stopAvrcpUpdates() {
- // Since AVRCP gets started after A2DP we may need to request it later in cycle.
- AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService();
-
- if (DBG) {
- Log.d(TAG, "stopAvrcpUpdates");
- }
- if (avrcpService != null && avrcpService.getConnectedDevices().size() == 1) {
- avrcpService.stopAvrcpUpdates();
- } else {
- Log.e(TAG, "stopAvrcpUpdates failed because of connection.");
- }
+ mA2dpSinkService.informAudioTrackGainNative(gain);
}
private void sendAvrcpPause() {
- // Since AVRCP gets started after A2DP we may need to request it later in cycle.
- AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService();
-
- if (DBG) {
- Log.d(TAG, "sendAvrcpPause");
- }
- if (avrcpService != null) {
- List<BluetoothDevice> connectedDevices = avrcpService.getConnectedDevices();
- if (!connectedDevices.isEmpty()) {
- BluetoothDevice targetDevice = connectedDevices.get(0);
- if (DBG) {
- Log.d(TAG, "Pausing AVRCP.");
- }
- avrcpService.sendPassThroughCmd(targetDevice,
- AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE,
- AvrcpControllerService.KEY_STATE_PRESSED);
- avrcpService.sendPassThroughCmd(targetDevice,
- AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE,
- AvrcpControllerService.KEY_STATE_RELEASED);
- }
- } else {
- Log.e(TAG, "Passthrough not sent, connection un-available.");
- }
+ BluetoothMediaBrowserService.pause();
}
private void sendAvrcpPlay() {
- // Since AVRCP gets started after A2DP we may need to request it later in cycle.
- AvrcpControllerService avrcpService = AvrcpControllerService.getAvrcpControllerService();
+ BluetoothMediaBrowserService.play();
+ }
- if (DBG) {
- Log.d(TAG, "sendAvrcpPlay");
+ private boolean inCallFromStreamingDevice() {
+ BluetoothDevice targetDevice = null;
+ List<BluetoothDevice> connectedDevices = mA2dpSinkService.getConnectedDevices();
+ if (!connectedDevices.isEmpty()) {
+ targetDevice = connectedDevices.get(0);
}
- if (avrcpService != null) {
- List<BluetoothDevice> connectedDevices = avrcpService.getConnectedDevices();
- if (!connectedDevices.isEmpty()) {
- BluetoothDevice targetDevice = connectedDevices.get(0);
- if (DBG) {
- Log.d(TAG, "Playing AVRCP.");
- }
- avrcpService.sendPassThroughCmd(targetDevice,
- AvrcpControllerService.PASS_THRU_CMD_ID_PLAY,
- AvrcpControllerService.KEY_STATE_PRESSED);
- avrcpService.sendPassThroughCmd(targetDevice,
- AvrcpControllerService.PASS_THRU_CMD_ID_PLAY,
- AvrcpControllerService.KEY_STATE_RELEASED);
- }
- } else {
- Log.e(TAG, "Passthrough not sent, connection un-available.");
+ HeadsetClientService headsetClientService = HeadsetClientService.getHeadsetClientService();
+ if (targetDevice != null && headsetClientService != null) {
+ return headsetClientService.getCurrentCalls(targetDevice).size() > 0;
}
+ return false;
}
synchronized int getAudioFocus() {
@@ -378,4 +376,9 @@
return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
}
+ private boolean shouldRequestFocus() {
+ return mContext.getResources()
+ .getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus);
+ }
+
}
diff --git a/src/com/android/bluetooth/a2dpsink/StackEvent.java b/src/com/android/bluetooth/a2dpsink/StackEvent.java
new file mode 100644
index 0000000..68a6ce9
--- /dev/null
+++ b/src/com/android/bluetooth/a2dpsink/StackEvent.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.bluetooth.a2dpsink;
+
+import android.bluetooth.BluetoothDevice;
+
+final class StackEvent {
+ // Event types for STACK_EVENT message
+ static final int EVENT_TYPE_NONE = 0;
+ static final int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1;
+ static final int EVENT_TYPE_AUDIO_STATE_CHANGED = 2;
+ static final int EVENT_TYPE_AUDIO_CONFIG_CHANGED = 3;
+
+ // match up with btav_connection_state_t enum of bt_av.h
+ static final int CONNECTION_STATE_DISCONNECTED = 0;
+ static final int CONNECTION_STATE_CONNECTING = 1;
+ static final int CONNECTION_STATE_CONNECTED = 2;
+ static final int CONNECTION_STATE_DISCONNECTING = 3;
+
+ // match up with btav_audio_state_t enum of bt_av.h
+ static final int AUDIO_STATE_REMOTE_SUSPEND = 0;
+ static final int AUDIO_STATE_STOPPED = 1;
+ static final int AUDIO_STATE_STARTED = 2;
+
+ int mType = EVENT_TYPE_NONE;
+ BluetoothDevice mDevice = null;
+ int mState = 0;
+ int mSampleRate = 0;
+ int mChannelCount = 0;
+
+ private StackEvent(int type) {
+ this.mType = type;
+ }
+
+ @Override
+ public String toString() {
+ switch (mType) {
+ case EVENT_TYPE_CONNECTION_STATE_CHANGED:
+ return "EVENT_TYPE_CONNECTION_STATE_CHANGED " + mState;
+ case EVENT_TYPE_AUDIO_STATE_CHANGED:
+ return "EVENT_TYPE_AUDIO_STATE_CHANGED " + mState;
+ case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
+ return "EVENT_TYPE_AUDIO_CONFIG_CHANGED " + mSampleRate + ":" + mChannelCount;
+ default:
+ return "Unknown";
+ }
+ }
+
+ static StackEvent connectionStateChanged(BluetoothDevice device, int state) {
+ StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ event.mDevice = device;
+ event.mState = state;
+ return event;
+ }
+
+ static StackEvent audioStateChanged(BluetoothDevice device, int state) {
+ StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED);
+ event.mDevice = device;
+ event.mState = state;
+ return event;
+ }
+
+ static StackEvent audioConfigChanged(BluetoothDevice device, int sampleRate,
+ int channelCount) {
+ StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED);
+ event.mDevice = device;
+ event.mSampleRate = sampleRate;
+ event.mChannelCount = channelCount;
+ return event;
+ }
+}
diff --git a/src/com/android/bluetooth/a2dpsink/mbs/A2dpMediaBrowserService.java b/src/com/android/bluetooth/a2dpsink/mbs/A2dpMediaBrowserService.java
deleted file mode 100644
index 739f17c..0000000
--- a/src/com/android/bluetooth/a2dpsink/mbs/A2dpMediaBrowserService.java
+++ /dev/null
@@ -1,561 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.bluetooth.a2dpsink.mbs;
-
-import android.bluetooth.BluetoothAvrcpController;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothProfile;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.media.MediaMetadata;
-import android.media.browse.MediaBrowser;
-import android.media.browse.MediaBrowser.MediaItem;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Parcelable;
-import android.service.media.MediaBrowserService;
-import android.util.Log;
-import android.util.Pair;
-
-import com.android.bluetooth.R;
-import com.android.bluetooth.a2dpsink.A2dpSinkService;
-import com.android.bluetooth.avrcpcontroller.AvrcpControllerService;
-import com.android.bluetooth.avrcpcontroller.BrowseTree;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Implements the MediaBrowserService interface to AVRCP and A2DP
- *
- * This service provides a means for external applications to access A2DP and AVRCP.
- * The applications are expected to use MediaBrowser (see API) and all the music
- * browsing/playback/metadata can be controlled via MediaBrowser and MediaController.
- *
- * The current behavior of MediaSession exposed by this service is as follows:
- * 1. MediaSession is active (i.e. SystemUI and other overview UIs can see updates) when device is
- * connected and first starts playing. Before it starts playing we do not active the session.
- * 1.1 The session is active throughout the duration of connection.
- * 2. The session is de-activated when the device disconnects. It will be connected again when (1)
- * happens.
- */
-public class A2dpMediaBrowserService extends MediaBrowserService {
- private static final String TAG = "A2dpMediaBrowserService";
- private static final boolean DBG = false;
- private static final boolean VDBG = false;
-
- private static final String UNKNOWN_BT_AUDIO = "__UNKNOWN_BT_AUDIO__";
- private static final float PLAYBACK_SPEED = 1.0f;
-
- // Message sent when A2DP device is disconnected.
- private static final int MSG_DEVICE_DISCONNECT = 0;
- // Message sent when A2DP device is connected.
- private static final int MSG_DEVICE_CONNECT = 2;
- // Message sent when we recieve a TRACK update from AVRCP profile over a connected A2DP device.
- private static final int MSG_TRACK = 4;
- // Internal message sent to trigger a AVRCP action.
- private static final int MSG_AVRCP_PASSTHRU = 5;
- // Internal message to trigger a getplaystatus command to remote.
- private static final int MSG_AVRCP_GET_PLAY_STATUS_NATIVE = 6;
- // Message sent when AVRCP browse is connected.
- private static final int MSG_DEVICE_BROWSE_CONNECT = 7;
- // Message sent when AVRCP browse is disconnected.
- private static final int MSG_DEVICE_BROWSE_DISCONNECT = 8;
- // Message sent when folder list is fetched.
- private static final int MSG_FOLDER_LIST = 9;
-
- // Custom actions for PTS testing.
- private static final String CUSTOM_ACTION_VOL_UP =
- "com.android.bluetooth.a2dpsink.mbs.CUSTOM_ACTION_VOL_UP";
- private static final String CUSTOM_ACTION_VOL_DN =
- "com.android.bluetooth.a2dpsink.mbs.CUSTOM_ACTION_VOL_DN";
- private static final String CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE =
- "com.android.bluetooth.a2dpsink.mbs.CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE";
-
- private MediaSession mSession;
- private MediaMetadata mA2dpMetadata;
-
- private AvrcpControllerService mAvrcpCtrlSrvc;
- private boolean mBrowseConnected = false;
- private BluetoothDevice mA2dpDevice = null;
- private A2dpSinkService mA2dpSinkService = null;
- private Handler mAvrcpCommandQueue;
- private final Map<String, Result<List<MediaItem>>> mParentIdToRequestMap = new HashMap<>();
-
- // Browsing related structures.
- private List<MediaItem> mNowPlayingList = null;
-
- private long mTransportControlFlags = PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY
- | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
-
- private static final class AvrcpCommandQueueHandler extends Handler {
- WeakReference<A2dpMediaBrowserService> mInst;
-
- AvrcpCommandQueueHandler(Looper looper, A2dpMediaBrowserService sink) {
- super(looper);
- mInst = new WeakReference<A2dpMediaBrowserService>(sink);
- }
-
- @Override
- public void handleMessage(Message msg) {
- A2dpMediaBrowserService inst = mInst.get();
- if (inst == null) {
- Log.e(TAG, "Parent class has died; aborting.");
- return;
- }
-
- switch (msg.what) {
- case MSG_DEVICE_CONNECT:
- inst.msgDeviceConnect((BluetoothDevice) msg.obj);
- break;
- case MSG_DEVICE_DISCONNECT:
- inst.msgDeviceDisconnect((BluetoothDevice) msg.obj);
- break;
- case MSG_TRACK:
- Pair<PlaybackState, MediaMetadata> pair =
- (Pair<PlaybackState, MediaMetadata>) (msg.obj);
- inst.msgTrack(pair.first, pair.second);
- break;
- case MSG_AVRCP_PASSTHRU:
- inst.msgPassThru((int) msg.obj);
- break;
- case MSG_AVRCP_GET_PLAY_STATUS_NATIVE:
- inst.msgGetPlayStatusNative();
- break;
- case MSG_DEVICE_BROWSE_CONNECT:
- inst.msgDeviceBrowseConnect((BluetoothDevice) msg.obj);
- break;
- case MSG_DEVICE_BROWSE_DISCONNECT:
- inst.msgDeviceBrowseDisconnect((BluetoothDevice) msg.obj);
- break;
- case MSG_FOLDER_LIST:
- inst.msgFolderList((Intent) msg.obj);
- break;
- default:
- Log.e(TAG, "Message not handled " + msg);
- }
- }
- }
-
- @Override
- public void onCreate() {
- if (DBG) Log.d(TAG, "onCreate");
- super.onCreate();
-
- mSession = new MediaSession(this, TAG);
- setSessionToken(mSession.getSessionToken());
- mSession.setCallback(mSessionCallbacks);
- mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
- | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
- mSession.setActive(true);
- mAvrcpCommandQueue = new AvrcpCommandQueueHandler(Looper.getMainLooper(), this);
-
- refreshInitialPlayingState();
-
- IntentFilter filter = new IntentFilter();
- filter.addAction(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
- filter.addAction(AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED);
- filter.addAction(AvrcpControllerService.ACTION_TRACK_EVENT);
- filter.addAction(AvrcpControllerService.ACTION_FOLDER_LIST);
- registerReceiver(mBtReceiver, filter);
-
- synchronized (this) {
- mParentIdToRequestMap.clear();
- }
- }
-
- @Override
- public void onDestroy() {
- if (DBG) Log.d(TAG, "onDestroy");
- mSession.release();
- unregisterReceiver(mBtReceiver);
- super.onDestroy();
- }
-
- @Override
- public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
- return new BrowserRoot(BrowseTree.ROOT, null);
- }
-
- @Override
- public synchronized void onLoadChildren(final String parentMediaId,
- final Result<List<MediaItem>> result) {
- if (mAvrcpCtrlSrvc == null) {
- Log.w(TAG, "AVRCP not yet connected.");
- result.sendResult(Collections.emptyList());
- return;
- }
-
- if (DBG) Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId);
- if (!mAvrcpCtrlSrvc.getChildren(mA2dpDevice, parentMediaId, 0, 0xff)) {
- result.sendResult(Collections.emptyList());
- return;
- }
-
- // Since we are using this thread from a binder thread we should make sure that
- // we synchronize against other such asynchronous calls.
- synchronized (this) {
- mParentIdToRequestMap.put(parentMediaId, result);
- }
- result.detach();
- }
-
- @Override
- public void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result) {
- }
-
- // Media Session Stuff.
- private MediaSession.Callback mSessionCallbacks = new MediaSession.Callback() {
- @Override
- public void onPlay() {
- if (DBG) Log.d(TAG, "onPlay");
- mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
- AvrcpControllerService.PASS_THRU_CMD_ID_PLAY).sendToTarget();
- // TRACK_EVENT should be fired eventually and the UI should be hence updated.
- }
-
- @Override
- public void onPause() {
- if (DBG) Log.d(TAG, "onPause");
- mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
- AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE).sendToTarget();
- // TRACK_EVENT should be fired eventually and the UI should be hence updated.
- }
-
- @Override
- public void onSkipToNext() {
- if (DBG) Log.d(TAG, "onSkipToNext");
- mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
- AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD).sendToTarget();
- // TRACK_EVENT should be fired eventually and the UI should be hence updated.
- }
-
- @Override
- public void onSkipToPrevious() {
- if (DBG) Log.d(TAG, "onSkipToPrevious");
- mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
- AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD).sendToTarget();
- // TRACK_EVENT should be fired eventually and the UI should be hence updated.
- }
-
- @Override
- public void onStop() {
- if (DBG) Log.d(TAG, "onStop");
- mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
- AvrcpControllerService.PASS_THRU_CMD_ID_STOP).sendToTarget();
- }
-
- @Override
- public void onPrepare() {
- if (DBG) Log.d(TAG, "onPrepare");
- if (mA2dpSinkService != null) {
- mA2dpSinkService.requestAudioFocus(mA2dpDevice, true);
- }
- }
-
- @Override
- public void onRewind() {
- if (DBG) Log.d(TAG, "onRewind");
- mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
- AvrcpControllerService.PASS_THRU_CMD_ID_REWIND).sendToTarget();
- // TRACK_EVENT should be fired eventually and the UI should be hence updated.
- }
-
- @Override
- public void onFastForward() {
- if (DBG) Log.d(TAG, "onFastForward");
- mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
- AvrcpControllerService.PASS_THRU_CMD_ID_FF).sendToTarget();
- // TRACK_EVENT should be fired eventually and the UI should be hence updated.
- }
-
- @Override
- public void onPlayFromMediaId(String mediaId, Bundle extras) {
- synchronized (A2dpMediaBrowserService.this) {
- // Play the item if possible.
- mAvrcpCtrlSrvc.fetchAttrAndPlayItem(mA2dpDevice, mediaId);
-
- // Since we request explicit playback here we should start the updates to UI.
- mAvrcpCtrlSrvc.startAvrcpUpdates();
- }
-
- // TRACK_EVENT should be fired eventually and the UI should be hence updated.
- }
-
- // Support VOL UP and VOL DOWN events for PTS testing.
- @Override
- public void onCustomAction(String action, Bundle extras) {
- if (DBG) Log.d(TAG, "onCustomAction " + action);
- if (CUSTOM_ACTION_VOL_UP.equals(action)) {
- mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
- AvrcpControllerService.PASS_THRU_CMD_ID_VOL_UP).sendToTarget();
- } else if (CUSTOM_ACTION_VOL_DN.equals(action)) {
- mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_PASSTHRU,
- AvrcpControllerService.PASS_THRU_CMD_ID_VOL_DOWN).sendToTarget();
- } else if (CUSTOM_ACTION_GET_PLAY_STATUS_NATIVE.equals(action)) {
- mAvrcpCommandQueue.obtainMessage(MSG_AVRCP_GET_PLAY_STATUS_NATIVE).sendToTarget();
- } else {
- Log.w(TAG, "Custom action " + action + " not supported.");
- }
- }
- };
-
- private BroadcastReceiver mBtReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (DBG) Log.d(TAG, "onReceive intent=" + intent);
- String action = intent.getAction();
- BluetoothDevice btDev =
- (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
- int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
-
- if (BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
- if (DBG) {
- Log.d(TAG, "handleConnectionStateChange: newState="
- + state + " btDev=" + btDev);
- }
-
- // Connected state will be handled when AVRCP BluetoothProfile gets connected.
- if (state == BluetoothProfile.STATE_CONNECTED) {
- mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_CONNECT, btDev).sendToTarget();
- } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
- // Set the playback state to unconnected.
- mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_DISCONNECT, btDev).sendToTarget();
- // If we have been pushing updates via the session then stop sending them since
- // we are not connected anymore.
- if (mSession.isActive()) {
- mSession.setActive(false);
- }
- }
- } else if (AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED.equals(
- action)) {
- if (state == BluetoothProfile.STATE_CONNECTED) {
- mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_BROWSE_CONNECT, btDev)
- .sendToTarget();
- } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
- mAvrcpCommandQueue.obtainMessage(MSG_DEVICE_BROWSE_DISCONNECT, btDev)
- .sendToTarget();
- }
- } else if (AvrcpControllerService.ACTION_TRACK_EVENT.equals(action)) {
- PlaybackState pbb =
- intent.getParcelableExtra(AvrcpControllerService.EXTRA_PLAYBACK);
- MediaMetadata mmd =
- intent.getParcelableExtra(AvrcpControllerService.EXTRA_METADATA);
- mAvrcpCommandQueue.obtainMessage(MSG_TRACK,
- new Pair<PlaybackState, MediaMetadata>(pbb, mmd)).sendToTarget();
- } else if (AvrcpControllerService.ACTION_FOLDER_LIST.equals(action)) {
- mAvrcpCommandQueue.obtainMessage(MSG_FOLDER_LIST, intent).sendToTarget();
- }
- }
- };
-
- private synchronized void msgDeviceConnect(BluetoothDevice device) {
- if (DBG) Log.d(TAG, "msgDeviceConnect");
- // We are connected to a new device via A2DP now.
- mA2dpDevice = device;
- mAvrcpCtrlSrvc = AvrcpControllerService.getAvrcpControllerService();
- if (mAvrcpCtrlSrvc == null) {
- Log.e(TAG, "!!!AVRCP Controller cannot be null");
- return;
- }
- refreshInitialPlayingState();
- }
-
-
- // Refresh the UI if we have a connected device and AVRCP is initialized.
- private synchronized void refreshInitialPlayingState() {
- if (mA2dpDevice == null) {
- if (DBG) Log.d(TAG, "device " + mA2dpDevice);
- return;
- }
-
- List<BluetoothDevice> devices = mAvrcpCtrlSrvc.getConnectedDevices();
- if (devices.size() == 0) {
- Log.w(TAG, "No devices connected yet");
- return;
- }
-
- if (mA2dpDevice != null && !mA2dpDevice.equals(devices.get(0))) {
- Log.w(TAG, "A2dp device : " + mA2dpDevice + " avrcp device " + devices.get(0));
- return;
- }
- mA2dpDevice = devices.get(0);
- mA2dpSinkService = A2dpSinkService.getA2dpSinkService();
-
- PlaybackState playbackState = mAvrcpCtrlSrvc.getPlaybackState(mA2dpDevice);
- // Add actions required for playback and rebuild the object.
- PlaybackState.Builder pbb = new PlaybackState.Builder(playbackState);
- playbackState = pbb.setActions(mTransportControlFlags).build();
-
- MediaMetadata mediaMetadata = mAvrcpCtrlSrvc.getMetaData(mA2dpDevice);
- if (VDBG) {
- Log.d(TAG, "Media metadata " + mediaMetadata + " playback state " + playbackState);
- }
- mSession.setMetadata(mAvrcpCtrlSrvc.getMetaData(mA2dpDevice));
- mSession.setPlaybackState(playbackState);
- }
-
- private void msgDeviceDisconnect(BluetoothDevice device) {
- if (DBG) Log.d(TAG, "msgDeviceDisconnect");
- if (mA2dpDevice == null) {
- Log.w(TAG, "Already disconnected - nothing to do here.");
- return;
- } else if (!mA2dpDevice.equals(device)) {
- Log.e(TAG,
- "Not the right device to disconnect current " + mA2dpDevice + " dc " + device);
- return;
- }
-
- // Unset the session.
- PlaybackState.Builder pbb = new PlaybackState.Builder();
- pbb = pbb.setState(PlaybackState.STATE_ERROR, PlaybackState.PLAYBACK_POSITION_UNKNOWN,
- PLAYBACK_SPEED)
- .setActions(mTransportControlFlags)
- .setErrorMessage(getString(R.string.bluetooth_disconnected));
- mSession.setPlaybackState(pbb.build());
-
- // Set device to null.
- mA2dpDevice = null;
- mBrowseConnected = false;
- // update playerList.
- notifyChildrenChanged("__ROOT__");
- }
-
- private void msgTrack(PlaybackState pb, MediaMetadata mmd) {
- if (VDBG) Log.d(TAG, "msgTrack: playback: " + pb + " mmd: " + mmd);
- // Log the current track position/content.
- MediaController controller = mSession.getController();
- PlaybackState prevPS = controller.getPlaybackState();
- MediaMetadata prevMM = controller.getMetadata();
-
- if (prevPS != null) {
- Log.d(TAG, "prevPS " + prevPS);
- }
-
- if (prevMM != null) {
- String title = prevMM.getString(MediaMetadata.METADATA_KEY_TITLE);
- long trackLen = prevMM.getLong(MediaMetadata.METADATA_KEY_DURATION);
- if (VDBG) Log.d(TAG, "prev MM title " + title + " track len " + trackLen);
- }
-
- if (mmd != null) {
- if (VDBG) Log.d(TAG, "msgTrack() mmd " + mmd.getDescription());
- mSession.setMetadata(mmd);
- }
-
- if (pb != null) {
- if (DBG) Log.d(TAG, "msgTrack() playbackstate " + pb);
- PlaybackState.Builder pbb = new PlaybackState.Builder(pb);
- pb = pbb.setActions(mTransportControlFlags).build();
- mSession.setPlaybackState(pb);
-
- // If we are now playing then we should start pushing updates via MediaSession so that
- // external UI (such as SystemUI) can show the currently playing music.
- if (pb.getState() == PlaybackState.STATE_PLAYING && !mSession.isActive()) {
- mSession.setActive(true);
- }
- }
- }
-
- private synchronized void msgPassThru(int cmd) {
- if (DBG) Log.d(TAG, "msgPassThru " + cmd);
- if (mA2dpDevice == null) {
- // We should have already disconnected - ignore this message.
- Log.w(TAG, "Already disconnected ignoring.");
- return;
- }
-
- // Send the pass through.
- mAvrcpCtrlSrvc.sendPassThroughCmd(mA2dpDevice, cmd,
- AvrcpControllerService.KEY_STATE_PRESSED);
- mAvrcpCtrlSrvc.sendPassThroughCmd(mA2dpDevice, cmd,
- AvrcpControllerService.KEY_STATE_RELEASED);
- }
-
- private synchronized void msgGetPlayStatusNative() {
- if (DBG) Log.d(TAG, "msgGetPlayStatusNative");
- if (mA2dpDevice == null) {
- // We should have already disconnected - ignore this message.
- Log.w(TAG, "Already disconnected ignoring.");
- return;
- }
-
- // Ask for a non cached version.
- mAvrcpCtrlSrvc.getPlaybackState(mA2dpDevice, false);
- }
-
- private void msgDeviceBrowseConnect(BluetoothDevice device) {
- if (DBG) Log.d(TAG, "msgDeviceBrowseConnect device " + device);
- // We should already be connected to this device over A2DP.
- if (!device.equals(mA2dpDevice)) {
- Log.e(TAG, "Browse connected over different device a2dp " + mA2dpDevice + " browse "
- + device);
- return;
- }
- mBrowseConnected = true;
- // update playerList
- notifyChildrenChanged("__ROOT__");
- }
-
- private void msgFolderList(Intent intent) {
- // Parse the folder list for children list and id.
- List<Parcelable> extraParcelableList =
- (ArrayList<Parcelable>) intent.getParcelableArrayListExtra(
- AvrcpControllerService.EXTRA_FOLDER_LIST);
- List<MediaItem> folderList = new ArrayList<MediaItem>();
- for (Parcelable p : extraParcelableList) {
- folderList.add((MediaItem) p);
- }
-
- String id = intent.getStringExtra(AvrcpControllerService.EXTRA_FOLDER_ID);
- if (VDBG) Log.d(TAG, "Parent: " + id + " Folder list: " + folderList);
- synchronized (this) {
- // If we have a result object then we should send the result back
- // to client since it is blocking otherwise we may have gotten more items
- // from remote device, hence let client know to fetch again.
- Result<List<MediaItem>> results = mParentIdToRequestMap.remove(id);
- if (results == null) {
- Log.w(TAG, "Request no longer exists, notifying that children changed.");
- notifyChildrenChanged(id);
- } else {
- results.sendResult(folderList);
- }
- }
- }
-
- private void msgDeviceBrowseDisconnect(BluetoothDevice device) {
- if (DBG) Log.d(TAG, "msgDeviceBrowseDisconnect device " + device);
- // Disconnect only if mA2dpDevice is non null
- if (!device.equals(mA2dpDevice)) {
- Log.w(TAG, "Browse disconnecting from different device a2dp " + mA2dpDevice + " browse "
- + device);
- return;
- }
- mBrowseConnected = false;
- }
-}
diff --git a/src/com/android/bluetooth/avrcp/AddressedMediaPlayer.java b/src/com/android/bluetooth/avrcp/AddressedMediaPlayer.java
deleted file mode 100644
index 1d4810a..0000000
--- a/src/com/android/bluetooth/avrcp/AddressedMediaPlayer.java
+++ /dev/null
@@ -1,601 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.bluetooth.avrcp;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.media.MediaDescription;
-import android.media.MediaMetadata;
-import android.media.session.MediaSession;
-import android.media.session.MediaSession.QueueItem;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.util.Log;
-
-import com.android.bluetooth.Utils;
-import com.android.bluetooth.btservice.ProfileService;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/*************************************************************************************************
- * Provides functionality required for Addressed Media Player, like Now Playing List related
- * browsing commands, control commands to the current addressed player(playItem, play, pause, etc)
- * Acts as an Interface to communicate with media controller APIs for NowPlayingItems.
- ************************************************************************************************/
-
-public class AddressedMediaPlayer {
- private static final String TAG = "AddressedMediaPlayer";
- private static final Boolean DEBUG = false;
-
- private static final long SINGLE_QID = 1;
- private static final String UNKNOWN_TITLE = "(unknown)";
-
- static private final String GPM_BUNDLE_METADATA_KEY =
- "com.google.android.music.mediasession.music_metadata";
-
- private AvrcpMediaRspInterface mMediaInterface;
- @NonNull private List<MediaSession.QueueItem> mNowPlayingList;
-
- private final List<MediaSession.QueueItem> mEmptyNowPlayingList;
-
- private long mLastTrackIdSent;
-
- public AddressedMediaPlayer(AvrcpMediaRspInterface mediaInterface) {
- mEmptyNowPlayingList = new ArrayList<MediaSession.QueueItem>();
- mNowPlayingList = mEmptyNowPlayingList;
- mMediaInterface = mediaInterface;
- mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
- }
-
- void cleanup() {
- if (DEBUG) {
- Log.v(TAG, "cleanup");
- }
- mNowPlayingList = mEmptyNowPlayingList;
- mMediaInterface = null;
- mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
- }
-
- /* get now playing list from addressed player */
- void getFolderItemsNowPlaying(byte[] bdaddr, AvrcpCmd.FolderItemsCmd reqObj,
- @Nullable MediaController mediaController) {
- if (mediaController == null) {
- // No players (if a player exists, we would have selected it)
- Log.e(TAG, "mediaController = null, sending no available players response");
- mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_AVBL_PLAY, null);
- return;
- }
- List<MediaSession.QueueItem> items = updateNowPlayingList(mediaController);
- getFolderItemsFilterAttr(bdaddr, reqObj, items, AvrcpConstants.BTRC_SCOPE_NOW_PLAYING,
- reqObj.mStartItem, reqObj.mEndItem, mediaController);
- }
-
- /* get item attributes for item in now playing list */
- void getItemAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd itemAttr,
- @Nullable MediaController mediaController) {
- int status = AvrcpConstants.RSP_NO_ERROR;
- long mediaId = ByteBuffer.wrap(itemAttr.mUid).getLong();
- List<MediaSession.QueueItem> items = updateNowPlayingList(mediaController);
-
- // NOTE: this is out-of-spec (AVRCP 1.6.1 sec 6.10.4.3, p90) but we answer it anyway
- // because some CTs ask for it.
- if (Arrays.equals(itemAttr.mUid, AvrcpConstants.TRACK_IS_SELECTED)) {
- mediaId = getActiveQueueItemId(mediaController);
- if (DEBUG) {
- Log.d(TAG, "getItemAttr: Remote requests for now playing contents, sending UID: "
- + mediaId);
- }
- }
-
- if (DEBUG) {
- Log.d(TAG, "getItemAttr-UID: 0x" + Utils.byteArrayToString(itemAttr.mUid));
- }
- for (MediaSession.QueueItem item : items) {
- if (item.getQueueId() == mediaId) {
- getItemAttrFilterAttr(bdaddr, itemAttr, item, mediaController);
- return;
- }
- }
-
- // Couldn't find it, so the id is invalid
- mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM, null);
- }
-
- /* Refresh and get the queue of now playing.
- */
- @NonNull
- List<MediaSession.QueueItem> updateNowPlayingList(@Nullable MediaController mediaController) {
- if (mediaController == null) {
- return mEmptyNowPlayingList;
- }
- List<MediaSession.QueueItem> items = mediaController.getQueue();
- if (items == null) {
- Log.i(TAG, "null queue from " + mediaController.getPackageName()
- + ", constructing single-item list");
-
- // Because we are database-unaware, we can just number the item here whatever we want
- // because they have to re-poll it every time.
- MediaMetadata metadata = mediaController.getMetadata();
- if (metadata == null) {
- Log.w(TAG, "Controller has no metadata!? Making an empty one");
- metadata = (new MediaMetadata.Builder()).build();
- }
-
- MediaDescription.Builder bob = new MediaDescription.Builder();
- MediaDescription desc = metadata.getDescription();
-
- // set the simple ones that MediaMetadata builds for us
- bob.setMediaId(desc.getMediaId());
- bob.setTitle(desc.getTitle());
- bob.setSubtitle(desc.getSubtitle());
- bob.setDescription(desc.getDescription());
- // fill the ones that we use later
- bob.setExtras(fillBundle(metadata, desc.getExtras()));
-
- // build queue item with the new metadata
- MediaSession.QueueItem current = new QueueItem(bob.build(), SINGLE_QID);
-
- items = new ArrayList<MediaSession.QueueItem>();
- items.add(current);
- }
-
- if (!items.equals(mNowPlayingList)) {
- sendNowPlayingListChanged();
- }
- mNowPlayingList = items;
-
- return mNowPlayingList;
- }
-
- private void sendNowPlayingListChanged() {
- if (mMediaInterface == null) {
- return;
- }
- if (DEBUG) {
- Log.d(TAG, "sendNowPlayingListChanged()");
- }
- mMediaInterface.nowPlayingChangedRsp(AvrcpConstants.NOTIFICATION_TYPE_CHANGED);
- }
-
- private Bundle fillBundle(MediaMetadata metadata, Bundle currentExtras) {
- if (metadata == null) {
- Log.i(TAG, "fillBundle: metadata is null");
- return currentExtras;
- }
-
- Bundle bundle = currentExtras;
- if (bundle == null) {
- bundle = new Bundle();
- }
-
- String[] stringKeys = {
- MediaMetadata.METADATA_KEY_TITLE,
- MediaMetadata.METADATA_KEY_ARTIST,
- MediaMetadata.METADATA_KEY_ALBUM,
- MediaMetadata.METADATA_KEY_GENRE
- };
- for (String key : stringKeys) {
- String current = bundle.getString(key);
- if (current == null) {
- bundle.putString(key, metadata.getString(key));
- }
- }
-
- String[] longKeys = {
- MediaMetadata.METADATA_KEY_TRACK_NUMBER,
- MediaMetadata.METADATA_KEY_NUM_TRACKS,
- MediaMetadata.METADATA_KEY_DURATION
- };
- for (String key : longKeys) {
- if (!bundle.containsKey(key)) {
- bundle.putLong(key, metadata.getLong(key));
- }
- }
- return bundle;
- }
-
- /* Instructs media player to play particular media item */
- void playItem(byte[] bdaddr, byte[] uid, @Nullable MediaController mediaController) {
- long qid = ByteBuffer.wrap(uid).getLong();
- List<MediaSession.QueueItem> items = updateNowPlayingList(mediaController);
-
- if (mediaController == null) {
- Log.e(TAG, "No mediaController when PlayItem " + qid + " requested");
- mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
- return;
- }
-
- MediaController.TransportControls mediaControllerCntrl =
- mediaController.getTransportControls();
-
- if (items == null) {
- Log.w(TAG, "nowPlayingItems is null");
- mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
- return;
- }
-
- for (MediaSession.QueueItem item : items) {
- if (qid == item.getQueueId()) {
- if (DEBUG) {
- Log.d(TAG, "Skipping to ID " + qid);
- }
- mediaControllerCntrl.skipToQueueItem(qid);
- mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR);
- return;
- }
- }
-
- Log.w(TAG, "Invalid now playing Queue ID " + qid);
- mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM);
- }
-
- void getTotalNumOfItems(byte[] bdaddr, @Nullable MediaController mediaController) {
- List<MediaSession.QueueItem> items = updateNowPlayingList(mediaController);
- if (DEBUG) {
- Log.d(TAG, "getTotalNumOfItems: " + items.size() + " items.");
- }
- mMediaInterface.getTotalNumOfItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, items.size());
- }
-
- void sendTrackChangeWithId(int type, @Nullable MediaController mediaController) {
- Log.d(TAG, "sendTrackChangeWithId (" + type + "): controller " + mediaController);
- long qid = getActiveQueueItemId(mediaController);
- byte[] track = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
- // The nowPlayingList changed: the new list has the full data for the current item
- mMediaInterface.trackChangedRsp(type, track);
- mLastTrackIdSent = qid;
- }
-
- /*
- * helper method to check if startItem and endItem index is with range of
- * MediaItem list. (Resultset containing all items in current path)
- */
- @Nullable
- private List<MediaSession.QueueItem> getQueueSubset(@NonNull List<MediaSession.QueueItem> items,
- long startItem, long endItem) {
- if (endItem > items.size()) {
- endItem = items.size() - 1;
- }
- if (startItem > Integer.MAX_VALUE) {
- startItem = Integer.MAX_VALUE;
- }
- try {
- List<MediaSession.QueueItem> selected =
- items.subList((int) startItem, (int) Math.min(items.size(), endItem + 1));
- if (selected.isEmpty()) {
- Log.i(TAG, "itemsSubList is empty.");
- return null;
- }
- return selected;
- } catch (IndexOutOfBoundsException ex) {
- Log.i(TAG, "Range (" + startItem + ", " + endItem + ") invalid");
- } catch (IllegalArgumentException ex) {
- Log.i(TAG, "Range start " + startItem + " > size (" + items.size() + ")");
- }
- return null;
- }
-
- /*
- * helper method to filter required attibutes before sending GetFolderItems
- * response
- */
- private void getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd folderItemsReqObj,
- @NonNull List<MediaSession.QueueItem> items, byte scope, long startItem, long endItem,
- @NonNull MediaController mediaController) {
- if (DEBUG) {
- Log.d(TAG,
- "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = " + endItem);
- }
-
- List<MediaSession.QueueItem> resultItems = getQueueSubset(items, startItem, endItem);
- /* check for index out of bound errors */
- if (resultItems == null) {
- Log.w(TAG, "getFolderItemsFilterAttr: resultItems is empty");
- mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null);
- return;
- }
-
- FolderItemsData folderDataNative = new FolderItemsData(resultItems.size());
-
- /* variables to accumulate attrs */
- ArrayList<String> attrArray = new ArrayList<String>();
- ArrayList<Integer> attrId = new ArrayList<Integer>();
-
- for (int itemIndex = 0; itemIndex < resultItems.size(); itemIndex++) {
- MediaSession.QueueItem item = resultItems.get(itemIndex);
- // get the queue id
- long qid = item.getQueueId();
- byte[] uid = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
-
- // get the array of uid from 2d to array 1D array
- for (int idx = 0; idx < AvrcpConstants.UID_SIZE; idx++) {
- folderDataNative.mItemUid[itemIndex * AvrcpConstants.UID_SIZE + idx] = uid[idx];
- }
-
- /* Set display name for current item */
- folderDataNative.mDisplayNames[itemIndex] =
- getAttrValue(AvrcpConstants.ATTRID_TITLE, item, mediaController);
-
- int maxAttributesRequested = 0;
- boolean isAllAttribRequested = false;
- /* check if remote requested for attributes */
- if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
- int attrCnt = 0;
-
- /* add requested attr ids to a temp array */
- if (folderItemsReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
- isAllAttribRequested = true;
- maxAttributesRequested = AvrcpConstants.MAX_NUM_ATTR;
- } else {
- /* get only the requested attribute ids from the request */
- maxAttributesRequested = folderItemsReqObj.mNumAttr;
- }
-
- /* lookup and copy values of attributes for ids requested above */
- for (int idx = 0; idx < maxAttributesRequested; idx++) {
- /* check if media player provided requested attributes */
- String value = null;
-
- int attribId =
- isAllAttribRequested ? (idx + 1) : folderItemsReqObj.mAttrIDs[idx];
- value = getAttrValue(attribId, item, mediaController);
- if (value != null) {
- attrArray.add(value);
- attrId.add(attribId);
- attrCnt++;
- }
- }
- /* add num attr actually received from media player for a particular item */
- folderDataNative.mAttributesNum[itemIndex] = attrCnt;
- }
- }
-
- /* copy filtered attr ids and attr values to response parameters */
- if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
- folderDataNative.mAttrIds = new int[attrId.size()];
- for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++) {
- folderDataNative.mAttrIds[attrIndex] = attrId.get(attrIndex);
- }
- folderDataNative.mAttrValues = attrArray.toArray(new String[attrArray.size()]);
- }
- for (int attrIndex = 0; attrIndex < folderDataNative.mAttributesNum.length; attrIndex++) {
- if (DEBUG) {
- Log.d(TAG, "folderDataNative.mAttributesNum"
- + folderDataNative.mAttributesNum[attrIndex] + " attrIndex " + attrIndex);
- }
- }
-
- /* create rsp object and send response to remote device */
- FolderItemsRsp rspObj =
- new FolderItemsRsp(AvrcpConstants.RSP_NO_ERROR, Avrcp.sUIDCounter, scope,
- folderDataNative.mNumItems, folderDataNative.mFolderTypes,
- folderDataNative.mPlayable, folderDataNative.mItemTypes,
- folderDataNative.mItemUid, folderDataNative.mDisplayNames,
- folderDataNative.mAttributesNum, folderDataNative.mAttrIds,
- folderDataNative.mAttrValues);
- mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
- }
-
- private String getAttrValue(int attr, MediaSession.QueueItem item,
- @Nullable MediaController mediaController) {
- String attrValue = null;
- if (item == null) {
- if (DEBUG) {
- Log.d(TAG, "getAttrValue received null item");
- }
- return null;
- }
- try {
- MediaDescription desc = item.getDescription();
- Bundle extras = desc.getExtras();
- boolean isCurrentTrack = item.getQueueId() == getActiveQueueItemId(mediaController);
- MediaMetadata data = null;
- if (isCurrentTrack) {
- if (DEBUG) {
- Log.d(TAG, "getAttrValue: item is active, using current data");
- }
- data = mediaController.getMetadata();
- if (data == null) {
- Log.e(TAG, "getMetadata didn't give us any metadata for the current track");
- }
- }
-
- if (data == null) {
- // TODO: This code can be removed when b/63117921 is resolved
- data = (MediaMetadata) extras.get(GPM_BUNDLE_METADATA_KEY);
- extras = null; // We no longer need the data in here
- }
-
- extras = fillBundle(data, extras);
-
- if (DEBUG) {
- Log.d(TAG, "getAttrValue: item " + item + " : " + desc);
- }
- switch (attr) {
- case AvrcpConstants.ATTRID_TITLE:
- /* Title is mandatory attribute */
- if (isCurrentTrack) {
- attrValue = extras.getString(MediaMetadata.METADATA_KEY_TITLE);
- } else {
- attrValue = desc.getTitle().toString();
- }
- break;
-
- case AvrcpConstants.ATTRID_ARTIST:
- attrValue = extras.getString(MediaMetadata.METADATA_KEY_ARTIST);
- break;
-
- case AvrcpConstants.ATTRID_ALBUM:
- attrValue = extras.getString(MediaMetadata.METADATA_KEY_ALBUM);
- break;
-
- case AvrcpConstants.ATTRID_TRACK_NUM:
- attrValue =
- Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER));
- break;
-
- case AvrcpConstants.ATTRID_NUM_TRACKS:
- attrValue =
- Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS));
- break;
-
- case AvrcpConstants.ATTRID_GENRE:
- attrValue = extras.getString(MediaMetadata.METADATA_KEY_GENRE);
- break;
-
- case AvrcpConstants.ATTRID_PLAY_TIME:
- attrValue = Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_DURATION));
- break;
-
- case AvrcpConstants.ATTRID_COVER_ART:
- Log.e(TAG, "getAttrValue: Cover art attribute not supported");
- return null;
-
- default:
- Log.e(TAG, "getAttrValue: Unknown attribute ID requested: " + attr);
- return null;
- }
- } catch (NullPointerException ex) {
- Log.w(TAG, "getAttrValue: attr id not found in result");
- /* checking if attribute is title, then it is mandatory and cannot send null */
- if (attr == AvrcpConstants.ATTRID_TITLE) {
- attrValue = "<Unknown Title>";
- } else {
- return null;
- }
- }
- if (DEBUG) {
- Log.d(TAG, "getAttrValue: attrvalue = " + attrValue + ", attr id:" + attr);
- }
- return attrValue;
- }
-
- private void getItemAttrFilterAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd mItemAttrReqObj,
- MediaSession.QueueItem mediaItem, @Nullable MediaController mediaController) {
- /* Response parameters */
- int[] attrIds = null; /* array of attr ids */
- String[] attrValues = null; /* array of attr values */
-
- /* variables to temperorily add attrs */
- ArrayList<String> attrArray = new ArrayList<String>();
- ArrayList<Integer> attrId = new ArrayList<Integer>();
- ArrayList<Integer> attrTempId = new ArrayList<Integer>();
-
- /* check if remote device has requested for attributes */
- if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
- if (mItemAttrReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
- for (int idx = 1; idx < AvrcpConstants.MAX_NUM_ATTR; idx++) {
- attrTempId.add(idx); /* attr id 0x00 is unused */
- }
- } else {
- /* get only the requested attribute ids from the request */
- for (int idx = 0; idx < mItemAttrReqObj.mNumAttr; idx++) {
- if (DEBUG) {
- Log.d(TAG, "getItemAttrFilterAttr: attr id[" + idx + "] :"
- + mItemAttrReqObj.mAttrIDs[idx]);
- }
- attrTempId.add(mItemAttrReqObj.mAttrIDs[idx]);
- }
- }
- }
-
- if (DEBUG) {
- Log.d(TAG, "getItemAttrFilterAttr: attr id list size:" + attrTempId.size());
- }
- /* lookup and copy values of attributes for ids requested above */
- for (int idx = 0; idx < attrTempId.size(); idx++) {
- /* check if media player provided requested attributes */
- String value = getAttrValue(attrTempId.get(idx), mediaItem, mediaController);
- if (value != null) {
- attrArray.add(value);
- attrId.add(attrTempId.get(idx));
- }
- }
-
- /* copy filtered attr ids and attr values to response parameters */
- if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
- attrIds = new int[attrId.size()];
-
- for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++) {
- attrIds[attrIndex] = attrId.get(attrIndex);
- }
-
- attrValues = attrArray.toArray(new String[attrId.size()]);
-
- /* create rsp object and send response */
- ItemAttrRsp rspObj = new ItemAttrRsp(AvrcpConstants.RSP_NO_ERROR, attrIds, attrValues);
- mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
- return;
- }
- }
-
- private long getActiveQueueItemId(@Nullable MediaController controller) {
- if (controller == null) {
- return MediaSession.QueueItem.UNKNOWN_ID;
- }
- PlaybackState state = controller.getPlaybackState();
- if (state == null || state.getState() == PlaybackState.STATE_BUFFERING
- || state.getState() == PlaybackState.STATE_NONE) {
- return MediaSession.QueueItem.UNKNOWN_ID;
- }
- long qid = state.getActiveQueueItemId();
- if (qid != MediaSession.QueueItem.UNKNOWN_ID) {
- return qid;
- }
- // Check if we're presenting a "one item queue"
- if (controller.getMetadata() != null) {
- return SINGLE_QID;
- }
- return MediaSession.QueueItem.UNKNOWN_ID;
- }
-
- String displayMediaItem(MediaSession.QueueItem item) {
- StringBuilder sb = new StringBuilder();
- sb.append("#");
- sb.append(item.getQueueId());
- sb.append(": ");
- sb.append(Utils.ellipsize(getAttrValue(AvrcpConstants.ATTRID_TITLE, item, null)));
- sb.append(" - ");
- sb.append(Utils.ellipsize(getAttrValue(AvrcpConstants.ATTRID_ALBUM, item, null)));
- sb.append(" by ");
- sb.append(Utils.ellipsize(getAttrValue(AvrcpConstants.ATTRID_ARTIST, item, null)));
- sb.append(" (");
- sb.append(getAttrValue(AvrcpConstants.ATTRID_PLAY_TIME, item, null));
- sb.append(" ");
- sb.append(getAttrValue(AvrcpConstants.ATTRID_TRACK_NUM, item, null));
- sb.append("/");
- sb.append(getAttrValue(AvrcpConstants.ATTRID_NUM_TRACKS, item, null));
- sb.append(") ");
- sb.append(getAttrValue(AvrcpConstants.ATTRID_GENRE, item, null));
- return sb.toString();
- }
-
- public void dump(StringBuilder sb, @Nullable MediaController mediaController) {
- ProfileService.println(sb, "AddressedPlayer info:");
- ProfileService.println(sb, "mLastTrackIdSent: " + mLastTrackIdSent);
- ProfileService.println(sb, "mNowPlayingList: " + mNowPlayingList.size() + " elements");
- long currentQueueId = getActiveQueueItemId(mediaController);
- for (MediaSession.QueueItem item : mNowPlayingList) {
- long itemId = item.getQueueId();
- ProfileService.println(sb,
- (itemId == currentQueueId ? "*" : " ") + displayMediaItem(item));
- }
- }
-}
diff --git a/src/com/android/bluetooth/avrcp/Avrcp.java b/src/com/android/bluetooth/avrcp/Avrcp.java
deleted file mode 100644
index e30ad41..0000000
--- a/src/com/android/bluetooth/avrcp/Avrcp.java
+++ /dev/null
@@ -1,3123 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.bluetooth.avrcp;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.bluetooth.BluetoothA2dp;
-import android.bluetooth.BluetoothAvrcp;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.media.AudioManager;
-import android.media.AudioPlaybackConfiguration;
-import android.media.MediaDescription;
-import android.media.MediaMetadata;
-import android.media.session.MediaSession;
-import android.media.session.MediaSessionManager;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.os.SystemClock;
-import android.os.UserManager;
-import android.util.Log;
-import android.view.KeyEvent;
-
-import com.android.bluetooth.R;
-import com.android.bluetooth.Utils;
-import com.android.bluetooth.btservice.ProfileService;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-/******************************************************************************
- * support Bluetooth AVRCP profile. support metadata, play status, event
- * notifications, address player selection and browse feature implementation.
- ******************************************************************************/
-
-public final class Avrcp {
- private static final boolean DEBUG = false;
- private static final String TAG = "Avrcp";
- private static final String ABSOLUTE_VOLUME_BLACKLIST = "absolute_volume_blacklist";
-
- private Context mContext;
- private final AudioManager mAudioManager;
- private volatile AvrcpMessageHandler mHandler;
- private Handler mAudioManagerPlaybackHandler;
- private AudioManagerPlaybackListener mAudioManagerPlaybackCb;
- private MediaSessionManager mMediaSessionManager;
- @Nullable private MediaController mMediaController;
- private MediaControllerListener mMediaControllerCb;
- private MediaAttributes mMediaAttributes;
- private long mLastQueueId;
- private PackageManager mPackageManager;
- private int mTransportControlFlags;
- @NonNull private PlaybackState mCurrentPlayState;
- private int mA2dpState;
- private boolean mAudioManagerIsPlaying;
- private int mPlayStatusChangedNT;
- private byte mReportedPlayStatus;
- private int mTrackChangedNT;
- private int mPlayPosChangedNT;
- private int mAddrPlayerChangedNT;
- private int mReportedPlayerID;
- private int mNowPlayingListChangedNT;
- private long mPlaybackIntervalMs;
- private long mLastReportedPosition;
- private long mNextPosMs;
- private long mPrevPosMs;
- private int mFeatures;
- private int mRemoteVolume;
- private int mLastRemoteVolume;
- private int mInitialRemoteVolume;
-
- /* Local volume in audio index 0-15 */
- private int mLocalVolume;
- private int mLastLocalVolume;
- private int mAbsVolThreshold;
-
- private String mAddress;
- private HashMap<Integer, Integer> mVolumeMapping;
-
- private int mLastDirection;
- private final int mVolumeStep;
- private final int mAudioStreamMax;
- private boolean mVolCmdSetInProgress;
- private int mAbsVolRetryTimes;
-
- private static final int NO_PLAYER_ID = 0;
-
- private int mCurrAddrPlayerID;
- private int mCurrBrowsePlayerID;
- private int mLastUsedPlayerID;
- private AvrcpMediaRsp mAvrcpMediaRsp;
-
- /* UID counter to be shared across different files. */
- static short sUIDCounter = AvrcpConstants.DEFAULT_UID_COUNTER;
-
- /* BTRC features */
- 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;
-
- /* AVRC response codes, from avrc_defs */
- 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;
-
- /* AVRC request commands from Native */
- private static final int MSG_NATIVE_REQ_GET_RC_FEATURES = 1;
- private static final int MSG_NATIVE_REQ_GET_PLAY_STATUS = 2;
- private static final int MSG_NATIVE_REQ_GET_ELEM_ATTRS = 3;
- private static final int MSG_NATIVE_REQ_REGISTER_NOTIFICATION = 4;
- private static final int MSG_NATIVE_REQ_VOLUME_CHANGE = 5;
- private static final int MSG_NATIVE_REQ_GET_FOLDER_ITEMS = 6;
- private static final int MSG_NATIVE_REQ_SET_ADDR_PLAYER = 7;
- private static final int MSG_NATIVE_REQ_SET_BR_PLAYER = 8;
- private static final int MSG_NATIVE_REQ_CHANGE_PATH = 9;
- private static final int MSG_NATIVE_REQ_PLAY_ITEM = 10;
- private static final int MSG_NATIVE_REQ_GET_ITEM_ATTR = 11;
- private static final int MSG_NATIVE_REQ_GET_TOTAL_NUM_OF_ITEMS = 12;
- private static final int MSG_NATIVE_REQ_PASS_THROUGH = 13;
-
- /* other AVRC messages */
- private static final int MSG_PLAY_INTERVAL_TIMEOUT = 14;
- private static final int MSG_SET_ABSOLUTE_VOLUME = 16;
- private static final int MSG_ABS_VOL_TIMEOUT = 17;
- private static final int MSG_SET_A2DP_AUDIO_STATE = 18;
- private static final int MSG_NOW_PLAYING_CHANGED_RSP = 19;
-
- private static final int CMD_TIMEOUT_DELAY = 2000;
- private static final int MAX_ERROR_RETRY_TIMES = 6;
- private static final int AVRCP_MAX_VOL = 127;
- private static final int AVRCP_BASE_VOLUME_STEP = 1;
-
- /* Communicates with MediaPlayer to fetch media content */
- private BrowsedMediaPlayer mBrowsedMediaPlayer;
-
- /* Addressed player handling */
- private AddressedMediaPlayer mAddressedMediaPlayer;
-
- /* List of Media player instances, useful for retrieving MediaPlayerList or MediaPlayerInfo */
- private SortedMap<Integer, MediaPlayerInfo> mMediaPlayerInfoList;
- private boolean mAvailablePlayerViewChanged;
-
- /* List of media players which supports browse */
- private List<BrowsePlayerInfo> mBrowsePlayerInfoList;
-
- /* Manage browsed players */
- private AvrcpBrowseManager mAvrcpBrowseManager;
-
- /* Broadcast receiver for device connections intent broadcasts */
- private final BroadcastReceiver mAvrcpReceiver = new AvrcpServiceBroadcastReceiver();
- private final BroadcastReceiver mBootReceiver = new AvrcpServiceBootReceiver();
-
- /* Recording passthrough key dispatches */
- private static final int PASSTHROUGH_LOG_MAX_SIZE = DEBUG ? 50 : 10;
- private EvictingQueue<MediaKeyLog> mPassthroughLogs; // Passthorugh keys dispatched
- private List<MediaKeyLog> mPassthroughPending; // Passthrough keys sent not dispatched yet
- private int mPassthroughDispatched; // Number of keys dispatched
-
- private class MediaKeyLog {
- private long mTimeSent;
- private long mTimeProcessed;
- private String mPackage;
- private KeyEvent mEvent;
-
- MediaKeyLog(long time, KeyEvent event) {
- mEvent = event;
- mTimeSent = time;
- }
-
- public boolean addDispatch(long time, KeyEvent event, String packageName) {
- if (mPackage != null) {
- return false;
- }
- if (event.getAction() != mEvent.getAction()) {
- return false;
- }
- if (event.getKeyCode() != mEvent.getKeyCode()) {
- return false;
- }
- mPackage = packageName;
- mTimeProcessed = time;
- return true;
- }
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append(android.text.format.DateFormat.format("MM-dd HH:mm:ss", mTimeSent));
- sb.append(" " + mEvent.toString());
- if (mPackage == null) {
- sb.append(" (undispatched)");
- } else {
- sb.append(" to " + mPackage);
- sb.append(" in " + (mTimeProcessed - mTimeSent) + "ms");
- }
- return sb.toString();
- }
- }
-
- static {
- classInitNative();
- }
-
- private Avrcp(Context context) {
- mMediaAttributes = new MediaAttributes(null);
- mLastQueueId = MediaSession.QueueItem.UNKNOWN_ID;
- mCurrentPlayState =
- new PlaybackState.Builder().setState(PlaybackState.STATE_NONE, -1L, 0.0f).build();
- mReportedPlayStatus = PLAYSTATUS_ERROR;
- mA2dpState = BluetoothA2dp.STATE_NOT_PLAYING;
- mAudioManagerIsPlaying = false;
- mPlayStatusChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
- mTrackChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
- mPlayPosChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
- mAddrPlayerChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
- mNowPlayingListChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
- mPlaybackIntervalMs = 0L;
- mLastReportedPosition = -1;
- mNextPosMs = -1;
- mPrevPosMs = -1;
- mFeatures = 0;
- mRemoteVolume = -1;
- mInitialRemoteVolume = -1;
- mLastRemoteVolume = -1;
- mLastDirection = 0;
- mVolCmdSetInProgress = false;
- mAbsVolRetryTimes = 0;
- mLocalVolume = -1;
- mLastLocalVolume = -1;
- mAbsVolThreshold = 0;
- mVolumeMapping = new HashMap<Integer, Integer>();
- mCurrAddrPlayerID = NO_PLAYER_ID;
- mReportedPlayerID = mCurrAddrPlayerID;
- mCurrBrowsePlayerID = 0;
- mContext = context;
- mLastUsedPlayerID = 0;
- mAddressedMediaPlayer = null;
-
- initNative();
-
- mMediaSessionManager =
- (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
- mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- mAudioStreamMax = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
- mVolumeStep = Math.max(AVRCP_BASE_VOLUME_STEP, AVRCP_MAX_VOL / mAudioStreamMax);
-
- Resources resources = context.getResources();
- if (resources != null) {
- mAbsVolThreshold =
- resources.getInteger(R.integer.a2dp_absolute_volume_initial_threshold);
-
- // Update the threshold if the thresholdPercent is valid
- int thresholdPercent =
- resources.getInteger(R.integer.a2dp_absolute_volume_initial_threshold_percent);
- if (thresholdPercent >= 0 && thresholdPercent <= 100) {
- mAbsVolThreshold = (thresholdPercent * mAudioStreamMax) / 100;
- }
- }
-
- // Register for package removal intent broadcasts for media button receiver persistence
- IntentFilter pkgFilter = new IntentFilter();
- pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
- pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
- pkgFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
- pkgFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
- pkgFilter.addDataScheme("package");
- context.registerReceiver(mAvrcpReceiver, pkgFilter);
-
- IntentFilter bootFilter = new IntentFilter();
- bootFilter.addAction(Intent.ACTION_USER_UNLOCKED);
- context.registerReceiver(mBootReceiver, bootFilter);
- }
-
- private synchronized void start() {
- HandlerThread thread = new HandlerThread("BluetoothAvrcpHandler");
- thread.start();
- Looper looper = thread.getLooper();
- mHandler = new AvrcpMessageHandler(looper);
- mAudioManagerPlaybackHandler = new Handler(looper);
- mAudioManagerPlaybackCb = new AudioManagerPlaybackListener();
- mMediaControllerCb = new MediaControllerListener();
- mAvrcpMediaRsp = new AvrcpMediaRsp();
- mMediaPlayerInfoList = new TreeMap<Integer, MediaPlayerInfo>();
- mAvailablePlayerViewChanged = false;
- mBrowsePlayerInfoList = Collections.synchronizedList(new ArrayList<BrowsePlayerInfo>());
- mPassthroughDispatched = 0;
- mPassthroughLogs = new EvictingQueue<MediaKeyLog>(PASSTHROUGH_LOG_MAX_SIZE);
- mPassthroughPending = Collections.synchronizedList(new ArrayList<MediaKeyLog>());
- if (mMediaSessionManager != null) {
- mMediaSessionManager.addOnActiveSessionsChangedListener(mActiveSessionListener, null,
- mHandler);
- mMediaSessionManager.setCallback(mButtonDispatchCallback, null);
- }
- mPackageManager = mContext.getApplicationContext().getPackageManager();
-
- /* create object to communicate with addressed player */
- mAddressedMediaPlayer = new AddressedMediaPlayer(mAvrcpMediaRsp);
-
- /* initialize BrowseMananger which manages Browse commands and response */
- mAvrcpBrowseManager = new AvrcpBrowseManager(mContext, mAvrcpMediaRsp);
-
- initMediaPlayersList();
-
- UserManager manager = UserManager.get(mContext);
- if (manager == null || manager.isUserUnlocked()) {
- if (DEBUG) {
- Log.d(TAG, "User already unlocked, initializing player lists");
- }
- // initialize browsable player list and build media player list
- buildBrowsablePlayerList();
- }
-
- mAudioManager.registerAudioPlaybackCallback(mAudioManagerPlaybackCb,
- mAudioManagerPlaybackHandler);
- }
-
- public static Avrcp make(Context context) {
- if (DEBUG) {
- Log.v(TAG, "make");
- }
- Avrcp ar = new Avrcp(context);
- ar.start();
- return ar;
- }
-
- public synchronized void doQuit() {
- if (DEBUG) {
- Log.d(TAG, "doQuit");
- }
- if (mAudioManager != null) {
- mAudioManager.unregisterAudioPlaybackCallback(mAudioManagerPlaybackCb);
- }
- if (mMediaController != null) {
- mMediaController.unregisterCallback(mMediaControllerCb);
- }
- if (mMediaSessionManager != null) {
- mMediaSessionManager.setCallback(null, null);
- mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveSessionListener);
- }
-
- mAudioManagerPlaybackHandler.removeCallbacksAndMessages(null);
- mHandler.removeCallbacksAndMessages(null);
- Looper looper = mHandler.getLooper();
- mHandler = null;
- if (looper != null) {
- looper.quitSafely();
- }
-
- mAudioManagerPlaybackHandler = null;
- mContext.unregisterReceiver(mAvrcpReceiver);
- mContext.unregisterReceiver(mBootReceiver);
-
- mAddressedMediaPlayer.cleanup();
- mAvrcpBrowseManager.cleanup();
- }
-
- public void cleanup() {
- if (DEBUG) {
- Log.d(TAG, "cleanup");
- }
- cleanupNative();
- if (mVolumeMapping != null) {
- mVolumeMapping.clear();
- }
- }
-
- private class AudioManagerPlaybackListener extends AudioManager.AudioPlaybackCallback {
- @Override
- public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
- super.onPlaybackConfigChanged(configs);
- boolean isPlaying = false;
- for (AudioPlaybackConfiguration config : configs) {
- if (DEBUG) {
- Log.d(TAG, "AudioManager Player: "
- + AudioPlaybackConfiguration.toLogFriendlyString(config));
- }
- if (config.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
- isPlaying = true;
- break;
- }
- }
- if (DEBUG) {
- Log.d(TAG, "AudioManager isPlaying: " + isPlaying);
- }
- if (mAudioManagerIsPlaying != isPlaying) {
- mAudioManagerIsPlaying = isPlaying;
- updateCurrentMediaState();
- }
- }
- }
-
- private class MediaControllerListener extends MediaController.Callback {
- @Override
- public void onMetadataChanged(MediaMetadata metadata) {
- if (DEBUG) {
- Log.v(TAG, "onMetadataChanged");
- }
- updateCurrentMediaState();
- }
-
- @Override
- public synchronized void onPlaybackStateChanged(PlaybackState state) {
- if (DEBUG) {
- Log.v(TAG, "onPlaybackStateChanged: state " + state.toString());
- }
-
- updateCurrentMediaState();
- }
-
- @Override
- public void onSessionDestroyed() {
- Log.v(TAG, "MediaController session destroyed");
- synchronized (Avrcp.this) {
- if (mMediaController != null) {
- removeMediaController(mMediaController.getWrappedInstance());
- }
- }
- }
-
- @Override
- public void onQueueChanged(List<MediaSession.QueueItem> queue) {
- if (queue == null) {
- Log.v(TAG, "onQueueChanged: received null queue");
- return;
- }
-
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "onQueueChanged: mHandler is already null");
- return;
- }
-
- Log.v(TAG, "onQueueChanged: NowPlaying list changed, Queue Size = "
- + queue.size());
- handler.sendEmptyMessage(MSG_NOW_PLAYING_CHANGED_RSP);
- }
- }
-
- /** Handles Avrcp messages. */
- private final class AvrcpMessageHandler extends Handler {
- private AvrcpMessageHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_NATIVE_REQ_GET_RC_FEATURES: {
- String address = (String) msg.obj;
- mFeatures = msg.arg1;
- mFeatures = modifyRcFeatureFromBlacklist(mFeatures, address);
- if (DEBUG) {
- Log.v(TAG,
- "MSG_NATIVE_REQ_GET_RC_FEATURES: address=" + address + ", features="
- + msg.arg1 + ", mFeatures=" + mFeatures);
- }
- mAudioManager.avrcpSupportsAbsoluteVolume(address, isAbsoluteVolumeSupported());
- mLastLocalVolume = -1;
- mRemoteVolume = -1;
- mLocalVolume = -1;
- mInitialRemoteVolume = -1;
- mAddress = address;
- if (mVolumeMapping != null) {
- mVolumeMapping.clear();
- }
- break;
- }
-
- case MSG_NATIVE_REQ_GET_PLAY_STATUS: {
- byte[] address = (byte[]) msg.obj;
- int btstate = getBluetoothPlayState(mCurrentPlayState);
- int length = (int) mMediaAttributes.getLength();
- int position = (int) getPlayPosition();
- if (DEBUG) {
- Log.v(TAG,
- "MSG_NATIVE_REQ_GET_PLAY_STATUS, responding with state " + btstate
- + " len " + length + " pos " + position);
- }
- getPlayStatusRspNative(address, btstate, length, position);
- break;
- }
-
- case MSG_NATIVE_REQ_GET_ELEM_ATTRS: {
- String[] textArray;
- AvrcpCmd.ElementAttrCmd elem = (AvrcpCmd.ElementAttrCmd) msg.obj;
- byte numAttr = elem.mNumAttr;
- int[] attrIds = elem.mAttrIDs;
- if (DEBUG) {
- Log.v(TAG, "MSG_NATIVE_REQ_GET_ELEM_ATTRS:numAttr=" + numAttr);
- }
- textArray = new String[numAttr];
- StringBuilder responseDebug = new StringBuilder();
- responseDebug.append("getElementAttr response: ");
- for (int i = 0; i < numAttr; ++i) {
- textArray[i] = mMediaAttributes.getString(attrIds[i]);
- responseDebug.append("[" + attrIds[i] + "=");
- if (attrIds[i] == AvrcpConstants.ATTRID_TITLE
- || attrIds[i] == AvrcpConstants.ATTRID_ARTIST
- || attrIds[i] == AvrcpConstants.ATTRID_ALBUM) {
- responseDebug.append(Utils.ellipsize(textArray[i]) + "] ");
- } else {
- responseDebug.append(textArray[i] + "] ");
- }
- }
- Log.v(TAG, responseDebug.toString());
- byte[] bdaddr = elem.mAddress;
- getElementAttrRspNative(bdaddr, numAttr, attrIds, textArray);
- break;
- }
-
- case MSG_NATIVE_REQ_REGISTER_NOTIFICATION:
- if (DEBUG) {
- Log.v(TAG,
- "MSG_NATIVE_REQ_REGISTER_NOTIFICATION:event=" + msg.arg1 + " param="
- + msg.arg2);
- }
- processRegisterNotification((byte[]) msg.obj, msg.arg1, msg.arg2);
- break;
-
- case MSG_NOW_PLAYING_CHANGED_RSP:
- if (DEBUG) {
- Log.v(TAG, "MSG_NOW_PLAYING_CHANGED_RSP");
- }
- removeMessages(MSG_NOW_PLAYING_CHANGED_RSP);
- updateCurrentMediaState();
- break;
-
- case MSG_PLAY_INTERVAL_TIMEOUT:
- sendPlayPosNotificationRsp(false);
- break;
-
- case MSG_NATIVE_REQ_VOLUME_CHANGE:
- if (!isAbsoluteVolumeSupported()) {
- if (DEBUG) {
- Log.v(TAG, "MSG_NATIVE_REQ_VOLUME_CHANGE ignored, not supported");
- }
- break;
- }
- byte absVol = (byte) ((byte) msg.arg1 & 0x7f); // discard MSB as it is RFD
- if (DEBUG) {
- Log.v(TAG, "MSG_NATIVE_REQ_VOLUME_CHANGE: volume=" + absVol + " ctype="
- + msg.arg2);
- }
-
- if (msg.arg2 == AVRC_RSP_ACCEPT || msg.arg2 == AVRC_RSP_REJ) {
- if (!mVolCmdSetInProgress) {
- Log.e(TAG, "Unsolicited response, ignored");
- break;
- }
- removeMessages(MSG_ABS_VOL_TIMEOUT);
-
- mVolCmdSetInProgress = false;
- mAbsVolRetryTimes = 0;
- }
-
- // convert remote volume to local volume
- int volIndex = convertToAudioStreamVolume(absVol);
- if (mInitialRemoteVolume == -1) {
- mInitialRemoteVolume = absVol;
- if (mAbsVolThreshold > 0 && mAbsVolThreshold < mAudioStreamMax
- && volIndex > mAbsVolThreshold) {
- if (DEBUG) {
- Log.v(TAG, "remote inital volume too high " + volIndex + ">"
- + mAbsVolThreshold);
- }
- Message msg1 = this.obtainMessage(MSG_SET_ABSOLUTE_VOLUME,
- mAbsVolThreshold, 0);
- this.sendMessage(msg1);
- mRemoteVolume = absVol;
- mLocalVolume = volIndex;
- break;
- }
- }
-
- if (mLocalVolume != volIndex && (msg.arg2 == AVRC_RSP_ACCEPT
- || msg.arg2 == AVRC_RSP_CHANGED || msg.arg2 == AVRC_RSP_INTERIM)) {
- /* If the volume has successfully changed */
- mLocalVolume = volIndex;
- if (mLastLocalVolume != -1 && msg.arg2 == AVRC_RSP_ACCEPT) {
- if (mLastLocalVolume != volIndex) {
- /* remote volume changed more than requested due to
- * local and remote has different volume steps */
- if (DEBUG) {
- Log.d(TAG,
- "Remote returned volume does not match desired volume "
- + mLastLocalVolume + " vs " + volIndex);
- }
- mLastLocalVolume = mLocalVolume;
- }
- }
-
- notifyVolumeChanged(mLocalVolume);
- mRemoteVolume = absVol;
- long pecentVolChanged = ((long) absVol * 100) / 0x7f;
- Log.e(TAG, "percent volume changed: " + pecentVolChanged + "%");
- } else if (msg.arg2 == AVRC_RSP_REJ) {
- Log.e(TAG, "setAbsoluteVolume call rejected");
- }
- break;
-
- case MSG_SET_ABSOLUTE_VOLUME:
- if (!isAbsoluteVolumeSupported()) {
- if (DEBUG) {
- Log.v(TAG, "ignore MSG_SET_ABSOLUTE_VOLUME");
- }
- break;
- }
-
- if (DEBUG) {
- Log.v(TAG, "MSG_SET_ABSOLUTE_VOLUME");
- }
-
- if (mVolCmdSetInProgress) {
- if (DEBUG) {
- Log.w(TAG, "There is already a volume command in progress.");
- }
- break;
- }
-
- // Remote device didn't set initial volume. Let's black list it
- if (mInitialRemoteVolume == -1) {
- if (DEBUG) {
- Log.d(TAG, "remote " + mAddress
- + " never tell us initial volume, black list it.");
- }
- blackListCurrentDevice("MSG_SET_ABSOLUTE_VOLUME");
- break;
- }
-
- int avrcpVolume = convertToAvrcpVolume(msg.arg1);
- avrcpVolume = Math.min(AVRCP_MAX_VOL, Math.max(0, avrcpVolume));
- if (DEBUG) {
- Log.d(TAG, "Setting volume to " + msg.arg1 + "-" + avrcpVolume);
- }
- if (setVolumeNative(avrcpVolume)) {
- sendMessageDelayed(obtainMessage(MSG_ABS_VOL_TIMEOUT), CMD_TIMEOUT_DELAY);
- mVolCmdSetInProgress = true;
- mLastRemoteVolume = avrcpVolume;
- mLastLocalVolume = msg.arg1;
- } else {
- if (DEBUG) {
- Log.d(TAG, "setVolumeNative failed");
- }
- }
- break;
-
- case MSG_ABS_VOL_TIMEOUT:
- if (DEBUG) {
- Log.v(TAG, "MSG_ABS_VOL_TIMEOUT: Volume change cmd timed out.");
- }
- mVolCmdSetInProgress = false;
- if (mAbsVolRetryTimes >= MAX_ERROR_RETRY_TIMES) {
- mAbsVolRetryTimes = 0;
- /* too many volume change failures, black list the device */
- blackListCurrentDevice("MSG_ABS_VOL_TIMEOUT");
- } else {
- mAbsVolRetryTimes += 1;
- if (setVolumeNative(mLastRemoteVolume)) {
- sendMessageDelayed(obtainMessage(MSG_ABS_VOL_TIMEOUT),
- CMD_TIMEOUT_DELAY);
- mVolCmdSetInProgress = true;
- }
- }
- break;
-
- case MSG_SET_A2DP_AUDIO_STATE:
- if (DEBUG) {
- Log.v(TAG, "MSG_SET_A2DP_AUDIO_STATE:" + msg.arg1);
- }
- mA2dpState = msg.arg1;
- updateCurrentMediaState();
- break;
-
- case MSG_NATIVE_REQ_GET_FOLDER_ITEMS: {
- AvrcpCmd.FolderItemsCmd folderObj = (AvrcpCmd.FolderItemsCmd) msg.obj;
- if (DEBUG) {
- Log.v(TAG, "MSG_NATIVE_REQ_GET_FOLDER_ITEMS " + folderObj);
- }
- switch (folderObj.mScope) {
- case AvrcpConstants.BTRC_SCOPE_PLAYER_LIST:
- handleMediaPlayerListRsp(folderObj);
- break;
- case AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM:
- case AvrcpConstants.BTRC_SCOPE_NOW_PLAYING:
- handleGetFolderItemBrowseResponse(folderObj, folderObj.mAddress);
- break;
- default:
- Log.e(TAG, "unknown scope for getfolderitems. scope = "
- + folderObj.mScope);
- getFolderItemsRspNative(folderObj.mAddress,
- AvrcpConstants.RSP_INV_SCOPE, (short) 0, (byte) 0, 0, null,
- null, null, null, null, null, null, null);
- }
- break;
- }
-
- case MSG_NATIVE_REQ_SET_ADDR_PLAYER:
- // object is bdaddr, argument 1 is the selected player id
- if (DEBUG) {
- Log.v(TAG, "MSG_NATIVE_REQ_SET_ADDR_PLAYER id=" + msg.arg1);
- }
- setAddressedPlayer((byte[]) msg.obj, msg.arg1);
- break;
-
- case MSG_NATIVE_REQ_GET_ITEM_ATTR:
- // msg object contains the item attribute object
- AvrcpCmd.ItemAttrCmd cmd = (AvrcpCmd.ItemAttrCmd) msg.obj;
- if (DEBUG) {
- Log.v(TAG, "MSG_NATIVE_REQ_GET_ITEM_ATTR " + cmd);
- }
- handleGetItemAttr(cmd);
- break;
-
- case MSG_NATIVE_REQ_SET_BR_PLAYER:
- // argument 1 is the selected player id
- if (DEBUG) {
- Log.v(TAG, "MSG_NATIVE_REQ_SET_BR_PLAYER id=" + msg.arg1);
- }
- setBrowsedPlayer((byte[]) msg.obj, msg.arg1);
- break;
-
- case MSG_NATIVE_REQ_CHANGE_PATH: {
- if (DEBUG) {
- Log.v(TAG, "MSG_NATIVE_REQ_CHANGE_PATH");
- }
- Bundle data = msg.getData();
- byte[] bdaddr = data.getByteArray("BdAddress");
- byte[] folderUid = data.getByteArray("folderUid");
- byte direction = data.getByte("direction");
- if (mAvrcpBrowseManager.getBrowsedMediaPlayer(bdaddr) != null) {
- mAvrcpBrowseManager.getBrowsedMediaPlayer(bdaddr)
- .changePath(folderUid, direction);
- } else {
- Log.e(TAG, "Remote requesting change path before setbrowsedplayer");
- changePathRspNative(bdaddr, AvrcpConstants.RSP_BAD_CMD, 0);
- }
- break;
- }
-
- case MSG_NATIVE_REQ_PLAY_ITEM: {
- Bundle data = msg.getData();
- byte[] bdaddr = data.getByteArray("BdAddress");
- byte[] uid = data.getByteArray("uid");
- byte scope = data.getByte("scope");
- if (DEBUG) {
- Log.v(TAG, "MSG_NATIVE_REQ_PLAY_ITEM scope=" + scope + " id="
- + Utils.byteArrayToString(uid));
- }
- handlePlayItemResponse(bdaddr, uid, scope);
- break;
- }
-
- case MSG_NATIVE_REQ_GET_TOTAL_NUM_OF_ITEMS:
- if (DEBUG) {
- Log.v(TAG, "MSG_NATIVE_REQ_GET_TOTAL_NUM_OF_ITEMS scope=" + msg.arg1);
- }
- // argument 1 is scope, object is bdaddr
- handleGetTotalNumOfItemsResponse((byte[]) msg.obj, (byte) msg.arg1);
- break;
-
- case MSG_NATIVE_REQ_PASS_THROUGH:
- if (DEBUG) {
- Log.v(TAG,
- "MSG_NATIVE_REQ_PASS_THROUGH: id=" + msg.arg1 + " st=" + msg.arg2);
- }
- // argument 1 is id, argument 2 is keyState
- handlePassthroughCmd(msg.arg1, msg.arg2);
- break;
-
- default:
- Log.e(TAG, "unknown message! msg.what=" + msg.what);
- break;
- }
- }
- }
-
- private PlaybackState updatePlaybackState() {
- PlaybackState newState = new PlaybackState.Builder().setState(PlaybackState.STATE_NONE,
- PlaybackState.PLAYBACK_POSITION_UNKNOWN, 0.0f).build();
- synchronized (this) {
- PlaybackState controllerState = null;
- if (mMediaController != null) {
- controllerState = mMediaController.getPlaybackState();
- }
-
- if (controllerState != null) {
- newState = controllerState;
- }
- // Use the AudioManager to update the playback state.
- // NOTE: We cannot use the
- // (mA2dpState == BluetoothA2dp.STATE_PLAYING)
- // check, because after Pause, the A2DP state remains in
- // STATE_PLAYING for 3 more seconds.
- // As a result of that, if we pause the music, on carkits the
- // Play status indicator will continue to display "Playing"
- // for 3 more seconds which can be confusing.
- if ((mAudioManagerIsPlaying && newState.getState() != PlaybackState.STATE_PLAYING) || (
- controllerState == null && mAudioManager != null
- && mAudioManager.isMusicActive())) {
- // Use AudioManager playback state if we don't have the state
- // from MediaControlller
- PlaybackState.Builder builder = new PlaybackState.Builder();
- if (mAudioManagerIsPlaying) {
- builder.setState(PlaybackState.STATE_PLAYING,
- PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1.0f);
- } else {
- builder.setState(PlaybackState.STATE_PAUSED,
- PlaybackState.PLAYBACK_POSITION_UNKNOWN, 0.0f);
- }
- newState = builder.build();
- }
- }
-
- byte newPlayStatus = getBluetoothPlayState(newState);
-
- /* update play status in global media player list */
- MediaPlayerInfo player = getAddressedPlayerInfo();
- if (player != null) {
- player.setPlayStatus(newPlayStatus);
- }
-
- if (DEBUG) {
- Log.v(TAG, "updatePlaybackState (" + mPlayStatusChangedNT + "): " + mReportedPlayStatus
- + "➡" + newPlayStatus + "(" + newState + ")");
- }
-
- if (newState != null) {
- mCurrentPlayState = newState;
- }
-
- return mCurrentPlayState;
- }
-
- private void sendPlaybackStatus(int playStatusChangedNT, byte playbackState) {
- registerNotificationRspPlayStatusNative(playStatusChangedNT, playbackState);
- mPlayStatusChangedNT = playStatusChangedNT;
- mReportedPlayStatus = playbackState;
- }
-
- private void updateTransportControls(int transportControlFlags) {
- mTransportControlFlags = transportControlFlags;
- }
-
- class MediaAttributes {
- private boolean mExists;
- private String mTitle;
- private String mArtistName;
- private String mAlbumName;
- private String mMediaNumber;
- private String mMediaTotalNumber;
- private String mGenre;
- private long mPlayingTimeMs;
-
- private static final int ATTR_TITLE = 1;
- private static final int ATTR_ARTIST_NAME = 2;
- private static final int ATTR_ALBUM_NAME = 3;
- private static final int ATTR_MEDIA_NUMBER = 4;
- private static final int ATTR_MEDIA_TOTAL_NUMBER = 5;
- private static final int ATTR_GENRE = 6;
- private static final int ATTR_PLAYING_TIME_MS = 7;
-
-
- MediaAttributes(MediaMetadata data) {
- mExists = data != null;
- if (!mExists) {
- return;
- }
-
- mArtistName = stringOrBlank(data.getString(MediaMetadata.METADATA_KEY_ARTIST));
- mAlbumName = stringOrBlank(data.getString(MediaMetadata.METADATA_KEY_ALBUM));
- mMediaNumber = longStringOrBlank(data.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER));
- mMediaTotalNumber =
- longStringOrBlank(data.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS));
- mGenre = stringOrBlank(data.getString(MediaMetadata.METADATA_KEY_GENRE));
- mPlayingTimeMs = data.getLong(MediaMetadata.METADATA_KEY_DURATION);
-
- // Try harder for the title.
- mTitle = data.getString(MediaMetadata.METADATA_KEY_TITLE);
-
- if (mTitle == null) {
- MediaDescription desc = data.getDescription();
- if (desc != null) {
- CharSequence val = desc.getDescription();
- if (val != null) {
- mTitle = val.toString();
- }
- }
- }
-
- if (mTitle == null) {
- mTitle = new String();
- }
- }
-
- public long getLength() {
- if (!mExists) {
- return 0L;
- }
- return mPlayingTimeMs;
- }
-
- public boolean equals(MediaAttributes other) {
- if (other == null) {
- return false;
- }
-
- if (mExists != other.mExists) {
- return false;
- }
-
- if (!mExists) {
- return true;
- }
-
- return (mTitle.equals(other.mTitle)) && (mArtistName.equals(other.mArtistName))
- && (mAlbumName.equals(other.mAlbumName)) && (mMediaNumber.equals(
- other.mMediaNumber)) && (mMediaTotalNumber.equals(other.mMediaTotalNumber))
- && (mGenre.equals(other.mGenre)) && (mPlayingTimeMs == other.mPlayingTimeMs);
- }
-
- public String getString(int attrId) {
- if (!mExists) {
- return new String();
- }
-
- switch (attrId) {
- case ATTR_TITLE:
- return mTitle;
- case ATTR_ARTIST_NAME:
- return mArtistName;
- case ATTR_ALBUM_NAME:
- return mAlbumName;
- case ATTR_MEDIA_NUMBER:
- return mMediaNumber;
- case ATTR_MEDIA_TOTAL_NUMBER:
- return mMediaTotalNumber;
- case ATTR_GENRE:
- return mGenre;
- case ATTR_PLAYING_TIME_MS:
- return Long.toString(mPlayingTimeMs);
- default:
- return new String();
- }
- }
-
- private String stringOrBlank(String s) {
- return s == null ? new String() : s;
- }
-
- private String longStringOrBlank(Long s) {
- return s == null ? new String() : s.toString();
- }
-
- @Override
- public String toString() {
- if (!mExists) {
- return "[MediaAttributes: none]";
- }
-
- return "[MediaAttributes: " + mTitle + " - " + mAlbumName + " by " + mArtistName + " ("
- + mPlayingTimeMs + " " + mMediaNumber + "/" + mMediaTotalNumber + ") " + mGenre
- + "]";
- }
-
- public String toRedactedString() {
- if (!mExists) {
- return "[MediaAttributes: none]";
- }
-
- return "[MediaAttributes: " + Utils.ellipsize(mTitle) + " - " + Utils.ellipsize(
- mAlbumName) + " by " + Utils.ellipsize(mArtistName) + " (" + mPlayingTimeMs
- + " " + mMediaNumber + "/" + mMediaTotalNumber + ") " + mGenre + "]";
- }
- }
-
- private void updateCurrentMediaState() {
- // Only do player updates when we aren't registering for track changes.
- MediaAttributes currentAttributes;
- PlaybackState newState = updatePlaybackState();
-
- synchronized (this) {
- if (mMediaController == null) {
- currentAttributes = new MediaAttributes(null);
- } else {
- currentAttributes = new MediaAttributes(mMediaController.getMetadata());
- }
- }
-
- byte newPlayStatus = getBluetoothPlayState(newState);
-
- if (newState.getState() != PlaybackState.STATE_BUFFERING
- && newState.getState() != PlaybackState.STATE_NONE) {
- long newQueueId = MediaSession.QueueItem.UNKNOWN_ID;
- if (newState != null) {
- newQueueId = newState.getActiveQueueItemId();
- }
- if (DEBUG) {
- Log.v(TAG,
- "Media update: id " + mLastQueueId + "➡" + newQueueId + "? " + currentAttributes
- .toRedactedString() + " : " + mMediaAttributes.toRedactedString());
- }
-
- if (mAvailablePlayerViewChanged) {
- registerNotificationRspAvalPlayerChangedNative(
- AvrcpConstants.NOTIFICATION_TYPE_CHANGED);
- mAvailablePlayerViewChanged = false;
- return;
- }
-
- if (mAddrPlayerChangedNT == AvrcpConstants.NOTIFICATION_TYPE_INTERIM
- && mReportedPlayerID != mCurrAddrPlayerID) {
- registerNotificationRspAvalPlayerChangedNative(
- AvrcpConstants.NOTIFICATION_TYPE_CHANGED);
- registerNotificationRspAddrPlayerChangedNative(
- AvrcpConstants.NOTIFICATION_TYPE_CHANGED, mCurrAddrPlayerID, sUIDCounter);
-
- mAvailablePlayerViewChanged = false;
- mAddrPlayerChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
- mReportedPlayerID = mCurrAddrPlayerID;
-
- // Update the now playing list without sending the notification
- mNowPlayingListChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
- mAddressedMediaPlayer.updateNowPlayingList(mMediaController);
- mNowPlayingListChangedNT = AvrcpConstants.NOTIFICATION_TYPE_INTERIM;
- }
-
- // Dont send now playing list changed if the player doesn't support browsing
- MediaPlayerInfo info = getAddressedPlayerInfo();
- if (info != null && info.isBrowseSupported()) {
- if (DEBUG) {
- Log.v(TAG, "Check if NowPlayingList is updated");
- }
- mAddressedMediaPlayer.updateNowPlayingList(mMediaController);
- }
-
- // Notify track changed if:
- // - The CT is registered for the notification
- // - Queue ID is UNKNOWN and MediaMetadata is different
- // - Queue ID is valid and different from last Queue ID sent
- if ((newQueueId == -1 || newQueueId != mLastQueueId)
- && mTrackChangedNT == AvrcpConstants.NOTIFICATION_TYPE_INTERIM
- && !currentAttributes.equals(mMediaAttributes)
- && newPlayStatus == PLAYSTATUS_PLAYING) {
- Log.v(TAG, "Send track changed");
- mMediaAttributes = currentAttributes;
- mLastQueueId = newQueueId;
- sendTrackChangedRsp(false);
- }
- } else {
- Log.i(TAG, "Skipping update due to invalid playback state");
- }
-
- // still send the updated play state if the playback state is none or buffering
- if (DEBUG) {
- Log.v(TAG, "play status change " + mReportedPlayStatus + "➡" + newPlayStatus
- + " mPlayStatusChangedNT: " + mPlayStatusChangedNT);
- }
- if (mPlayStatusChangedNT == AvrcpConstants.NOTIFICATION_TYPE_INTERIM || (mReportedPlayStatus
- != newPlayStatus)) {
- sendPlaybackStatus(AvrcpConstants.NOTIFICATION_TYPE_CHANGED, newPlayStatus);
- }
-
- sendPlayPosNotificationRsp(false);
- }
-
- private void getRcFeaturesRequestFromNative(byte[] address, int features) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "getRcFeaturesRequestFromNative: mHandler is already null");
- return;
- }
-
- Message msg = handler.obtainMessage(MSG_NATIVE_REQ_GET_RC_FEATURES, features, 0,
- Utils.getAddressStringFromByte(address));
- handler.sendMessage(msg);
- }
-
- private void getPlayStatusRequestFromNative(byte[] address) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "getPlayStatusRequestFromNative: mHandler is already null");
- return;
- }
-
- Message msg = handler.obtainMessage(MSG_NATIVE_REQ_GET_PLAY_STATUS);
- msg.obj = address;
- handler.sendMessage(msg);
- }
-
- private void getElementAttrRequestFromNative(byte[] address, byte numAttr, int[] attrs) {
- AvrcpCmd avrcpCmdobj = new AvrcpCmd();
- AvrcpCmd.ElementAttrCmd elemAttr = avrcpCmdobj.new ElementAttrCmd(address, numAttr, attrs);
- Message msg = mHandler.obtainMessage(MSG_NATIVE_REQ_GET_ELEM_ATTRS);
- msg.obj = elemAttr;
- mHandler.sendMessage(msg);
- }
-
- private void registerNotificationRequestFromNative(byte[] address, int eventId, int param) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) {
- Log.d(TAG, "registerNotificationRequestFromNative: mHandler is already null");
- }
- return;
- }
- Message msg = handler.obtainMessage(MSG_NATIVE_REQ_REGISTER_NOTIFICATION, eventId, param);
- msg.obj = address;
- handler.sendMessage(msg);
- }
-
- private void processRegisterNotification(byte[] address, int eventId, int param) {
- switch (eventId) {
- case EVT_PLAY_STATUS_CHANGED:
- mPlayStatusChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
- updatePlaybackState();
- sendPlaybackStatus(AvrcpConstants.NOTIFICATION_TYPE_INTERIM, mReportedPlayStatus);
- break;
-
- case EVT_TRACK_CHANGED:
- Log.v(TAG, "Track changed notification enabled");
- mTrackChangedNT = AvrcpConstants.NOTIFICATION_TYPE_INTERIM;
- sendTrackChangedRsp(true);
- break;
-
- case EVT_PLAY_POS_CHANGED:
- mPlayPosChangedNT = AvrcpConstants.NOTIFICATION_TYPE_INTERIM;
- mPlaybackIntervalMs = (long) param * 1000L;
- sendPlayPosNotificationRsp(true);
- break;
-
- case EVT_AVBL_PLAYERS_CHANGED:
- /* Notify remote available players changed */
- if (DEBUG) {
- Log.d(TAG, "Available Players notification enabled");
- }
- registerNotificationRspAvalPlayerChangedNative(
- AvrcpConstants.NOTIFICATION_TYPE_INTERIM);
- break;
-
- case EVT_ADDR_PLAYER_CHANGED:
- /* Notify remote addressed players changed */
- if (DEBUG) {
- Log.d(TAG, "Addressed Player notification enabled");
- }
- registerNotificationRspAddrPlayerChangedNative(
- AvrcpConstants.NOTIFICATION_TYPE_INTERIM, mCurrAddrPlayerID, sUIDCounter);
- mAddrPlayerChangedNT = AvrcpConstants.NOTIFICATION_TYPE_INTERIM;
- mReportedPlayerID = mCurrAddrPlayerID;
- break;
-
- case EVENT_UIDS_CHANGED:
- if (DEBUG) {
- Log.d(TAG, "UIDs changed notification enabled");
- }
- registerNotificationRspUIDsChangedNative(AvrcpConstants.NOTIFICATION_TYPE_INTERIM,
- sUIDCounter);
- break;
-
- case EVENT_NOW_PLAYING_CONTENT_CHANGED:
- if (DEBUG) {
- Log.d(TAG, "Now Playing List changed notification enabled");
- }
- /* send interim response to remote device */
- mNowPlayingListChangedNT = AvrcpConstants.NOTIFICATION_TYPE_INTERIM;
- if (!registerNotificationRspNowPlayingChangedNative(
- AvrcpConstants.NOTIFICATION_TYPE_INTERIM)) {
- Log.e(TAG, "EVENT_NOW_PLAYING_CONTENT_CHANGED: "
- + "registerNotificationRspNowPlayingChangedNative for Interim rsp "
- + "failed!");
- }
- break;
- }
- }
-
- private void handlePassthroughCmdRequestFromNative(byte[] address, int id, int keyState) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) {
- Log.d(TAG, "handlePassthroughCmdRequestFromNative: mHandler is already null");
- }
- return;
- }
-
- Message msg = handler.obtainMessage(MSG_NATIVE_REQ_PASS_THROUGH, id, keyState);
- handler.sendMessage(msg);
- }
-
- private void sendTrackChangedRsp(boolean registering) {
- if (!registering && mTrackChangedNT != AvrcpConstants.NOTIFICATION_TYPE_INTERIM) {
- if (DEBUG) {
- Log.d(TAG, "sendTrackChangedRsp: Not registered or registering.");
- }
- return;
- }
-
- mTrackChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
- if (registering) {
- mTrackChangedNT = AvrcpConstants.NOTIFICATION_TYPE_INTERIM;
- }
-
- MediaPlayerInfo info = getAddressedPlayerInfo();
- // for non-browsable players or no player
- if (info != null && !info.isBrowseSupported()) {
- byte[] track = AvrcpConstants.TRACK_IS_SELECTED;
- if (!mMediaAttributes.mExists) {
- track = AvrcpConstants.NO_TRACK_SELECTED;
- }
- registerNotificationRspTrackChangeNative(mTrackChangedNT, track);
- return;
- }
-
- mAddressedMediaPlayer.sendTrackChangeWithId(mTrackChangedNT, mMediaController);
- }
-
- private long getPlayPosition() {
- if (mCurrentPlayState == null) {
- return -1L;
- }
-
- if (mCurrentPlayState.getPosition() == PlaybackState.PLAYBACK_POSITION_UNKNOWN) {
- return -1L;
- }
-
- if (isPlayingState(mCurrentPlayState)) {
- long sinceUpdate =
- (SystemClock.elapsedRealtime() - mCurrentPlayState.getLastPositionUpdateTime());
- return sinceUpdate + mCurrentPlayState.getPosition();
- }
-
- return mCurrentPlayState.getPosition();
- }
-
- private boolean isPlayingState(@Nullable PlaybackState state) {
- if (state == null) {
- return false;
- }
- return (state != null) && (state.getState() == PlaybackState.STATE_PLAYING);
- }
-
- /**
- * Sends a play position notification, or schedules one to be
- * sent later at an appropriate time. If |requested| is true,
- * does both because this was called in reponse to a request from the
- * TG.
- */
- private void sendPlayPosNotificationRsp(boolean requested) {
- if (!requested && mPlayPosChangedNT != AvrcpConstants.NOTIFICATION_TYPE_INTERIM) {
- if (DEBUG) {
- Log.d(TAG, "sendPlayPosNotificationRsp: Not registered or requesting.");
- }
- return;
- }
-
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "sendPlayPosNotificationRsp: handler is already null");
- return;
- }
-
- long playPositionMs = getPlayPosition();
- String debugLine = "sendPlayPosNotificationRsp: ";
-
- // mNextPosMs is set to -1 when the previous position was invalid
- // so this will be true if the new position is valid & old was invalid.
- // mPlayPositionMs is set to -1 when the new position is invalid,
- // and the old mPrevPosMs is >= 0 so this is true when the new is invalid
- // and the old was valid.
- if (DEBUG) {
- debugLine += "(" + requested + ") " + mPrevPosMs + " <=? " + playPositionMs + " <=? "
- + mNextPosMs;
- if (isPlayingState(mCurrentPlayState)) {
- debugLine += " Playing";
- }
- debugLine += " State: " + mCurrentPlayState.getState();
- }
- if (requested || (
- (mLastReportedPosition != playPositionMs) && (playPositionMs >= mNextPosMs) || (
- playPositionMs <= mPrevPosMs))) {
- if (!requested) {
- mPlayPosChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
- }
- registerNotificationRspPlayPosNative(mPlayPosChangedNT, (int) playPositionMs);
- mLastReportedPosition = playPositionMs;
- if (playPositionMs != PlaybackState.PLAYBACK_POSITION_UNKNOWN) {
- mNextPosMs = playPositionMs + mPlaybackIntervalMs;
- mPrevPosMs = playPositionMs - mPlaybackIntervalMs;
- } else {
- mNextPosMs = -1;
- mPrevPosMs = -1;
- }
- }
-
- handler.removeMessages(MSG_PLAY_INTERVAL_TIMEOUT);
- if (mPlayPosChangedNT == AvrcpConstants.NOTIFICATION_TYPE_INTERIM && isPlayingState(
- mCurrentPlayState)) {
- Message msg = handler.obtainMessage(MSG_PLAY_INTERVAL_TIMEOUT);
- long delay = mPlaybackIntervalMs;
- if (mNextPosMs != -1) {
- delay = mNextPosMs - (playPositionMs > 0 ? playPositionMs : 0);
- }
- if (DEBUG) {
- debugLine += " Timeout " + delay + "ms";
- }
- handler.sendMessageDelayed(msg, delay);
- }
- if (DEBUG) {
- Log.d(TAG, debugLine);
- }
- }
-
- /**
- * This is called from AudioService. It will return whether this device supports abs volume.
- * NOT USED AT THE MOMENT.
- */
- public boolean isAbsoluteVolumeSupported() {
- return ((mFeatures & BTRC_FEAT_ABSOLUTE_VOLUME) != 0);
- }
-
- /**
- * We get this call from AudioService. This will send a message to our handler object,
- * requesting our handler to call setVolumeNative()
- */
- public void setAbsoluteVolume(int volume) {
- if (volume == mLocalVolume) {
- if (DEBUG) {
- Log.v(TAG, "setAbsoluteVolume is setting same index, ignore " + volume);
- }
- return;
- }
-
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "setAbsoluteVolume: mHandler is already null");
- return;
- }
-
- Message msg = handler.obtainMessage(MSG_SET_ABSOLUTE_VOLUME, volume, 0);
- handler.sendMessage(msg);
- }
-
- /* Called in the native layer as a btrc_callback to return the volume set on the carkit in the
- * case when the volume is change locally on the carkit. This notification is not called when
- * the volume is changed from the phone.
- *
- * This method will send a message to our handler to change the local stored volume and notify
- * AudioService to update the UI
- */
- private void volumeChangeRequestFromNative(byte[] address, int volume, int ctype) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "volumeChangeRequestFromNative: mHandler is already null");
- return;
- }
-
- Message msg = handler.obtainMessage(MSG_NATIVE_REQ_VOLUME_CHANGE, volume, ctype);
- Bundle data = new Bundle();
- data.putByteArray("BdAddress", address);
- msg.setData(data);
- handler.sendMessage(msg);
- }
-
- private void getFolderItemsRequestFromNative(byte[] address, byte scope, long startItem,
- long endItem, byte numAttr, int[] attrIds) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "getFolderItemsRequestFromNative: mHandler is already null");
- return;
- }
- AvrcpCmd avrcpCmdobj = new AvrcpCmd();
- AvrcpCmd.FolderItemsCmd folderObj =
- avrcpCmdobj.new FolderItemsCmd(address, scope, startItem, endItem, numAttr,
- attrIds);
- Message msg = handler.obtainMessage(MSG_NATIVE_REQ_GET_FOLDER_ITEMS, 0, 0);
- msg.obj = folderObj;
- handler.sendMessage(msg);
- }
-
- private void setAddressedPlayerRequestFromNative(byte[] address, int playerId) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "setAddressedPlayerRequestFromNative: mHandler is already null");
- return;
- }
-
- Message msg = handler.obtainMessage(MSG_NATIVE_REQ_SET_ADDR_PLAYER, playerId, 0);
- msg.obj = address;
- handler.sendMessage(msg);
- }
-
- private void setBrowsedPlayerRequestFromNative(byte[] address, int playerId) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "setBrowsedPlayerRequestFromNative: mHandler is already null");
- return;
- }
-
- Message msg = handler.obtainMessage(MSG_NATIVE_REQ_SET_BR_PLAYER, playerId, 0);
- msg.obj = address;
- handler.sendMessage(msg);
- }
-
- private void changePathRequestFromNative(byte[] address, byte direction, byte[] folderUid) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "changePathRequestFromNative: mHandler is already null");
- return;
- }
-
- Bundle data = new Bundle();
- Message msg = handler.obtainMessage(MSG_NATIVE_REQ_CHANGE_PATH);
- data.putByteArray("BdAddress", address);
- data.putByteArray("folderUid", folderUid);
- data.putByte("direction", direction);
- msg.setData(data);
- handler.sendMessage(msg);
- }
-
- private void getItemAttrRequestFromNative(byte[] address, byte scope, byte[] itemUid,
- int uidCounter, byte numAttr, int[] attrs) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "getItemAttrRequestFromNative: mHandler is already null");
- return;
- }
- AvrcpCmd avrcpCmdobj = new AvrcpCmd();
- AvrcpCmd.ItemAttrCmd itemAttr =
- avrcpCmdobj.new ItemAttrCmd(address, scope, itemUid, uidCounter, numAttr, attrs);
- Message msg = handler.obtainMessage(MSG_NATIVE_REQ_GET_ITEM_ATTR);
- msg.obj = itemAttr;
- handler.sendMessage(msg);
- }
-
- private void searchRequestFromNative(byte[] address, int charsetId, byte[] searchStr) {
- /* Search is not supported */
- Log.w(TAG, "searchRequestFromNative: search is not supported");
- searchRspNative(address, AvrcpConstants.RSP_SRCH_NOT_SPRTD, 0, 0);
- }
-
- private void playItemRequestFromNative(byte[] address, byte scope, int uidCounter, byte[] uid) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "playItemRequestFromNative: mHandler is already null");
- return;
- }
-
- Bundle data = new Bundle();
- Message msg = handler.obtainMessage(MSG_NATIVE_REQ_PLAY_ITEM);
- data.putByteArray("BdAddress", address);
- data.putByteArray("uid", uid);
- data.putInt("uidCounter", uidCounter);
- data.putByte("scope", scope);
-
- msg.setData(data);
- handler.sendMessage(msg);
- }
-
- private void addToPlayListRequestFromNative(byte[] address, byte scope, byte[] uid,
- int uidCounter) {
- /* add to NowPlaying not supported */
- Log.w(TAG, "addToPlayListRequestFromNative: not supported! scope=" + scope);
- addToNowPlayingRspNative(address, AvrcpConstants.RSP_INTERNAL_ERR);
- }
-
- private void getTotalNumOfItemsRequestFromNative(byte[] address, byte scope) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "getTotalNumOfItemsRequestFromNative: mHandler is already null");
- return;
- }
-
- Bundle data = new Bundle();
- Message msg = handler.obtainMessage(MSG_NATIVE_REQ_GET_TOTAL_NUM_OF_ITEMS);
- msg.arg1 = scope;
- msg.obj = address;
- handler.sendMessage(msg);
- }
-
- private void notifyVolumeChanged(int volume) {
- mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume,
- AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_BLUETOOTH_ABS_VOLUME);
- }
-
- private int convertToAudioStreamVolume(int volume) {
- // Rescale volume to match AudioSystem's volume
- return (int) Math.floor((double) volume * mAudioStreamMax / AVRCP_MAX_VOL);
- }
-
- private int convertToAvrcpVolume(int volume) {
- return (int) Math.ceil((double) volume * AVRCP_MAX_VOL / mAudioStreamMax);
- }
-
- private void blackListCurrentDevice(String reason) {
- mFeatures &= ~BTRC_FEAT_ABSOLUTE_VOLUME;
- mAudioManager.avrcpSupportsAbsoluteVolume(mAddress, isAbsoluteVolumeSupported());
-
- SharedPreferences pref =
- mContext.getSharedPreferences(ABSOLUTE_VOLUME_BLACKLIST, Context.MODE_PRIVATE);
- SharedPreferences.Editor editor = pref.edit();
-
- StringBuilder sb = new StringBuilder();
- sb.append("Time: ");
- sb.append(android.text.format.DateFormat.format("yyyy/MM/dd HH:mm:ss",
- System.currentTimeMillis()));
- sb.append(" Reason: ");
- sb.append(reason);
- editor.putString(mAddress, sb.toString());
- editor.apply();
- }
-
- private int modifyRcFeatureFromBlacklist(int feature, String address) {
- SharedPreferences pref =
- mContext.getSharedPreferences(ABSOLUTE_VOLUME_BLACKLIST, Context.MODE_PRIVATE);
- if (!pref.contains(address)) {
- return feature;
- }
- return feature & ~BTRC_FEAT_ABSOLUTE_VOLUME;
- }
-
- public void resetBlackList(String address) {
- SharedPreferences pref =
- mContext.getSharedPreferences(ABSOLUTE_VOLUME_BLACKLIST, Context.MODE_PRIVATE);
- SharedPreferences.Editor editor = pref.edit();
- editor.remove(address);
- editor.apply();
- }
-
- /**
- * This is called from A2dpStateMachine to set A2dp audio state.
- */
- public void setA2dpAudioState(int state) {
- final AvrcpMessageHandler handler = mHandler;
- if (handler == null) {
- if (DEBUG) Log.d(TAG, "setA2dpAudioState: mHandler is already null");
- return;
- }
-
- Message msg = handler.obtainMessage(MSG_SET_A2DP_AUDIO_STATE, state, 0);
- handler.sendMessage(msg);
- }
-
- private class AvrcpServiceBootReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (action.equals(Intent.ACTION_USER_UNLOCKED)) {
- if (DEBUG) {
- Log.d(TAG, "User unlocked, initializing player lists");
- }
- /* initializing media player's list */
- buildBrowsablePlayerList();
- }
- }
- }
-
- private class AvrcpServiceBroadcastReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (DEBUG) {
- Log.d(TAG, "AvrcpServiceBroadcastReceiver-> Action: " + action);
- }
-
- if (action.equals(Intent.ACTION_PACKAGE_REMOVED) || action.equals(
- Intent.ACTION_PACKAGE_DATA_CLEARED)) {
- if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
- // a package is being removed, not replaced
- String packageName = intent.getData().getSchemeSpecificPart();
- if (packageName != null) {
- handlePackageModified(packageName, true);
- }
- }
-
- } else if (action.equals(Intent.ACTION_PACKAGE_ADDED) || action.equals(
- Intent.ACTION_PACKAGE_CHANGED)) {
- String packageName = intent.getData().getSchemeSpecificPart();
- if (DEBUG) {
- Log.d(TAG, "AvrcpServiceBroadcastReceiver-> packageName: " + packageName);
- }
- if (packageName != null) {
- handlePackageModified(packageName, false);
- }
- }
- }
- }
-
- private void handlePackageModified(String packageName, boolean removed) {
- if (DEBUG) {
- Log.d(TAG, "packageName: " + packageName + " removed: " + removed);
- }
-
- if (removed) {
- removeMediaPlayerInfo(packageName);
- // old package is removed, updating local browsable player's list
- if (isBrowseSupported(packageName)) {
- removePackageFromBrowseList(packageName);
- }
- } else {
- // new package has been added.
- if (isBrowsableListUpdated(packageName)) {
- // Rebuilding browsable players list
- buildBrowsablePlayerList();
- }
- }
- }
-
- private boolean isBrowsableListUpdated(String newPackageName) {
- // getting the browsable media players list from package manager
- Intent intent = new Intent("android.media.browse.MediaBrowserService");
- List<ResolveInfo> resInfos =
- mPackageManager.queryIntentServices(intent, PackageManager.MATCH_ALL);
- for (ResolveInfo resolveInfo : resInfos) {
- if (resolveInfo.serviceInfo.packageName.equals(newPackageName)) {
- if (DEBUG) {
- Log.d(TAG,
- "isBrowsableListUpdated: package includes MediaBrowserService, true");
- }
- return true;
- }
- }
-
- // if list has different size
- if (resInfos.size() != mBrowsePlayerInfoList.size()) {
- if (DEBUG) {
- Log.d(TAG, "isBrowsableListUpdated: browsable list size mismatch, true");
- }
- return true;
- }
-
- Log.d(TAG, "isBrowsableListUpdated: false");
- return false;
- }
-
- private void removePackageFromBrowseList(String packageName) {
- if (DEBUG) {
- Log.d(TAG, "removePackageFromBrowseList: " + packageName);
- }
- synchronized (mBrowsePlayerInfoList) {
- int browseInfoID = getBrowseId(packageName);
- if (browseInfoID != -1) {
- mBrowsePlayerInfoList.remove(browseInfoID);
- }
- }
- }
-
- /*
- * utility function to get the browse player index from global browsable
- * list. It may return -1 if specified package name is not in the list.
- */
- private int getBrowseId(String packageName) {
- boolean response = false;
- int browseInfoID = 0;
- synchronized (mBrowsePlayerInfoList) {
- for (BrowsePlayerInfo info : mBrowsePlayerInfoList) {
- if (info.packageName.equals(packageName)) {
- response = true;
- break;
- }
- browseInfoID++;
- }
- }
-
- if (!response) {
- browseInfoID = -1;
- }
-
- if (DEBUG) {
- Log.d(TAG, "getBrowseId for packageName: " + packageName + " , browseInfoID: "
- + browseInfoID);
- }
- return browseInfoID;
- }
-
- private void setAddressedPlayer(byte[] bdaddr, int selectedId) {
- String functionTag = "setAddressedPlayer(" + selectedId + "): ";
-
- synchronized (mMediaPlayerInfoList) {
- if (mMediaPlayerInfoList.isEmpty()) {
- Log.w(TAG, functionTag + "no players, send no available players");
- setAddressedPlayerRspNative(bdaddr, AvrcpConstants.RSP_NO_AVBL_PLAY);
- return;
- }
- if (!mMediaPlayerInfoList.containsKey(selectedId)) {
- Log.w(TAG, functionTag + "invalid id, sending response back ");
- setAddressedPlayerRspNative(bdaddr, AvrcpConstants.RSP_INV_PLAYER);
- return;
- }
-
- if (isPlayerAlreadyAddressed(selectedId)) {
- MediaPlayerInfo info = getAddressedPlayerInfo();
- Log.i(TAG, functionTag + "player already addressed: " + info);
- setAddressedPlayerRspNative(bdaddr, AvrcpConstants.RSP_NO_ERROR);
- return;
- }
- // register new Media Controller Callback and update the current IDs
- if (!updateCurrentController(selectedId, mCurrBrowsePlayerID)) {
- Log.e(TAG, functionTag + "updateCurrentController failed!");
- setAddressedPlayerRspNative(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
- return;
- }
- // If we don't have a controller, try to launch the player
- MediaPlayerInfo info = getAddressedPlayerInfo();
- if (info.getMediaController() == null) {
- Intent launch = mPackageManager.getLaunchIntentForPackage(info.getPackageName());
- Log.i(TAG, functionTag + "launching player " + launch);
- mContext.startActivity(launch);
- }
- }
- setAddressedPlayerRspNative(bdaddr, AvrcpConstants.RSP_NO_ERROR);
- }
-
- private void setBrowsedPlayer(byte[] bdaddr, int selectedId) {
- int status = AvrcpConstants.RSP_NO_ERROR;
-
- // checking for error cases
- if (mMediaPlayerInfoList.isEmpty()) {
- status = AvrcpConstants.RSP_NO_AVBL_PLAY;
- Log.w(TAG, "setBrowsedPlayer: No available players! ");
- } else {
- // Workaround for broken controllers selecting ID 0
- // Seen at least on Ford, Chevrolet MyLink
- if (selectedId == 0) {
- Log.w(TAG, "setBrowsedPlayer: workaround invalid id 0");
- selectedId = mCurrAddrPlayerID;
- }
-
- // update current browse player id and start browsing service
- updateNewIds(mCurrAddrPlayerID, selectedId);
- String browsedPackage = getPackageName(selectedId);
-
- if (!isPackageNameValid(browsedPackage)) {
- Log.w(TAG, " Invalid package for id:" + mCurrBrowsePlayerID);
- status = AvrcpConstants.RSP_INV_PLAYER;
- } else if (!isBrowseSupported(browsedPackage)) {
- Log.w(TAG, "Browse unsupported for id:" + mCurrBrowsePlayerID + ", packagename : "
- + browsedPackage);
- status = AvrcpConstants.RSP_PLAY_NOT_BROW;
- } else if (!startBrowseService(bdaddr, browsedPackage)) {
- Log.e(TAG, "service cannot be started for browse player id:" + mCurrBrowsePlayerID
- + ", packagename : " + browsedPackage);
- status = AvrcpConstants.RSP_INTERNAL_ERR;
- }
- }
-
- if (status != AvrcpConstants.RSP_NO_ERROR) {
- setBrowsedPlayerRspNative(bdaddr, status, (byte) 0x00, 0, null);
- }
-
- if (DEBUG) {
- Log.d(TAG, "setBrowsedPlayer for selectedId: " + selectedId + " , status: " + status);
- }
- }
-
- private MediaSessionManager.OnActiveSessionsChangedListener mActiveSessionListener =
- new MediaSessionManager.OnActiveSessionsChangedListener() {
-
- @Override
- public void onActiveSessionsChanged(
- List<android.media.session.MediaController> newControllers) {
- Set<String> updatedPackages = new HashSet<String>();
- // Update the current players
- for (android.media.session.MediaController controller : newControllers) {
- String packageName = controller.getPackageName();
- if (DEBUG) {
- Log.v(TAG, "ActiveSession: " + MediaControllerFactory.wrap(controller));
- }
- // Only use the first (highest priority) controller from each package
- if (updatedPackages.contains(packageName)) {
- continue;
- }
- addMediaPlayerController(controller);
- updatedPackages.add(packageName);
- }
-
- if (newControllers.size() > 0 && getAddressedPlayerInfo() == null) {
- if (DEBUG) {
- Log.v(TAG, "No addressed player but active sessions, taking first.");
- }
- setAddressedMediaSessionPackage(newControllers.get(0).getPackageName());
- }
- updateCurrentMediaState();
- }
- };
-
- private void setAddressedMediaSessionPackage(@Nullable String packageName) {
- if (packageName == null) {
- // Should only happen when there's no media players, reset to no available player.
- updateCurrentController(0, mCurrBrowsePlayerID);
- return;
- }
- if (packageName.equals("com.android.server.telecom")) {
- Log.d(TAG, "Ignore addressed media session change to telecom");
- return;
- }
- // No change.
- if (getPackageName(mCurrAddrPlayerID).equals(packageName)) {
- return;
- }
- if (DEBUG) {
- Log.v(TAG, "Changing addressed media session to " + packageName);
- }
- // If the player doesn't exist, we need to add it.
- if (getMediaPlayerInfo(packageName) == null) {
- addMediaPlayerPackage(packageName);
- updateCurrentMediaState();
- }
- synchronized (mMediaPlayerInfoList) {
- for (Map.Entry<Integer, MediaPlayerInfo> entry : mMediaPlayerInfoList.entrySet()) {
- if (entry.getValue().getPackageName().equals(packageName)) {
- int newAddrID = entry.getKey();
- if (DEBUG) {
- Log.v(TAG, "Set addressed #" + newAddrID + " " + entry.getValue());
- }
- updateCurrentController(newAddrID, mCurrBrowsePlayerID);
- updateCurrentMediaState();
- return;
- }
- }
- }
- // We shouldn't ever get here.
- Log.e(TAG, "Player info for " + packageName + " doesn't exist!");
- }
-
- private void setActiveMediaSession(MediaSession.Token token) {
- android.media.session.MediaController activeController =
- new android.media.session.MediaController(mContext, token);
- if (activeController.getPackageName().equals("com.android.server.telecom")) {
- Log.d(TAG, "Ignore active media session change to telecom");
- return;
- }
- if (DEBUG) {
- Log.v(TAG, "Set active media session " + activeController.getPackageName());
- }
- addMediaPlayerController(activeController);
- setAddressedMediaSessionPackage(activeController.getPackageName());
- }
-
- private boolean startBrowseService(byte[] bdaddr, String packageName) {
- boolean status = true;
-
- /* creating new instance for Browse Media Player */
- String browseService = getBrowseServiceName(packageName);
- if (!browseService.isEmpty()) {
- mAvrcpBrowseManager.getBrowsedMediaPlayer(bdaddr)
- .setBrowsed(packageName, browseService);
- } else {
- Log.w(TAG, "No Browser service available for " + packageName);
- status = false;
- }
-
- if (DEBUG) {
- Log.d(TAG,
- "startBrowseService for packageName: " + packageName + ", status = " + status);
- }
- return status;
- }
-
- private String getBrowseServiceName(String packageName) {
- String browseServiceName = "";
-
- // getting the browse service name from browse player info
- synchronized (mBrowsePlayerInfoList) {
- int browseInfoID = getBrowseId(packageName);
- if (browseInfoID != -1) {
- browseServiceName = mBrowsePlayerInfoList.get(browseInfoID).serviceClass;
- }
- }
-
- if (DEBUG) {
- Log.d(TAG, "getBrowseServiceName for packageName: " + packageName
- + ", browseServiceName = " + browseServiceName);
- }
- return browseServiceName;
- }
-
- void buildBrowsablePlayerList() {
- synchronized (mBrowsePlayerInfoList) {
- mBrowsePlayerInfoList.clear();
- Intent intent = new Intent(android.service.media.MediaBrowserService.SERVICE_INTERFACE);
- List<ResolveInfo> playerList =
- mPackageManager.queryIntentServices(intent, PackageManager.MATCH_ALL);
-
- for (ResolveInfo info : playerList) {
- String displayableName = info.loadLabel(mPackageManager).toString();
- String serviceName = info.serviceInfo.name;
- String packageName = info.serviceInfo.packageName;
-
- if (DEBUG) {
- Log.d(TAG, "Adding " + serviceName + " to list of browsable players");
- }
- BrowsePlayerInfo currentPlayer =
- new BrowsePlayerInfo(packageName, displayableName, serviceName);
- mBrowsePlayerInfoList.add(currentPlayer);
- MediaPlayerInfo playerInfo = getMediaPlayerInfo(packageName);
- MediaController controller =
- (playerInfo == null) ? null : playerInfo.getMediaController();
- // Refresh the media player entry so it notices we can browse
- if (controller != null) {
- addMediaPlayerController(controller.getWrappedInstance());
- } else {
- addMediaPlayerPackage(packageName);
- }
- }
- updateCurrentMediaState();
- }
- }
-
- /* Initializes list of media players identified from session manager active sessions */
- private void initMediaPlayersList() {
- synchronized (mMediaPlayerInfoList) {
- // Clearing old browsable player's list
- mMediaPlayerInfoList.clear();
-
- if (mMediaSessionManager == null) {
- if (DEBUG) {
- Log.w(TAG, "initMediaPlayersList: no media session manager!");
- }
- return;
- }
-
- List<android.media.session.MediaController> controllers =
- mMediaSessionManager.getActiveSessions(null);
- if (DEBUG) {
- Log.v(TAG, "initMediaPlayerInfoList: " + controllers.size() + " controllers");
- }
- /* Initializing all media players */
- for (android.media.session.MediaController controller : controllers) {
- addMediaPlayerController(controller);
- }
-
- updateCurrentMediaState();
-
- if (mMediaPlayerInfoList.size() > 0) {
- // Set the first one as the Addressed Player
- updateCurrentController(mMediaPlayerInfoList.firstKey(), -1);
- }
- }
- }
-
- private List<android.media.session.MediaController> getMediaControllers() {
- List<android.media.session.MediaController> controllers =
- new ArrayList<android.media.session.MediaController>();
- synchronized (mMediaPlayerInfoList) {
- for (MediaPlayerInfo info : mMediaPlayerInfoList.values()) {
- MediaController controller = info.getMediaController();
- if (controller != null) {
- controllers.add(controller.getWrappedInstance());
- }
- }
- }
- return controllers;
- }
-
- /** Add (or update) a player to the media player list without a controller */
- private boolean addMediaPlayerPackage(String packageName) {
- MediaPlayerInfo info = new MediaPlayerInfo(null, AvrcpConstants.PLAYER_TYPE_AUDIO,
- AvrcpConstants.PLAYER_SUBTYPE_NONE, PLAYSTATUS_STOPPED,
- getFeatureBitMask(packageName), packageName, getAppLabel(packageName));
- return addMediaPlayerInfo(info);
- }
-
- /** Add (or update) a player to the media player list given an active controller */
- private boolean addMediaPlayerController(android.media.session.MediaController controller) {
- String packageName = controller.getPackageName();
- MediaPlayerInfo info = new MediaPlayerInfo(MediaControllerFactory.wrap(controller),
- AvrcpConstants.PLAYER_TYPE_AUDIO, AvrcpConstants.PLAYER_SUBTYPE_NONE,
- getBluetoothPlayState(controller.getPlaybackState()),
- getFeatureBitMask(packageName), controller.getPackageName(),
- getAppLabel(packageName));
- return addMediaPlayerInfo(info);
- }
-
- /** Add or update a player to the media player list given the MediaPlayerInfo object.
- * @return true if an item was updated, false if it was added instead
- */
- private boolean addMediaPlayerInfo(MediaPlayerInfo info) {
- int updateId = -1;
- boolean updated = false;
- boolean currentRemoved = false;
- if (info.getPackageName().equals("com.android.server.telecom")) {
- Log.d(TAG, "Skip adding telecom to the media player info list");
- return updated;
- }
- synchronized (mMediaPlayerInfoList) {
- for (Map.Entry<Integer, MediaPlayerInfo> entry : mMediaPlayerInfoList.entrySet()) {
- MediaPlayerInfo current = entry.getValue();
- int id = entry.getKey();
- if (info.getPackageName().equals(current.getPackageName())) {
- if (!current.equalView(info)) {
- // If we would present a different player, make it a new player
- // so that controllers know whether a player is browsable or not.
- mMediaPlayerInfoList.remove(id);
- currentRemoved = (mCurrAddrPlayerID == id);
- break;
- }
- updateId = id;
- updated = true;
- break;
- }
- }
- if (updateId == -1) {
- // New player
- mLastUsedPlayerID++;
- updateId = mLastUsedPlayerID;
- mAvailablePlayerViewChanged = true;
- }
- mMediaPlayerInfoList.put(updateId, info);
- }
- if (DEBUG) {
- Log.d(TAG, (updated ? "update #" : "add #") + updateId + ":" + info.toString());
- }
- if (currentRemoved || updateId == mCurrAddrPlayerID) {
- updateCurrentController(updateId, mCurrBrowsePlayerID);
- }
- return updated;
- }
-
- /** Remove all players related to |packageName| from the media player info list */
- private MediaPlayerInfo removeMediaPlayerInfo(String packageName) {
- synchronized (mMediaPlayerInfoList) {
- int removeKey = -1;
- for (Map.Entry<Integer, MediaPlayerInfo> entry : mMediaPlayerInfoList.entrySet()) {
- if (entry.getValue().getPackageName().equals(packageName)) {
- removeKey = entry.getKey();
- break;
- }
- }
- if (removeKey != -1) {
- if (DEBUG) {
- Log.d(TAG, "remove #" + removeKey + ":" + mMediaPlayerInfoList.get(removeKey));
- }
- mAvailablePlayerViewChanged = true;
- return mMediaPlayerInfoList.remove(removeKey);
- }
-
- return null;
- }
- }
-
- /** Remove the controller referenced by |controller| from any player in the list */
- private void removeMediaController(@Nullable android.media.session.MediaController controller) {
- if (controller == null) {
- return;
- }
- synchronized (mMediaPlayerInfoList) {
- for (Map.Entry<Integer, MediaPlayerInfo> entry : mMediaPlayerInfoList.entrySet()) {
- MediaPlayerInfo info = entry.getValue();
- MediaController c = info.getMediaController();
- if (c != null && c.equals(controller)) {
- info.setMediaController(null);
- if (entry.getKey() == mCurrAddrPlayerID) {
- updateCurrentController(mCurrAddrPlayerID, mCurrBrowsePlayerID);
- }
- }
- }
- }
- }
-
- /*
- * utility function to get the playback state of any media player through
- * media controller APIs.
- */
- private byte getBluetoothPlayState(PlaybackState pbState) {
- if (pbState == null) {
- Log.w(TAG, "playState object null, sending STOPPED");
- return PLAYSTATUS_STOPPED;
- }
-
- switch (pbState.getState()) {
- case PlaybackState.STATE_PLAYING:
- return PLAYSTATUS_PLAYING;
-
- case PlaybackState.STATE_BUFFERING:
- case PlaybackState.STATE_STOPPED:
- case PlaybackState.STATE_NONE:
- case PlaybackState.STATE_CONNECTING:
- return PLAYSTATUS_STOPPED;
-
- case PlaybackState.STATE_PAUSED:
- return PLAYSTATUS_PAUSED;
-
- case PlaybackState.STATE_FAST_FORWARDING:
- case PlaybackState.STATE_SKIPPING_TO_NEXT:
- case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM:
- return PLAYSTATUS_FWD_SEEK;
-
- case PlaybackState.STATE_REWINDING:
- case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
- return PLAYSTATUS_REV_SEEK;
-
- case PlaybackState.STATE_ERROR:
- default:
- return PLAYSTATUS_ERROR;
- }
- }
-
- /*
- * utility function to get the feature bit mask of any media player through
- * package name
- */
- private short[] getFeatureBitMask(String packageName) {
-
- ArrayList<Short> featureBitsList = new ArrayList<Short>();
-
- /* adding default feature bits */
- featureBitsList.add(AvrcpConstants.AVRC_PF_PLAY_BIT_NO);
- featureBitsList.add(AvrcpConstants.AVRC_PF_STOP_BIT_NO);
- featureBitsList.add(AvrcpConstants.AVRC_PF_PAUSE_BIT_NO);
- featureBitsList.add(AvrcpConstants.AVRC_PF_REWIND_BIT_NO);
- featureBitsList.add(AvrcpConstants.AVRC_PF_FAST_FWD_BIT_NO);
- featureBitsList.add(AvrcpConstants.AVRC_PF_FORWARD_BIT_NO);
- featureBitsList.add(AvrcpConstants.AVRC_PF_BACKWARD_BIT_NO);
- featureBitsList.add(AvrcpConstants.AVRC_PF_ADV_CTRL_BIT_NO);
-
- /* Add/Modify browse player supported features. */
- if (isBrowseSupported(packageName)) {
- featureBitsList.add(AvrcpConstants.AVRC_PF_BROWSE_BIT_NO);
- featureBitsList.add(AvrcpConstants.AVRC_PF_UID_UNIQUE_BIT_NO);
- featureBitsList.add(AvrcpConstants.AVRC_PF_NOW_PLAY_BIT_NO);
- featureBitsList.add(AvrcpConstants.AVRC_PF_GET_NUM_OF_ITEMS_BIT_NO);
- }
-
- // converting arraylist to array for response
- short[] featureBitsArray = new short[featureBitsList.size()];
-
- for (int i = 0; i < featureBitsList.size(); i++) {
- featureBitsArray[i] = featureBitsList.get(i).shortValue();
- }
-
- return featureBitsArray;
- }
-
- /**
- * Checks the Package name if it supports Browsing or not.
- *
- * @param packageName - name of the package to get the Id.
- * @return true if it supports browsing, else false.
- */
- private boolean isBrowseSupported(String packageName) {
- synchronized (mBrowsePlayerInfoList) {
- /* check if Browsable Player's list contains this package name */
- for (BrowsePlayerInfo info : mBrowsePlayerInfoList) {
- if (info.packageName.equals(packageName)) {
- if (DEBUG) {
- Log.v(TAG, "isBrowseSupported for " + packageName + ": true");
- }
- return true;
- }
- }
- }
-
- if (DEBUG) {
- Log.v(TAG, "isBrowseSupported for " + packageName + ": false");
- }
- return false;
- }
-
- private String getPackageName(int id) {
- MediaPlayerInfo player = null;
- synchronized (mMediaPlayerInfoList) {
- player = mMediaPlayerInfoList.getOrDefault(id, null);
- }
-
- if (player == null) {
- Log.w(TAG, "No package name for player (" + id + " not valid)");
- return "";
- }
-
- String packageName = player.getPackageName();
- if (DEBUG) {
- Log.v(TAG, "Player " + id + " package: " + packageName);
- }
- return packageName;
- }
-
- /* from the global object, getting the current browsed player's package name */
- private String getCurrentBrowsedPlayer(byte[] bdaddr) {
- String browsedPlayerPackage = "";
-
- Map<String, BrowsedMediaPlayer> connList = mAvrcpBrowseManager.getConnList();
- String bdaddrStr = new String(bdaddr);
- if (connList.containsKey(bdaddrStr)) {
- browsedPlayerPackage = connList.get(bdaddrStr).getPackageName();
- }
- if (DEBUG) {
- Log.v(TAG, "getCurrentBrowsedPlayerPackage: " + browsedPlayerPackage);
- }
- return browsedPlayerPackage;
- }
-
- /* Returns the MediaPlayerInfo for the currently addressed media player */
- private MediaPlayerInfo getAddressedPlayerInfo() {
- synchronized (mMediaPlayerInfoList) {
- return mMediaPlayerInfoList.getOrDefault(mCurrAddrPlayerID, null);
- }
- }
-
- /*
- * Utility function to get the Media player info from package name returns
- * null if package name not found in media players list
- */
- private MediaPlayerInfo getMediaPlayerInfo(String packageName) {
- synchronized (mMediaPlayerInfoList) {
- if (mMediaPlayerInfoList.isEmpty()) {
- if (DEBUG) {
- Log.v(TAG, "getMediaPlayerInfo: Media players list empty");
- }
- return null;
- }
-
- for (MediaPlayerInfo info : mMediaPlayerInfoList.values()) {
- if (packageName.equals(info.getPackageName())) {
- if (DEBUG) {
- Log.v(TAG, "getMediaPlayerInfo: Found " + packageName);
- }
- return info;
- }
- }
- if (DEBUG) {
- Log.w(TAG, "getMediaPlayerInfo: " + packageName + " not found");
- }
- return null;
- }
- }
-
- /* prepare media list & return the media player list response object */
- private MediaPlayerListRsp prepareMediaPlayerRspObj() {
- synchronized (mMediaPlayerInfoList) {
- // TODO(apanicke): This hack will go away as soon as a developer
- // option to enable or disable player selection is created. Right
- // now this is needed to fix BMW i3 carkits and any other carkits
- // that might try to connect to a player that isnt the current
- // player based on this list
- int numPlayers = 1;
-
- int[] playerIds = new int[numPlayers];
- byte[] playerTypes = new byte[numPlayers];
- int[] playerSubTypes = new int[numPlayers];
- String[] displayableNameArray = new String[numPlayers];
- byte[] playStatusValues = new byte[numPlayers];
- short[] featureBitMaskValues =
- new short[numPlayers * AvrcpConstants.AVRC_FEATURE_MASK_SIZE];
-
- // Reserve the first spot for the currently addressed player if
- // we have one
- int players = mMediaPlayerInfoList.containsKey(mCurrAddrPlayerID) ? 1 : 0;
- for (Map.Entry<Integer, MediaPlayerInfo> entry : mMediaPlayerInfoList.entrySet()) {
- int idx = players;
- if (entry.getKey() == mCurrAddrPlayerID) {
- idx = 0;
- } else {
- continue; // TODO(apanicke): Remove, see above note
- }
- MediaPlayerInfo info = entry.getValue();
- playerIds[idx] = entry.getKey();
- playerTypes[idx] = info.getMajorType();
- playerSubTypes[idx] = info.getSubType();
- displayableNameArray[idx] = info.getDisplayableName();
- playStatusValues[idx] = info.getPlayStatus();
-
- short[] featureBits = info.getFeatureBitMask();
- for (int numBit = 0; numBit < featureBits.length; numBit++) {
- /* gives which octet this belongs to */
- byte octet = (byte) (featureBits[numBit] / 8);
- /* gives the bit position within the octet */
- byte bit = (byte) (featureBits[numBit] % 8);
- featureBitMaskValues[(idx * AvrcpConstants.AVRC_FEATURE_MASK_SIZE) + octet] |=
- (1 << bit);
- }
-
- /* printLogs */
- if (DEBUG) {
- Log.d(TAG, "Player " + playerIds[idx] + ": " + displayableNameArray[idx]
- + " type: " + playerTypes[idx] + ", " + playerSubTypes[idx]
- + " status: " + playStatusValues[idx]);
- }
-
- if (idx != 0) {
- players++;
- }
- }
-
- if (DEBUG) {
- Log.d(TAG, "prepareMediaPlayerRspObj: numPlayers = " + numPlayers);
- }
-
- return new MediaPlayerListRsp(AvrcpConstants.RSP_NO_ERROR, sUIDCounter, numPlayers,
- AvrcpConstants.BTRC_ITEM_PLAYER, playerIds, playerTypes, playerSubTypes,
- playStatusValues, featureBitMaskValues, displayableNameArray);
- }
- }
-
- /* build media player list and send it to remote. */
- private void handleMediaPlayerListRsp(AvrcpCmd.FolderItemsCmd folderObj) {
- MediaPlayerListRsp rspObj = null;
- synchronized (mMediaPlayerInfoList) {
- int numPlayers = mMediaPlayerInfoList.size();
- if (numPlayers == 0) {
- mediaPlayerListRspNative(folderObj.mAddress, AvrcpConstants.RSP_NO_AVBL_PLAY,
- (short) 0, (byte) 0, 0, null, null, null, null, null, null);
- return;
- }
- if (folderObj.mStartItem >= numPlayers) {
- Log.i(TAG, "handleMediaPlayerListRsp: start = " + folderObj.mStartItem
- + " > num of items = " + numPlayers);
- mediaPlayerListRspNative(folderObj.mAddress, AvrcpConstants.RSP_INV_RANGE,
- (short) 0, (byte) 0, 0, null, null, null, null, null, null);
- return;
- }
- rspObj = prepareMediaPlayerRspObj();
- }
- if (DEBUG) {
- Log.d(TAG, "handleMediaPlayerListRsp: sending " + rspObj.mNumItems + " players");
- }
- mediaPlayerListRspNative(folderObj.mAddress, rspObj.mStatus, rspObj.mUIDCounter,
- rspObj.mItemType, rspObj.mNumItems, rspObj.mPlayerIds, rspObj.mPlayerTypes,
- rspObj.mPlayerSubTypes, rspObj.mPlayStatusValues, rspObj.mFeatureBitMaskValues,
- rspObj.mPlayerNameList);
- }
-
- /* unregister to the old controller, update new IDs and register to the new controller */
- private boolean updateCurrentController(int addrId, int browseId) {
- boolean registerRsp = true;
-
- updateNewIds(addrId, browseId);
-
- MediaController newController = null;
- MediaPlayerInfo info = getAddressedPlayerInfo();
- if (info != null) {
- newController = info.getMediaController();
- }
-
- if (DEBUG) {
- Log.d(TAG, "updateCurrentController: " + mMediaController + " to " + newController);
- }
- synchronized (this) {
- if (mMediaController == null || (!mMediaController.equals(newController))) {
- if (mMediaController != null) {
- mMediaController.unregisterCallback(mMediaControllerCb);
- }
- mMediaController = newController;
- if (mMediaController != null) {
- mMediaController.registerCallback(mMediaControllerCb, mHandler);
- } else {
- registerRsp = false;
- }
- }
- }
- updateCurrentMediaState();
- return registerRsp;
- }
-
- /* Handle getfolderitems for scope = VFS, Search, NowPlayingList */
- private void handleGetFolderItemBrowseResponse(AvrcpCmd.FolderItemsCmd folderObj,
- byte[] bdaddr) {
- int status = AvrcpConstants.RSP_NO_ERROR;
-
- /* Browsed player is already set */
- if (folderObj.mScope == AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM) {
- if (mAvrcpBrowseManager.getBrowsedMediaPlayer(bdaddr) == null) {
- Log.e(TAG, "handleGetFolderItemBrowseResponse: no browsed player set for "
- + Utils.getAddressStringFromByte(bdaddr));
- getFolderItemsRspNative(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR, (short) 0,
- (byte) 0x00, 0, null, null, null, null, null, null, null, null);
- return;
- }
- mAvrcpBrowseManager.getBrowsedMediaPlayer(bdaddr).getFolderItemsVFS(folderObj);
- return;
- }
- if (folderObj.mScope == AvrcpConstants.BTRC_SCOPE_NOW_PLAYING) {
- mAddressedMediaPlayer.getFolderItemsNowPlaying(bdaddr, folderObj, mMediaController);
- return;
- }
-
- /* invalid scope */
- Log.e(TAG, "handleGetFolderItemBrowseResponse: unknown scope " + folderObj.mScope);
- getFolderItemsRspNative(bdaddr, AvrcpConstants.RSP_INV_SCOPE, (short) 0, (byte) 0x00, 0,
- null, null, null, null, null, null, null, null);
- }
-
- /* utility function to update the global values of current Addressed and browsed player */
- private void updateNewIds(int addrId, int browseId) {
- if (DEBUG) {
- Log.v(TAG,
- "updateNewIds: Addressed:" + mCurrAddrPlayerID + " to " + addrId + ", Browse:"
- + mCurrBrowsePlayerID + " to " + browseId);
- }
- mCurrAddrPlayerID = addrId;
- mCurrBrowsePlayerID = browseId;
- }
-
- /* Getting the application's displayable name from package name */
- private String getAppLabel(String packageName) {
- ApplicationInfo appInfo = null;
- try {
- appInfo = mPackageManager.getApplicationInfo(packageName, 0);
- } catch (NameNotFoundException e) {
- e.printStackTrace();
- }
-
- return (String) (appInfo != null ? mPackageManager.getApplicationLabel(appInfo)
- : "Unknown");
- }
-
- private void handlePlayItemResponse(byte[] bdaddr, byte[] uid, byte scope) {
- if (scope == AvrcpConstants.BTRC_SCOPE_NOW_PLAYING) {
- mAddressedMediaPlayer.playItem(bdaddr, uid, mMediaController);
- } else {
- if (!isAddrPlayerSameAsBrowsed(bdaddr)) {
- Log.w(TAG, "Remote requesting play item on uid which may not be recognized by"
- + "current addressed player");
- playItemRspNative(bdaddr, AvrcpConstants.RSP_INV_ITEM);
- }
-
- if (mAvrcpBrowseManager.getBrowsedMediaPlayer(bdaddr) != null) {
- mAvrcpBrowseManager.getBrowsedMediaPlayer(bdaddr).playItem(uid, scope);
- } else {
- Log.e(TAG, "handlePlayItemResponse: Remote requested playitem "
- + "before setbrowsedplayer");
- playItemRspNative(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
- }
- }
- }
-
- private void handleGetItemAttr(AvrcpCmd.ItemAttrCmd itemAttr) {
- if (itemAttr.mUidCounter != sUIDCounter) {
- Log.e(TAG, "handleGetItemAttr: invaild uid counter.");
- getItemAttrRspNative(itemAttr.mAddress, AvrcpConstants.RSP_UID_CHANGED, (byte) 0, null,
- null);
- return;
- }
- if (itemAttr.mScope == AvrcpConstants.BTRC_SCOPE_NOW_PLAYING) {
- if (mCurrAddrPlayerID == NO_PLAYER_ID) {
- getItemAttrRspNative(itemAttr.mAddress, AvrcpConstants.RSP_NO_AVBL_PLAY, (byte) 0,
- null, null);
- return;
- }
- mAddressedMediaPlayer.getItemAttr(itemAttr.mAddress, itemAttr, mMediaController);
- return;
- }
- // All other scopes use browsed player
- if (mAvrcpBrowseManager.getBrowsedMediaPlayer(itemAttr.mAddress) != null) {
- mAvrcpBrowseManager.getBrowsedMediaPlayer(itemAttr.mAddress).getItemAttr(itemAttr);
- } else {
- Log.e(TAG, "Could not get attributes. mBrowsedMediaPlayer is null");
- getItemAttrRspNative(itemAttr.mAddress, AvrcpConstants.RSP_INTERNAL_ERR, (byte) 0, null,
- null);
- }
- }
-
- private void handleGetTotalNumOfItemsResponse(byte[] bdaddr, byte scope) {
- // for scope as media player list
- if (scope == AvrcpConstants.BTRC_SCOPE_PLAYER_LIST) {
- int numPlayers = 0;
- synchronized (mMediaPlayerInfoList) {
- numPlayers = mMediaPlayerInfoList.size();
- }
- if (DEBUG) {
- Log.d(TAG, "handleGetTotalNumOfItemsResponse: " + numPlayers + " players.");
- }
- getTotalNumOfItemsRspNative(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, numPlayers);
- } else if (scope == AvrcpConstants.BTRC_SCOPE_NOW_PLAYING) {
- mAddressedMediaPlayer.getTotalNumOfItems(bdaddr, mMediaController);
- } else {
- // for FileSystem browsing scopes as VFS, Now Playing
- if (mAvrcpBrowseManager.getBrowsedMediaPlayer(bdaddr) != null) {
- mAvrcpBrowseManager.getBrowsedMediaPlayer(bdaddr).getTotalNumOfItems(scope);
- } else {
- Log.e(TAG, "Could not get Total NumOfItems. mBrowsedMediaPlayer is null");
- getTotalNumOfItemsRspNative(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR, 0, 0);
- }
- }
-
- }
-
- /* check if browsed player and addressed player are same */
- private boolean isAddrPlayerSameAsBrowsed(byte[] bdaddr) {
- String browsedPlayer = getCurrentBrowsedPlayer(bdaddr);
-
- if (!isPackageNameValid(browsedPlayer)) {
- Log.w(TAG, "Browsed player name empty");
- return false;
- }
-
- MediaPlayerInfo info = getAddressedPlayerInfo();
- String packageName = (info == null) ? "<none>" : info.getPackageName();
- if (info == null || !packageName.equals(browsedPlayer)) {
- if (DEBUG) {
- Log.d(TAG, browsedPlayer + " is not addressed player " + packageName);
- }
- return false;
- }
- return true;
- }
-
- /* checks if package name is not null or empty */
- private boolean isPackageNameValid(String browsedPackage) {
- boolean isValid = (browsedPackage != null && browsedPackage.length() > 0);
- if (DEBUG) {
- Log.d(TAG, "isPackageNameValid: browsedPackage = " + browsedPackage + "isValid = "
- + isValid);
- }
- return isValid;
- }
-
- /* checks if selected addressed player is already addressed */
- private boolean isPlayerAlreadyAddressed(int selectedId) {
- // checking if selected ID is same as the current addressed player id
- boolean isAddressed = (mCurrAddrPlayerID == selectedId);
- if (DEBUG) {
- Log.d(TAG, "isPlayerAlreadyAddressed: isAddressed = " + isAddressed);
- }
- return isAddressed;
- }
-
- public void dump(StringBuilder sb) {
- sb.append("AVRCP:\n");
- ProfileService.println(sb, "mMediaAttributes: " + mMediaAttributes.toRedactedString());
- ProfileService.println(sb, "mTransportControlFlags: " + mTransportControlFlags);
- ProfileService.println(sb, "mCurrentPlayState: " + mCurrentPlayState);
- ProfileService.println(sb, "mPlayStatusChangedNT: " + mPlayStatusChangedNT);
- ProfileService.println(sb, "mTrackChangedNT: " + mTrackChangedNT);
- ProfileService.println(sb, "mPlaybackIntervalMs: " + mPlaybackIntervalMs);
- ProfileService.println(sb, "mPlayPosChangedNT: " + mPlayPosChangedNT);
- ProfileService.println(sb, "mNextPosMs: " + mNextPosMs);
- ProfileService.println(sb, "mPrevPosMs: " + mPrevPosMs);
- ProfileService.println(sb, "mFeatures: " + mFeatures);
- ProfileService.println(sb, "mRemoteVolume: " + mRemoteVolume);
- ProfileService.println(sb, "mLastRemoteVolume: " + mLastRemoteVolume);
- ProfileService.println(sb, "mLastDirection: " + mLastDirection);
- ProfileService.println(sb, "mVolumeStep: " + mVolumeStep);
- ProfileService.println(sb, "mAudioStreamMax: " + mAudioStreamMax);
- ProfileService.println(sb, "mVolCmdSetInProgress: " + mVolCmdSetInProgress);
- ProfileService.println(sb, "mAbsVolRetryTimes: " + mAbsVolRetryTimes);
- ProfileService.println(sb, "mVolumeMapping: " + mVolumeMapping.toString());
- synchronized (this) {
- if (mMediaController != null) {
- ProfileService.println(sb,
- "mMediaController: " + mMediaController.getWrappedInstance() + " pkg "
- + mMediaController.getPackageName());
- }
- }
- ProfileService.println(sb, "");
- ProfileService.println(sb, "Media Players:");
- synchronized (mMediaPlayerInfoList) {
- for (Map.Entry<Integer, MediaPlayerInfo> entry : mMediaPlayerInfoList.entrySet()) {
- int key = entry.getKey();
- ProfileService.println(sb,
- ((mCurrAddrPlayerID == key) ? " *#" : " #") + entry.getKey() + ": " + entry
- .getValue());
- }
- }
-
- ProfileService.println(sb, "");
- mAddressedMediaPlayer.dump(sb, mMediaController);
-
- ProfileService.println(sb, "");
- ProfileService.println(sb, mPassthroughDispatched + " passthrough operations: ");
- if (mPassthroughDispatched > mPassthroughLogs.size()) {
- ProfileService.println(sb, " (last " + mPassthroughLogs.size() + ")");
- }
- synchronized (mPassthroughLogs) {
- for (MediaKeyLog log : mPassthroughLogs) {
- ProfileService.println(sb, " " + log);
- }
- }
- synchronized (mPassthroughPending) {
- for (MediaKeyLog log : mPassthroughPending) {
- ProfileService.println(sb, " " + log);
- }
- }
-
- // Print the blacklisted devices (for absolute volume control)
- SharedPreferences pref =
- mContext.getSharedPreferences(ABSOLUTE_VOLUME_BLACKLIST, Context.MODE_PRIVATE);
- Map<String, ?> allKeys = pref.getAll();
- ProfileService.println(sb, "");
- ProfileService.println(sb, "Runtime Blacklisted Devices (absolute volume):");
- if (allKeys.isEmpty()) {
- ProfileService.println(sb, " None");
- } else {
- for (Map.Entry<String, ?> entry : allKeys.entrySet()) {
- String key = entry.getKey();
- Object value = entry.getValue();
- if (value instanceof String) {
- ProfileService.println(sb, " " + key + " " + value);
- } else {
- ProfileService.println(sb, " " + key + " Reason: Unknown");
- }
- }
- }
- }
-
- public class AvrcpBrowseManager {
- public Map<String, BrowsedMediaPlayer> connList = new HashMap<String, BrowsedMediaPlayer>();
- private AvrcpMediaRspInterface mMediaInterface;
- private Context mContext;
-
- public AvrcpBrowseManager(Context context, AvrcpMediaRspInterface mediaInterface) {
- mContext = context;
- mMediaInterface = mediaInterface;
- }
-
- public void cleanup() {
- Iterator entries = connList.entrySet().iterator();
- while (entries.hasNext()) {
- Map.Entry entry = (Map.Entry) entries.next();
- BrowsedMediaPlayer browsedMediaPlayer = (BrowsedMediaPlayer) entry.getValue();
- if (browsedMediaPlayer != null) {
- browsedMediaPlayer.cleanup();
- }
- }
- // clean up the map
- connList.clear();
- }
-
- // get the a free media player interface based on the passed bd address
- // if the no items is found for the passed media player then it assignes a
- // available media player interface
- public BrowsedMediaPlayer getBrowsedMediaPlayer(byte[] bdaddr) {
- BrowsedMediaPlayer mediaPlayer;
- String bdaddrStr = new String(bdaddr);
- if (connList.containsKey(bdaddrStr)) {
- mediaPlayer = connList.get(bdaddrStr);
- } else {
- mediaPlayer = new BrowsedMediaPlayer(bdaddr, mContext, mMediaInterface);
- connList.put(bdaddrStr, mediaPlayer);
- }
- return mediaPlayer;
- }
-
- // clears the details pertaining to passed bdaddres
- public boolean clearBrowsedMediaPlayer(byte[] bdaddr) {
- String bdaddrStr = new String(bdaddr);
- if (connList.containsKey(bdaddrStr)) {
- connList.remove(bdaddrStr);
- return true;
- }
- return false;
- }
-
- public Map<String, BrowsedMediaPlayer> getConnList() {
- return connList;
- }
-
- /* Helper function to convert colon separated bdaddr to byte string */
- private byte[] hexStringToByteArray(String s) {
- int len = s.length();
- byte[] data = new byte[len / 2];
- for (int i = 0; i < len; i += 2) {
- data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(
- s.charAt(i + 1), 16));
- }
- return data;
- }
- }
-
- /*
- * private class which handles responses from AvrcpMediaManager. Maps responses to native
- * responses. This class implements the AvrcpMediaRspInterface interface.
- */
- private class AvrcpMediaRsp implements AvrcpMediaRspInterface {
- private static final String TAG = "AvrcpMediaRsp";
-
- @Override
- public void setAddrPlayerRsp(byte[] address, int rspStatus) {
- if (!setAddressedPlayerRspNative(address, rspStatus)) {
- Log.e(TAG, "setAddrPlayerRsp failed!");
- }
- }
-
- @Override
- public void setBrowsedPlayerRsp(byte[] address, int rspStatus, byte depth, int numItems,
- String[] textArray) {
- if (!setBrowsedPlayerRspNative(address, rspStatus, depth, numItems, textArray)) {
- Log.e(TAG, "setBrowsedPlayerRsp failed!");
- }
- }
-
- @Override
- public void mediaPlayerListRsp(byte[] address, int rspStatus, MediaPlayerListRsp rspObj) {
- if (rspObj != null && rspStatus == AvrcpConstants.RSP_NO_ERROR) {
- if (!mediaPlayerListRspNative(address, rspStatus, sUIDCounter, rspObj.mItemType,
- rspObj.mNumItems, rspObj.mPlayerIds, rspObj.mPlayerTypes,
- rspObj.mPlayerSubTypes, rspObj.mPlayStatusValues,
- rspObj.mFeatureBitMaskValues, rspObj.mPlayerNameList)) {
- Log.e(TAG, "mediaPlayerListRsp failed!");
- }
- } else {
- Log.e(TAG, "mediaPlayerListRsp: rspObj is null");
- if (!mediaPlayerListRspNative(address, rspStatus, sUIDCounter, (byte) 0x00, 0, null,
- null, null, null, null, null)) {
- Log.e(TAG, "mediaPlayerListRsp failed!");
- }
- }
- }
-
- @Override
- public void folderItemsRsp(byte[] address, int rspStatus, FolderItemsRsp rspObj) {
- if (rspObj != null && rspStatus == AvrcpConstants.RSP_NO_ERROR) {
- if (!getFolderItemsRspNative(address, rspStatus, sUIDCounter, rspObj.mScope,
- rspObj.mNumItems, rspObj.mFolderTypes, rspObj.mPlayable, rspObj.mItemTypes,
- rspObj.mItemUid, rspObj.mDisplayNames, rspObj.mAttributesNum,
- rspObj.mAttrIds, rspObj.mAttrValues)) {
- Log.e(TAG, "getFolderItemsRspNative failed!");
- }
- } else {
- Log.e(TAG, "folderItemsRsp: rspObj is null or rspStatus is error:" + rspStatus);
- if (!getFolderItemsRspNative(address, rspStatus, sUIDCounter, (byte) 0x00, 0, null,
- null, null, null, null, null, null, null)) {
- Log.e(TAG, "getFolderItemsRspNative failed!");
- }
- }
-
- }
-
- @Override
- public void changePathRsp(byte[] address, int rspStatus, int numItems) {
- if (!changePathRspNative(address, rspStatus, numItems)) {
- Log.e(TAG, "changePathRspNative failed!");
- }
- }
-
- @Override
- public void getItemAttrRsp(byte[] address, int rspStatus, ItemAttrRsp rspObj) {
- if (rspObj != null && rspStatus == AvrcpConstants.RSP_NO_ERROR) {
- if (!getItemAttrRspNative(address, rspStatus, rspObj.mNumAttr,
- rspObj.mAttributesIds, rspObj.mAttributesArray)) {
- Log.e(TAG, "getItemAttrRspNative failed!");
- }
- } else {
- Log.e(TAG, "getItemAttrRsp: rspObj is null or rspStatus is error:" + rspStatus);
- if (!getItemAttrRspNative(address, rspStatus, (byte) 0x00, null, null)) {
- Log.e(TAG, "getItemAttrRspNative failed!");
- }
- }
- }
-
- @Override
- public void playItemRsp(byte[] address, int rspStatus) {
- if (!playItemRspNative(address, rspStatus)) {
- Log.e(TAG, "playItemRspNative failed!");
- }
- }
-
- @Override
- public void getTotalNumOfItemsRsp(byte[] address, int rspStatus, int uidCounter,
- int numItems) {
- if (!getTotalNumOfItemsRspNative(address, rspStatus, sUIDCounter, numItems)) {
- Log.e(TAG, "getTotalNumOfItemsRspNative failed!");
- }
- }
-
- @Override
- public void addrPlayerChangedRsp(int type, int playerId, int uidCounter) {
- if (!registerNotificationRspAddrPlayerChangedNative(type, playerId, sUIDCounter)) {
- Log.e(TAG, "registerNotificationRspAddrPlayerChangedNative failed!");
- }
- }
-
- @Override
- public void avalPlayerChangedRsp(byte[] address, int type) {
- if (!registerNotificationRspAvalPlayerChangedNative(type)) {
- Log.e(TAG, "registerNotificationRspAvalPlayerChangedNative failed!");
- }
- }
-
- @Override
- public void uidsChangedRsp(int type) {
- if (!registerNotificationRspUIDsChangedNative(type, sUIDCounter)) {
- Log.e(TAG, "registerNotificationRspUIDsChangedNative failed!");
- }
- }
-
- @Override
- public void nowPlayingChangedRsp(int type) {
- if (mNowPlayingListChangedNT != AvrcpConstants.NOTIFICATION_TYPE_INTERIM) {
- if (DEBUG) {
- Log.d(TAG, "NowPlayingListChanged: Not registered or requesting.");
- }
- return;
- }
-
- if (!registerNotificationRspNowPlayingChangedNative(type)) {
- Log.e(TAG, "registerNotificationRspNowPlayingChangedNative failed!");
- }
- mNowPlayingListChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
- }
-
- @Override
- public void trackChangedRsp(int type, byte[] uid) {
- if (!registerNotificationRspTrackChangeNative(type, uid)) {
- Log.e(TAG, "registerNotificationRspTrackChangeNative failed!");
- }
- }
- }
-
- /* getters for some private variables */
- public AvrcpBrowseManager getAvrcpBrowseManager() {
- return mAvrcpBrowseManager;
- }
-
- /* PASSTHROUGH COMMAND MANAGEMENT */
-
- void handlePassthroughCmd(int op, int state) {
- int code = avrcpPassthroughToKeyCode(op);
- if (code == KeyEvent.KEYCODE_UNKNOWN) {
- Log.w(TAG, "Ignoring passthrough of unknown key " + op + " state " + state);
- return;
- }
- int action = KeyEvent.ACTION_DOWN;
- if (state == AvrcpConstants.KEY_STATE_RELEASE) {
- action = KeyEvent.ACTION_UP;
- }
- KeyEvent event = new KeyEvent(action, code);
- if (!KeyEvent.isMediaKey(code)) {
- Log.w(TAG, "Passthrough non-media key " + op + " (code " + code + ") state " + state);
- }
-
- mMediaSessionManager.dispatchMediaKeyEvent(event);
- addKeyPending(event);
- }
-
- private int avrcpPassthroughToKeyCode(int operation) {
- switch (operation) {
- case BluetoothAvrcp.PASSTHROUGH_ID_UP:
- return KeyEvent.KEYCODE_DPAD_UP;
- case BluetoothAvrcp.PASSTHROUGH_ID_DOWN:
- return KeyEvent.KEYCODE_DPAD_DOWN;
- case BluetoothAvrcp.PASSTHROUGH_ID_LEFT:
- return KeyEvent.KEYCODE_DPAD_LEFT;
- case BluetoothAvrcp.PASSTHROUGH_ID_RIGHT:
- return KeyEvent.KEYCODE_DPAD_RIGHT;
- case BluetoothAvrcp.PASSTHROUGH_ID_RIGHT_UP:
- return KeyEvent.KEYCODE_DPAD_UP_RIGHT;
- case BluetoothAvrcp.PASSTHROUGH_ID_RIGHT_DOWN:
- return KeyEvent.KEYCODE_DPAD_DOWN_RIGHT;
- case BluetoothAvrcp.PASSTHROUGH_ID_LEFT_UP:
- return KeyEvent.KEYCODE_DPAD_UP_LEFT;
- case BluetoothAvrcp.PASSTHROUGH_ID_LEFT_DOWN:
- return KeyEvent.KEYCODE_DPAD_DOWN_LEFT;
- case BluetoothAvrcp.PASSTHROUGH_ID_0:
- return KeyEvent.KEYCODE_NUMPAD_0;
- case BluetoothAvrcp.PASSTHROUGH_ID_1:
- return KeyEvent.KEYCODE_NUMPAD_1;
- case BluetoothAvrcp.PASSTHROUGH_ID_2:
- return KeyEvent.KEYCODE_NUMPAD_2;
- case BluetoothAvrcp.PASSTHROUGH_ID_3:
- return KeyEvent.KEYCODE_NUMPAD_3;
- case BluetoothAvrcp.PASSTHROUGH_ID_4:
- return KeyEvent.KEYCODE_NUMPAD_4;
- case BluetoothAvrcp.PASSTHROUGH_ID_5:
- return KeyEvent.KEYCODE_NUMPAD_5;
- case BluetoothAvrcp.PASSTHROUGH_ID_6:
- return KeyEvent.KEYCODE_NUMPAD_6;
- case BluetoothAvrcp.PASSTHROUGH_ID_7:
- return KeyEvent.KEYCODE_NUMPAD_7;
- case BluetoothAvrcp.PASSTHROUGH_ID_8:
- return KeyEvent.KEYCODE_NUMPAD_8;
- case BluetoothAvrcp.PASSTHROUGH_ID_9:
- return KeyEvent.KEYCODE_NUMPAD_9;
- case BluetoothAvrcp.PASSTHROUGH_ID_DOT:
- return KeyEvent.KEYCODE_NUMPAD_DOT;
- case BluetoothAvrcp.PASSTHROUGH_ID_ENTER:
- return KeyEvent.KEYCODE_NUMPAD_ENTER;
- case BluetoothAvrcp.PASSTHROUGH_ID_CLEAR:
- return KeyEvent.KEYCODE_CLEAR;
- case BluetoothAvrcp.PASSTHROUGH_ID_CHAN_UP:
- return KeyEvent.KEYCODE_CHANNEL_UP;
- case BluetoothAvrcp.PASSTHROUGH_ID_CHAN_DOWN:
- return KeyEvent.KEYCODE_CHANNEL_DOWN;
- case BluetoothAvrcp.PASSTHROUGH_ID_PREV_CHAN:
- return KeyEvent.KEYCODE_LAST_CHANNEL;
- case BluetoothAvrcp.PASSTHROUGH_ID_INPUT_SEL:
- return KeyEvent.KEYCODE_TV_INPUT;
- case BluetoothAvrcp.PASSTHROUGH_ID_DISP_INFO:
- return KeyEvent.KEYCODE_INFO;
- case BluetoothAvrcp.PASSTHROUGH_ID_HELP:
- return KeyEvent.KEYCODE_HELP;
- case BluetoothAvrcp.PASSTHROUGH_ID_PAGE_UP:
- return KeyEvent.KEYCODE_PAGE_UP;
- case BluetoothAvrcp.PASSTHROUGH_ID_PAGE_DOWN:
- return KeyEvent.KEYCODE_PAGE_DOWN;
- case BluetoothAvrcp.PASSTHROUGH_ID_POWER:
- return KeyEvent.KEYCODE_POWER;
- case BluetoothAvrcp.PASSTHROUGH_ID_VOL_UP:
- return KeyEvent.KEYCODE_VOLUME_UP;
- case BluetoothAvrcp.PASSTHROUGH_ID_VOL_DOWN:
- return KeyEvent.KEYCODE_VOLUME_DOWN;
- case BluetoothAvrcp.PASSTHROUGH_ID_MUTE:
- return KeyEvent.KEYCODE_MUTE;
- case BluetoothAvrcp.PASSTHROUGH_ID_PLAY:
- return KeyEvent.KEYCODE_MEDIA_PLAY;
- case BluetoothAvrcp.PASSTHROUGH_ID_STOP:
- return KeyEvent.KEYCODE_MEDIA_STOP;
- case BluetoothAvrcp.PASSTHROUGH_ID_PAUSE:
- return KeyEvent.KEYCODE_MEDIA_PAUSE;
- case BluetoothAvrcp.PASSTHROUGH_ID_RECORD:
- return KeyEvent.KEYCODE_MEDIA_RECORD;
- case BluetoothAvrcp.PASSTHROUGH_ID_REWIND:
- return KeyEvent.KEYCODE_MEDIA_REWIND;
- case BluetoothAvrcp.PASSTHROUGH_ID_FAST_FOR:
- return KeyEvent.KEYCODE_MEDIA_FAST_FORWARD;
- case BluetoothAvrcp.PASSTHROUGH_ID_EJECT:
- return KeyEvent.KEYCODE_MEDIA_EJECT;
- case BluetoothAvrcp.PASSTHROUGH_ID_FORWARD:
- return KeyEvent.KEYCODE_MEDIA_NEXT;
- case BluetoothAvrcp.PASSTHROUGH_ID_BACKWARD:
- return KeyEvent.KEYCODE_MEDIA_PREVIOUS;
- case BluetoothAvrcp.PASSTHROUGH_ID_F1:
- return KeyEvent.KEYCODE_F1;
- case BluetoothAvrcp.PASSTHROUGH_ID_F2:
- return KeyEvent.KEYCODE_F2;
- case BluetoothAvrcp.PASSTHROUGH_ID_F3:
- return KeyEvent.KEYCODE_F3;
- case BluetoothAvrcp.PASSTHROUGH_ID_F4:
- return KeyEvent.KEYCODE_F4;
- case BluetoothAvrcp.PASSTHROUGH_ID_F5:
- return KeyEvent.KEYCODE_F5;
- // Fallthrough for all unknown key mappings
- case BluetoothAvrcp.PASSTHROUGH_ID_SELECT:
- case BluetoothAvrcp.PASSTHROUGH_ID_ROOT_MENU:
- case BluetoothAvrcp.PASSTHROUGH_ID_SETUP_MENU:
- case BluetoothAvrcp.PASSTHROUGH_ID_CONT_MENU:
- case BluetoothAvrcp.PASSTHROUGH_ID_FAV_MENU:
- case BluetoothAvrcp.PASSTHROUGH_ID_EXIT:
- case BluetoothAvrcp.PASSTHROUGH_ID_SOUND_SEL:
- case BluetoothAvrcp.PASSTHROUGH_ID_ANGLE:
- case BluetoothAvrcp.PASSTHROUGH_ID_SUBPICT:
- case BluetoothAvrcp.PASSTHROUGH_ID_VENDOR:
- default:
- return KeyEvent.KEYCODE_UNKNOWN;
- }
- }
-
- private void addKeyPending(KeyEvent event) {
- mPassthroughPending.add(new MediaKeyLog(System.currentTimeMillis(), event));
- }
-
- private void recordKeyDispatched(KeyEvent event, String packageName) {
- long time = System.currentTimeMillis();
- Log.v(TAG, "recordKeyDispatched: " + event + " dispatched to " + packageName);
- setAddressedMediaSessionPackage(packageName);
- synchronized (mPassthroughPending) {
- Iterator<MediaKeyLog> pending = mPassthroughPending.iterator();
- while (pending.hasNext()) {
- MediaKeyLog log = pending.next();
- if (log.addDispatch(time, event, packageName)) {
- mPassthroughDispatched++;
- mPassthroughLogs.add(log);
- pending.remove();
- return;
- }
- }
- Log.w(TAG, "recordKeyDispatch: can't find matching log!");
- }
- }
-
- private final MediaSessionManager.Callback mButtonDispatchCallback =
- new MediaSessionManager.Callback() {
- @Override
- public void onMediaKeyEventDispatched(KeyEvent event, MediaSession.Token token) {
- // Get the package name
- android.media.session.MediaController controller =
- new android.media.session.MediaController(mContext, token);
- String targetPackage = controller.getPackageName();
- recordKeyDispatched(event, targetPackage);
- }
-
- @Override
- public void onMediaKeyEventDispatched(KeyEvent event, ComponentName receiver) {
- recordKeyDispatched(event, receiver.getPackageName());
- }
-
- @Override
- public void onAddressedPlayerChanged(MediaSession.Token token) {
- setActiveMediaSession(token);
- }
-
- @Override
- public void onAddressedPlayerChanged(ComponentName receiver) {
- if (receiver == null) {
- // No active sessions, and no session to revive, give up.
- setAddressedMediaSessionPackage(null);
- return;
- }
- // We can still get a passthrough which will revive this player.
- setAddressedMediaSessionPackage(receiver.getPackageName());
- }
- };
-
- // Do not modify without updating the HAL bt_rc.h files.
-
- // match up with btrc_play_status_t enum of bt_rc.h
- static final byte PLAYSTATUS_STOPPED = 0;
- static final byte PLAYSTATUS_PLAYING = 1;
- static final byte PLAYSTATUS_PAUSED = 2;
- static final byte PLAYSTATUS_FWD_SEEK = 3;
- static final byte PLAYSTATUS_REV_SEEK = 4;
- static final byte PLAYSTATUS_ERROR = (byte) 255;
-
- // match up with btrc_media_attr_t enum of bt_rc.h
- static final int MEDIA_ATTR_TITLE = 1;
- static final int MEDIA_ATTR_ARTIST = 2;
- static final int MEDIA_ATTR_ALBUM = 3;
- static final int MEDIA_ATTR_TRACK_NUM = 4;
- static final int MEDIA_ATTR_NUM_TRACKS = 5;
- static final int MEDIA_ATTR_GENRE = 6;
- static final int MEDIA_ATTR_PLAYING_TIME = 7;
-
- // match up with btrc_event_id_t enum of bt_rc.h
- static final int EVT_PLAY_STATUS_CHANGED = 1;
- static final int EVT_TRACK_CHANGED = 2;
- static final int EVT_TRACK_REACHED_END = 3;
- static final int EVT_TRACK_REACHED_START = 4;
- static final int EVT_PLAY_POS_CHANGED = 5;
- static final int EVT_BATT_STATUS_CHANGED = 6;
- static final int EVT_SYSTEM_STATUS_CHANGED = 7;
- static final int EVT_APP_SETTINGS_CHANGED = 8;
- static final int EVENT_NOW_PLAYING_CONTENT_CHANGED = 9;
- static final int EVT_AVBL_PLAYERS_CHANGED = 0xa;
- static final int EVT_ADDR_PLAYER_CHANGED = 0xb;
- static final int EVENT_UIDS_CHANGED = 0x0c;
-
- private static native void classInitNative();
-
- private native void initNative();
-
- private native void cleanupNative();
-
- private native boolean getPlayStatusRspNative(byte[] address, int playStatus, int songLen,
- int songPos);
-
- private native boolean getElementAttrRspNative(byte[] address, byte numAttr, int[] attrIds,
- String[] textArray);
-
- private native boolean registerNotificationRspPlayStatusNative(int type, int playStatus);
-
- private native boolean registerNotificationRspTrackChangeNative(int type, byte[] track);
-
- private native boolean registerNotificationRspPlayPosNative(int type, int playPos);
-
- private native boolean setVolumeNative(int volume);
-
- private native boolean sendPassThroughCommandNative(int keyCode, int keyState);
-
- private native boolean setAddressedPlayerRspNative(byte[] address, int rspStatus);
-
- private native boolean setBrowsedPlayerRspNative(byte[] address, int rspStatus, byte depth,
- int numItems, String[] textArray);
-
- private native boolean mediaPlayerListRspNative(byte[] address, int rsStatus, int uidCounter,
- byte itemType, int numItems, int[] playerIds, byte[] playerTypes, int[] playerSubTypes,
- byte[] playStatusValues, short[] featureBitMaskValues, String[] textArray);
-
- private native boolean getFolderItemsRspNative(byte[] address, int rspStatus, short uidCounter,
- byte scope, int numItems, byte[] folderTypes, byte[] playable, byte[] itemTypes,
- byte[] itemUidArray, String[] textArray, int[] attributesNum, int[] attributesIds,
- String[] attributesArray);
-
- private native boolean changePathRspNative(byte[] address, int rspStatus, int numItems);
-
- private native boolean getItemAttrRspNative(byte[] address, int rspStatus, byte numAttr,
- int[] attrIds, String[] textArray);
-
- private native boolean playItemRspNative(byte[] address, int rspStatus);
-
- private native boolean getTotalNumOfItemsRspNative(byte[] address, int rspStatus,
- int uidCounter, int numItems);
-
- private native boolean searchRspNative(byte[] address, int rspStatus, int uidCounter,
- int numItems);
-
- private native boolean addToNowPlayingRspNative(byte[] address, int rspStatus);
-
- private native boolean registerNotificationRspAddrPlayerChangedNative(int type, int playerId,
- int uidCounter);
-
- private native boolean registerNotificationRspAvalPlayerChangedNative(int type);
-
- private native boolean registerNotificationRspUIDsChangedNative(int type, int uidCounter);
-
- private native boolean registerNotificationRspNowPlayingChangedNative(int type);
-
-}
diff --git a/src/com/android/bluetooth/avrcp/AvrcpConstants.java b/src/com/android/bluetooth/avrcp/AvrcpConstants.java
deleted file mode 100644
index 50a2eeb..0000000
--- a/src/com/android/bluetooth/avrcp/AvrcpConstants.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.bluetooth.avrcp;
-
-/*************************************************************************************************
- * Grouped all HAL constants into a file to be consistent with the stack.
- * Moved the constants used in Avrcp to this new file to be used across multiple files.
- * Helps in easier modifications and future enhancements in the constants.
- ************************************************************************************************/
-
-/*
- * @hide
- */
-final class AvrcpConstants {
-
- /* Do not modify without upating the HAL bt_rc.h file */
- /** Response Error codes **/
- static final byte RSP_BAD_CMD = 0x00; /* Invalid command */
- static final byte RSP_BAD_PARAM = 0x01; /* Invalid parameter */
- static final byte RSP_NOT_FOUND = 0x02; /* Specified parameter is
- * wrong or not found */
- static final byte RSP_INTERNAL_ERR = 0x03; /* Internal Error */
- static final byte RSP_NO_ERROR = 0x04; /* Operation Success */
- static final byte RSP_UID_CHANGED = 0x05; /* UIDs changed */
- static final byte RSP_RESERVED = 0x06; /* Reserved */
- static final byte RSP_INV_DIRN = 0x07; /* Invalid direction */
- static final byte RSP_INV_DIRECTORY = 0x08; /* Invalid directory */
- static final byte RSP_INV_ITEM = 0x09; /* Invalid Item */
- static final byte RSP_INV_SCOPE = 0x0a; /* Invalid scope */
- static final byte RSP_INV_RANGE = 0x0b; /* Invalid range */
- static final byte RSP_DIRECTORY = 0x0c; /* UID is a directory */
- static final byte RSP_MEDIA_IN_USE = 0x0d; /* Media in use */
- static final byte RSP_PLAY_LIST_FULL = 0x0e; /* Playing list full */
- static final byte RSP_SRCH_NOT_SPRTD = 0x0f; /* Search not supported */
- static final byte RSP_SRCH_IN_PROG = 0x10; /* Search in progress */
- static final byte RSP_INV_PLAYER = 0x11; /* Invalid player */
- static final byte RSP_PLAY_NOT_BROW = 0x12; /* Player not browsable */
- static final byte RSP_PLAY_NOT_ADDR = 0x13; /* Player not addressed */
- static final byte RSP_INV_RESULTS = 0x14; /* Invalid results */
- static final byte RSP_NO_AVBL_PLAY = 0x15; /* No available players */
- static final byte RSP_ADDR_PLAY_CHGD = 0x16; /* Addressed player changed */
-
- /* valid scopes for get_folder_items */
- static final byte BTRC_SCOPE_PLAYER_LIST = 0x00; /* Media Player List */
- static final byte BTRC_SCOPE_FILE_SYSTEM = 0x01; /* Virtual File System */
- static final byte BTRC_SCOPE_SEARCH = 0x02; /* Search */
- static final byte BTRC_SCOPE_NOW_PLAYING = 0x03; /* Now Playing */
-
- /* valid directions for change path */
- static final byte DIR_UP = 0x00;
- static final byte DIR_DOWN = 0x01;
-
- /* item type to browse */
- static final byte BTRC_ITEM_PLAYER = 0x01;
- static final byte BTRC_ITEM_FOLDER = 0x02;
- static final byte BTRC_ITEM_MEDIA = 0x03;
-
- /* valid folder types */
- static final byte FOLDER_TYPE_MIXED = 0x00;
- static final byte FOLDER_TYPE_TITLES = 0x01;
- static final byte FOLDER_TYPE_ALBUMS = 0x02;
- static final byte FOLDER_TYPE_ARTISTS = 0x03;
- static final byte FOLDER_TYPE_GENRES = 0x04;
- static final byte FOLDER_TYPE_PLAYLISTS = 0x05;
- static final byte FOLDER_TYPE_YEARS = 0x06;
-
- /* valid playable flags */
- static final byte ITEM_NOT_PLAYABLE = 0x00;
- static final byte ITEM_PLAYABLE = 0x01;
-
- /* valid Attribute ids for media elements */
- static final int ATTRID_TITLE = 0x01;
- static final int ATTRID_ARTIST = 0x02;
- static final int ATTRID_ALBUM = 0x03;
- static final int ATTRID_TRACK_NUM = 0x04;
- static final int ATTRID_NUM_TRACKS = 0x05;
- static final int ATTRID_GENRE = 0x06;
- static final int ATTRID_PLAY_TIME = 0x07;
- static final int ATTRID_COVER_ART = 0x08;
-
- /* constants to send in Track change response */
- static final byte[] NO_TRACK_SELECTED = {
- (byte) 0xFF,
- (byte) 0xFF,
- (byte) 0xFF,
- (byte) 0xFF,
- (byte) 0xFF,
- (byte) 0xFF,
- (byte) 0xFF,
- (byte) 0xFF
- };
- static final byte[] TRACK_IS_SELECTED = {
- (byte) 0x00,
- (byte) 0x00,
- (byte) 0x00,
- (byte) 0x00,
- (byte) 0x00,
- (byte) 0x00,
- (byte) 0x00,
- (byte) 0x00
- };
-
- /* UID size */
- static final int UID_SIZE = 8;
-
- static final short DEFAULT_UID_COUNTER = 0x0000;
-
- /* Bitmask size for Media Players */
- static final int AVRC_FEATURE_MASK_SIZE = 16;
-
- /* Maximum attributes for media item */
- static final int MAX_NUM_ATTR = 8;
-
- /* notification types for remote device */
- static final int NOTIFICATION_TYPE_INTERIM = 0;
- static final int NOTIFICATION_TYPE_CHANGED = 1;
-
- static final int TRACK_ID_SIZE = 8;
-
- /* player feature bit mask constants */
- static final short AVRC_PF_PLAY_BIT_NO = 40;
- static final short AVRC_PF_STOP_BIT_NO = 41;
- static final short AVRC_PF_PAUSE_BIT_NO = 42;
- static final short AVRC_PF_REWIND_BIT_NO = 44;
- static final short AVRC_PF_FAST_FWD_BIT_NO = 45;
- static final short AVRC_PF_FORWARD_BIT_NO = 47;
- static final short AVRC_PF_BACKWARD_BIT_NO = 48;
- static final short AVRC_PF_ADV_CTRL_BIT_NO = 58;
- static final short AVRC_PF_BROWSE_BIT_NO = 59;
- static final short AVRC_PF_ADD2NOWPLAY_BIT_NO = 61;
- static final short AVRC_PF_UID_UNIQUE_BIT_NO = 62;
- static final short AVRC_PF_NOW_PLAY_BIT_NO = 65;
- static final short AVRC_PF_GET_NUM_OF_ITEMS_BIT_NO = 67;
-
- static final byte PLAYER_TYPE_AUDIO = 1;
- static final int PLAYER_SUBTYPE_NONE = 0;
-
- // match up with btrc_play_status_t enum of bt_rc.h
- static final int PLAYSTATUS_STOPPED = 0;
- static final int PLAYSTATUS_PLAYING = 1;
- static final int PLAYSTATUS_PAUSED = 2;
- static final int PLAYSTATUS_FWD_SEEK = 3;
- static final int PLAYSTATUS_REV_SEEK = 4;
- static final int PLAYSTATUS_ERROR = 255;
-
- static final byte NUM_ATTR_ALL = (byte) 0x00;
- static final byte NUM_ATTR_NONE = (byte) 0xFF;
-
- static final int KEY_STATE_PRESS = 1;
- static final int KEY_STATE_RELEASE = 0;
-}
diff --git a/src/com/android/bluetooth/avrcp/AvrcpHelperClasses.java b/src/com/android/bluetooth/avrcp/AvrcpHelperClasses.java
deleted file mode 100644
index ea6d7b8..0000000
--- a/src/com/android/bluetooth/avrcp/AvrcpHelperClasses.java
+++ /dev/null
@@ -1,451 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.bluetooth.avrcp;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
-import com.android.bluetooth.Utils;
-
-import java.util.ArrayDeque;
-import java.util.Arrays;
-import java.util.Collection;
-
-/*************************************************************************************************
- * Helper classes used for callback/response of browsing commands:-
- * 1) To bundle parameters for native callbacks/response.
- * 2) Stores information of Addressed and Browsed Media Players.
- ************************************************************************************************/
-
-class AvrcpCmd {
-
- AvrcpCmd() {}
-
- /* Helper classes to pass parameters from callbacks to Avrcp handler */
- class FolderItemsCmd {
- byte mScope;
- long mStartItem;
- long mEndItem;
- byte mNumAttr;
- int[] mAttrIDs;
- public byte[] mAddress;
-
- FolderItemsCmd(byte[] address, byte scope, long startItem, long endItem, byte numAttr,
- int[] attrIds) {
- mAddress = address;
- this.mScope = scope;
- this.mStartItem = startItem;
- this.mEndItem = endItem;
- this.mNumAttr = numAttr;
- this.mAttrIDs = attrIds;
- }
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append("[FolderItemCmd: scope " + mScope);
- sb.append(" start " + mStartItem);
- sb.append(" end " + mEndItem);
- sb.append(" numAttr " + mNumAttr);
- sb.append(" attrs: ");
- for (int i = 0; i < mNumAttr; i++) {
- sb.append(mAttrIDs[i] + " ");
- }
- return sb.toString();
- }
- }
-
- class ItemAttrCmd {
- byte mScope;
- byte[] mUid;
- int mUidCounter;
- byte mNumAttr;
- int[] mAttrIDs;
- public byte[] mAddress;
-
- ItemAttrCmd(byte[] address, byte scope, byte[] uid, int uidCounter, byte numAttr,
- int[] attrIDs) {
- mAddress = address;
- mScope = scope;
- mUid = uid;
- mUidCounter = uidCounter;
- mNumAttr = numAttr;
- mAttrIDs = attrIDs;
- }
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append("[ItemAttrCmd: scope " + mScope);
- sb.append(" uid " + Utils.byteArrayToString(mUid));
- sb.append(" numAttr " + mNumAttr);
- sb.append(" attrs: ");
- for (int i = 0; i < mNumAttr; i++) {
- sb.append(mAttrIDs[i] + " ");
- }
- return sb.toString();
- }
- }
-
- class ElementAttrCmd {
- byte mNumAttr;
- int[] mAttrIDs;
- public byte[] mAddress;
-
- ElementAttrCmd(byte[] address, byte numAttr, int[] attrIDs) {
- mAddress = address;
- mNumAttr = numAttr;
- mAttrIDs = attrIDs;
- }
- }
-}
-
-/* Helper classes to pass parameters to native response */
-class MediaPlayerListRsp {
- byte mStatus;
- short mUIDCounter;
- byte mItemType;
- int[] mPlayerIds;
- byte[] mPlayerTypes;
- int[] mPlayerSubTypes;
- byte[] mPlayStatusValues;
- short[] mFeatureBitMaskValues;
- String[] mPlayerNameList;
- int mNumItems;
-
- MediaPlayerListRsp(byte status, short uidCounter, int numItems, byte itemType, int[] playerIds,
- byte[] playerTypes, int[] playerSubTypes, byte[] playStatusValues,
- short[] featureBitMaskValues, String[] playerNameList) {
- this.mStatus = status;
- this.mUIDCounter = uidCounter;
- this.mNumItems = numItems;
- this.mItemType = itemType;
- this.mPlayerIds = playerIds;
- this.mPlayerTypes = playerTypes;
- this.mPlayerSubTypes = new int[numItems];
- this.mPlayerSubTypes = playerSubTypes;
- this.mPlayStatusValues = new byte[numItems];
- this.mPlayStatusValues = playStatusValues;
- int bitMaskSize = AvrcpConstants.AVRC_FEATURE_MASK_SIZE;
- this.mFeatureBitMaskValues = new short[numItems * bitMaskSize];
- for (int bitMaskIndex = 0; bitMaskIndex < (numItems * bitMaskSize); bitMaskIndex++) {
- this.mFeatureBitMaskValues[bitMaskIndex] = featureBitMaskValues[bitMaskIndex];
- }
- this.mPlayerNameList = playerNameList;
- }
-}
-
-class FolderItemsRsp {
- byte mStatus;
- short mUIDCounter;
- byte mScope;
- int mNumItems;
- byte[] mFolderTypes;
- byte[] mPlayable;
- byte[] mItemTypes;
- byte[] mItemUid;
- String[] mDisplayNames; /* display name of the item. Eg: Folder name or song name */
- int[] mAttributesNum;
- int[] mAttrIds;
- String[] mAttrValues;
-
- FolderItemsRsp(byte status, short uidCounter, byte scope, int numItems, byte[] folderTypes,
- byte[] playable, byte[] itemTypes, byte[] itemsUid, String[] displayNameArray,
- int[] attributesNum, int[] attrIds, String[] attrValues) {
- this.mStatus = status;
- this.mUIDCounter = uidCounter;
- this.mScope = scope;
- this.mNumItems = numItems;
- this.mFolderTypes = folderTypes;
- this.mPlayable = playable;
- this.mItemTypes = itemTypes;
- this.mItemUid = itemsUid;
- this.mDisplayNames = displayNameArray;
- this.mAttributesNum = attributesNum;
- this.mAttrIds = attrIds;
- this.mAttrValues = attrValues;
- }
-}
-
-class ItemAttrRsp {
- byte mStatus;
- byte mNumAttr;
- int[] mAttributesIds;
- String[] mAttributesArray;
-
- ItemAttrRsp(byte status, int[] attributesIds, String[] attributesArray) {
- mStatus = status;
- mNumAttr = (byte) attributesIds.length;
- mAttributesIds = attributesIds;
- mAttributesArray = attributesArray;
- }
-}
-
-/* stores information of Media Players in the system */
-class MediaPlayerInfo {
-
- private byte mMajorType;
- private int mSubType;
- private byte mPlayStatus;
- private short[] mFeatureBitMask;
- @NonNull private String mPackageName;
- @NonNull private String mDisplayableName;
- @Nullable private MediaController mMediaController;
-
- MediaPlayerInfo(@Nullable MediaController controller, byte majorType, int subType,
- byte playStatus, short[] featureBitMask, @NonNull String packageName,
- @Nullable String displayableName) {
- this.setMajorType(majorType);
- this.setSubType(subType);
- this.mPlayStatus = playStatus;
- // store a copy the FeatureBitMask array
- this.mFeatureBitMask = Arrays.copyOf(featureBitMask, featureBitMask.length);
- Arrays.sort(this.mFeatureBitMask);
- this.setPackageName(packageName);
- this.setDisplayableName(displayableName);
- this.setMediaController(controller);
- }
-
- /* getters and setters */
- byte getPlayStatus() {
- return mPlayStatus;
- }
-
- void setPlayStatus(byte playStatus) {
- this.mPlayStatus = playStatus;
- }
-
- MediaController getMediaController() {
- return mMediaController;
- }
-
- void setMediaController(MediaController mediaController) {
- if (mediaController != null) {
- this.mPackageName = mediaController.getPackageName();
- }
- this.mMediaController = mediaController;
- }
-
- void setPackageName(@NonNull String name) {
- // Controller determines package name when it is set.
- if (mMediaController != null) {
- return;
- }
- this.mPackageName = name;
- }
-
- String getPackageName() {
- if (mMediaController != null) {
- return mMediaController.getPackageName();
- } else if (mPackageName != null) {
- return mPackageName;
- }
- return null;
- }
-
- byte getMajorType() {
- return mMajorType;
- }
-
- void setMajorType(byte majorType) {
- this.mMajorType = majorType;
- }
-
- int getSubType() {
- return mSubType;
- }
-
- void setSubType(int subType) {
- this.mSubType = subType;
- }
-
- String getDisplayableName() {
- return mDisplayableName;
- }
-
- void setDisplayableName(@Nullable String displayableName) {
- if (displayableName == null) {
- displayableName = "";
- }
- this.mDisplayableName = displayableName;
- }
-
- short[] getFeatureBitMask() {
- return mFeatureBitMask;
- }
-
- void setFeatureBitMask(short[] featureBitMask) {
- synchronized (this) {
- this.mFeatureBitMask = Arrays.copyOf(featureBitMask, featureBitMask.length);
- Arrays.sort(this.mFeatureBitMask);
- }
- }
-
- boolean isBrowseSupported() {
- synchronized (this) {
- if (this.mFeatureBitMask == null) {
- return false;
- }
- for (short bit : this.mFeatureBitMask) {
- if (bit == AvrcpConstants.AVRC_PF_BROWSE_BIT_NO) {
- return true;
- }
- }
- }
- return false;
- }
-
- /** Tests if the view of this player presented to the controller is different enough to
- * justify sending an Available Players Changed update */
- public boolean equalView(MediaPlayerInfo other) {
- return (this.mMajorType == other.getMajorType()) && (this.mSubType == other.getSubType())
- && Arrays.equals(this.mFeatureBitMask, other.getFeatureBitMask())
- && this.mDisplayableName.equals(other.getDisplayableName());
- }
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append("MediaPlayerInfo ");
- sb.append(getPackageName());
- sb.append(" (as '" + getDisplayableName() + "')");
- sb.append(" Type = " + getMajorType());
- sb.append(", SubType = " + getSubType());
- sb.append(", Status = " + mPlayStatus);
- sb.append(" Feature Bits [");
- short[] bits = getFeatureBitMask();
- for (int i = 0; i < bits.length; i++) {
- if (i != 0) {
- sb.append(" ");
- }
- sb.append(bits[i]);
- }
- sb.append("] Controller: ");
- sb.append(getMediaController());
- return sb.toString();
- }
-}
-
-/* stores information for browsable Media Players available in the system */
-class BrowsePlayerInfo {
- public String packageName;
- public String displayableName;
- public String serviceClass;
-
- BrowsePlayerInfo(String packageName, String displayableName, String serviceClass) {
- this.packageName = packageName;
- this.displayableName = displayableName;
- this.serviceClass = serviceClass;
- }
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append("BrowsePlayerInfo ");
- sb.append(packageName);
- sb.append(" ( as '" + displayableName + "')");
- sb.append(" service " + serviceClass);
- return sb.toString();
- }
-}
-
-class FolderItemsData {
- /* initialize sizes for rsp parameters */ int mNumItems;
- int[] mAttributesNum;
- byte[] mFolderTypes;
- byte[] mItemTypes;
- byte[] mPlayable;
- byte[] mItemUid;
- String[] mDisplayNames;
- int[] mAttrIds;
- String[] mAttrValues;
- int mAttrCounter;
-
- FolderItemsData(int size) {
- mNumItems = size;
- mAttributesNum = new int[size];
-
- mFolderTypes = new byte[size]; /* folderTypes */
- mItemTypes = new byte[size]; /* folder or media item */
- mPlayable = new byte[size];
- Arrays.fill(mFolderTypes, AvrcpConstants.FOLDER_TYPE_MIXED);
- Arrays.fill(mItemTypes, AvrcpConstants.BTRC_ITEM_MEDIA);
- Arrays.fill(mPlayable, AvrcpConstants.ITEM_PLAYABLE);
-
- mItemUid = new byte[size * AvrcpConstants.UID_SIZE];
- mDisplayNames = new String[size];
-
- mAttrIds = null; /* array of attr ids */
- mAttrValues = null; /* array of attr values */
- }
-}
-
-/** A queue that evicts the first element when you add an element to the end when it reaches a
- * maximum size.
- * This is useful for keeping a FIFO queue of items where the items drop off the front, i.e. a log
- * with a maximum size.
- */
-class EvictingQueue<E> extends ArrayDeque<E> {
- private int mMaxSize;
-
- EvictingQueue(int maxSize) {
- super();
- mMaxSize = maxSize;
- }
-
- EvictingQueue(int maxSize, int initialElements) {
- super(initialElements);
- mMaxSize = maxSize;
- }
-
- EvictingQueue(int maxSize, Collection<? extends E> c) {
- super(c);
- mMaxSize = maxSize;
- }
-
- @Override
- public void addFirst(E e) {
- if (super.size() == mMaxSize) {
- return;
- }
- super.addFirst(e);
- }
-
- @Override
- public void addLast(E e) {
- if (super.size() == mMaxSize) {
- super.remove();
- }
- super.addLast(e);
- }
-
- @Override
- public boolean offerFirst(E e) {
- if (super.size() == mMaxSize) {
- return false;
- }
- return super.offerFirst(e);
- }
-
- @Override
- public boolean offerLast(E e) {
- if (super.size() == mMaxSize) {
- super.remove();
- }
- return super.offerLast(e);
- }
-}
diff --git a/src/com/android/bluetooth/avrcp/AvrcpMediaRspInterface.java b/src/com/android/bluetooth/avrcp/AvrcpMediaRspInterface.java
deleted file mode 100644
index 8cab68e..0000000
--- a/src/com/android/bluetooth/avrcp/AvrcpMediaRspInterface.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.bluetooth.avrcp;
-
-
-/*************************************************************************************************
- * Interface for classes which handle callbacks from AvrcpMediaManager.
- * These callbacks should map to native responses and used to communicate with the native layer.
- ************************************************************************************************/
-
-public interface AvrcpMediaRspInterface {
- void setAddrPlayerRsp(byte[] address, int rspStatus);
-
- void setBrowsedPlayerRsp(byte[] address, int rspStatus, byte depth, int numItems,
- String[] textArray);
-
- void mediaPlayerListRsp(byte[] address, int rspStatus, MediaPlayerListRsp rspObj);
-
- void folderItemsRsp(byte[] address, int rspStatus, FolderItemsRsp rspObj);
-
- void changePathRsp(byte[] address, int rspStatus, int numItems);
-
- void getItemAttrRsp(byte[] address, int rspStatus, ItemAttrRsp rspObj);
-
- void playItemRsp(byte[] address, int rspStatus);
-
- void getTotalNumOfItemsRsp(byte[] address, int rspStatus, int uidCounter, int numItems);
-
- void addrPlayerChangedRsp(int type, int playerId, int uidCounter);
-
- void avalPlayerChangedRsp(byte[] address, int type);
-
- void uidsChangedRsp(int type);
-
- void nowPlayingChangedRsp(int type);
-
- void trackChangedRsp(int type, byte[] uid);
-}
-
diff --git a/src/com/android/bluetooth/newavrcp/AvrcpNativeInterface.java b/src/com/android/bluetooth/avrcp/AvrcpNativeInterface.java
similarity index 96%
rename from src/com/android/bluetooth/newavrcp/AvrcpNativeInterface.java
rename to src/com/android/bluetooth/avrcp/AvrcpNativeInterface.java
index 4976237..7faa543 100644
--- a/src/com/android/bluetooth/newavrcp/AvrcpNativeInterface.java
+++ b/src/com/android/bluetooth/avrcp/AvrcpNativeInterface.java
@@ -27,7 +27,7 @@
* data.
*/
public class AvrcpNativeInterface {
- private static final String TAG = "NewAvrcpNativeInterface";
+ private static final String TAG = "AvrcpNativeInterface";
private static final boolean DEBUG = true;
private static AvrcpNativeInterface sInstance;
@@ -218,9 +218,9 @@
mAvrcpService.deviceDisconnected(device);
}
- void sendVolumeChanged(int volume) {
+ void sendVolumeChanged(String bdaddr, int volume) {
d("sendVolumeChanged: volume=" + volume);
- sendVolumeChangedNative(volume);
+ sendVolumeChangedNative(bdaddr, volume);
}
void setVolume(int volume) {
@@ -245,7 +245,7 @@
private native void cleanupNative();
private native boolean connectDeviceNative(String bdaddr);
private native boolean disconnectDeviceNative(String bdaddr);
- private native void sendVolumeChangedNative(int volume);
+ private native void sendVolumeChangedNative(String bdaddr, int volume);
private static void d(String msg) {
if (DEBUG) {
diff --git a/src/com/android/bluetooth/newavrcp/AvrcpTargetService.java b/src/com/android/bluetooth/avrcp/AvrcpTargetService.java
similarity index 83%
rename from src/com/android/bluetooth/newavrcp/AvrcpTargetService.java
rename to src/com/android/bluetooth/avrcp/AvrcpTargetService.java
index c7029ce..61ed75e 100644
--- a/src/com/android/bluetooth/newavrcp/AvrcpTargetService.java
+++ b/src/com/android/bluetooth/avrcp/AvrcpTargetService.java
@@ -18,6 +18,7 @@
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
import android.bluetooth.IBluetoothAvrcpTarget;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -34,6 +35,7 @@
import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.bluetooth.btservice.ServiceFactory;
import java.util.List;
import java.util.Objects;
@@ -43,7 +45,7 @@
* @hide
*/
public class AvrcpTargetService extends ProfileService {
- private static final String TAG = "NewAvrcpTargetService";
+ private static final String TAG = "AvrcpTargetService";
private static final boolean DEBUG = true;
private static final String AVRCP_ENABLE_PROPERTY = "persist.bluetooth.enablenewavrcp";
@@ -55,6 +57,7 @@
private AvrcpBroadcastReceiver mReceiver;
private AvrcpNativeInterface mNativeInterface;
private AvrcpVolumeManager mVolumeManager;
+ private ServiceFactory mFactory = new ServiceFactory();
// Only used to see if the metadata has changed from its previous value
private MediaData mCurrentData;
@@ -65,6 +68,8 @@
MediaPlayerList.FolderUpdateCallback {
@Override
public void run(MediaData data) {
+ if (mNativeInterface == null) return;
+
boolean metadata = !Objects.equals(mCurrentData.metadata, data.metadata);
boolean state = !MediaPlayerWrapper.playstateEquals(mCurrentData.state, data.state);
boolean queue = !Objects.equals(mCurrentData.queue, data.queue);
@@ -81,6 +86,8 @@
@Override
public void run(boolean availablePlayers, boolean addressedPlayers,
boolean uids) {
+ if (mNativeInterface == null) return;
+
mNativeInterface.sendFolderUpdate(availablePlayers, addressedPlayers, uids);
}
}
@@ -94,6 +101,19 @@
// Update all the playback status info for each connected device
mNativeInterface.sendMediaUpdate(false, true, false);
+ } else if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
+ if (mNativeInterface == null) return;
+
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ if (device == null) return;
+
+ int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+ if (state == BluetoothProfile.STATE_DISCONNECTED) {
+ // If there is no connection, disconnectDevice() will do nothing
+ if (mNativeInterface.disconnectDevice(device.getAddress())) {
+ Log.d(TAG, "request to disconnect device " + device);
+ }
+ }
}
}
}
@@ -140,11 +160,6 @@
Log.i(TAG, "Starting the AVRCP Target Service");
mCurrentData = new MediaData(null, null, null);
- mReceiver = new AvrcpBroadcastReceiver();
- IntentFilter filter = new IntentFilter();
- filter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
- registerReceiver(mReceiver, filter);
-
if (!SystemProperties.getBoolean(AVRCP_ENABLE_PROPERTY, true)) {
Log.w(TAG, "Skipping initialization of the new AVRCP Target Service");
sInstance = null;
@@ -156,15 +171,21 @@
mMediaPlayerList = new MediaPlayerList(Looper.myLooper(), this);
+ mNativeInterface = AvrcpNativeInterface.getInterface();
+ mNativeInterface.init(AvrcpTargetService.this);
+
+ mVolumeManager = new AvrcpVolumeManager(this, mAudioManager, mNativeInterface);
+
UserManager userManager = UserManager.get(getApplicationContext());
if (userManager.isUserUnlocked()) {
mMediaPlayerList.init(new ListCallback());
}
- mNativeInterface = AvrcpNativeInterface.getInterface();
- mNativeInterface.init(AvrcpTargetService.this);
-
- mVolumeManager = new AvrcpVolumeManager(this, mAudioManager, mNativeInterface);
+ mReceiver = new AvrcpBroadcastReceiver();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
+ filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+ registerReceiver(mReceiver, filter);
// Only allow the service to be used once it is initialized
sInstance = this;
@@ -227,30 +248,46 @@
public void storeVolumeForDevice(BluetoothDevice device) {
if (device == null) return;
+ List<BluetoothDevice> HAActiveDevices = null;
+ if (mFactory.getHearingAidService() != null) {
+ HAActiveDevices = mFactory.getHearingAidService().getActiveDevices();
+ }
+ if (HAActiveDevices != null
+ && (HAActiveDevices.get(0) != null || HAActiveDevices.get(1) != null)) {
+ Log.d(TAG, "Do not store volume when Hearing Aid devices is active");
+ return;
+ }
mVolumeManager.storeVolumeForDevice(device);
}
/**
+ * Remove the stored volume for a device.
+ */
+ public void removeStoredVolumeForDevice(BluetoothDevice device) {
+ if (device == null) return;
+
+ mVolumeManager.removeStoredVolumeForDevice(device);
+ }
+
+ /**
* Retrieve the remembered volume for a device. Returns -1 if there is no volume for the
* device.
*/
public int getRememberedVolumeForDevice(BluetoothDevice device) {
if (device == null) return -1;
- return mVolumeManager.getVolume(device, -1);
+ return mVolumeManager.getVolume(device, mVolumeManager.getNewDeviceVolume());
}
// TODO (apanicke): Add checks to blacklist Absolute Volume devices if they behave poorly.
void setVolume(int avrcpVolume) {
- int deviceVolume =
- (int) Math.floor((double) avrcpVolume * sDeviceMaxVolume / AVRCP_MAX_VOL);
- if (DEBUG) {
- Log.d(TAG, "SendVolumeChanged: avrcpVolume=" + avrcpVolume
- + " deviceVolume=" + deviceVolume
- + " sDeviceMaxVolume=" + sDeviceMaxVolume);
+ BluetoothDevice activeDevice = mFactory.getA2dpService().getActiveDevice();
+ if (activeDevice == null) {
+ Log.d(TAG, "setVolume: no active device");
+ return;
}
- mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, deviceVolume,
- AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_BLUETOOTH_ABS_VOLUME);
+
+ mVolumeManager.setVolume(activeDevice, avrcpVolume);
}
/**
@@ -258,15 +295,13 @@
* volume.
*/
public void sendVolumeChanged(int deviceVolume) {
- int avrcpVolume =
- (int) Math.floor((double) deviceVolume * AVRCP_MAX_VOL / sDeviceMaxVolume);
- if (avrcpVolume > 127) avrcpVolume = 127;
- if (DEBUG) {
- Log.d(TAG, "SendVolumeChanged: avrcpVolume=" + avrcpVolume
- + " deviceVolume=" + deviceVolume
- + " sDeviceMaxVolume=" + sDeviceMaxVolume);
+ BluetoothDevice activeDevice = mFactory.getA2dpService().getActiveDevice();
+ if (activeDevice == null) {
+ Log.d(TAG, "sendVolumeChanged: no active device");
+ return;
}
- mNativeInterface.sendVolumeChanged(avrcpVolume);
+
+ mVolumeManager.sendVolumeChanged(activeDevice, deviceVolume);
}
Metadata getCurrentSongInfo() {
diff --git a/src/com/android/bluetooth/newavrcp/AvrcpVolumeManager.java b/src/com/android/bluetooth/avrcp/AvrcpVolumeManager.java
similarity index 80%
rename from src/com/android/bluetooth/newavrcp/AvrcpVolumeManager.java
rename to src/com/android/bluetooth/avrcp/AvrcpVolumeManager.java
index 2e2ce42..c1369f8 100644
--- a/src/com/android/bluetooth/newavrcp/AvrcpVolumeManager.java
+++ b/src/com/android/bluetooth/avrcp/AvrcpVolumeManager.java
@@ -32,7 +32,7 @@
import java.util.Objects;
class AvrcpVolumeManager extends AudioDeviceCallback {
- public static final String TAG = "NewAvrcpVolumeManager";
+ public static final String TAG = "AvrcpVolumeManager";
public static final boolean DEBUG = true;
// All volumes are stored at system volume values, not AVRCP values
@@ -81,7 +81,7 @@
if (mDeviceMap.get(device)) {
int avrcpVolume = systemToAvrcpVolume(savedVolume);
Log.i(TAG, "switchVolumeDevice: Updating device volume: avrcpVolume=" + avrcpVolume);
- mNativeInterface.sendVolumeChanged(avrcpVolume);
+ mNativeInterface.sendVolumeChanged(device.getAddress(), avrcpVolume);
}
}
@@ -115,20 +115,35 @@
volumeMapEditor.apply();
}
- void storeVolumeForDevice(BluetoothDevice device) {
+ synchronized void storeVolumeForDevice(@NonNull BluetoothDevice device) {
+ if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
+ return;
+ }
SharedPreferences.Editor pref = getVolumeMap().edit();
int storeVolume = mAudioManager.getStreamVolume(STREAM_MUSIC);
Log.i(TAG, "storeVolume: Storing stream volume level for device " + device
+ " : " + storeVolume);
mVolumeMap.put(device, storeVolume);
pref.putInt(device.getAddress(), storeVolume);
-
// Always use apply() since it is asynchronous, otherwise the call can hang waiting for
// storage to be written.
pref.apply();
}
- int getVolume(@NonNull BluetoothDevice device, int defaultValue) {
+ synchronized void removeStoredVolumeForDevice(@NonNull BluetoothDevice device) {
+ if (device.getBondState() != BluetoothDevice.BOND_NONE) {
+ return;
+ }
+ SharedPreferences.Editor pref = getVolumeMap().edit();
+ Log.i(TAG, "RemoveStoredVolume: Remove stored stream volume level for device " + device);
+ mVolumeMap.remove(device);
+ pref.remove(device.getAddress());
+ // Always use apply() since it is asynchronous, otherwise the call can hang waiting for
+ // storage to be written.
+ pref.apply();
+ }
+
+ synchronized int getVolume(@NonNull BluetoothDevice device, int defaultValue) {
if (!mVolumeMap.containsKey(device)) {
Log.w(TAG, "getVolume: Couldn't find volume preference for device: " + device);
return defaultValue;
@@ -138,6 +153,36 @@
return mVolumeMap.get(device);
}
+ public int getNewDeviceVolume() {
+ return sNewDeviceVolume;
+ }
+
+ void setVolume(@NonNull BluetoothDevice device, int avrcpVolume) {
+ int deviceVolume =
+ (int) Math.floor((double) avrcpVolume * sDeviceMaxVolume / AVRCP_MAX_VOL);
+ if (DEBUG) {
+ Log.d(TAG, "setVolume: avrcpVolume=" + avrcpVolume
+ + " deviceVolume=" + deviceVolume
+ + " sDeviceMaxVolume=" + sDeviceMaxVolume);
+ }
+ mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, deviceVolume,
+ AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_BLUETOOTH_ABS_VOLUME);
+ storeVolumeForDevice(device);
+ }
+
+ void sendVolumeChanged(@NonNull BluetoothDevice device, int deviceVolume) {
+ int avrcpVolume =
+ (int) Math.floor((double) deviceVolume * AVRCP_MAX_VOL / sDeviceMaxVolume);
+ if (avrcpVolume > 127) avrcpVolume = 127;
+ if (DEBUG) {
+ Log.d(TAG, "sendVolumeChanged: avrcpVolume=" + avrcpVolume
+ + " deviceVolume=" + deviceVolume
+ + " sDeviceMaxVolume=" + sDeviceMaxVolume);
+ }
+ mNativeInterface.sendVolumeChanged(device.getAddress(), avrcpVolume);
+ storeVolumeForDevice(device);
+ }
+
@Override
public synchronized void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
if (mCurrentDevice == null) {
diff --git a/src/com/android/bluetooth/newavrcp/BrowsablePlayerConnector.java b/src/com/android/bluetooth/avrcp/BrowsablePlayerConnector.java
similarity index 90%
rename from src/com/android/bluetooth/newavrcp/BrowsablePlayerConnector.java
rename to src/com/android/bluetooth/avrcp/BrowsablePlayerConnector.java
index ab23dbf..b5886c2 100644
--- a/src/com/android/bluetooth/newavrcp/BrowsablePlayerConnector.java
+++ b/src/com/android/bluetooth/avrcp/BrowsablePlayerConnector.java
@@ -39,7 +39,7 @@
* when constructing BrowsedPlayerWrappers by hand.
*/
public class BrowsablePlayerConnector {
- private static final String TAG = "NewAvrcpBrowsablePlayerConnector";
+ private static final String TAG = "AvrcpBrowsablePlayerConnector";
private static final boolean DEBUG = true;
private static final long CONNECT_TIMEOUT_MS = 10000; // Time in ms to wait for a connection
@@ -152,11 +152,7 @@
case MSG_TIMEOUT: {
Log.v(TAG, "Timed out waiting for players");
- for (BrowsedPlayerWrapper wrapper : mPendingPlayers) {
- if (DEBUG) Log.d(TAG, "Disconnecting " + wrapper.getPackageName());
- wrapper.disconnect();
- }
- mPendingPlayers.clear();
+ removePendingPlayers();
} break;
}
@@ -169,4 +165,21 @@
}
};
}
+
+ private void removePendingPlayers() {
+ for (BrowsedPlayerWrapper wrapper : mPendingPlayers) {
+ if (DEBUG) Log.d(TAG, "Disconnecting " + wrapper.getPackageName());
+ wrapper.disconnect();
+ }
+ mPendingPlayers.clear();
+ }
+
+ void cleanup() {
+ if (mPendingPlayers.size() != 0) {
+ Log.i(TAG, "Bluetooth turn off with " + mPendingPlayers.size() + " pending player(s)");
+ mHandler.removeMessages(MSG_TIMEOUT);
+ removePendingPlayers();
+ mHandler = null;
+ }
+ }
}
diff --git a/src/com/android/bluetooth/avrcp/BrowsedMediaPlayer.java b/src/com/android/bluetooth/avrcp/BrowsedMediaPlayer.java
deleted file mode 100644
index 0889d90..0000000
--- a/src/com/android/bluetooth/avrcp/BrowsedMediaPlayer.java
+++ /dev/null
@@ -1,842 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.bluetooth.avrcp;
-
-import android.annotation.NonNull;
-import android.content.ComponentName;
-import android.content.Context;
-import android.media.MediaDescription;
-import android.media.MediaMetadata;
-import android.media.browse.MediaBrowser;
-import android.media.browse.MediaBrowser.MediaItem;
-import android.media.session.MediaSession;
-import android.os.Bundle;
-import android.util.Log;
-
-import java.math.BigInteger;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Stack;
-
-/*************************************************************************************************
- * Provides functionality required for Browsed Media Player like browsing Virtual File System, get
- * Item Attributes, play item from the file system, etc.
- * Acts as an Interface to communicate with Media Browsing APIs for browsing FileSystem.
- ************************************************************************************************/
-
-class BrowsedMediaPlayer {
- private static final boolean DEBUG = false;
- private static final String TAG = "BrowsedMediaPlayer";
-
- /* connection state with MediaBrowseService */
- private static final int DISCONNECTED = 0;
- private static final int CONNECTED = 1;
- private static final int SUSPENDED = 2;
-
- private static final String[] ROOT_FOLDER = {"root"};
-
- /* package and service name of target Media Player which is set for browsing */
- private String mPackageName;
- private String mConnectingPackageName;
- private String mClassName;
- private Context mContext;
- private AvrcpMediaRspInterface mMediaInterface;
- private byte[] mBDAddr;
-
- /* Object used to connect to MediaBrowseService of Media Player */
- private MediaBrowser mMediaBrowser = null;
- private MediaController mMediaController = null;
-
- /* The mediaId to be used for subscribing for children using the MediaBrowser */
- private String mMediaId = null;
- private String mRootFolderUid = null;
- private int mConnState = DISCONNECTED;
-
- /* stores the path trail during changePath */
- private Stack<String> mPathStack = null;
-
- /* Number of items in current folder */
- private int mCurrFolderNumItems = 0;
-
- /* store mapping between uid(Avrcp) and mediaId(Media Player). */
- private HashMap<Integer, String> mHmap = new HashMap<Integer, String>();
-
- /* command objects from avrcp handler */
- private AvrcpCmd.FolderItemsCmd mFolderItemsReqObj;
-
- /* store result of getfolderitems with scope="vfs" */
- private List<MediaBrowser.MediaItem> mFolderItems = null;
-
- /* Connection state callback handler */
- class MediaConnectionCallback extends MediaBrowser.ConnectionCallback {
- private String mCallbackPackageName;
- private MediaBrowser mBrowser;
-
- MediaConnectionCallback(String packageName) {
- this.mCallbackPackageName = packageName;
- }
-
- public void setBrowser(MediaBrowser b) {
- mBrowser = b;
- }
-
- @Override
- public void onConnected() {
- mConnState = CONNECTED;
- if (DEBUG) {
- Log.d(TAG, "mediaBrowser CONNECTED to " + mPackageName);
- }
- /* perform init tasks and set player as browsed player on successful connection */
- onBrowseConnect(mCallbackPackageName, mBrowser);
-
- // Remove what could be a circular dependency causing GC to never happen on this object
- mBrowser = null;
- }
-
- @Override
- public void onConnectionFailed() {
- mConnState = DISCONNECTED;
- // Remove what could be a circular dependency causing GC to never happen on this object
- mBrowser = null;
- Log.e(TAG, "mediaBrowser Connection failed with " + mPackageName
- + ", Sending fail response!");
- mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR,
- (byte) 0x00, 0, null);
- }
-
- @Override
- public void onConnectionSuspended() {
- mBrowser = null;
- mConnState = SUSPENDED;
- Log.e(TAG, "mediaBrowser SUSPENDED connection with " + mPackageName);
- }
- }
-
- /* Subscription callback handler. Subscribe to a folder to get its contents */
- private MediaBrowser.SubscriptionCallback mFolderItemsCb =
- new MediaBrowser.SubscriptionCallback() {
-
- @Override
- public void onChildrenLoaded(String parentId,
- List<MediaBrowser.MediaItem> children) {
- if (DEBUG) {
- Log.d(TAG, "OnChildren Loaded folder items: childrens= " + children.size());
- }
-
- /*
- * cache current folder items and send as rsp when remote requests
- * get_folder_items (scope = vfs)
- */
- if (mFolderItems == null) {
- if (DEBUG) {
- Log.d(TAG, "sending setbrowsed player rsp");
- }
- mFolderItems = children;
- mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR,
- (byte) 0x00, children.size(), ROOT_FOLDER);
- } else {
- mFolderItems = children;
- mCurrFolderNumItems = mFolderItems.size();
- mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR,
- mCurrFolderNumItems);
- }
- mMediaBrowser.unsubscribe(parentId);
- }
-
- /* UID is invalid */
- @Override
- public void onError(String id) {
- Log.e(TAG, "set browsed player rsp. Could not get root folder items");
- mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR,
- (byte) 0x00, 0, null);
- }
- };
-
- /* callback from media player in response to getitemAttr request */
- private class ItemAttribSubscriber extends MediaBrowser.SubscriptionCallback {
- private String mMediaId;
- private AvrcpCmd.ItemAttrCmd mAttrReq;
-
- ItemAttribSubscriber(@NonNull AvrcpCmd.ItemAttrCmd attrReq, @NonNull String mediaId) {
- mAttrReq = attrReq;
- mMediaId = mediaId;
- }
-
- @Override
- public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) {
- String logprefix = "ItemAttribSubscriber(" + mMediaId + "): ";
- if (DEBUG) {
- Log.d(TAG, logprefix + "OnChildren Loaded");
- }
- int status = AvrcpConstants.RSP_INV_ITEM;
-
- if (children == null) {
- Log.w(TAG, logprefix + "children list is null parentId: " + parentId);
- } else {
- /* find the item in the folder */
- for (MediaBrowser.MediaItem item : children) {
- if (item.getMediaId().equals(mMediaId)) {
- if (DEBUG) {
- Log.d(TAG, logprefix + "found item");
- }
- getItemAttrFilterAttr(item);
- status = AvrcpConstants.RSP_NO_ERROR;
- break;
- }
- }
- }
- /* Send only error from here, in case of success, getItemAttrFilterAttr sends */
- if (status != AvrcpConstants.RSP_NO_ERROR) {
- Log.e(TAG, logprefix + "not able to find item from " + parentId);
- mMediaInterface.getItemAttrRsp(mBDAddr, status, null);
- }
- mMediaBrowser.unsubscribe(parentId);
- }
-
- @Override
- public void onError(String id) {
- Log.e(TAG, "Could not get attributes from media player id: " + id);
- mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, null);
- }
-
- /* helper method to filter required attibuteand send GetItemAttr response */
- private void getItemAttrFilterAttr(@NonNull MediaBrowser.MediaItem mediaItem) {
- /* Response parameters */
- int[] attrIds = null; /* array of attr ids */
- String[] attrValues = null; /* array of attr values */
-
- /* variables to temperorily add attrs */
- ArrayList<Integer> attrIdArray = new ArrayList<Integer>();
- ArrayList<String> attrValueArray = new ArrayList<String>();
- ArrayList<Integer> attrReqIds = new ArrayList<Integer>();
-
- if (mAttrReq.mNumAttr == AvrcpConstants.NUM_ATTR_NONE) {
- // Note(jamuraa): the stack should never send this, remove?
- Log.i(TAG, "getItemAttrFilterAttr: No attributes requested");
- mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_BAD_PARAM, null);
- return;
- }
-
- /* check if remote device has requested all attributes */
- if (mAttrReq.mNumAttr == AvrcpConstants.NUM_ATTR_ALL
- || mAttrReq.mNumAttr == AvrcpConstants.MAX_NUM_ATTR) {
- for (int idx = 1; idx <= AvrcpConstants.MAX_NUM_ATTR; idx++) {
- attrReqIds.add(idx); /* attr id 0x00 is unused */
- }
- } else {
- /* get only the requested attribute ids from the request */
- for (int idx = 0; idx < mAttrReq.mNumAttr; idx++) {
- attrReqIds.add(mAttrReq.mAttrIDs[idx]);
- }
- }
-
- /* lookup and copy values of attributes for ids requested above */
- for (int attrId : attrReqIds) {
- /* check if media player provided requested attributes */
- String value = getAttrValue(attrId, mediaItem);
- if (value != null) {
- attrIdArray.add(attrId);
- attrValueArray.add(value);
- }
- }
-
- /* copy filtered attr ids and attr values to response parameters */
- attrIds = new int[attrIdArray.size()];
- for (int i = 0; i < attrIdArray.size(); i++) {
- attrIds[i] = attrIdArray.get(i);
- }
-
- attrValues = attrValueArray.toArray(new String[attrIdArray.size()]);
-
- /* create rsp object and send response */
- ItemAttrRsp rspObj = new ItemAttrRsp(AvrcpConstants.RSP_NO_ERROR, attrIds, attrValues);
- mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
- }
- }
-
- /* Constructor */
- BrowsedMediaPlayer(byte[] address, Context context,
- AvrcpMediaRspInterface mAvrcpMediaRspInterface) {
- mContext = context;
- mMediaInterface = mAvrcpMediaRspInterface;
- mBDAddr = address;
- }
-
- /* initialize mediacontroller in order to communicate with media player. */
- private void onBrowseConnect(String connectedPackage, MediaBrowser browser) {
- if (!connectedPackage.equals(mConnectingPackageName)) {
- Log.w(TAG, "onBrowseConnect: recieved callback for package we aren't connecting to "
- + connectedPackage);
- return;
- }
- mConnectingPackageName = null;
-
- if (browser == null) {
- Log.e(TAG, "onBrowseConnect: received a null browser for " + connectedPackage);
- mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR,
- (byte) 0x00, 0, null);
- return;
- }
-
- MediaSession.Token token = null;
- try {
- if (!browser.isConnected()) {
- Log.e(TAG, "setBrowsedPlayer: " + mPackageName + "not connected");
- } else if ((token = browser.getSessionToken()) == null) {
- Log.e(TAG, "setBrowsedPlayer: " + mPackageName + "no Session token");
- } else {
- /* update to the new MediaBrowser */
- if (mMediaBrowser != null) {
- mMediaBrowser.disconnect();
- }
- mMediaBrowser = browser;
- mPackageName = connectedPackage;
-
- /* get rootfolder uid from media player */
- if (mMediaId == null) {
- mMediaId = mMediaBrowser.getRoot();
- /*
- * assuming that root folder uid will not change on uids changed
- */
- mRootFolderUid = mMediaId;
- /* store root folder uid to stack */
- mPathStack.push(mMediaId);
- }
-
- mMediaController = MediaControllerFactory.make(mContext, token);
- /* get root folder items */
- mMediaBrowser.subscribe(mRootFolderUid, mFolderItemsCb);
- return;
- }
- } catch (NullPointerException ex) {
- Log.e(TAG, "setBrowsedPlayer : Null pointer during init");
- ex.printStackTrace();
- }
-
- mMediaInterface.setBrowsedPlayerRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, (byte) 0x00,
- 0, null);
- }
-
- public void setBrowsed(String packageName, String cls) {
- mConnectingPackageName = packageName;
- mClassName = cls;
- /* cleanup variables from previous browsed calls */
- mFolderItems = null;
- mMediaId = null;
- mRootFolderUid = null;
- /*
- * create stack to store the navigation trail (current folder ID). This
- * will be required while navigating up the folder
- */
- mPathStack = new Stack<String>();
-
- /* Bind to MediaBrowseService of MediaPlayer */
- MediaConnectionCallback callback = new MediaConnectionCallback(packageName);
- MediaBrowser tempBrowser =
- new MediaBrowser(mContext, new ComponentName(packageName, mClassName), callback,
- null);
- callback.setBrowser(tempBrowser);
-
- tempBrowser.connect();
- }
-
- /* called when connection to media player is closed */
- public void cleanup() {
- if (DEBUG) {
- Log.d(TAG, "cleanup");
- }
-
- if (mConnState != DISCONNECTED) {
- mMediaBrowser.disconnect();
- }
-
- mHmap = null;
- mMediaController = null;
- mMediaBrowser = null;
- mPathStack = null;
- }
-
- public boolean isPlayerConnected() {
- if (mMediaBrowser == null) {
- if (DEBUG) {
- Log.d(TAG, "isPlayerConnected: mMediaBrowser = null!");
- }
- return false;
- }
-
- return mMediaBrowser.isConnected();
- }
-
- /* returns number of items in new path as reponse */
- public void changePath(byte[] folderUid, byte direction) {
- if (DEBUG) {
- Log.d(TAG, "changePath.direction = " + direction);
- }
- String newPath = "";
-
- if (!isPlayerConnected()) {
- Log.w(TAG, "changePath: disconnected from player service, sending internal error");
- mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, 0);
- return;
- }
-
- if (mMediaBrowser == null) {
- Log.e(TAG, "Media browser is null, sending internal error");
- mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, 0);
- return;
- }
-
- /* check direction and change the path */
- if (direction == AvrcpConstants.DIR_DOWN) { /* move down */
- if ((newPath = byteToString(folderUid)) == null) {
- Log.e(TAG, "Could not get media item from folder Uid, sending err response");
- mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_ITEM, 0);
- } else if (!isBrowsableFolderDn(newPath)) {
- /* new path is not browsable */
- Log.e(TAG, "ItemUid received from changePath cmd is not browsable");
- mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_DIRECTORY, 0);
- } else if (mPathStack.peek().equals(newPath)) {
- /* new_folder is same as current folder */
- Log.e(TAG, "new_folder is same as current folder, Invalid direction!");
- mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_DIRN, 0);
- } else {
- mMediaBrowser.subscribe(newPath, mFolderItemsCb);
- /* assume that call is success and update stack with new folder path */
- mPathStack.push(newPath);
- }
- } else if (direction == AvrcpConstants.DIR_UP) { /* move up */
- if (!isBrowsableFolderUp()) {
- /* Already on the root, cannot allow up: PTS: test case TC_TG_MCN_CB_BI_02_C
- * This is required, otherwise some CT will keep on sending change path up
- * until they receive error */
- Log.w(TAG, "Cannot go up from now, already in the root, Invalid direction!");
- mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_DIRN, 0);
- } else {
- /* move folder up */
- mPathStack.pop();
- newPath = mPathStack.peek();
- mMediaBrowser.subscribe(newPath, mFolderItemsCb);
- }
- } else { /* invalid direction */
- Log.w(TAG, "changePath : Invalid direction " + direction);
- mMediaInterface.changePathRsp(mBDAddr, AvrcpConstants.RSP_INV_DIRN, 0);
- }
- }
-
- public void getItemAttr(AvrcpCmd.ItemAttrCmd itemAttr) {
- String mediaID;
- if (DEBUG) {
- Log.d(TAG, "getItemAttr");
- }
-
- /* check if uid is valid by doing a lookup in hashmap */
- mediaID = byteToString(itemAttr.mUid);
- if (mediaID == null) {
- Log.e(TAG, "uid is invalid");
- mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_INV_ITEM, null);
- return;
- }
-
- /* check scope */
- if (itemAttr.mScope != AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM) {
- Log.e(TAG, "invalid scope");
- mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_INV_SCOPE, null);
- return;
- }
-
- if (mMediaBrowser == null) {
- Log.e(TAG, "mMediaBrowser is null");
- mMediaInterface.getItemAttrRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, null);
- return;
- }
-
- /* Subscribe to the parent to list items and retrieve the right one */
- mMediaBrowser.subscribe(mPathStack.peek(), new ItemAttribSubscriber(itemAttr, mediaID));
- }
-
- public void getTotalNumOfItems(byte scope) {
- if (DEBUG) {
- Log.d(TAG, "getTotalNumOfItems scope = " + scope);
- }
- if (scope != AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM) {
- Log.e(TAG, "getTotalNumOfItems error" + scope);
- mMediaInterface.getTotalNumOfItemsRsp(mBDAddr, AvrcpConstants.RSP_INV_SCOPE, 0, 0);
- return;
- }
-
- if (mFolderItems == null) {
- Log.e(TAG, "mFolderItems is null, sending internal error");
- /* folderitems were not fetched during change path */
- mMediaInterface.getTotalNumOfItemsRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, 0, 0);
- return;
- }
-
- /* find num items using size of already cached folder items */
- mMediaInterface.getTotalNumOfItemsRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR, 0,
- mFolderItems.size());
- }
-
- public void getFolderItemsVFS(AvrcpCmd.FolderItemsCmd reqObj) {
- if (!isPlayerConnected()) {
- Log.e(TAG, "unable to connect to media player, sending internal error");
- /* unable to connect to media player. Send error response to remote device */
- mMediaInterface.folderItemsRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, null);
- return;
- }
-
- if (DEBUG) {
- Log.d(TAG, "getFolderItemsVFS");
- }
- mFolderItemsReqObj = reqObj;
-
- if (mFolderItems == null) {
- /* Failed to fetch folder items from media player. Send error to remote device */
- Log.e(TAG, "Failed to fetch folder items during getFolderItemsVFS");
- mMediaInterface.folderItemsRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR, null);
- return;
- }
-
- /* Filter attributes based on the request and send response to remote device */
- getFolderItemsFilterAttr(mBDAddr, reqObj, mFolderItems,
- AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM, mFolderItemsReqObj.mStartItem,
- mFolderItemsReqObj.mEndItem);
- }
-
- /* Instructs media player to play particular media item */
- public void playItem(byte[] uid, byte scope) {
- String folderUid;
-
- if (isPlayerConnected()) {
- /* check if uid is valid */
- if ((folderUid = byteToString(uid)) == null) {
- Log.e(TAG, "uid is invalid!");
- mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_INV_ITEM);
- return;
- }
-
- if (mMediaController != null) {
- MediaController.TransportControls mediaControllerCntrl =
- mMediaController.getTransportControls();
- if (DEBUG) {
- Log.d(TAG, "Sending playID: " + folderUid);
- }
-
- if (scope == AvrcpConstants.BTRC_SCOPE_FILE_SYSTEM) {
- mediaControllerCntrl.playFromMediaId(folderUid, null);
- mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_NO_ERROR);
- } else {
- Log.e(TAG, "playItem received for invalid scope!");
- mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_INV_SCOPE);
- }
- } else {
- Log.e(TAG, "mediaController is null");
- mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR);
- }
- } else {
- Log.e(TAG, "playItem: Not connected to media player");
- mMediaInterface.playItemRsp(mBDAddr, AvrcpConstants.RSP_INTERNAL_ERR);
- }
- }
-
- /*
- * helper method to check if startItem and endItem index is with range of
- * MediaItem list. (Resultset containing all items in current path)
- */
- private List<MediaBrowser.MediaItem> checkIndexOutofBounds(byte[] bdaddr,
- List<MediaBrowser.MediaItem> children, long startItem, long endItem) {
- if (endItem >= children.size()) {
- endItem = children.size() - 1;
- }
- if (startItem >= Integer.MAX_VALUE) {
- startItem = Integer.MAX_VALUE;
- }
- try {
- List<MediaBrowser.MediaItem> childrenSubList =
- children.subList((int) startItem, (int) endItem + 1);
- if (childrenSubList.isEmpty()) {
- Log.i(TAG, "childrenSubList is empty.");
- throw new IndexOutOfBoundsException();
- }
- return childrenSubList;
- } catch (IndexOutOfBoundsException ex) {
- Log.w(TAG, "Index out of bounds start item =" + startItem + " end item = " + Math.min(
- children.size(), endItem + 1));
- return null;
- } catch (IllegalArgumentException ex) {
- Log.i(TAG, "Index out of bounds start item =" + startItem + " > size");
- return null;
- }
- }
-
-
- /*
- * helper method to filter required attibutes before sending GetFolderItems response
- */
- public void getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd mFolderItemsReqObj,
- List<MediaBrowser.MediaItem> children, byte scope, long startItem, long endItem) {
- if (DEBUG) {
- Log.d(TAG,
- "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = " + endItem);
- }
-
- List<MediaBrowser.MediaItem> resultItems = new ArrayList<MediaBrowser.MediaItem>();
-
- if (children == null) {
- Log.e(TAG, "Error: children are null in getFolderItemsFilterAttr");
- mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null);
- return;
- }
-
- /* check for index out of bound errors */
- resultItems = checkIndexOutofBounds(bdaddr, children, startItem, endItem);
- if (resultItems == null) {
- Log.w(TAG, "resultItems is null.");
- mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null);
- return;
- }
- FolderItemsData folderDataNative = new FolderItemsData(resultItems.size());
-
- /* variables to temperorily add attrs */
- ArrayList<String> attrArray = new ArrayList<String>();
- ArrayList<Integer> attrId = new ArrayList<Integer>();
-
- for (int itemIndex = 0; itemIndex < resultItems.size(); itemIndex++) {
- /* item type. Needs to be set by media player */
- MediaBrowser.MediaItem item = resultItems.get(itemIndex);
- int flags = item.getFlags();
- if ((flags & MediaBrowser.MediaItem.FLAG_BROWSABLE) != 0) {
- folderDataNative.mItemTypes[itemIndex] = AvrcpConstants.BTRC_ITEM_FOLDER;
- } else {
- folderDataNative.mItemTypes[itemIndex] = AvrcpConstants.BTRC_ITEM_MEDIA;
- }
-
- /* set playable */
- if ((flags & MediaBrowser.MediaItem.FLAG_PLAYABLE) != 0) {
- folderDataNative.mPlayable[itemIndex] = AvrcpConstants.ITEM_PLAYABLE;
- } else {
- folderDataNative.mPlayable[itemIndex] = AvrcpConstants.ITEM_NOT_PLAYABLE;
- }
- /* set uid for current item */
- byte[] uid = stringToByte(item.getDescription().getMediaId());
- for (int idx = 0; idx < AvrcpConstants.UID_SIZE; idx++) {
- folderDataNative.mItemUid[itemIndex * AvrcpConstants.UID_SIZE + idx] = uid[idx];
- }
-
- /* Set display name for current item */
- folderDataNative.mDisplayNames[itemIndex] =
- getAttrValue(AvrcpConstants.ATTRID_TITLE, item);
-
- int maxAttributesRequested = 0;
- boolean isAllAttribRequested = false;
- /* check if remote requested for attributes */
- if (mFolderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
- int attrCnt = 0;
-
- /* add requested attr ids to a temp array */
- if (mFolderItemsReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
- isAllAttribRequested = true;
- maxAttributesRequested = AvrcpConstants.MAX_NUM_ATTR;
- } else {
- /* get only the requested attribute ids from the request */
- maxAttributesRequested = mFolderItemsReqObj.mNumAttr;
- }
-
- /* lookup and copy values of attributes for ids requested above */
- for (int idx = 0; idx < maxAttributesRequested; idx++) {
- /* check if media player provided requested attributes */
- String value = null;
-
- int attribId =
- isAllAttribRequested ? (idx + 1) : mFolderItemsReqObj.mAttrIDs[idx];
- value = getAttrValue(attribId, resultItems.get(itemIndex));
- if (value != null) {
- attrArray.add(value);
- attrId.add(attribId);
- attrCnt++;
- }
- }
- /* add num attr actually received from media player for a particular item */
- folderDataNative.mAttributesNum[itemIndex] = attrCnt;
- }
- }
-
- /* copy filtered attr ids and attr values to response parameters */
- if (attrId.size() > 0) {
- folderDataNative.mAttrIds = new int[attrId.size()];
- for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++) {
- folderDataNative.mAttrIds[attrIndex] = attrId.get(attrIndex);
- }
- folderDataNative.mAttrValues = attrArray.toArray(new String[attrArray.size()]);
- }
-
- /* create rsp object and send response to remote device */
- FolderItemsRsp rspObj =
- new FolderItemsRsp(AvrcpConstants.RSP_NO_ERROR, Avrcp.sUIDCounter, scope,
- folderDataNative.mNumItems, folderDataNative.mFolderTypes,
- folderDataNative.mPlayable, folderDataNative.mItemTypes,
- folderDataNative.mItemUid, folderDataNative.mDisplayNames,
- folderDataNative.mAttributesNum, folderDataNative.mAttrIds,
- folderDataNative.mAttrValues);
- mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
- }
-
- public static String getAttrValue(int attr, MediaBrowser.MediaItem item) {
- String attrValue = null;
- try {
- MediaDescription desc = item.getDescription();
- Bundle extras = desc.getExtras();
- switch (attr) {
- /* Title is mandatory attribute */
- case AvrcpConstants.ATTRID_TITLE:
- attrValue = desc.getTitle().toString();
- break;
-
- case AvrcpConstants.ATTRID_ARTIST:
- attrValue = extras.getString(MediaMetadata.METADATA_KEY_ARTIST);
- break;
-
- case AvrcpConstants.ATTRID_ALBUM:
- attrValue = extras.getString(MediaMetadata.METADATA_KEY_ALBUM);
- break;
-
- case AvrcpConstants.ATTRID_TRACK_NUM:
- attrValue = extras.getString(MediaMetadata.METADATA_KEY_TRACK_NUMBER);
- break;
-
- case AvrcpConstants.ATTRID_NUM_TRACKS:
- attrValue = extras.getString(MediaMetadata.METADATA_KEY_NUM_TRACKS);
- break;
-
- case AvrcpConstants.ATTRID_GENRE:
- attrValue = extras.getString(MediaMetadata.METADATA_KEY_GENRE);
- break;
-
- case AvrcpConstants.ATTRID_PLAY_TIME:
- attrValue = extras.getString(MediaMetadata.METADATA_KEY_DURATION);
- break;
-
- case AvrcpConstants.ATTRID_COVER_ART:
- Log.e(TAG, "getAttrValue: Cover art attribute not supported");
- return null;
-
- default:
- Log.e(TAG, "getAttrValue: Unknown attribute ID requested: " + attr);
- return null;
- }
- } catch (NullPointerException ex) {
- Log.w(TAG, "getAttrValue: attr id not found in result");
- /* checking if attribute is title, then it is mandatory and cannot send null */
- if (attr == AvrcpConstants.ATTRID_TITLE) {
- attrValue = "<Unknown Title>";
- } else {
- return null;
- }
- }
- if (DEBUG) {
- Log.d(TAG, "getAttrValue: attrvalue = " + attrValue + "attr id:" + attr);
- }
- return attrValue;
- }
-
-
- public String getPackageName() {
- return mPackageName;
- }
-
- /* Helper methods */
-
- /* check if item is browsable Down*/
- private boolean isBrowsableFolderDn(String uid) {
- for (MediaBrowser.MediaItem item : mFolderItems) {
- if (item.getMediaId().equals(uid) && (
- (item.getFlags() & MediaBrowser.MediaItem.FLAG_BROWSABLE)
- == MediaBrowser.MediaItem.FLAG_BROWSABLE)) {
- return true;
- }
- }
- return false;
- }
-
- /* check if browsable Up*/
- private boolean isBrowsableFolderUp() {
- if (mPathStack.peek().equals(mRootFolderUid)) {
- /* Already on the root, cannot go up */
- return false;
- }
- return true;
- }
-
- /* convert uid to mediaId */
- private String byteToString(byte[] byteArray) {
- int uid = new BigInteger(byteArray).intValue();
- String mediaId = mHmap.get(uid);
- return mediaId;
- }
-
- /* convert mediaId to uid */
- private byte[] stringToByte(String mediaId) {
- /* check if this mediaId already exists in hashmap */
- if (!mHmap.containsValue(mediaId)) { /* add to hashmap */
- // Offset by one as uid 0 is reserved
- int uid = mHmap.size() + 1;
- mHmap.put(uid, mediaId);
- return intToByteArray(uid);
- } else { /* search key for give mediaId */
- for (int uid : mHmap.keySet()) {
- if (mHmap.get(uid).equals(mediaId)) {
- return intToByteArray(uid);
- }
- }
- }
- return null;
- }
-
- /* converts queue item received from getQueue call, to MediaItem used by FilterAttr method */
- private List<MediaBrowser.MediaItem> queueItem2MediaItem(
- List<MediaSession.QueueItem> tempItems) {
-
- List<MediaBrowser.MediaItem> tempMedia = new ArrayList<MediaBrowser.MediaItem>();
- for (int itemCount = 0; itemCount < tempItems.size(); itemCount++) {
- MediaDescription.Builder build = new MediaDescription.Builder();
- build.setMediaId(Long.toString(tempItems.get(itemCount).getQueueId()));
- build.setTitle(tempItems.get(itemCount).getDescription().getTitle());
- build.setExtras(tempItems.get(itemCount).getDescription().getExtras());
- MediaDescription des = build.build();
- MediaItem item = new MediaItem((des), MediaItem.FLAG_PLAYABLE);
- tempMedia.add(item);
- }
- return tempMedia;
- }
-
- /* convert integer to byte array of size 8 bytes */
- public byte[] intToByteArray(int value) {
- int index = 0;
- byte[] encodedValue = new byte[AvrcpConstants.UID_SIZE];
-
- encodedValue[index++] = (byte) 0x00;
- encodedValue[index++] = (byte) 0x00;
- encodedValue[index++] = (byte) 0x00;
- encodedValue[index++] = (byte) 0x00;
- encodedValue[index++] = (byte) (value >> 24);
- encodedValue[index++] = (byte) (value >> 16);
- encodedValue[index++] = (byte) (value >> 8);
- encodedValue[index++] = (byte) value;
-
- return encodedValue;
- }
-}
diff --git a/src/com/android/bluetooth/newavrcp/BrowsedPlayerWrapper.java b/src/com/android/bluetooth/avrcp/BrowsedPlayerWrapper.java
similarity index 99%
rename from src/com/android/bluetooth/newavrcp/BrowsedPlayerWrapper.java
rename to src/com/android/bluetooth/avrcp/BrowsedPlayerWrapper.java
index 93afd17..b704aae 100644
--- a/src/com/android/bluetooth/newavrcp/BrowsedPlayerWrapper.java
+++ b/src/com/android/bluetooth/avrcp/BrowsedPlayerWrapper.java
@@ -33,7 +33,7 @@
* Right now this is ok because the BrowsablePlayerConnector will handle timeouts.
*/
class BrowsedPlayerWrapper {
- private static final String TAG = "NewAvrcpBrowsedPlayerWrapper";
+ private static final String TAG = "AvrcpBrowsedPlayerWrapper";
private static final boolean DEBUG = true;
enum ConnectionState {
diff --git a/src/com/android/bluetooth/newavrcp/GPMWrapper.java b/src/com/android/bluetooth/avrcp/GPMWrapper.java
similarity index 97%
rename from src/com/android/bluetooth/newavrcp/GPMWrapper.java
rename to src/com/android/bluetooth/avrcp/GPMWrapper.java
index a7f41fb..87e5ec0 100644
--- a/src/com/android/bluetooth/newavrcp/GPMWrapper.java
+++ b/src/com/android/bluetooth/avrcp/GPMWrapper.java
@@ -25,7 +25,7 @@
* methods to allow Google Play Music to match the default behaviour of MediaPlayerWrapper.
*/
class GPMWrapper extends MediaPlayerWrapper {
- private static final String TAG = "NewAvrcpGPMWrapper";
+ private static final String TAG = "AvrcpGPMWrapper";
private static final boolean DEBUG = true;
@Override
diff --git a/src/com/android/bluetooth/newavrcp/MediaPlayerList.java b/src/com/android/bluetooth/avrcp/MediaPlayerList.java
similarity index 96%
rename from src/com/android/bluetooth/newavrcp/MediaPlayerList.java
rename to src/com/android/bluetooth/avrcp/MediaPlayerList.java
index 74ea039..29f4cf9 100644
--- a/src/com/android/bluetooth/newavrcp/MediaPlayerList.java
+++ b/src/com/android/bluetooth/avrcp/MediaPlayerList.java
@@ -57,7 +57,7 @@
* player would effectively cause player switch by sending a play command to that player.
*/
public class MediaPlayerList {
- private static final String TAG = "NewAvrcpMediaPlayerList";
+ private static final String TAG = "AvrcpMediaPlayerList";
private static final boolean DEBUG = true;
static boolean sTesting = false;
@@ -89,6 +89,7 @@
private int mActivePlayerId = NO_ACTIVE_PLAYER;
private AvrcpTargetService.ListCallback mCallback;
+ private BrowsablePlayerConnector mBrowsablePlayerConnector;
interface MediaUpdateCallback {
void run(MediaData data);
@@ -140,8 +141,8 @@
.getPackageManager()
.queryIntentServices(intent, PackageManager.MATCH_ALL);
- BrowsablePlayerConnector.connectToPlayers(mContext, mLooper, playerList,
- (List<BrowsedPlayerWrapper> players) -> {
+ mBrowsablePlayerConnector = BrowsablePlayerConnector.connectToPlayers(mContext, mLooper,
+ playerList, (List<BrowsedPlayerWrapper> players) -> {
Log.i(TAG, "init: Browsable Player list size is " + players.size());
// Check to see if the list has been cleaned up before this completed
@@ -196,6 +197,9 @@
}
mMediaPlayers.clear();
+ if (mBrowsablePlayerConnector != null) {
+ mBrowsablePlayerConnector.cleanup();
+ }
for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) {
player.disconnect();
}
@@ -207,8 +211,10 @@
}
int getFreeMediaPlayerId() {
- int id = 0;
- while (mMediaPlayerIds.containsValue(++id)) {}
+ int id = 1;
+ while (mMediaPlayerIds.containsValue(id)) {
+ id++;
+ }
return id;
}
@@ -620,6 +626,12 @@
android.media.session.MediaController controller =
new android.media.session.MediaController(mContext, token);
+ if (mMediaSessionManager == null) {
+ Log.w(TAG, "onAddressedPlayerChanged(Token): Unexpected callback "
+ + "from the MediaSessionManager");
+ return;
+ }
+
if (!mMediaPlayerIds.containsKey(controller.getPackageName())) {
// Since we have a controller, we can try to to recover by adding the
// player and then setting it as active.
@@ -634,6 +646,12 @@
@Override
public void onAddressedPlayerChanged(ComponentName receiver) {
+ if (mMediaSessionManager == null) {
+ Log.w(TAG, "onAddressedPlayerChanged(Component): Unexpected callback "
+ + "from the MediaSessionManager");
+ return;
+ }
+
if (receiver == null) {
return;
}
diff --git a/src/com/android/bluetooth/newavrcp/MediaPlayerWrapper.java b/src/com/android/bluetooth/avrcp/MediaPlayerWrapper.java
similarity index 95%
rename from src/com/android/bluetooth/newavrcp/MediaPlayerWrapper.java
rename to src/com/android/bluetooth/avrcp/MediaPlayerWrapper.java
index 256e771..e4176b3 100644
--- a/src/com/android/bluetooth/newavrcp/MediaPlayerWrapper.java
+++ b/src/com/android/bluetooth/avrcp/MediaPlayerWrapper.java
@@ -23,10 +23,11 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
-import android.support.annotation.GuardedBy;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.List;
import java.util.Objects;
@@ -38,8 +39,8 @@
* with that.
*/
class MediaPlayerWrapper {
- private static final String TAG = "NewAvrcpMediaPlayerWrapper";
- private static final boolean DEBUG = true;
+ private static final String TAG = "AvrcpMediaPlayerWrapper";
+ private static final boolean DEBUG = false;
static boolean sTesting = false;
private MediaController mMediaController;
@@ -61,14 +62,18 @@
void mediaUpdatedCallback(MediaData data);
}
- boolean isReady() {
+ boolean isPlaybackStateReady() {
if (getPlaybackState() == null) {
- d("isReady(): PlaybackState is null");
+ d("isPlaybackStateReady(): PlaybackState is null");
return false;
}
+ return true;
+ }
+
+ boolean isMetadataReady() {
if (getMetadata() == null) {
- d("isReady(): Metadata is null");
+ d("isMetadataReady(): Metadata is null");
return false;
}
@@ -371,13 +376,16 @@
@Override
public void onMetadataChanged(@Nullable MediaMetadata metadata) {
- if (!isReady()) {
+ if (!isMetadataReady()) {
Log.v(TAG, "onMetadataChanged(): " + mPackageName
+ " tried to update with no queue");
return;
}
- Log.v(TAG, "onMetadataChanged(): " + mPackageName + " : " + Util.toMetadata(metadata));
+ if (DEBUG) {
+ Log.v(TAG, "onMetadataChanged(): " + mPackageName + " : "
+ + Util.toMetadata(metadata));
+ }
if (!Objects.equals(metadata, getMetadata())) {
e("The callback metadata doesn't match controller metadata");
@@ -402,7 +410,7 @@
@Override
public void onPlaybackStateChanged(@Nullable PlaybackState state) {
- if (!isReady()) {
+ if (!isPlaybackStateReady()) {
Log.v(TAG, "onPlaybackStateChanged(): " + mPackageName
+ " tried to update with no queue");
return;
@@ -431,7 +439,7 @@
@Override
public void onQueueChanged(@Nullable List<MediaSession.QueueItem> queue) {
- if (!isReady()) {
+ if (!isPlaybackStateReady() || !isMetadataReady()) {
Log.v(TAG, "onQueueChanged(): " + mPackageName
+ " tried to update with no queue");
return;
diff --git a/src/com/android/bluetooth/newavrcp/helpers/AvrcpPassthrough.java b/src/com/android/bluetooth/avrcp/helpers/AvrcpPassthrough.java
similarity index 100%
rename from src/com/android/bluetooth/newavrcp/helpers/AvrcpPassthrough.java
rename to src/com/android/bluetooth/avrcp/helpers/AvrcpPassthrough.java
diff --git a/src/com/android/bluetooth/newavrcp/helpers/Folder.java b/src/com/android/bluetooth/avrcp/helpers/Folder.java
similarity index 100%
rename from src/com/android/bluetooth/newavrcp/helpers/Folder.java
rename to src/com/android/bluetooth/avrcp/helpers/Folder.java
diff --git a/src/com/android/bluetooth/newavrcp/helpers/ListItem.java b/src/com/android/bluetooth/avrcp/helpers/ListItem.java
similarity index 100%
rename from src/com/android/bluetooth/newavrcp/helpers/ListItem.java
rename to src/com/android/bluetooth/avrcp/helpers/ListItem.java
diff --git a/src/com/android/bluetooth/newavrcp/helpers/MediaData.java b/src/com/android/bluetooth/avrcp/helpers/MediaData.java
similarity index 100%
rename from src/com/android/bluetooth/newavrcp/helpers/MediaData.java
rename to src/com/android/bluetooth/avrcp/helpers/MediaData.java
diff --git a/src/com/android/bluetooth/newavrcp/helpers/Metadata.java b/src/com/android/bluetooth/avrcp/helpers/Metadata.java
similarity index 100%
rename from src/com/android/bluetooth/newavrcp/helpers/Metadata.java
rename to src/com/android/bluetooth/avrcp/helpers/Metadata.java
diff --git a/src/com/android/bluetooth/newavrcp/helpers/PlayStatus.java b/src/com/android/bluetooth/avrcp/helpers/PlayStatus.java
similarity index 100%
rename from src/com/android/bluetooth/newavrcp/helpers/PlayStatus.java
rename to src/com/android/bluetooth/avrcp/helpers/PlayStatus.java
diff --git a/src/com/android/bluetooth/newavrcp/helpers/PlayerInfo.java b/src/com/android/bluetooth/avrcp/helpers/PlayerInfo.java
similarity index 100%
rename from src/com/android/bluetooth/newavrcp/helpers/PlayerInfo.java
rename to src/com/android/bluetooth/avrcp/helpers/PlayerInfo.java
diff --git a/src/com/android/bluetooth/newavrcp/helpers/Util.java b/src/com/android/bluetooth/avrcp/helpers/Util.java
similarity index 99%
rename from src/com/android/bluetooth/newavrcp/helpers/Util.java
rename to src/com/android/bluetooth/avrcp/helpers/Util.java
index eb84b0a..e09377b 100644
--- a/src/com/android/bluetooth/newavrcp/helpers/Util.java
+++ b/src/com/android/bluetooth/avrcp/helpers/Util.java
@@ -29,7 +29,7 @@
import java.util.List;
class Util {
- public static String TAG = "NewAvrcpUtil";
+ public static String TAG = "AvrcpUtil";
public static boolean DEBUG = false;
private static final String GPM_KEY = "com.google.android.music.mediasession.music_metadata";
diff --git a/src/com/android/bluetooth/avrcp/mockable/MediaController.java b/src/com/android/bluetooth/avrcp/mockable/MediaController.java
index 934a454..5c7b73f 100644
--- a/src/com/android/bluetooth/avrcp/mockable/MediaController.java
+++ b/src/com/android/bluetooth/avrcp/mockable/MediaController.java
@@ -44,13 +44,13 @@
public android.media.session.MediaController.TransportControls mTransportDelegate;
public TransportControls mTransportControls;
- public MediaController(@NonNull android.media.session.MediaController delegate) {
+ MediaController(@NonNull android.media.session.MediaController delegate) {
mDelegate = delegate;
mTransportDelegate = delegate.getTransportControls();
mTransportControls = new TransportControls();
}
- public MediaController(Context context, MediaSession.Token token) {
+ MediaController(Context context, MediaSession.Token token) {
mDelegate = new android.media.session.MediaController(context, token);
mTransportDelegate = mDelegate.getTransportControls();
mTransportControls = new TransportControls();
@@ -65,101 +65,169 @@
return mTransportControls;
}
+ /**
+ * Wrapper for MediaController.dispatchMediaButtonEvent(KeyEvent keyEvent)
+ */
public boolean dispatchMediaButtonEvent(@NonNull KeyEvent keyEvent) {
return mDelegate.dispatchMediaButtonEvent(keyEvent);
}
+ /**
+ * Wrapper for MediaController.getPlaybackState()
+ */
@Nullable
public PlaybackState getPlaybackState() {
return mDelegate.getPlaybackState();
}
+
+ /**
+ * Wrapper for MediaController.getMetadata()
+ */
@Nullable
public MediaMetadata getMetadata() {
return mDelegate.getMetadata();
}
+ /**
+ * Wrapper for MediaController.getQueue()
+ */
@Nullable
public List<MediaSession.QueueItem> getQueue() {
return mDelegate.getQueue();
}
+ /**
+ * Wrapper for MediaController.getQueueTitle()
+ */
@Nullable
public CharSequence getQueueTitle() {
return mDelegate.getQueueTitle();
}
+ /**
+ * Wrapper for MediaController.getExtras()
+ */
@Nullable
public Bundle getExtras() {
return mDelegate.getExtras();
}
+ /**
+ * Wrapper for MediaController.getRatingType()
+ */
public int getRatingType() {
return mDelegate.getRatingType();
}
+ /**
+ * Wrapper for MediaController.getFlags()
+ */
public long getFlags() {
return mDelegate.getFlags();
}
+ /**
+ * Wrapper for MediaController.getPlaybackInfo()
+ */
@Nullable
public android.media.session.MediaController.PlaybackInfo getPlaybackInfo() {
return mDelegate.getPlaybackInfo();
}
+
+ /**
+ * Wrapper for MediaController.getSessionActivity()
+ */
@Nullable
public PendingIntent getSessionActivity() {
return mDelegate.getSessionActivity();
}
+ /**
+ * Wrapper for MediaController.getSessionToken()
+ */
@NonNull
public MediaSession.Token getSessionToken() {
return mDelegate.getSessionToken();
}
+ /**
+ * Wrapper for MediaController.setVolumeTo(int value, int flags)
+ */
public void setVolumeTo(int value, int flags) {
mDelegate.setVolumeTo(value, flags);
}
+ /**
+ * Wrapper for MediaController.adjustVolume(int direction, int flags)
+ */
public void adjustVolume(int direction, int flags) {
mDelegate.adjustVolume(direction, flags);
}
+ /**
+ * Wrapper for MediaController.registerCallback(Callback callback)
+ */
public void registerCallback(@NonNull Callback callback) {
//TODO(apanicke): Add custom callback struct to be able to analyze and
// delegate callbacks
mDelegate.registerCallback(callback);
}
+ /**
+ * Wrapper for MediaController.registerCallback(Callback callback, Handler handler)
+ */
public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
mDelegate.registerCallback(callback, handler);
}
+ /**
+ * Wrapper for MediaController.unregisterCallback(Callback callback)
+ */
public void unregisterCallback(@NonNull Callback callback) {
mDelegate.unregisterCallback(callback);
}
+ /**
+ * Wrapper for MediaController.sendCommand(String command, Bundle args, ResultReceiver cb)
+ */
public void sendCommand(@NonNull String command, @Nullable Bundle args,
@Nullable ResultReceiver cb) {
mDelegate.sendCommand(command, args, cb);
}
+ /**
+ * Wrapper for MediaController.getPackageName()
+ */
public String getPackageName() {
return mDelegate.getPackageName();
}
+ /**
+ * Wrapper for MediaController.getTag()
+ */
public String getTag() {
return mDelegate.getTag();
}
+ /**
+ * Wrapper for MediaController.controlsSameSession(MediaController other)
+ */
public boolean controlsSameSession(MediaController other) {
return mDelegate.controlsSameSession(other.getWrappedInstance());
}
+ /**
+ * Wrapper for MediaController.controlsSameSession(MediaController other)
+ */
public boolean controlsSameSession(android.media.session.MediaController other) {
return mDelegate.controlsSameSession(other);
}
+ /**
+ * Wrapper for MediaController.equals(Object other)
+ */
@Override
public boolean equals(Object o) {
if (o instanceof android.media.session.MediaController) {
@@ -171,6 +239,9 @@
return false;
}
+ /**
+ * Wrapper for MediaController.toString()
+ */
@Override
public String toString() {
MediaMetadata data = getMetadata();
@@ -179,83 +250,146 @@
mDelegate.hashCode()) + ") " + desc;
}
+ /**
+ * Wrapper for MediaController.Callback
+ */
public abstract static class Callback extends android.media.session.MediaController.Callback {}
+ /**
+ * Wrapper for MediaController.TransportControls
+ */
public class TransportControls {
+ /**
+ * Wrapper for MediaController.TransportControls.prepare()
+ */
public void prepare() {
mTransportDelegate.prepare();
}
+ /**
+ * Wrapper for MediaController.TransportControls.prepareFromMediaId()
+ */
public void prepareFromMediaId(String mediaId, Bundle extras) {
mTransportDelegate.prepareFromMediaId(mediaId, extras);
}
+ /**
+ * Wrapper for MediaController.TransportControls.prepareFromSearch()
+ */
public void prepareFromSearch(String query, Bundle extras) {
mTransportDelegate.prepareFromSearch(query, extras);
}
+ /**
+ * Wrapper for MediaController.TransportControls.prepareFromUri()
+ */
public void prepareFromUri(Uri uri, Bundle extras) {
mTransportDelegate.prepareFromUri(uri, extras);
}
+ /**
+ * Wrapper for MediaController.TransportControls.play()
+ */
public void play() {
mTransportDelegate.play();
}
+ /**
+ * Wrapper for MediaController.TransportControls.playFromMediaId()
+ */
public void playFromMediaId(String mediaId, Bundle extras) {
mTransportDelegate.playFromMediaId(mediaId, extras);
}
+ /**
+ * Wrapper for MediaController.TransportControls.playFromSearch()
+ */
public void playFromSearch(String query, Bundle extras) {
mTransportDelegate.playFromSearch(query, extras);
}
+ /**
+ * Wrapper for MediaController.TransportControls.playFromUri()
+ */
public void playFromUri(Uri uri, Bundle extras) {
mTransportDelegate.playFromUri(uri, extras);
}
+ /**
+ * Wrapper for MediaController.TransportControls.skipToQueueItem()
+ */
public void skipToQueueItem(long id) {
mTransportDelegate.skipToQueueItem(id);
}
+ /**
+ * Wrapper for MediaController.TransportControls.pause()
+ */
public void pause() {
mTransportDelegate.pause();
}
+ /**
+ * Wrapper for MediaController.TransportControls.stop()
+ */
public void stop() {
mTransportDelegate.stop();
}
+ /**
+ * Wrapper for MediaController.TransportControls.seekTo()
+ */
public void seekTo(long pos) {
mTransportDelegate.seekTo(pos);
}
+ /**
+ * Wrapper for MediaController.TransportControls.fastForward()
+ */
public void fastForward() {
mTransportDelegate.fastForward();
}
+ /**
+ * Wrapper for MediaController.TransportControls.skipToNext()
+ */
public void skipToNext() {
mTransportDelegate.skipToNext();
}
+ /**
+ * Wrapper for MediaController.TransportControls.rewind()
+ */
public void rewind() {
mTransportDelegate.rewind();
}
+ /**
+ * Wrapper for MediaController.TransportControls.skipToPrevious()
+ */
public void skipToPrevious() {
mTransportDelegate.skipToPrevious();
}
+ /**
+ * Wrapper for MediaController.TransportControls.setRating()
+ */
public void setRating(Rating rating) {
mTransportDelegate.setRating(rating);
}
+ /**
+ * Wrapper for MediaController.TransportControls.sendCustomAction()
+ */
public void sendCustomAction(@NonNull PlaybackState.CustomAction customAction,
@Nullable Bundle args) {
mTransportDelegate.sendCustomAction(customAction, args);
}
+ /**
+ * Wrapper for MediaController.TransportControls.sendCustomAction()
+ */
public void sendCustomAction(@NonNull String action, @Nullable Bundle args) {
mTransportDelegate.sendCustomAction(action, args);
}
diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java
index 6111ec4..693b4a2 100644
--- a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java
+++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerService.java
@@ -21,14 +21,11 @@
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.IBluetoothAvrcpController;
+import android.content.Intent;
import android.media.MediaDescription;
-import android.media.MediaMetadata;
-import android.media.browse.MediaBrowser;
import android.media.browse.MediaBrowser.MediaItem;
import android.media.session.PlaybackState;
import android.os.Bundle;
-import android.os.HandlerThread;
-import android.os.Message;
import android.util.Log;
import com.android.bluetooth.Utils;
@@ -37,15 +34,21 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
/**
* Provides Bluetooth AVRCP Controller profile, as a service in the Bluetooth application.
*/
public class AvrcpControllerService extends ProfileService {
static final String TAG = "AvrcpControllerService";
- static final boolean DBG = false;
- static final boolean VDBG = false;
+ static final int MAXIMUM_CONNECTED_DEVICES = 5;
+ static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
+
+ public static final String MEDIA_ITEM_UID_KEY = "media-item-uid-key";
/*
* Play State Values from JNI
*/
@@ -56,97 +59,19 @@
private static final byte JNI_PLAY_STATUS_REV_SEEK = 0x04;
private static final byte JNI_PLAY_STATUS_ERROR = -1;
- /*
- * Browsing Media Item Attribute IDs
- * This should be kept in sync with BTRC_MEDIA_ATTR_ID_* in bt_rc.h
+ /* Folder/Media Item scopes.
+ * Keep in sync with AVRCP 1.6 sec. 6.10.1
*/
- private static final int JNI_MEDIA_ATTR_ID_INVALID = -1;
- private static final int JNI_MEDIA_ATTR_ID_TITLE = 0x00000001;
- private static final int JNI_MEDIA_ATTR_ID_ARTIST = 0x00000002;
- private static final int JNI_MEDIA_ATTR_ID_ALBUM = 0x00000003;
- private static final int JNI_MEDIA_ATTR_ID_TRACK_NUM = 0x00000004;
- private static final int JNI_MEDIA_ATTR_ID_NUM_TRACKS = 0x00000005;
- private static final int JNI_MEDIA_ATTR_ID_GENRE = 0x00000006;
- private static final int JNI_MEDIA_ATTR_ID_PLAYING_TIME = 0x00000007;
+ public static final byte BROWSE_SCOPE_PLAYER_LIST = 0x00;
+ public static final byte BROWSE_SCOPE_VFS = 0x01;
+ public static final byte BROWSE_SCOPE_SEARCH = 0x02;
+ public static final byte BROWSE_SCOPE_NOW_PLAYING = 0x03;
- /*
- * Browsing folder types
- * This should be kept in sync with BTRC_FOLDER_TYPE_* in bt_rc.h
+ /* Folder navigation directions
+ * This is borrowed from AVRCP 1.6 spec and must be kept with same values
*/
- private static final int JNI_FOLDER_TYPE_TITLES = 0x01;
- private static final int JNI_FOLDER_TYPE_ALBUMS = 0x02;
- private static final int JNI_FOLDER_TYPE_ARTISTS = 0x03;
- private static final int JNI_FOLDER_TYPE_GENRES = 0x04;
- private static final int JNI_FOLDER_TYPE_PLAYLISTS = 0x05;
- private static final int JNI_FOLDER_TYPE_YEARS = 0x06;
-
- /*
- * AVRCP Error types as defined in spec. Also they should be in sync with btrc_status_t.
- * NOTE: Not all may be defined.
- */
- private static final int JNI_AVRC_STS_NO_ERROR = 0x04;
- private static final int JNI_AVRC_INV_RANGE = 0x0b;
-
- /**
- * Intent used to broadcast the change in browse connection state of the AVRCP Controller
- * profile.
- *
- * <p>This intent will have 2 extras:
- * <ul>
- * <li> {@link BluetoothProfile#EXTRA_STATE} - The current state of the profile. </li>
- * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
- * </ul>
- *
- * <p>{@link #EXTRA_STATE} can be any of
- * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
- * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
- *
- * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
- * receive.
- */
- public static final String ACTION_BROWSE_CONNECTION_STATE_CHANGED =
- "android.bluetooth.avrcp-controller.profile.action.BROWSE_CONNECTION_STATE_CHANGED";
-
- /**
- * intent used to broadcast the change in metadata state of playing track on the avrcp
- * ag.
- *
- * <p>this intent will have the two extras:
- * <ul>
- * <li> {@link #extra_metadata} - {@link mediametadata} containing the current metadata.</li>
- * <li> {@link #extra_playback} - {@link playbackstate} containing the current playback
- * state. </li>
- * </ul>
- */
- public static final String ACTION_TRACK_EVENT =
- "android.bluetooth.avrcp-controller.profile.action.TRACK_EVENT";
-
- /**
- * Intent used to broadcast the change of folder list.
- *
- * <p>This intent will have the one extra:
- * <ul>
- * <li> {@link #EXTRA_FOLDER_LIST} - array of {@link MediaBrowser#MediaItem}
- * containing the folder listing of currently selected folder.
- * </ul>
- */
- public static final String ACTION_FOLDER_LIST =
- "android.bluetooth.avrcp-controller.profile.action.FOLDER_LIST";
-
- public static final String EXTRA_FOLDER_LIST =
- "android.bluetooth.avrcp-controller.profile.extra.FOLDER_LIST";
-
- public static final String EXTRA_FOLDER_ID = "com.android.bluetooth.avrcp.EXTRA_FOLDER_ID";
- public static final String EXTRA_FOLDER_BT_ID =
- "com.android.bluetooth.avrcp-controller.EXTRA_FOLDER_BT_ID";
-
- public static final String EXTRA_METADATA =
- "android.bluetooth.avrcp-controller.profile.extra.METADATA";
-
- public static final String EXTRA_PLAYBACK =
- "android.bluetooth.avrcp-controller.profile.extra.PLAYBACK";
-
- public static final String MEDIA_ITEM_UID_KEY = "media-item-uid-key";
+ public static final byte FOLDER_NAVIGATION_DIRECTION_UP = 0x00;
+ public static final byte FOLDER_NAVIGATION_DIRECTION_DOWN = 0x01;
/*
* KeyCoded for Pass Through Commands
@@ -165,461 +90,109 @@
public static final int KEY_STATE_PRESSED = 0;
public static final int KEY_STATE_RELEASED = 1;
- /* Group Navigation Key Codes */
- public static final int PASS_THRU_CMD_ID_NEXT_GRP = 0x00;
- public static final int PASS_THRU_CMD_ID_PREV_GRP = 0x01;
+ static BrowseTree sBrowseTree;
+ private static AvrcpControllerService sService;
+ private final BluetoothAdapter mAdapter;
- /* Folder navigation directions
- * This is borrowed from AVRCP 1.6 spec and must be kept with same values
- */
- public static final int FOLDER_NAVIGATION_DIRECTION_UP = 0x00;
- public static final int FOLDER_NAVIGATION_DIRECTION_DOWN = 0x01;
-
- /* Folder/Media Item scopes.
- * Keep in sync with AVRCP 1.6 sec. 6.10.1
- */
- public static final int BROWSE_SCOPE_PLAYER_LIST = 0x00;
- public static final int BROWSE_SCOPE_VFS = 0x01;
- public static final int BROWSE_SCOPE_SEARCH = 0x02;
- public static final int BROWSE_SCOPE_NOW_PLAYING = 0x03;
-
- private AvrcpControllerStateMachine mAvrcpCtSm;
- private static AvrcpControllerService sAvrcpControllerService;
- // UID size is 8 bytes (AVRCP 1.6 spec)
- private static final byte[] EMPTY_UID = {0, 0, 0, 0, 0, 0, 0, 0};
-
- // We only support one device.
- private BluetoothDevice mConnectedDevice = null;
- // If browse is supported (only valid if mConnectedDevice != null).
- private boolean mBrowseConnected = false;
- // Caches the current browse folder. If this is null then root is the currently browsed folder
- // (which also has no UID).
- private String mCurrentBrowseFolderUID = null;
+ protected Map<BluetoothDevice, AvrcpControllerStateMachine> mDeviceStateMap =
+ new ConcurrentHashMap<>(1);
static {
classInitNative();
}
public AvrcpControllerService() {
- initNative();
- }
-
- @Override
- protected IProfileServiceBinder initBinder() {
- return new BluetoothAvrcpControllerBinder(this);
+ super();
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
}
@Override
protected boolean start() {
- HandlerThread thread = new HandlerThread("BluetoothAvrcpHandler");
- thread.start();
- mAvrcpCtSm = new AvrcpControllerStateMachine(this);
- mAvrcpCtSm.start();
+ initNative();
+ sBrowseTree = new BrowseTree(null);
+ sService = this;
- setAvrcpControllerService(this);
+ // Start the media browser service.
+ Intent startIntent = new Intent(this, BluetoothMediaBrowserService.class);
+ startService(startIntent);
return true;
}
@Override
protected boolean stop() {
- setAvrcpControllerService(null);
- if (mAvrcpCtSm != null) {
- mAvrcpCtSm.doQuit();
+ Intent stopIntent = new Intent(this, BluetoothMediaBrowserService.class);
+ stopService(stopIntent);
+ for (AvrcpControllerStateMachine stateMachine : mDeviceStateMap.values()) {
+ stateMachine.quitNow();
}
+
+ sService = null;
+ sBrowseTree = null;
return true;
}
- //API Methods
-
- public static synchronized AvrcpControllerService getAvrcpControllerService() {
- if (sAvrcpControllerService == null) {
- Log.w(TAG, "getAvrcpControllerService(): service is null");
- return null;
- }
- if (!sAvrcpControllerService.isAvailable()) {
- Log.w(TAG, "getAvrcpControllerService(): service is not available ");
- return null;
- }
- return sAvrcpControllerService;
+ public static AvrcpControllerService getAvrcpControllerService() {
+ return sService;
}
- private static synchronized void setAvrcpControllerService(AvrcpControllerService instance) {
- if (DBG) {
- Log.d(TAG, "setAvrcpControllerService(): set to: " + instance);
- }
- sAvrcpControllerService = instance;
+ protected AvrcpControllerStateMachine newStateMachine(BluetoothDevice device) {
+ return new AvrcpControllerStateMachine(device, this);
}
- public synchronized List<BluetoothDevice> getConnectedDevices() {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
- if (mConnectedDevice != null) {
- devices.add(mConnectedDevice);
+ private void refreshContents(BrowseTree.BrowseNode node) {
+ if (node.mDevice == null) {
+ return;
}
- return devices;
+ AvrcpControllerStateMachine stateMachine = getStateMachine(node.mDevice);
+ if (stateMachine != null) {
+ stateMachine.requestContents(node);
+ }
}
+ /*Java API*/
+
/**
- * This function only supports STATE_CONNECTED
+ * Get a List of MediaItems that are children of the specified media Id
+ *
+ * @param parentMediaId The player or folder to get the contents of
+ * @return List of Children if available, an empty list if there are none,
+ * or null if a search must be performed.
*/
- public synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
- for (int i = 0; i < states.length; i++) {
- if (states[i] == BluetoothProfile.STATE_CONNECTED && mConnectedDevice != null) {
- devices.add(mConnectedDevice);
+ public synchronized List<MediaItem> getContents(String parentMediaId) {
+ if (DBG) Log.d(TAG, "getContents(" + parentMediaId + ")");
+
+ BrowseTree.BrowseNode requestedNode = sBrowseTree.findBrowseNodeByID(parentMediaId);
+ if (requestedNode == null) {
+ for (AvrcpControllerStateMachine stateMachine : mDeviceStateMap.values()) {
+ requestedNode = stateMachine.findNode(parentMediaId);
+ if (requestedNode != null) {
+ Log.d(TAG, "Found a node");
+ break;
+ }
}
}
- return devices;
- }
- public synchronized int getConnectionState(BluetoothDevice device) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- return (mConnectedDevice != null ? BluetoothProfile.STATE_CONNECTED
- : BluetoothProfile.STATE_DISCONNECTED);
- }
-
- public synchronized void sendGroupNavigationCmd(BluetoothDevice device, int keyCode,
- int keyState) {
- Log.v(TAG, "sendGroupNavigationCmd keyCode: " + keyCode + " keyState: " + keyState);
- if (device == null) {
- Log.e(TAG, "sendGroupNavigationCmd device is null");
- }
-
- if (!(device.equals(mConnectedDevice))) {
- Log.e(TAG, " Device does not match " + device + " connected " + mConnectedDevice);
- return;
- }
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_SEND_GROUP_NAVIGATION_CMD,
- keyCode, keyState, device);
- mAvrcpCtSm.sendMessage(msg);
- }
-
- public synchronized void sendPassThroughCmd(BluetoothDevice device, int keyCode, int keyState) {
- Log.v(TAG, "sendPassThroughCmd keyCode: " + keyCode + " keyState: " + keyState);
- if (device == null) {
- Log.e(TAG, "sendPassThroughCmd Device is null");
- return;
- }
-
- if (!device.equals(mConnectedDevice)) {
- Log.w(TAG, " Device does not match device " + device + " conn " + mConnectedDevice);
- return;
- }
-
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- Message msg =
- mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_SEND_PASS_THROUGH_CMD,
- keyCode, keyState, device);
- mAvrcpCtSm.sendMessage(msg);
- }
-
- public void startAvrcpUpdates() {
- mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_START_METADATA_BROADCASTS)
- .sendToTarget();
- }
-
- public void stopAvrcpUpdates() {
- mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_STOP_METADATA_BROADCASTS)
- .sendToTarget();
- }
-
- public synchronized MediaMetadata getMetaData(BluetoothDevice device) {
- if (DBG) {
- Log.d(TAG, "getMetaData");
- }
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- if (device == null) {
- Log.e(TAG, "getMetadata device is null");
+ if (requestedNode == null) {
+ if (DBG) Log.d(TAG, "Didn't find a node");
return null;
+ } else {
+ if (!requestedNode.isCached()) {
+ if (DBG) Log.d(TAG, "node is not cached");
+ refreshContents(requestedNode);
+ }
+ if (DBG) Log.d(TAG, "Returning contents");
+ return requestedNode.getContents();
}
-
- if (!device.equals(mConnectedDevice)) {
- return null;
- }
- return mAvrcpCtSm.getCurrentMetaData();
}
- public PlaybackState getPlaybackState(BluetoothDevice device) {
- // Get the cached state by default.
- return getPlaybackState(device, true);
- }
-
- // cached can be used to force a getPlaybackState command. Useful for PTS testing.
- public synchronized PlaybackState getPlaybackState(BluetoothDevice device, boolean cached) {
- if (DBG) {
- Log.d(TAG, "getPlayBackState device = " + device);
- }
-
- if (device == null) {
- Log.e(TAG, "getPlaybackState device is null");
- return null;
- }
-
- if (!device.equals(mConnectedDevice)) {
- Log.e(TAG, "Device " + device + " does not match connected deivce " + mConnectedDevice);
- return null;
-
- }
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- return mAvrcpCtSm.getCurrentPlayBackState(cached);
- }
-
- public synchronized BluetoothAvrcpPlayerSettings getPlayerSettings(BluetoothDevice device) {
- if (DBG) {
- Log.d(TAG, "getPlayerApplicationSetting ");
- }
-
- if (device == null) {
- Log.e(TAG, "getPlayerSettings device is null");
- return null;
- }
-
- if (!device.equals(mConnectedDevice)) {
- Log.e(TAG, "device " + device + " does not match connected device " + mConnectedDevice);
- return null;
- }
-
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-
- /* Do nothing */
- return null;
- }
-
- public boolean setPlayerApplicationSetting(BluetoothAvrcpPlayerSettings plAppSetting) {
- if (DBG) {
- Log.d(TAG, "getPlayerApplicationSetting");
- }
-
- /* Do nothing */
- return false;
- }
-
- /**
- * Fetches the list of children for the parentID node.
- *
- * This function manages the overall tree for browsing structure.
- *
- * Arguments:
- * device - Device to browse content for.
- * parentMediaId - ID of the parent that we need to browse content for. Since most
- * of the players are database unware, fetching a root invalidates all the children.
- * start - number of item to start scanning from
- * items - number of items to fetch
- */
- public synchronized boolean getChildren(BluetoothDevice device, String parentMediaId, int start,
- int items) {
- if (DBG) {
- Log.d(TAG, "getChildren device = " + device + " parent " + parentMediaId);
- }
-
- if (device == null) {
- Log.e(TAG, "getChildren device is null");
- return false;
- }
-
- if (!device.equals(mConnectedDevice)) {
- Log.e(TAG, "getChildren device " + device + " does not match " + mConnectedDevice);
- return false;
- }
-
- if (!mBrowseConnected) {
- Log.e(TAG, "getChildren browse not yet connected");
- return false;
- }
-
- if (!mAvrcpCtSm.isConnected()) {
- return false;
- }
- mAvrcpCtSm.getChildren(parentMediaId, start, items);
- return true;
- }
-
- public synchronized boolean getNowPlayingList(BluetoothDevice device, String id, int start,
- int items) {
- if (DBG) {
- Log.d(TAG, "getNowPlayingList device = " + device + " start = " + start + "items = "
- + items);
- }
-
- if (device == null) {
- Log.e(TAG, "getNowPlayingList device is null");
- return false;
- }
-
- if (!device.equals(mConnectedDevice)) {
- Log.e(TAG,
- "getNowPlayingList device " + device + " does not match " + mConnectedDevice);
- return false;
- }
-
- if (!mBrowseConnected) {
- Log.e(TAG, "getNowPlayingList browse not yet connected");
- return false;
- }
-
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-
- Message msg =
- mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_NOW_PLAYING_LIST,
- start, items, id);
- mAvrcpCtSm.sendMessage(msg);
- return true;
- }
-
- public synchronized boolean getFolderList(BluetoothDevice device, String id, int start,
- int items) {
- if (DBG) {
- Log.d(TAG, "getFolderListing device = " + device + " start = " + start + "items = "
- + items);
- }
-
- if (device == null) {
- Log.e(TAG, "getFolderListing device is null");
- return false;
- }
-
- if (!device.equals(mConnectedDevice)) {
- Log.e(TAG, "getFolderListing device " + device + " does not match " + mConnectedDevice);
- return false;
- }
-
- if (!mBrowseConnected) {
- Log.e(TAG, "getFolderListing browse not yet connected");
- return false;
- }
-
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-
- Message msg =
- mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_FOLDER_LIST, start,
- items, id);
- mAvrcpCtSm.sendMessage(msg);
- return true;
- }
-
- public synchronized boolean getPlayerList(BluetoothDevice device, int start, int items) {
- if (DBG) {
- Log.d(TAG,
- "getPlayerList device = " + device + " start = " + start + "items = " + items);
- }
-
- if (device == null) {
- Log.e(TAG, "getPlayerList device is null");
- return false;
- }
-
- if (!device.equals(mConnectedDevice)) {
- Log.e(TAG, "getPlayerList device " + device + " does not match " + mConnectedDevice);
- return false;
- }
-
- if (!mBrowseConnected) {
- Log.e(TAG, "getPlayerList browse not yet connected");
- return false;
- }
-
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-
- Message msg =
- mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_PLAYER_LIST, start,
- items);
- mAvrcpCtSm.sendMessage(msg);
- return true;
- }
-
- public synchronized boolean changeFolderPath(BluetoothDevice device, int direction, String uid,
- String fid) {
- if (DBG) {
- Log.d(TAG, "changeFolderPath device = " + device + " direction " + direction + " uid "
- + uid);
- }
-
- if (device == null) {
- Log.e(TAG, "changeFolderPath device is null");
- return false;
- }
-
- if (!device.equals(mConnectedDevice)) {
- Log.e(TAG, "changeFolderPath device " + device + " does not match " + mConnectedDevice);
- return false;
- }
-
- if (!mBrowseConnected) {
- Log.e(TAG, "changeFolderPath browse not yet connected");
- return false;
- }
-
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-
- Bundle b = new Bundle();
- b.putString(EXTRA_FOLDER_ID, fid);
- b.putString(EXTRA_FOLDER_BT_ID, uid);
- Message msg =
- mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_CHANGE_FOLDER_PATH,
- direction, 0, b);
- mAvrcpCtSm.sendMessage(msg);
- return true;
- }
-
- public synchronized boolean setBrowsedPlayer(BluetoothDevice device, int id, String fid) {
- if (DBG) {
- Log.d(TAG, "setBrowsedPlayer device = " + device + " id" + id + " fid " + fid);
- }
-
- if (device == null) {
- Log.e(TAG, "setBrowsedPlayer device is null");
- return false;
- }
-
- if (!device.equals(mConnectedDevice)) {
- Log.e(TAG, "changeFolderPath device " + device + " does not match " + mConnectedDevice);
- return false;
- }
-
- if (!mBrowseConnected) {
- Log.e(TAG, "setBrowsedPlayer browse not yet connected");
- return false;
- }
-
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-
- Message msg =
- mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_SET_BROWSED_PLAYER, id,
- 0, fid);
- mAvrcpCtSm.sendMessage(msg);
- return true;
- }
-
- public synchronized void fetchAttrAndPlayItem(BluetoothDevice device, String uid) {
- if (DBG) {
- Log.d(TAG, "fetchAttrAndPlayItem device = " + device + " uid " + uid);
- }
-
- if (device == null) {
- Log.e(TAG, "fetchAttrAndPlayItem device is null");
- return;
- }
-
- if (!device.equals(mConnectedDevice)) {
- Log.e(TAG, "fetchAttrAndPlayItem device " + device + " does not match "
- + mConnectedDevice);
- return;
- }
-
- if (!mBrowseConnected) {
- Log.e(TAG, "fetchAttrAndPlayItem browse not yet connected");
- return;
- }
- mAvrcpCtSm.fetchAttrAndPlayItem(uid);
+ @Override
+ protected IProfileServiceBinder initBinder() {
+ return new AvrcpControllerServiceBinder(this);
}
//Binder object: Must be static class or memory leak may occur
- private static class BluetoothAvrcpControllerBinder extends IBluetoothAvrcpController.Stub
+ private static class AvrcpControllerServiceBinder extends IBluetoothAvrcpController.Stub
implements IProfileServiceBinder {
-
private AvrcpControllerService mService;
private AvrcpControllerService getService() {
@@ -628,14 +201,14 @@
return null;
}
- if (mService != null && mService.isAvailable()) {
+ if (mService != null) {
return mService;
}
return null;
}
- BluetoothAvrcpControllerBinder(AvrcpControllerService svc) {
- mService = svc;
+ AvrcpControllerServiceBinder(AvrcpControllerService service) {
+ mService = service;
}
@Override
@@ -667,153 +240,102 @@
if (service == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
-
- if (device == null) {
- throw new IllegalStateException("Device cannot be null!");
- }
-
return service.getConnectionState(device);
}
@Override
public void sendGroupNavigationCmd(BluetoothDevice device, int keyCode, int keyState) {
- Log.v(TAG, "Binder Call: sendGroupNavigationCmd");
- AvrcpControllerService service = getService();
- if (service == null) {
- return;
- }
+ Log.w(TAG, "sendGroupNavigationCmd not implemented");
+ return;
+ }
- if (device == null) {
- throw new IllegalStateException("Device cannot be null!");
- }
-
- service.sendGroupNavigationCmd(device, keyCode, keyState);
+ @Override
+ public boolean setPlayerApplicationSetting(BluetoothAvrcpPlayerSettings settings) {
+ Log.w(TAG, "setPlayerApplicationSetting not implemented");
+ return false;
}
@Override
public BluetoothAvrcpPlayerSettings getPlayerSettings(BluetoothDevice device) {
- Log.v(TAG, "Binder Call: getPlayerApplicationSetting ");
- AvrcpControllerService service = getService();
- if (service == null) {
- return null;
- }
-
- if (device == null) {
- throw new IllegalStateException("Device cannot be null!");
- }
-
- return service.getPlayerSettings(device);
- }
-
- @Override
- public boolean setPlayerApplicationSetting(BluetoothAvrcpPlayerSettings plAppSetting) {
- Log.v(TAG, "Binder Call: setPlayerApplicationSetting ");
- AvrcpControllerService service = getService();
- if (service == null) {
- return false;
- }
- return service.setPlayerApplicationSetting(plAppSetting);
+ Log.w(TAG, "getPlayerSettings not implemented");
+ return null;
}
}
+
+ /* JNI API*/
// Called by JNI when a passthrough key was received.
private void handlePassthroughRsp(int id, int keyState, byte[] address) {
- Log.d(TAG,
- "passthrough response received as: key: " + id + " state: " + keyState + "address:"
- + address);
+ if (DBG) {
+ Log.d(TAG, "passthrough response received as: key: " + id
+ + " state: " + keyState + "address:" + address);
+ }
}
private void handleGroupNavigationRsp(int id, int keyState) {
- Log.d(TAG, "group navigation response received as: key: " + id + " state: " + keyState);
+ if (DBG) {
+ Log.d(TAG, "group navigation response received as: key: " + id + " state: "
+ + keyState);
+ }
}
// Called by JNI when a device has connected or disconnected.
- private synchronized void onConnectionStateChanged(boolean rcConnected, boolean brConnected,
- byte[] address) {
+ private synchronized void onConnectionStateChanged(boolean remoteControlConnected,
+ boolean browsingConnected, byte[] address) {
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
- Log.d(TAG, "onConnectionStateChanged " + rcConnected + " " + brConnected + device
- + " conn device " + mConnectedDevice);
+ if (DBG) {
+ Log.d(TAG, "onConnectionStateChanged " + remoteControlConnected + " "
+ + browsingConnected + device);
+ }
if (device == null) {
Log.e(TAG, "onConnectionStateChanged Device is null");
return;
}
- // Adjust the AVRCP connection state.
- int oldState = (device.equals(mConnectedDevice) ? BluetoothProfile.STATE_CONNECTED
- : BluetoothProfile.STATE_DISCONNECTED);
- int newState = (rcConnected ? BluetoothProfile.STATE_CONNECTED
- : BluetoothProfile.STATE_DISCONNECTED);
-
- if (rcConnected && oldState == BluetoothProfile.STATE_DISCONNECTED) {
- /* AVRCPControllerService supports single connection */
- if (mConnectedDevice != null) {
- Log.d(TAG, "A Connection already exists, returning");
- return;
- }
- mConnectedDevice = device;
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_CONNECTION_CHANGE, newState,
- oldState, device);
- mAvrcpCtSm.sendMessage(msg);
- } else if (!rcConnected && oldState == BluetoothProfile.STATE_CONNECTED) {
- mConnectedDevice = null;
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_CONNECTION_CHANGE, newState,
- oldState, device);
- mAvrcpCtSm.sendMessage(msg);
- }
-
- // Adjust the browse connection state. If RC is connected we should have already sent the
- // connection status out.
- if (rcConnected && brConnected) {
- mBrowseConnected = true;
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE);
- msg.arg1 = 1;
- msg.obj = device;
- mAvrcpCtSm.sendMessage(msg);
+ StackEvent event =
+ StackEvent.connectionStateChanged(remoteControlConnected, browsingConnected);
+ AvrcpControllerStateMachine stateMachine = getOrCreateStateMachine(device);
+ if (remoteControlConnected || browsingConnected) {
+ stateMachine.connect(event);
+ } else {
+ stateMachine.disconnect();
}
}
// Called by JNI to notify Avrcp of features supported by the Remote device.
private void getRcFeatures(byte[] address, int features) {
- BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
- Message msg =
- mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_RC_FEATURES,
- features, 0, device);
- mAvrcpCtSm.sendMessage(msg);
+ /* Do Nothing. */
}
// Called by JNI
private void setPlayerAppSettingRsp(byte[] address, byte accepted) {
- /* Do Nothing. */
+ /* Do Nothing. */
}
// Called by JNI when remote wants to receive absolute volume notifications.
private synchronized void handleRegisterNotificationAbsVol(byte[] address, byte label) {
- Log.d(TAG, "handleRegisterNotificationAbsVol ");
- BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
- if (device != null && !device.equals(mConnectedDevice)) {
- Log.e(TAG, "handleRegisterNotificationAbsVol device not found " + address);
- return;
+ if (DBG) {
+ Log.d(TAG, "handleRegisterNotificationAbsVol");
}
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION,
- (int) label, 0);
- mAvrcpCtSm.sendMessage(msg);
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+ stateMachine.sendMessage(
+ AvrcpControllerStateMachine.MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION);
+ }
}
// Called by JNI when remote wants to set absolute volume.
private synchronized void handleSetAbsVolume(byte[] address, byte absVol, byte label) {
- Log.d(TAG, "handleSetAbsVolume ");
- BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
- if (device != null && !device.equals(mConnectedDevice)) {
- Log.e(TAG, "handleSetAbsVolume device not found " + address);
- return;
+ if (DBG) {
+ Log.d(TAG, "handleSetAbsVolume ");
}
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_ABS_VOL_CMD, absVol, label);
- mAvrcpCtSm.sendMessage(msg);
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+ stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_ABS_VOL_CMD,
+ absVol);
+ }
}
// Called by JNI when a track changes and local AvrcpController is registered for updates.
@@ -822,24 +344,13 @@
if (DBG) {
Log.d(TAG, "onTrackChanged");
}
- BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
- if (device != null && !device.equals(mConnectedDevice)) {
- Log.e(TAG, "onTrackChanged device not found " + address);
- return;
- }
- List<Integer> attrList = new ArrayList<>();
- for (int attr : attributes) {
- attrList.add(attr);
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+ stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_TRACK_CHANGED,
+ TrackInfo.getMetadata(attributes, attribVals));
}
- List<String> attrValList = Arrays.asList(attribVals);
- TrackInfo trackInfo = new TrackInfo(attrList, attrValList);
- if (VDBG) {
- Log.d(TAG, "onTrackChanged " + trackInfo);
- }
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_TRACK_CHANGED, trackInfo);
- mAvrcpCtSm.sendMessage(msg);
}
// Called by JNI periodically based upon timer to update play position
@@ -849,14 +360,12 @@
Log.d(TAG, "onPlayPositionChanged pos " + currSongPosition);
}
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
- if (device != null && !device.equals(mConnectedDevice)) {
- Log.e(TAG, "onPlayPositionChanged not found device not found " + address);
- return;
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+ stateMachine.sendMessage(
+ AvrcpControllerStateMachine.MESSAGE_PROCESS_PLAY_POS_CHANGED,
+ songLen, currSongPosition);
}
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_PLAY_POS_CHANGED,
- songLen, currSongPosition);
- mAvrcpCtSm.sendMessage(msg);
}
// Called by JNI on changes of play status
@@ -864,11 +373,6 @@
if (DBG) {
Log.d(TAG, "onPlayStatusChanged " + playStatus);
}
- BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
- if (device != null && !device.equals(mConnectedDevice)) {
- Log.e(TAG, "onPlayStatusChanged not found device not found " + address);
- return;
- }
int playbackState = PlaybackState.STATE_NONE;
switch (playStatus) {
case JNI_PLAY_STATUS_STOPPED:
@@ -884,14 +388,17 @@
playbackState = PlaybackState.STATE_FAST_FORWARDING;
break;
case JNI_PLAY_STATUS_REV_SEEK:
- playbackState = PlaybackState.STATE_FAST_FORWARDING;
+ playbackState = PlaybackState.STATE_REWINDING;
break;
default:
playbackState = PlaybackState.STATE_NONE;
}
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_PLAY_STATUS_CHANGED, playbackState);
- mAvrcpCtSm.sendMessage(msg);
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+ stateMachine.sendMessage(
+ AvrcpControllerStateMachine.MESSAGE_PROCESS_PLAY_STATUS_CHANGED, playbackState);
+ }
}
// Called by JNI to report remote Player's capabilities
@@ -901,13 +408,14 @@
Log.d(TAG, "handlePlayerAppSetting rspLen = " + rspLen);
}
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
- if (device != null && !device.equals(mConnectedDevice)) {
- Log.e(TAG, "handlePlayerAppSetting not found device not found " + address);
- return;
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+ PlayerApplicationSettings supportedSettings =
+ PlayerApplicationSettings.makeSupportedSettings(playerAttribRsp);
+ stateMachine.sendMessage(
+ AvrcpControllerStateMachine.MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS,
+ supportedSettings);
}
- PlayerApplicationSettings supportedSettings =
- PlayerApplicationSettings.makeSupportedSettings(playerAttribRsp);
- /* Do nothing */
}
private synchronized void onPlayerAppSettingChanged(byte[] address, byte[] playerAttribRsp,
@@ -916,50 +424,50 @@
Log.d(TAG, "onPlayerAppSettingChanged ");
}
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
- if (device != null && !device.equals(mConnectedDevice)) {
- Log.e(TAG, "onPlayerAppSettingChanged not found device not found " + address);
- return;
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+
+ PlayerApplicationSettings currentSettings =
+ PlayerApplicationSettings.makeSettings(playerAttribRsp);
+ stateMachine.sendMessage(
+ AvrcpControllerStateMachine.MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS,
+ currentSettings);
}
- PlayerApplicationSettings desiredSettings =
- PlayerApplicationSettings.makeSettings(playerAttribRsp);
- /* Do nothing */
}
// Browsing related JNI callbacks.
- void handleGetFolderItemsRsp(int status, MediaItem[] items) {
+ void handleGetFolderItemsRsp(byte[] address, int status, MediaItem[] items) {
if (DBG) {
Log.d(TAG, "handleGetFolderItemsRsp called with status " + status + " items "
+ items.length + " items.");
}
-
- if (status == JNI_AVRC_INV_RANGE) {
- Log.w(TAG, "Sending out of range message.");
- // Send a special message since this could be used by state machine
- // to take as a signal that fetch is finished.
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE);
- mAvrcpCtSm.sendMessage(msg);
- return;
- }
-
for (MediaItem item : items) {
if (VDBG) {
- Log.d(TAG, "media item: " + item + " uid: " + item.getDescription().getMediaId());
+ Log.d(TAG, "media item: " + item + " uid: "
+ + item.getDescription().getMediaId());
}
}
ArrayList<MediaItem> itemsList = new ArrayList<>();
for (MediaItem item : items) {
itemsList.add(item);
}
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS, itemsList);
- mAvrcpCtSm.sendMessage(msg);
+
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+
+ stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS,
+ itemsList);
+ }
}
- void handleGetPlayerItemsRsp(AvrcpPlayer[] items) {
+
+ void handleGetPlayerItemsRsp(byte[] address, AvrcpPlayer[] items) {
if (DBG) {
Log.d(TAG, "handleGetFolderItemsRsp called with " + items.length + " items.");
}
+
for (AvrcpPlayer item : items) {
if (VDBG) {
Log.d(TAG, "bt player item: " + item);
@@ -970,28 +478,31 @@
itemsList.add(p);
}
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_PLAYER_ITEMS, itemsList);
- mAvrcpCtSm.sendMessage(msg);
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+ stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_PLAYER_ITEMS,
+ itemsList);
+ }
}
// JNI Helper functions to convert native objects to java.
- MediaItem createFromNativeMediaItem(byte[] uid, int type, String name, int[] attrIds,
+ MediaItem createFromNativeMediaItem(long uid, int type, String name, int[] attrIds,
String[] attrVals) {
if (VDBG) {
- Log.d(TAG, "createFromNativeMediaItem uid: " + uid + " type " + type + " name " + name
- + " attrids " + attrIds + " attrVals " + attrVals);
+ Log.d(TAG, "createFromNativeMediaItem uid: " + uid + " type " + type + " name "
+ + name + " attrids " + attrIds + " attrVals " + attrVals);
}
MediaDescription.Builder mdb = new MediaDescription.Builder();
Bundle mdExtra = new Bundle();
- mdExtra.putString(MEDIA_ITEM_UID_KEY, byteUIDToHexString(uid));
+ mdExtra.putLong(MEDIA_ITEM_UID_KEY, uid);
mdb.setExtras(mdExtra);
+
// Generate a random UUID. We do this since database unaware TGs can send multiple
// items with same MEDIA_ITEM_UID_KEY.
mdb.setMediaId(UUID.randomUUID().toString());
-
// Concise readable name.
mdb.setTitle(name);
@@ -1001,23 +512,20 @@
return new MediaItem(mdb.build(), MediaItem.FLAG_PLAYABLE);
}
- MediaItem createFromNativeFolderItem(byte[] uid, int type, String name, int playable) {
+ MediaItem createFromNativeFolderItem(long uid, int type, String name, int playable) {
if (VDBG) {
- Log.d(TAG, "createFromNativeFolderItem uid: " + uid + " type " + type + " name " + name
- + " playable " + playable);
+ Log.d(TAG, "createFromNativeFolderItem uid: " + uid + " type " + type + " name "
+ + name + " playable " + playable);
}
MediaDescription.Builder mdb = new MediaDescription.Builder();
- // Covert the byte to a hex string. The coversion can be done back here to a
- // byte array when needed.
Bundle mdExtra = new Bundle();
- mdExtra.putString(MEDIA_ITEM_UID_KEY, byteUIDToHexString(uid));
+ mdExtra.putLong(MEDIA_ITEM_UID_KEY, uid);
mdb.setExtras(mdExtra);
// Generate a random UUID. We do this since database unaware TGs can send multiple
// items with same MEDIA_ITEM_UID_KEY.
mdb.setMediaId(UUID.randomUUID().toString());
-
// Concise readable name.
mdb.setTitle(name);
@@ -1028,112 +536,285 @@
int playStatus, int playerType) {
if (VDBG) {
Log.d(TAG,
- "createFromNativePlayerItem name: " + name + " transportFlags " + transportFlags
- + " play status " + playStatus + " player type " + playerType);
+ "createFromNativePlayerItem name: " + name + " transportFlags "
+ + transportFlags + " play status " + playStatus + " player type "
+ + playerType);
}
- AvrcpPlayer player = new AvrcpPlayer(id, name, 0, playStatus, playerType);
+ AvrcpPlayer player = new AvrcpPlayer(id, name, transportFlags, playStatus, playerType);
return player;
}
- private void handleChangeFolderRsp(int count) {
+ private void handleChangeFolderRsp(byte[] address, int count) {
if (DBG) {
Log.d(TAG, "handleChangeFolderRsp count: " + count);
}
- Message msg =
- mAvrcpCtSm.obtainMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_FOLDER_PATH,
- count);
- mAvrcpCtSm.sendMessage(msg);
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+ stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_FOLDER_PATH,
+ count);
+ }
}
- private void handleSetBrowsedPlayerRsp(int items, int depth) {
+ private void handleSetBrowsedPlayerRsp(byte[] address, int items, int depth) {
if (DBG) {
Log.d(TAG, "handleSetBrowsedPlayerRsp depth: " + depth);
}
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_BROWSED_PLAYER, items, depth);
- mAvrcpCtSm.sendMessage(msg);
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+ stateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_BROWSED_PLAYER,
+ items, depth);
+ }
}
- private void handleSetAddressedPlayerRsp(int status) {
+ private void handleSetAddressedPlayerRsp(byte[] address, int status) {
if (DBG) {
Log.d(TAG, "handleSetAddressedPlayerRsp status: " + status);
}
- Message msg = mAvrcpCtSm.obtainMessage(
- AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_ADDRESSED_PLAYER);
- mAvrcpCtSm.sendMessage(msg);
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+ stateMachine.sendMessage(
+ AvrcpControllerStateMachine.MESSAGE_PROCESS_SET_ADDRESSED_PLAYER);
+ }
+ }
+
+ private void handleAddressedPlayerChanged(byte[] address, int id) {
+ if (DBG) {
+ Log.d(TAG, "handleAddressedPlayerChanged id: " + id);
+ }
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+ stateMachine.sendMessage(
+ AvrcpControllerStateMachine.MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED, id);
+ }
+ }
+
+ private void handleNowPlayingContentChanged(byte[] address) {
+ if (DBG) {
+ Log.d(TAG, "handleNowPlayingContentChanged");
+ }
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address);
+
+ AvrcpControllerStateMachine stateMachine = getStateMachine(device);
+ if (stateMachine != null) {
+ stateMachine.nowPlayingContentChanged();
+ }
+ }
+
+ /* Generic Profile Code */
+
+ /**
+ * Disconnect the given Bluetooth device.
+ *
+ * @return true if disconnect is successful, false otherwise.
+ */
+ public synchronized boolean disconnect(BluetoothDevice device) {
+ if (DBG) {
+ StringBuilder sb = new StringBuilder();
+ dump(sb);
+ Log.d(TAG, "MAP disconnect device: " + device
+ + ", InstanceMap start state: " + sb.toString());
+ }
+ AvrcpControllerStateMachine stateMachine = mDeviceStateMap.get(device);
+ // a map state machine instance doesn't exist. maybe it is already gone?
+ if (stateMachine == null) {
+ return false;
+ }
+ int connectionState = stateMachine.getState();
+ if (connectionState != BluetoothProfile.STATE_CONNECTED
+ && connectionState != BluetoothProfile.STATE_CONNECTING) {
+ return false;
+ }
+ stateMachine.disconnect();
+ if (DBG) {
+ StringBuilder sb = new StringBuilder();
+ dump(sb);
+ Log.d(TAG, "MAP disconnect device: " + device
+ + ", InstanceMap start state: " + sb.toString());
+ }
+ return true;
+ }
+
+ /**
+ * Remove state machine from device map once it is no longer needed.
+ */
+ public void removeStateMachine(AvrcpControllerStateMachine stateMachine) {
+ mDeviceStateMap.remove(stateMachine.getDevice());
+ }
+
+ public List<BluetoothDevice> getConnectedDevices() {
+ return getDevicesMatchingConnectionStates(new int[]{BluetoothAdapter.STATE_CONNECTED});
+ }
+
+ protected AvrcpControllerStateMachine getStateMachine(BluetoothDevice device) {
+ return mDeviceStateMap.get(device);
+ }
+
+ protected AvrcpControllerStateMachine getOrCreateStateMachine(BluetoothDevice device) {
+ AvrcpControllerStateMachine stateMachine = mDeviceStateMap.get(device);
+ if (stateMachine == null) {
+ stateMachine = newStateMachine(device);
+ mDeviceStateMap.put(device, stateMachine);
+ stateMachine.start();
+ }
+ return stateMachine;
+ }
+
+ List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
+ List<BluetoothDevice> deviceList = new ArrayList<>();
+ Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
+ int connectionState;
+ for (BluetoothDevice device : bondedDevices) {
+ connectionState = getConnectionState(device);
+ if (DBG) Log.d(TAG, "Device: " + device + "State: " + connectionState);
+ for (int i = 0; i < states.length; i++) {
+ if (connectionState == states[i]) {
+ deviceList.add(device);
+ }
+ }
+ }
+ if (DBG) Log.d(TAG, deviceList.toString());
+ Log.d(TAG, "GetDevicesDone");
+ return deviceList;
+ }
+
+ synchronized int getConnectionState(BluetoothDevice device) {
+ AvrcpControllerStateMachine stateMachine = mDeviceStateMap.get(device);
+ return (stateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED
+ : stateMachine.getState();
}
@Override
public void dump(StringBuilder sb) {
super.dump(sb);
- mAvrcpCtSm.dump(sb);
+ ProfileService.println(sb, "Devices Tracked = " + mDeviceStateMap.size());
+
+ for (AvrcpControllerStateMachine stateMachine : mDeviceStateMap.values()) {
+ ProfileService.println(sb,
+ "==== StateMachine for " + stateMachine.getDevice() + " ====");
+ stateMachine.dump(sb);
+ }
+ sb.append("\n sBrowseTree: " + sBrowseTree.toString());
}
- public static String byteUIDToHexString(byte[] uid) {
- StringBuilder sb = new StringBuilder();
- for (byte b : uid) {
- sb.append(String.format("%02X", b));
- }
- return sb.toString();
- }
-
- public static byte[] hexStringToByteUID(String uidStr) {
- if (uidStr == null) {
- Log.e(TAG, "Null hex string.");
- return EMPTY_UID;
- } else if (uidStr.length() % 2 == 1) {
- // Odd length strings should not be possible.
- Log.e(TAG, "Odd length hex string " + uidStr);
- return EMPTY_UID;
- }
- int len = uidStr.length();
- byte[] data = new byte[len / 2];
- for (int i = 0; i < len; i += 2) {
- data[i / 2] = (byte) ((Character.digit(uidStr.charAt(i), 16) << 4) + Character.digit(
- uidStr.charAt(i + 1), 16));
- }
- return data;
- }
-
+ /*JNI*/
private static native void classInitNative();
private native void initNative();
private native void cleanupNative();
- static native boolean sendPassThroughCommandNative(byte[] address, int keyCode, int keyState);
+ /**
+ * Send button press commands to addressed device
+ *
+ * @param keyCode key code as defined in AVRCP specification
+ * @param keyState 0 = key pressed, 1 = key released
+ * @return command was sent
+ */
+ public native boolean sendPassThroughCommandNative(byte[] address, int keyCode, int keyState);
- static native boolean sendGroupNavigationCommandNative(byte[] address, int keyCode,
+ /**
+ * Send group navigation commands
+ *
+ * @param keyCode next/previous
+ * @param keyState state
+ * @return command was sent
+ */
+ public native boolean sendGroupNavigationCommandNative(byte[] address, int keyCode,
int keyState);
- static native void setPlayerApplicationSettingValuesNative(byte[] address, byte numAttrib,
- byte[] atttibIds, byte[] attribVal);
+ /**
+ * Change player specific settings such as shuffle
+ *
+ * @param numAttrib number of settings being sent
+ * @param attribIds list of settings to be changed
+ * @param attribVal list of settings values
+ */
+ public native void setPlayerApplicationSettingValuesNative(byte[] address, byte numAttrib,
+ byte[] attribIds, byte[] attribVal);
- /* This api is used to send response to SET_ABS_VOL_CMD */
- static native void sendAbsVolRspNative(byte[] address, int absVol, int label);
+ /**
+ * Send response to set absolute volume
+ *
+ * @param absVol new volume
+ * @param label label
+ */
+ public native void sendAbsVolRspNative(byte[] address, int absVol, int label);
- /* This api is used to inform remote for any volume level changes */
- static native void sendRegisterAbsVolRspNative(byte[] address, byte rspType, int absVol,
+ /**
+ * Register for any volume level changes
+ *
+ * @param rspType type of response
+ * @param absVol current volume
+ * @param label label
+ */
+ public native void sendRegisterAbsVolRspNative(byte[] address, byte rspType, int absVol,
int label);
- /* API used to fetch the playback state */
- static native void getPlaybackStateNative(byte[] address);
+ /**
+ * Fetch the playback state
+ */
+ public native void getPlaybackStateNative(byte[] address);
- /* API used to fetch the current now playing list */
- static native void getNowPlayingListNative(byte[] address, int start, int end);
+ /**
+ * Fetch the current now playing list
+ *
+ * @param start first index to retrieve
+ * @param end last index to retrieve
+ */
+ public native void getNowPlayingListNative(byte[] address, int start, int end);
- /* API used to fetch the current folder's listing */
- static native void getFolderListNative(byte[] address, int start, int end);
+ /**
+ * Fetch the current folder's listing
+ *
+ * @param start first index to retrieve
+ * @param end last index to retrieve
+ */
+ public native void getFolderListNative(byte[] address, int start, int end);
- /* API used to fetch the listing of players */
- static native void getPlayerListNative(byte[] address, int start, int end);
+ /**
+ * Fetch the listing of players
+ *
+ * @param start first index to retrieve
+ * @param end last index to retrieve
+ */
+ public native void getPlayerListNative(byte[] address, int start, int end);
- /* API used to change the folder */
- static native void changeFolderPathNative(byte[] address, byte direction, byte[] uid);
+ /**
+ * Change the current browsed folder
+ *
+ * @param direction up/down
+ * @param uid folder unique id
+ */
+ public native void changeFolderPathNative(byte[] address, byte direction, long uid);
- static native void playItemNative(byte[] address, byte scope, byte[] uid, int uidCounter);
+ /**
+ * Play item with provided uid
+ *
+ * @param scope scope of item to played
+ * @param uid song unique id
+ * @param uidCounter counter
+ */
+ public native void playItemNative(byte[] address, byte scope, long uid, int uidCounter);
- static native void setBrowsedPlayerNative(byte[] address, int playerId);
+ /**
+ * Set a specific player for browsing
+ *
+ * @param playerId player number
+ */
+ public native void setBrowsedPlayerNative(byte[] address, int playerId);
- static native void setAddressedPlayerNative(byte[] address, int playerId);
+ /**
+ * Set a specific player for handling playback commands
+ *
+ * @param playerId player number
+ */
+ public native void setAddressedPlayerNative(byte[] address, int playerId);
}
diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
index 3077664..83ba850 100644
--- a/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
+++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachine.java
@@ -17,23 +17,22 @@
package com.android.bluetooth.avrcpcontroller;
import android.bluetooth.BluetoothAvrcpController;
-import android.bluetooth.BluetoothAvrcpPlayerSettings;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
-import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.media.AudioManager;
-import android.media.MediaDescription;
import android.media.MediaMetadata;
import android.media.browse.MediaBrowser.MediaItem;
-import android.media.session.PlaybackState;
import android.os.Bundle;
import android.os.Message;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
+import android.util.SparseArray;
import com.android.bluetooth.BluetoothMetricsProto;
+import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
import com.android.bluetooth.a2dpsink.A2dpSinkService;
import com.android.bluetooth.btservice.MetricsLogger;
@@ -49,53 +48,47 @@
* and interactions with a remote controlable device.
*/
class AvrcpControllerStateMachine extends StateMachine {
+ static final String TAG = "AvrcpControllerStateMachine";
+ static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
- // commands from Binder service
- static final int MESSAGE_SEND_PASS_THROUGH_CMD = 1;
- static final int MESSAGE_SEND_GROUP_NAVIGATION_CMD = 3;
- static final int MESSAGE_GET_NOW_PLAYING_LIST = 5;
- static final int MESSAGE_GET_FOLDER_LIST = 6;
- static final int MESSAGE_GET_PLAYER_LIST = 7;
- static final int MESSAGE_CHANGE_FOLDER_PATH = 8;
- static final int MESSAGE_FETCH_ATTR_AND_PLAY_ITEM = 9;
- static final int MESSAGE_SET_BROWSED_PLAYER = 10;
+ //0->99 Events from Outside
+ public static final int CONNECT = 1;
+ public static final int DISCONNECT = 2;
- // commands from native layer
- static final int MESSAGE_PROCESS_SET_ABS_VOL_CMD = 103;
- static final int MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION = 104;
- static final int MESSAGE_PROCESS_TRACK_CHANGED = 105;
- static final int MESSAGE_PROCESS_PLAY_POS_CHANGED = 106;
- static final int MESSAGE_PROCESS_PLAY_STATUS_CHANGED = 107;
- static final int MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION = 108;
- static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS = 109;
- static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE = 110;
- static final int MESSAGE_PROCESS_GET_PLAYER_ITEMS = 111;
- static final int MESSAGE_PROCESS_FOLDER_PATH = 112;
- static final int MESSAGE_PROCESS_SET_BROWSED_PLAYER = 113;
- static final int MESSAGE_PROCESS_SET_ADDRESSED_PLAYER = 114;
+ //100->199 Internal Events
+ protected static final int CLEANUP = 100;
+ private static final int CONNECT_TIMEOUT = 101;
- // commands from A2DP sink
- static final int MESSAGE_STOP_METADATA_BROADCASTS = 201;
- static final int MESSAGE_START_METADATA_BROADCASTS = 202;
+ //200->299 Events from Native
+ static final int STACK_EVENT = 200;
+ static final int MESSAGE_INTERNAL_CMD_TIMEOUT = 201;
- // commands for connection
- static final int MESSAGE_PROCESS_RC_FEATURES = 301;
- static final int MESSAGE_PROCESS_CONNECTION_CHANGE = 302;
- static final int MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE = 303;
+ static final int MESSAGE_PROCESS_SET_ABS_VOL_CMD = 203;
+ static final int MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION = 204;
+ static final int MESSAGE_PROCESS_TRACK_CHANGED = 205;
+ static final int MESSAGE_PROCESS_PLAY_POS_CHANGED = 206;
+ static final int MESSAGE_PROCESS_PLAY_STATUS_CHANGED = 207;
+ static final int MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION = 208;
+ static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS = 209;
+ static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE = 210;
+ static final int MESSAGE_PROCESS_GET_PLAYER_ITEMS = 211;
+ static final int MESSAGE_PROCESS_FOLDER_PATH = 212;
+ static final int MESSAGE_PROCESS_SET_BROWSED_PLAYER = 213;
+ static final int MESSAGE_PROCESS_SET_ADDRESSED_PLAYER = 214;
+ static final int MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED = 215;
+ static final int MESSAGE_PROCESS_NOW_PLAYING_CONTENTS_CHANGED = 216;
+ static final int MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS = 217;
+ static final int MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS = 218;
- // Interal messages
- static final int MESSAGE_INTERNAL_BROWSE_DEPTH_INCREMENT = 401;
- static final int MESSAGE_INTERNAL_MOVE_N_LEVELS_UP = 402;
- static final int MESSAGE_INTERNAL_CMD_TIMEOUT = 403;
+ //300->399 Events for Browsing
+ static final int MESSAGE_GET_FOLDER_ITEMS = 300;
+ static final int MESSAGE_PLAY_ITEM = 301;
+ static final int MSG_AVRCP_PASSTHRU = 302;
+ static final int MSG_AVRCP_SET_SHUFFLE = 303;
+ static final int MSG_AVRCP_SET_REPEAT = 304;
+
static final int MESSAGE_INTERNAL_ABS_VOL_TIMEOUT = 404;
- static final int ABS_VOL_TIMEOUT_MILLIS = 1000; //1s
- static final int CMD_TIMEOUT_MILLIS = 5000; // 5s
- // Fetch only 20 items at a time.
- static final int GET_FOLDER_ITEMS_PAGINATION_SIZE = 20;
- // Fetch no more than 1000 items per directory.
- static final int MAX_FOLDER_ITEMS = 1000;
-
/*
* Base value for absolute volume from JNI
*/
@@ -107,531 +100,485 @@
private static final byte NOTIFICATION_RSP_TYPE_INTERIM = 0x00;
private static final byte NOTIFICATION_RSP_TYPE_CHANGED = 0x01;
-
- private static final String TAG = "AvrcpControllerSM";
- private static final boolean DBG = true;
- private static final boolean VDBG = false;
-
- private final Context mContext;
private final AudioManager mAudioManager;
+ private final boolean mIsVolumeFixed;
- private final State mDisconnected;
- private final State mConnected;
- private final SetBrowsedPlayer mSetBrowsedPlayer;
- private final SetAddresedPlayerAndPlayItem mSetAddrPlayer;
- private final ChangeFolderPath mChangeFolderPath;
- private final GetFolderList mGetFolderList;
- private final GetPlayerListing mGetPlayerListing;
- private final MoveToRoot mMoveToRoot;
+ protected final BluetoothDevice mDevice;
+ protected final byte[] mDeviceAddress;
+ protected final AvrcpControllerService mService;
+ protected final Disconnected mDisconnected;
+ protected final Connecting mConnecting;
+ protected final Connected mConnected;
+ protected final Disconnecting mDisconnecting;
- private final Object mLock = new Object();
- private static final ArrayList<MediaItem> EMPTY_MEDIA_ITEM_LIST = new ArrayList<>();
- private static final MediaMetadata EMPTY_MEDIA_METADATA = new MediaMetadata.Builder().build();
+ protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
- // APIs exist to access these so they must be thread safe
- private Boolean mIsConnected = false;
- private RemoteDevice mRemoteDevice;
- private AvrcpPlayer mAddressedPlayer;
-
- // Only accessed from State Machine processMessage
+ boolean mRemoteControlConnected = false;
+ boolean mBrowsingConnected = false;
+ final BrowseTree mBrowseTree;
+ private AvrcpPlayer mAddressedPlayer = new AvrcpPlayer();
+ private int mAddressedPlayerId = -1;
+ private SparseArray<AvrcpPlayer> mAvailablePlayerList = new SparseArray<AvrcpPlayer>();
private int mVolumeChangedNotificationsToIgnore = 0;
- private int mPreviousPercentageVol = -1;
+ private int mVolumeNotificationLabel = -1;
- // Depth from root of current browsing. This can be used to move to root directly.
- private int mBrowseDepth = 0;
+ GetFolderList mGetFolderList = null;
- // Browse tree.
- private BrowseTree mBrowseTree = new BrowseTree();
+ //Number of items to get in a single fetch
+ static final int ITEM_PAGE_SIZE = 20;
+ static final int CMD_TIMEOUT_MILLIS = 10000;
+ static final int ABS_VOL_TIMEOUT_MILLIS = 1000; //1s
- AvrcpControllerStateMachine(Context context) {
+ AvrcpControllerStateMachine(BluetoothDevice device, AvrcpControllerService service) {
super(TAG);
- mContext = context;
+ mDevice = device;
+ mDeviceAddress = Utils.getByteAddress(mDevice);
+ mService = service;
+ logD(device.toString());
- mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
- IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
- mContext.registerReceiver(mBroadcastReceiver, filter);
-
+ mBrowseTree = new BrowseTree(mDevice);
mDisconnected = new Disconnected();
+ mConnecting = new Connecting();
mConnected = new Connected();
-
- // Used to change folder path and fetch the new folder listing.
- mSetBrowsedPlayer = new SetBrowsedPlayer();
- mSetAddrPlayer = new SetAddresedPlayerAndPlayItem();
- mChangeFolderPath = new ChangeFolderPath();
- mGetFolderList = new GetFolderList();
- mGetPlayerListing = new GetPlayerListing();
- mMoveToRoot = new MoveToRoot();
+ mDisconnecting = new Disconnecting();
addState(mDisconnected);
+ addState(mConnecting);
addState(mConnected);
+ addState(mDisconnecting);
- // Any action that needs blocking other requests to the state machine will be implemented as
- // a separate substate of the mConnected state. Once transtition to the sub-state we should
- // only handle the messages that are relevant to the sub-action. Everything else should be
- // deferred so that once we transition to the mConnected we can process them hence.
- addState(mSetBrowsedPlayer, mConnected);
- addState(mSetAddrPlayer, mConnected);
- addState(mChangeFolderPath, mConnected);
+ mGetFolderList = new GetFolderList();
addState(mGetFolderList, mConnected);
- addState(mGetPlayerListing, mConnected);
- addState(mMoveToRoot, mConnected);
+ mAudioManager = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE);
+ mIsVolumeFixed = mAudioManager.isVolumeFixed();
setInitialState(mDisconnected);
}
- class Disconnected extends State {
+ BrowseTree.BrowseNode findNode(String parentMediaId) {
+ logD("FindNode");
+ return mBrowseTree.findBrowseNodeByID(parentMediaId);
+ }
+
+ /**
+ * Get the current connection state
+ *
+ * @return current State
+ */
+ public int getState() {
+ return mMostRecentState;
+ }
+
+ /**
+ * Get the underlying device tracked by this state machine
+ *
+ * @return device in focus
+ */
+ public synchronized BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * send the connection event asynchronously
+ */
+ public boolean connect(StackEvent event) {
+ if (event.mBrowsingConnected) {
+ onBrowsingConnected();
+ }
+ mRemoteControlConnected = event.mRemoteControlConnected;
+ sendMessage(CONNECT);
+ return true;
+ }
+
+ /**
+ * send the Disconnect command asynchronously
+ */
+ public void disconnect() {
+ sendMessage(DISCONNECT);
+ }
+
+ /**
+ * Dump the current State Machine to the string builder.
+ *
+ * @param sb output string
+ */
+ public void dump(StringBuilder sb) {
+ ProfileService.println(sb, "mDevice: " + mDevice.getAddress() + "("
+ + mDevice.getName() + ") " + this.toString());
+ }
+
+ @Override
+ protected void unhandledMessage(Message msg) {
+ Log.w(TAG, "Unhandled message in state " + getCurrentState() + "msg.what=" + msg.what);
+ }
+
+ private static void logD(String message) {
+ if (DBG) {
+ Log.d(TAG, message);
+ }
+ }
+
+ synchronized void onBrowsingConnected() {
+ if (mBrowsingConnected) return;
+ mService.sBrowseTree.mRootNode.addChild(mBrowseTree.mRootNode);
+ BluetoothMediaBrowserService.notifyChanged(mService
+ .sBrowseTree.mRootNode);
+ BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState());
+ mBrowsingConnected = true;
+ }
+
+ synchronized void onBrowsingDisconnected() {
+ if (!mBrowsingConnected) return;
+ mAddressedPlayer.setPlayStatus(PlaybackStateCompat.STATE_ERROR);
+ mAddressedPlayer.updateCurrentTrack(null);
+ mBrowseTree.mNowPlayingNode.setCached(false);
+ BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mNowPlayingNode);
+ PlaybackStateCompat.Builder pbb = new PlaybackStateCompat.Builder();
+ pbb.setState(PlaybackStateCompat.STATE_ERROR, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN,
+ 1.0f).setActions(0);
+ pbb.setErrorMessage(mService.getString(R.string.bluetooth_disconnected));
+ BluetoothMediaBrowserService.notifyChanged(pbb.build());
+ mService.sBrowseTree.mRootNode.removeChild(
+ mBrowseTree.mRootNode);
+ BluetoothMediaBrowserService.notifyChanged(mService
+ .sBrowseTree.mRootNode);
+ BluetoothMediaBrowserService.trackChanged(null);
+ mBrowsingConnected = false;
+ }
+
+ private void notifyChanged(BrowseTree.BrowseNode node) {
+ BluetoothMediaBrowserService.notifyChanged(node);
+ }
+
+ void requestContents(BrowseTree.BrowseNode node) {
+ sendMessage(MESSAGE_GET_FOLDER_ITEMS, node);
+
+ logD("Fetching " + node);
+ }
+
+ void nowPlayingContentChanged() {
+ mBrowseTree.mNowPlayingNode.setCached(false);
+ sendMessage(MESSAGE_GET_FOLDER_ITEMS, mBrowseTree.mNowPlayingNode);
+ }
+
+ protected class Disconnected extends State {
+ @Override
+ public void enter() {
+ logD("Enter Disconnected");
+ if (mMostRecentState != BluetoothProfile.STATE_DISCONNECTED) {
+ sendMessage(CLEANUP);
+ }
+ broadcastConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTED);
+ }
@Override
- public boolean processMessage(Message msg) {
- if (DBG) Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
- switch (msg.what) {
- case MESSAGE_PROCESS_CONNECTION_CHANGE:
- if (msg.arg1 == BluetoothProfile.STATE_CONNECTED) {
- mBrowseTree.init();
- transitionTo(mConnected);
- BluetoothDevice rtDevice = (BluetoothDevice) msg.obj;
- synchronized (mLock) {
- mRemoteDevice = new RemoteDevice(rtDevice);
- mAddressedPlayer = new AvrcpPlayer();
- mIsConnected = true;
- }
- MetricsLogger.logProfileConnectionEvent(
- BluetoothMetricsProto.ProfileId.AVRCP_CONTROLLER);
- Intent intent = new Intent(
- BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
- intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
- BluetoothProfile.STATE_DISCONNECTED);
- intent.putExtra(BluetoothProfile.EXTRA_STATE,
- BluetoothProfile.STATE_CONNECTED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, rtDevice);
- mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
- }
+ public boolean processMessage(Message message) {
+ switch (message.what) {
+ case CONNECT:
+ logD("Connect");
+ transitionTo(mConnecting);
break;
-
- default:
- Log.w(TAG,
- "Currently Disconnected not handling " + dumpMessageString(msg.what));
- return false;
+ case CLEANUP:
+ mService.removeStateMachine(AvrcpControllerStateMachine.this);
+ break;
}
return true;
}
}
+ protected class Connecting extends State {
+ @Override
+ public void enter() {
+ logD("Enter Connecting");
+ broadcastConnectionStateChanged(BluetoothProfile.STATE_CONNECTING);
+ transitionTo(mConnected);
+ }
+ }
+
+
class Connected extends State {
- @Override
- public boolean processMessage(Message msg) {
- if (DBG) Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
- A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
- synchronized (mLock) {
- switch (msg.what) {
- case MESSAGE_SEND_PASS_THROUGH_CMD:
- BluetoothDevice device = (BluetoothDevice) msg.obj;
- AvrcpControllerService.sendPassThroughCommandNative(
- Utils.getByteAddress(device), msg.arg1, msg.arg2);
- if (a2dpSinkService != null) {
- if (DBG) Log.d(TAG, " inform AVRCP Commands to A2DP Sink ");
- a2dpSinkService.informAvrcpPassThroughCmd(device, msg.arg1, msg.arg2);
- }
- break;
-
- case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
- AvrcpControllerService.sendGroupNavigationCommandNative(
- mRemoteDevice.getBluetoothAddress(), msg.arg1, msg.arg2);
- break;
-
- case MESSAGE_GET_NOW_PLAYING_LIST:
- mGetFolderList.setFolder((String) msg.obj);
- mGetFolderList.setBounds((int) msg.arg1, (int) msg.arg2);
- mGetFolderList.setScope(AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING);
- transitionTo(mGetFolderList);
- break;
-
- case MESSAGE_GET_FOLDER_LIST:
- // Whenever we transition we set the information for folder we need to
- // return result.
- mGetFolderList.setBounds(msg.arg1, msg.arg2);
- mGetFolderList.setFolder((String) msg.obj);
- mGetFolderList.setScope(AvrcpControllerService.BROWSE_SCOPE_VFS);
- transitionTo(mGetFolderList);
- break;
-
- case MESSAGE_GET_PLAYER_LIST:
- AvrcpControllerService.getPlayerListNative(
- mRemoteDevice.getBluetoothAddress(), (byte) msg.arg1,
- (byte) msg.arg2);
- transitionTo(mGetPlayerListing);
- sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
- break;
-
- case MESSAGE_CHANGE_FOLDER_PATH: {
- int direction = msg.arg1;
- Bundle b = (Bundle) msg.obj;
- String uid = b.getString(AvrcpControllerService.EXTRA_FOLDER_BT_ID);
- String fid = b.getString(AvrcpControllerService.EXTRA_FOLDER_ID);
-
- // String is encoded as a Hex String (mostly for display purposes)
- // hence convert this back to real byte string.
- AvrcpControllerService.changeFolderPathNative(
- mRemoteDevice.getBluetoothAddress(), (byte) msg.arg1,
- AvrcpControllerService.hexStringToByteUID(uid));
- mChangeFolderPath.setFolder(fid);
- transitionTo(mChangeFolderPath);
- sendMessage(MESSAGE_INTERNAL_BROWSE_DEPTH_INCREMENT, (byte) msg.arg1);
- sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
- break;
- }
-
- case MESSAGE_FETCH_ATTR_AND_PLAY_ITEM: {
- int scope = msg.arg1;
- String playItemUid = (String) msg.obj;
- BrowseTree.BrowseNode currBrPlayer = mBrowseTree.getCurrentBrowsedPlayer();
- BrowseTree.BrowseNode currAddrPlayer =
- mBrowseTree.getCurrentAddressedPlayer();
- if (DBG) {
- Log.d(TAG, "currBrPlayer " + currBrPlayer + " currAddrPlayer "
- + currAddrPlayer);
- }
-
- if (currBrPlayer == null || currBrPlayer.equals(currAddrPlayer)) {
- // String is encoded as a Hex String (mostly for display purposes)
- // hence convert this back to real byte string.
- // NOTE: It may be possible that sending play while the same item is
- // playing leads to reset of track.
- AvrcpControllerService.playItemNative(
- mRemoteDevice.getBluetoothAddress(), (byte) scope,
- AvrcpControllerService.hexStringToByteUID(playItemUid),
- (int) 0);
- } else {
- // Send out the request for setting addressed player.
- AvrcpControllerService.setAddressedPlayerNative(
- mRemoteDevice.getBluetoothAddress(),
- currBrPlayer.getPlayerID());
- mSetAddrPlayer.setItemAndScope(currBrPlayer.getID(), playItemUid,
- scope);
- transitionTo(mSetAddrPlayer);
- }
- break;
- }
-
- case MESSAGE_SET_BROWSED_PLAYER: {
- AvrcpControllerService.setBrowsedPlayerNative(
- mRemoteDevice.getBluetoothAddress(), (int) msg.arg1);
- mSetBrowsedPlayer.setFolder((String) msg.obj);
- transitionTo(mSetBrowsedPlayer);
- break;
- }
-
- case MESSAGE_PROCESS_SET_ADDRESSED_PLAYER:
- AvrcpControllerService.getPlayerListNative(
- mRemoteDevice.getBluetoothAddress(), 0, 255);
- transitionTo(mGetPlayerListing);
- sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
- break;
-
-
- case MESSAGE_PROCESS_CONNECTION_CHANGE:
- if (msg.arg1 == BluetoothProfile.STATE_DISCONNECTED) {
- synchronized (mLock) {
- mIsConnected = false;
- mRemoteDevice = null;
- }
- mBrowseTree.clear();
- transitionTo(mDisconnected);
- BluetoothDevice rtDevice = (BluetoothDevice) msg.obj;
- Intent intent = new Intent(
- BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
- intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
- BluetoothProfile.STATE_CONNECTED);
- intent.putExtra(BluetoothProfile.EXTRA_STATE,
- BluetoothProfile.STATE_DISCONNECTED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, rtDevice);
- mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
- }
- break;
-
- case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
- // Service tells us if the browse is connected or disconnected.
- // This is useful only for deciding whether to send browse commands rest of
- // the connection state handling should be done via the message
- // MESSAGE_PROCESS_CONNECTION_CHANGE.
- Intent intent = new Intent(
- AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, (BluetoothDevice) msg.obj);
- if (DBG) {
- Log.d(TAG, "Browse connection state " + msg.arg1);
- }
- if (msg.arg1 == 1) {
- intent.putExtra(BluetoothProfile.EXTRA_STATE,
- BluetoothProfile.STATE_CONNECTED);
- } else if (msg.arg1 == 0) {
- intent.putExtra(BluetoothProfile.EXTRA_STATE,
- BluetoothProfile.STATE_DISCONNECTED);
- // If browse is disconnected, the next time we connect we should
- // be at the ROOT.
- mBrowseDepth = 0;
- } else {
- Log.w(TAG, "Incorrect browse state " + msg.arg1);
- }
-
- mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
- break;
-
- case MESSAGE_PROCESS_RC_FEATURES:
- mRemoteDevice.setRemoteFeatures(msg.arg1);
- break;
-
- case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
- mVolumeChangedNotificationsToIgnore++;
- removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
- sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT,
- ABS_VOL_TIMEOUT_MILLIS);
- setAbsVolume(msg.arg1, msg.arg2);
- break;
-
- case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION: {
- mRemoteDevice.setNotificationLabel(msg.arg1);
- mRemoteDevice.setAbsVolNotificationRequested(true);
- int percentageVol = getVolumePercentage();
- if (DBG) {
- Log.d(TAG, " Sending Interim Response = " + percentageVol + " label "
- + msg.arg1);
- }
- AvrcpControllerService.sendRegisterAbsVolRspNative(
- mRemoteDevice.getBluetoothAddress(), NOTIFICATION_RSP_TYPE_INTERIM,
- percentageVol, mRemoteDevice.getNotificationLabel());
- }
- break;
-
- case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION: {
- if (mVolumeChangedNotificationsToIgnore > 0) {
- mVolumeChangedNotificationsToIgnore--;
- if (mVolumeChangedNotificationsToIgnore == 0) {
- removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
- }
- } else {
- if (mRemoteDevice.getAbsVolNotificationRequested()) {
- int percentageVol = getVolumePercentage();
- if (percentageVol != mPreviousPercentageVol) {
- AvrcpControllerService.sendRegisterAbsVolRspNative(
- mRemoteDevice.getBluetoothAddress(),
- NOTIFICATION_RSP_TYPE_CHANGED, percentageVol,
- mRemoteDevice.getNotificationLabel());
- mPreviousPercentageVol = percentageVol;
- mRemoteDevice.setAbsVolNotificationRequested(false);
- }
- }
- }
- }
- break;
-
- case MESSAGE_INTERNAL_ABS_VOL_TIMEOUT:
- // Volume changed notifications should come back promptly from the
- // AudioManager, if for some reason some notifications were squashed don't
- // prevent future notifications.
- if (DBG) Log.d(TAG, "Timed out on volume changed notification");
- mVolumeChangedNotificationsToIgnore = 0;
- break;
-
- case MESSAGE_PROCESS_TRACK_CHANGED:
- // Music start playing automatically and update Metadata
- mAddressedPlayer.updateCurrentTrack((TrackInfo) msg.obj);
- broadcastMetaDataChanged(
- mAddressedPlayer.getCurrentTrack().getMediaMetaData());
- break;
-
- case MESSAGE_PROCESS_PLAY_POS_CHANGED:
- if (msg.arg2 != -1) {
- mAddressedPlayer.setPlayTime(msg.arg2);
- broadcastPlayBackStateChanged(getCurrentPlayBackState());
- }
- break;
-
- case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
- int status = msg.arg1;
- mAddressedPlayer.setPlayStatus(status);
- broadcastPlayBackStateChanged(getCurrentPlayBackState());
- if (status == PlaybackState.STATE_PLAYING) {
- a2dpSinkService.informTGStatePlaying(mRemoteDevice.mBTDevice, true);
- } else if (status == PlaybackState.STATE_PAUSED
- || status == PlaybackState.STATE_STOPPED) {
- a2dpSinkService.informTGStatePlaying(mRemoteDevice.mBTDevice, false);
- }
- break;
-
- default:
- return false;
- }
- }
- return true;
- }
- }
-
- // Handle the change folder path meta-action.
- // a) Send Change folder command
- // b) Once successful transition to folder fetch state.
- class ChangeFolderPath extends CmdState {
- private static final String STATE_TAG = "AVRCPSM.ChangeFolderPath";
- private int mTmpIncrDirection;
- private String mID = "";
-
- public void setFolder(String id) {
- mID = id;
- }
+ private static final String STATE_TAG = "Avrcp.ConnectedAvrcpController";
+ private int mCurrentlyHeldKey = 0;
@Override
public void enter() {
+ if (mMostRecentState == BluetoothProfile.STATE_CONNECTING) {
+ broadcastConnectionStateChanged(BluetoothProfile.STATE_CONNECTED);
+ BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks);
+ } else {
+ logD("ReEnteringConnected");
+ }
super.enter();
- mTmpIncrDirection = -1;
}
@Override
public boolean processMessage(Message msg) {
- if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what);
+ logD(STATE_TAG + " processMessage " + msg.what);
switch (msg.what) {
- case MESSAGE_INTERNAL_BROWSE_DEPTH_INCREMENT:
- mTmpIncrDirection = msg.arg1;
- break;
-
- case MESSAGE_PROCESS_FOLDER_PATH: {
- // Fetch the listing of objects in this folder.
- if (DBG) {
- Log.d(STATE_TAG,
- "MESSAGE_PROCESS_FOLDER_PATH returned " + msg.arg1 + " elements");
- }
-
- // Update the folder depth.
- if (mTmpIncrDirection
- == AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP) {
- mBrowseDepth -= 1;
- } else if (mTmpIncrDirection
- == AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN) {
- mBrowseDepth += 1;
- } else {
- throw new IllegalStateException("incorrect nav " + mTmpIncrDirection);
- }
- if (DBG) Log.d(STATE_TAG, "New browse depth " + mBrowseDepth);
-
- if (msg.arg1 > 0) {
- sendMessage(MESSAGE_GET_FOLDER_LIST, 0, msg.arg1 - 1, mID);
- } else {
- // Return an empty response to the upper layer.
- broadcastFolderList(mID, EMPTY_MEDIA_ITEM_LIST);
- }
- mBrowseTree.setCurrentBrowsedFolder(mID);
- transitionTo(mConnected);
- break;
- }
-
- case MESSAGE_INTERNAL_CMD_TIMEOUT:
- // We timed out changing folders. It is imperative we tell
- // the upper layers that we failed by giving them an empty list.
- Log.e(STATE_TAG, "change folder failed, sending empty list.");
- broadcastFolderList(mID, EMPTY_MEDIA_ITEM_LIST);
- transitionTo(mConnected);
- break;
-
- case MESSAGE_SEND_PASS_THROUGH_CMD:
- case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
+ mVolumeChangedNotificationsToIgnore++;
+ removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
+ sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT,
+ ABS_VOL_TIMEOUT_MILLIS);
+ handleAbsVolumeRequest(msg.arg1, msg.arg2);
+ return true;
+
case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
+ mVolumeNotificationLabel = msg.arg1;
+ mService.sendRegisterAbsVolRspNative(mDeviceAddress,
+ NOTIFICATION_RSP_TYPE_INTERIM,
+ getAbsVolume(), mVolumeNotificationLabel);
+ return true;
+
+ case MESSAGE_GET_FOLDER_ITEMS:
+ transitionTo(mGetFolderList);
+ return true;
+
+ case MESSAGE_PLAY_ITEM:
+ //Set Addressed Player
+ playItem((BrowseTree.BrowseNode) msg.obj);
+ return true;
+
+ case MSG_AVRCP_PASSTHRU:
+ passThru(msg.arg1);
+ return true;
+
+ case MSG_AVRCP_SET_REPEAT:
+ setRepeat(msg.arg1);
+ return true;
+
+ case MSG_AVRCP_SET_SHUFFLE:
+ setShuffle(msg.arg1);
+ return true;
+
case MESSAGE_PROCESS_TRACK_CHANGED:
- case MESSAGE_PROCESS_PLAY_POS_CHANGED:
+ mAddressedPlayer.updateCurrentTrack((MediaMetadata) msg.obj);
+ BluetoothMediaBrowserService.trackChanged((MediaMetadata) msg.obj);
+ return true;
+
case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
- case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
- case MESSAGE_STOP_METADATA_BROADCASTS:
- case MESSAGE_START_METADATA_BROADCASTS:
- case MESSAGE_PROCESS_CONNECTION_CHANGE:
- case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
- // All of these messages should be handled by parent state immediately.
- return false;
+ mAddressedPlayer.setPlayStatus(msg.arg1);
+ BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState());
+ if (mAddressedPlayer.getPlaybackState().getState()
+ == PlaybackStateCompat.STATE_PLAYING
+ && A2dpSinkService.getFocusState() == AudioManager.AUDIOFOCUS_NONE
+ && !shouldRequestFocus()) {
+ sendMessage(MSG_AVRCP_PASSTHRU,
+ AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
+ }
+ return true;
+
+ case MESSAGE_PROCESS_PLAY_POS_CHANGED:
+ if (msg.arg2 != -1) {
+ mAddressedPlayer.setPlayTime(msg.arg2);
+
+ BluetoothMediaBrowserService.notifyChanged(
+ mAddressedPlayer.getPlaybackState());
+ }
+ return true;
+
+ case MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED:
+ mAddressedPlayerId = msg.arg1;
+ logD("AddressedPlayer = " + mAddressedPlayerId);
+ AvrcpPlayer updatedPlayer = mAvailablePlayerList.get(mAddressedPlayerId);
+ if (updatedPlayer != null) {
+ mAddressedPlayer = updatedPlayer;
+ logD("AddressedPlayer = " + mAddressedPlayer.getName());
+ } else {
+ mBrowseTree.mRootNode.setCached(false);
+ mBrowseTree.mRootNode.setExpectedChildren(255);
+ BluetoothMediaBrowserService.notifyChanged(mBrowseTree.mRootNode);
+ }
+ return true;
+
+ case MESSAGE_PROCESS_SUPPORTED_APPLICATION_SETTINGS:
+ mAddressedPlayer.setSupportedPlayerApplicationSettings(
+ (PlayerApplicationSettings) msg.obj);
+ BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState());
+ return true;
+
+ case MESSAGE_PROCESS_CURRENT_APPLICATION_SETTINGS:
+ mAddressedPlayer.setCurrentPlayerApplicationSettings(
+ (PlayerApplicationSettings) msg.obj);
+ BluetoothMediaBrowserService.notifyChanged(mAddressedPlayer.getPlaybackState());
+ return true;
+
+ case DISCONNECT:
+ transitionTo(mDisconnecting);
+ return true;
default:
- if (DBG) {
- Log.d(STATE_TAG, "deferring message " + msg.what + " to Connected state.");
- }
- deferMessage(msg);
+ return super.processMessage(msg);
}
- return true;
+
+ }
+
+ private void playItem(BrowseTree.BrowseNode node) {
+ if (node == null) {
+ Log.w(TAG, "Invalid item to play");
+ } else {
+ mService.playItemNative(
+ mDeviceAddress, node.getScope(),
+ node.getBluetoothID(), 0);
+ }
+ }
+
+ private synchronized void passThru(int cmd) {
+ logD("msgPassThru " + cmd);
+ // Some keys should be held until the next event.
+ if (mCurrentlyHeldKey != 0) {
+ mService.sendPassThroughCommandNative(
+ mDeviceAddress, mCurrentlyHeldKey,
+ AvrcpControllerService.KEY_STATE_RELEASED);
+
+ if (mCurrentlyHeldKey == cmd) {
+ // Return to prevent starting FF/FR operation again
+ mCurrentlyHeldKey = 0;
+ return;
+ } else {
+ // FF/FR is in progress and other operation is desired
+ // so after stopping FF/FR, not returning so that command
+ // can be sent for the desired operation.
+ mCurrentlyHeldKey = 0;
+ }
+ }
+
+ // Send the pass through.
+ mService.sendPassThroughCommandNative(mDeviceAddress, cmd,
+ AvrcpControllerService.KEY_STATE_PRESSED);
+
+ if (isHoldableKey(cmd)) {
+ // Release cmd next time a command is sent.
+ mCurrentlyHeldKey = cmd;
+ } else {
+ mService.sendPassThroughCommandNative(mDeviceAddress,
+ cmd, AvrcpControllerService.KEY_STATE_RELEASED);
+ }
+ }
+
+ private boolean isHoldableKey(int cmd) {
+ return (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_REWIND)
+ || (cmd == AvrcpControllerService.PASS_THRU_CMD_ID_FF);
+ }
+
+ private void setRepeat(int repeatMode) {
+ mService.setPlayerApplicationSettingValuesNative(mDeviceAddress, (byte) 1,
+ new byte[]{PlayerApplicationSettings.REPEAT_STATUS}, new byte[]{
+ PlayerApplicationSettings.mapAvrcpPlayerSettingstoBTattribVal(
+ PlayerApplicationSettings.REPEAT_STATUS, repeatMode)});
+ }
+
+ private void setShuffle(int shuffleMode) {
+ mService.setPlayerApplicationSettingValuesNative(mDeviceAddress, (byte) 1,
+ new byte[]{PlayerApplicationSettings.SHUFFLE_STATUS}, new byte[]{
+ PlayerApplicationSettings.mapAvrcpPlayerSettingstoBTattribVal(
+ PlayerApplicationSettings.SHUFFLE_STATUS, shuffleMode)});
}
}
// Handle the get folder listing action
// a) Fetch the listing of folders
// b) Once completed return the object listing
- class GetFolderList extends CmdState {
- private static final String STATE_TAG = "AVRCPSM.GetFolderList";
+ class GetFolderList extends State {
+ private static final String STATE_TAG = "Avrcp.GetFolderList";
- String mID = "";
- int mStartInd;
- int mEndInd;
- int mCurrInd;
- int mScope;
- private ArrayList<MediaItem> mFolderList = new ArrayList<>();
+ boolean mAbort;
+ BrowseTree.BrowseNode mBrowseNode;
+ BrowseTree.BrowseNode mNextStep;
@Override
public void enter() {
+ logD(STATE_TAG + " Entering GetFolderList");
// Setup the timeouts.
+ sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
super.enter();
- mCurrInd = 0;
- mFolderList.clear();
- callNativeFunctionForScope(mStartInd,
- Math.min(mEndInd, mStartInd + GET_FOLDER_ITEMS_PAGINATION_SIZE - 1));
- }
-
- public void setScope(int scope) {
- mScope = scope;
- }
-
- public void setFolder(String id) {
- if (DBG) Log.d(STATE_TAG, "Setting folder to " + id);
- mID = id;
- }
-
- public void setBounds(int startInd, int endInd) {
- if (DBG) {
- Log.d(STATE_TAG, "startInd " + startInd + " endInd " + endInd);
+ mAbort = false;
+ Message msg = getCurrentMessage();
+ if (msg.what == MESSAGE_GET_FOLDER_ITEMS) {
+ {
+ logD(STATE_TAG + " new Get Request");
+ mBrowseNode = (BrowseTree.BrowseNode) msg.obj;
+ }
}
- mStartInd = startInd;
- mEndInd = Math.min(endInd, MAX_FOLDER_ITEMS);
+
+ if (mBrowseNode == null) {
+ transitionTo(mConnected);
+ } else {
+ navigateToFolderOrRetrieve(mBrowseNode);
+ }
}
@Override
public boolean processMessage(Message msg) {
- Log.d(STATE_TAG, "processMessage " + msg.what);
+ logD(STATE_TAG + " processMessage " + msg.what);
switch (msg.what) {
case MESSAGE_PROCESS_GET_FOLDER_ITEMS:
ArrayList<MediaItem> folderList = (ArrayList<MediaItem>) msg.obj;
- mFolderList.addAll(folderList);
- if (DBG) {
- Log.d(STATE_TAG,
- "Start " + mStartInd + " End " + mEndInd + " Curr " + mCurrInd
- + " received " + folderList.size());
- }
- mCurrInd += folderList.size();
+ int endIndicator = mBrowseNode.getExpectedChildren() - 1;
+ logD("GetFolderItems: End " + endIndicator
+ + " received " + folderList.size());
// Always update the node so that the user does not wait forever
// for the list to populate.
- sendFolderBroadcastAndUpdateNode();
+ mBrowseNode.addChildren(folderList);
+ notifyChanged(mBrowseNode);
- if (mCurrInd > mEndInd || folderList.size() == 0) {
+ if (mBrowseNode.getChildrenCount() >= endIndicator || folderList.size() == 0
+ || mAbort) {
// If we have fetched all the elements or if the remotes sends us 0 elements
// (which can lead us into a loop since mCurrInd does not proceed) we simply
// abort.
+ mBrowseNode.setCached(true);
transitionTo(mConnected);
} else {
// Fetch the next set of items.
- callNativeFunctionForScope(mCurrInd, Math.min(mEndInd,
- mCurrInd + GET_FOLDER_ITEMS_PAGINATION_SIZE - 1));
+ fetchContents(mBrowseNode);
// Reset the timeout message since we are doing a new fetch now.
removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
}
break;
+ case MESSAGE_PROCESS_SET_BROWSED_PLAYER:
+ mBrowseTree.setCurrentBrowsedPlayer(mNextStep.getID(), msg.arg1, msg.arg2);
+ removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
+ sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
+ navigateToFolderOrRetrieve(mBrowseNode);
+ break;
+
+ case MESSAGE_PROCESS_FOLDER_PATH:
+ mBrowseTree.setCurrentBrowsedFolder(mNextStep.getID());
+ mBrowseTree.getCurrentBrowsedFolder().setExpectedChildren(msg.arg1);
+
+ if (mAbort) {
+ transitionTo(mConnected);
+ } else {
+ removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
+ sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
+ navigateToFolderOrRetrieve(mBrowseNode);
+ }
+ break;
+
+ case MESSAGE_PROCESS_GET_PLAYER_ITEMS:
+ BrowseTree.BrowseNode rootNode = mBrowseTree.mRootNode;
+ if (!rootNode.isCached()) {
+ List<AvrcpPlayer> playerList = (List<AvrcpPlayer>) msg.obj;
+ mAvailablePlayerList.clear();
+ for (AvrcpPlayer player : playerList) {
+ mAvailablePlayerList.put(player.getId(), player);
+ }
+ rootNode.addChildren(playerList);
+ mBrowseTree.setCurrentBrowsedFolder(BrowseTree.ROOT);
+ rootNode.setExpectedChildren(playerList.size());
+ rootNode.setCached(true);
+ notifyChanged(rootNode);
+ }
+ transitionTo(mConnected);
+ break;
case MESSAGE_INTERNAL_CMD_TIMEOUT:
// We have timed out to execute the request, we should simply send
// whatever listing we have gotten until now.
- sendFolderBroadcastAndUpdateNode();
+ Log.w(TAG, "TIMEOUT");
transitionTo(mConnected);
break;
@@ -639,651 +586,302 @@
// If we have gotten an error for OUT OF RANGE we have
// already sent all the items to the client hence simply
// transition to Connected state here.
+ mBrowseNode.setCached(true);
transitionTo(mConnected);
break;
- case MESSAGE_CHANGE_FOLDER_PATH:
- case MESSAGE_FETCH_ATTR_AND_PLAY_ITEM:
- case MESSAGE_GET_PLAYER_LIST:
- case MESSAGE_GET_NOW_PLAYING_LIST:
- case MESSAGE_SET_BROWSED_PLAYER:
- // A new request has come in, no need to fetch more.
- mEndInd = 0;
- deferMessage(msg);
+ case MESSAGE_GET_FOLDER_ITEMS:
+ if (!mBrowseNode.equals(msg.obj)) {
+ if (shouldAbort(mBrowseNode.getScope(),
+ ((BrowseTree.BrowseNode) msg.obj).getScope())) {
+ mAbort = true;
+ }
+ deferMessage(msg);
+ logD("GetFolderItems: Go Get Another Directory");
+ } else {
+ logD("GetFolderItems: Get The Same Directory, ignore");
+ }
break;
- case MESSAGE_SEND_PASS_THROUGH_CMD:
- case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
- case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
- case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
- case MESSAGE_PROCESS_TRACK_CHANGED:
- case MESSAGE_PROCESS_PLAY_POS_CHANGED:
- case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
- case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
- case MESSAGE_STOP_METADATA_BROADCASTS:
- case MESSAGE_START_METADATA_BROADCASTS:
- case MESSAGE_PROCESS_CONNECTION_CHANGE:
- case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
+ default:
// All of these messages should be handled by parent state immediately.
return false;
-
- default:
- if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
- deferMessage(msg);
}
return true;
}
- private void sendFolderBroadcastAndUpdateNode() {
- BrowseTree.BrowseNode bn = mBrowseTree.findBrowseNodeByID(mID);
- if (bn == null) {
- Log.e(TAG, "Can not find BrowseNode by ID: " + mID);
- return;
+ /**
+ * shouldAbort calculates the cases where fetching the current directory is no longer
+ * necessary.
+ *
+ * @return true: a new folder in the same scope
+ * a new player while fetching contents of a folder
+ * false: other cases, specifically Now Playing while fetching a folder
+ */
+ private boolean shouldAbort(int currentScope, int fetchScope) {
+ if ((currentScope == fetchScope)
+ || (currentScope == AvrcpControllerService.BROWSE_SCOPE_VFS
+ && fetchScope == AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST)) {
+ return true;
}
- if (bn.isPlayer()) {
- // Add the now playing folder.
- MediaDescription.Builder mdb = new MediaDescription.Builder();
- mdb.setMediaId(BrowseTree.NOW_PLAYING_PREFIX + ":" + bn.getPlayerID());
- mdb.setTitle(BrowseTree.NOW_PLAYING_PREFIX);
- Bundle mdBundle = new Bundle();
- mdBundle.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY,
- BrowseTree.NOW_PLAYING_PREFIX + ":" + bn.getID());
- mdb.setExtras(mdBundle);
- mFolderList.add(new MediaItem(mdb.build(), MediaItem.FLAG_BROWSABLE));
- }
- mBrowseTree.refreshChildren(bn, mFolderList);
- broadcastFolderList(mID, mFolderList);
-
- // For now playing we need to set the current browsed folder here.
- // For normal folders it is set after ChangeFolderPath.
- if (mScope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
- mBrowseTree.setCurrentBrowsedFolder(mID);
- }
+ return false;
}
- private void callNativeFunctionForScope(int start, int end) {
- switch (mScope) {
+ private void fetchContents(BrowseTree.BrowseNode target) {
+ int start = target.getChildrenCount();
+ int end = Math.min(target.getExpectedChildren(), target.getChildrenCount()
+ + ITEM_PAGE_SIZE) - 1;
+ switch (target.getScope()) {
+ case AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST:
+ mService.getPlayerListNative(mDeviceAddress,
+ start, end);
+ break;
case AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING:
- AvrcpControllerService.getNowPlayingListNative(
- mRemoteDevice.getBluetoothAddress(), start, end);
+ mService.getNowPlayingListNative(
+ mDeviceAddress, start, end);
break;
case AvrcpControllerService.BROWSE_SCOPE_VFS:
- AvrcpControllerService.getFolderListNative(mRemoteDevice.getBluetoothAddress(),
+ mService.getFolderListNative(mDeviceAddress,
start, end);
break;
default:
- Log.e(STATE_TAG, "Scope " + mScope + " cannot be handled here.");
+ Log.e(TAG, STATE_TAG + " Scope " + target.getScope()
+ + " cannot be handled here.");
}
}
- }
- // Handle the get player listing action
- // a) Fetch the listing of players
- // b) Once completed return the object listing
- class GetPlayerListing extends CmdState {
- private static final String STATE_TAG = "AVRCPSM.GetPlayerList";
-
- @Override
- public boolean processMessage(Message msg) {
- if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what);
- switch (msg.what) {
- case MESSAGE_PROCESS_GET_PLAYER_ITEMS:
- List<AvrcpPlayer> playerList = (List<AvrcpPlayer>) msg.obj;
- mBrowseTree.refreshChildren(BrowseTree.ROOT, playerList);
- ArrayList<MediaItem> mediaItemList = new ArrayList<>();
- for (BrowseTree.BrowseNode c : mBrowseTree.findBrowseNodeByID(BrowseTree.ROOT)
- .getChildren()) {
- mediaItemList.add(c.getMediaItem());
- }
- broadcastFolderList(BrowseTree.ROOT, mediaItemList);
- mBrowseTree.setCurrentBrowsedFolder(BrowseTree.ROOT);
+ /* One of several things can happen when trying to get a folder list
+ *
+ *
+ * 0: The folder handle is no longer valid
+ * 1: The folder contents can be retrieved directly (NowPlaying, Root, Current)
+ * 2: The folder is a browsable player
+ * 3: The folder is a non browsable player
+ * 4: The folder is not a child of the current folder
+ * 5: The folder is a child of the current folder
+ *
+ */
+ private void navigateToFolderOrRetrieve(BrowseTree.BrowseNode target) {
+ mNextStep = mBrowseTree.getNextStepToFolder(target);
+ logD("NAVIGATING From "
+ + mBrowseTree.getCurrentBrowsedFolder().toString());
+ logD("NAVIGATING Toward " + target.toString());
+ if (mNextStep == null) {
+ return;
+ } else if (target.equals(mBrowseTree.mNowPlayingNode)
+ || target.equals(mBrowseTree.mRootNode)
+ || mNextStep.equals(mBrowseTree.getCurrentBrowsedFolder())) {
+ fetchContents(mNextStep);
+ } else if (mNextStep.isPlayer()) {
+ logD("NAVIGATING Player " + mNextStep.toString());
+ if (mNextStep.isBrowsable()) {
+ mService.setBrowsedPlayerNative(
+ mDeviceAddress, (int) mNextStep.getBluetoothID());
+ } else {
+ logD("Player doesn't support browsing");
+ mNextStep.setCached(true);
transitionTo(mConnected);
- break;
+ }
+ } else if (mNextStep.equals(mBrowseTree.mNavigateUpNode)) {
+ logD("NAVIGATING UP " + mNextStep.toString());
+ mNextStep = mBrowseTree.getCurrentBrowsedFolder().getParent();
+ mBrowseTree.getCurrentBrowsedFolder().setCached(false);
- case MESSAGE_INTERNAL_CMD_TIMEOUT:
- // We have timed out to execute the request.
- // Send an empty list here.
- broadcastFolderList(BrowseTree.ROOT, EMPTY_MEDIA_ITEM_LIST);
- transitionTo(mConnected);
- break;
+ mService.changeFolderPathNative(
+ mDeviceAddress,
+ AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP,
+ 0);
- case MESSAGE_SEND_PASS_THROUGH_CMD:
- case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
- case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
- case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
- case MESSAGE_PROCESS_TRACK_CHANGED:
- case MESSAGE_PROCESS_PLAY_POS_CHANGED:
- case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
- case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
- case MESSAGE_STOP_METADATA_BROADCASTS:
- case MESSAGE_START_METADATA_BROADCASTS:
- case MESSAGE_PROCESS_CONNECTION_CHANGE:
- case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
- // All of these messages should be handled by parent state immediately.
- return false;
-
- default:
- if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
- deferMessage(msg);
+ } else {
+ logD("NAVIGATING DOWN " + mNextStep.toString());
+ mService.changeFolderPathNative(
+ mDeviceAddress,
+ AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN,
+ mNextStep.getBluetoothID());
}
- return true;
- }
- }
-
- class MoveToRoot extends CmdState {
- private static final String STATE_TAG = "AVRCPSM.MoveToRoot";
- private String mID = "";
-
- public void setFolder(String id) {
- if (DBG) Log.d(STATE_TAG, "setFolder " + id);
- mID = id;
- }
-
- @Override
- public void enter() {
- // Setup the timeouts.
- super.enter();
-
- // We need to move mBrowseDepth levels up. The following message is
- // completely internal to this state.
- sendMessage(MESSAGE_INTERNAL_MOVE_N_LEVELS_UP);
- }
-
- @Override
- public boolean processMessage(Message msg) {
- if (DBG) {
- Log.d(STATE_TAG, "processMessage " + msg.what + " browse depth " + mBrowseDepth);
- }
- switch (msg.what) {
- case MESSAGE_INTERNAL_MOVE_N_LEVELS_UP:
- if (mBrowseDepth == 0) {
- Log.w(STATE_TAG, "Already in root!");
- transitionTo(mConnected);
- sendMessage(MESSAGE_GET_FOLDER_LIST, 0, 0xff, mID);
- } else {
- AvrcpControllerService.changeFolderPathNative(
- mRemoteDevice.getBluetoothAddress(),
- (byte) AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP,
- AvrcpControllerService.hexStringToByteUID(null));
- }
- break;
-
- case MESSAGE_PROCESS_FOLDER_PATH:
- mBrowseDepth -= 1;
- if (DBG) Log.d(STATE_TAG, "New browse depth " + mBrowseDepth);
- if (mBrowseDepth < 0) {
- throw new IllegalArgumentException("Browse depth negative!");
- }
-
- sendMessage(MESSAGE_INTERNAL_MOVE_N_LEVELS_UP);
- break;
-
- case MESSAGE_INTERNAL_CMD_TIMEOUT:
- broadcastFolderList(BrowseTree.ROOT, EMPTY_MEDIA_ITEM_LIST);
- transitionTo(mConnected);
- break;
-
- case MESSAGE_SEND_PASS_THROUGH_CMD:
- case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
- case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
- case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
- case MESSAGE_PROCESS_TRACK_CHANGED:
- case MESSAGE_PROCESS_PLAY_POS_CHANGED:
- case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
- case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
- case MESSAGE_STOP_METADATA_BROADCASTS:
- case MESSAGE_START_METADATA_BROADCASTS:
- case MESSAGE_PROCESS_CONNECTION_CHANGE:
- case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
- // All of these messages should be handled by parent state immediately.
- return false;
-
- default:
- if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
- deferMessage(msg);
- }
- return true;
- }
- }
-
- class SetBrowsedPlayer extends CmdState {
- private static final String STATE_TAG = "AVRCPSM.SetBrowsedPlayer";
- String mID = "";
-
- public void setFolder(String id) {
- mID = id;
- }
-
- @Override
- public boolean processMessage(Message msg) {
- if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what);
- switch (msg.what) {
- case MESSAGE_PROCESS_SET_BROWSED_PLAYER:
- // Set the new depth.
- if (DBG) Log.d(STATE_TAG, "player depth " + msg.arg2);
- mBrowseDepth = msg.arg2;
-
- // If we already on top of player and there is no content.
- // This should very rarely happen.
- if (mBrowseDepth == 0 && msg.arg1 == 0) {
- broadcastFolderList(mID, EMPTY_MEDIA_ITEM_LIST);
- transitionTo(mConnected);
- } else {
- // Otherwise move to root and fetch the listing.
- // the MoveToRoot#enter() function takes care of fetch.
- mMoveToRoot.setFolder(mID);
- transitionTo(mMoveToRoot);
- }
- mBrowseTree.setCurrentBrowsedFolder(mID);
- // Also set the browsed player here.
- mBrowseTree.setCurrentBrowsedPlayer(mID);
- break;
-
- case MESSAGE_INTERNAL_CMD_TIMEOUT:
- broadcastFolderList(mID, EMPTY_MEDIA_ITEM_LIST);
- transitionTo(mConnected);
- break;
-
- case MESSAGE_SEND_PASS_THROUGH_CMD:
- case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
- case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
- case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
- case MESSAGE_PROCESS_TRACK_CHANGED:
- case MESSAGE_PROCESS_PLAY_POS_CHANGED:
- case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
- case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
- case MESSAGE_STOP_METADATA_BROADCASTS:
- case MESSAGE_START_METADATA_BROADCASTS:
- case MESSAGE_PROCESS_CONNECTION_CHANGE:
- case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
- // All of these messages should be handled by parent state immediately.
- return false;
-
- default:
- if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
- deferMessage(msg);
- }
- return true;
- }
- }
-
- class SetAddresedPlayerAndPlayItem extends CmdState {
- private static final String STATE_TAG = "AVRCPSM.SetAddresedPlayerAndPlayItem";
- int mScope;
- String mPlayItemId;
- String mAddrPlayerId;
-
- public void setItemAndScope(String addrPlayerId, String playItemId, int scope) {
- mAddrPlayerId = addrPlayerId;
- mPlayItemId = playItemId;
- mScope = scope;
- }
-
- @Override
- public boolean processMessage(Message msg) {
- if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what);
- switch (msg.what) {
- case MESSAGE_PROCESS_SET_ADDRESSED_PLAYER:
- // Set the new addressed player.
- mBrowseTree.setCurrentAddressedPlayer(mAddrPlayerId);
-
- // And now play the item.
- AvrcpControllerService.playItemNative(mRemoteDevice.getBluetoothAddress(),
- (byte) mScope, AvrcpControllerService.hexStringToByteUID(mPlayItemId),
- (int) 0);
-
- // Transition to connected state here.
- transitionTo(mConnected);
- break;
-
- case MESSAGE_INTERNAL_CMD_TIMEOUT:
- transitionTo(mConnected);
- break;
-
- case MESSAGE_SEND_PASS_THROUGH_CMD:
- case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
- case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
- case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
- case MESSAGE_PROCESS_TRACK_CHANGED:
- case MESSAGE_PROCESS_PLAY_POS_CHANGED:
- case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
- case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
- case MESSAGE_STOP_METADATA_BROADCASTS:
- case MESSAGE_START_METADATA_BROADCASTS:
- case MESSAGE_PROCESS_CONNECTION_CHANGE:
- case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
- // All of these messages should be handled by parent state immediately.
- return false;
-
- default:
- if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
- deferMessage(msg);
- }
- return true;
- }
- }
-
- // Class template for commands. Each state should do the following:
- // (a) In enter() send a timeout message which could be tracked in the
- // processMessage() stage.
- // (b) In exit() remove all the timeouts.
- //
- // Essentially the lifecycle of a timeout should be bounded to a CmdState always.
- abstract class CmdState extends State {
- @Override
- public void enter() {
- sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
}
@Override
public void exit() {
removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
+ mBrowseNode = null;
+ super.exit();
}
}
- // Interface APIs
- boolean isConnected() {
- synchronized (mLock) {
- return mIsConnected;
- }
- }
-
- void doQuit() {
- try {
- mContext.unregisterReceiver(mBroadcastReceiver);
- } catch (IllegalArgumentException expected) {
- // If the receiver was never registered unregister will throw an
- // IllegalArgumentException.
- }
- quit();
- }
-
- void dump(StringBuilder sb) {
- ProfileService.println(sb, "StateMachine: " + this.toString());
- }
-
- MediaMetadata getCurrentMetaData() {
- synchronized (mLock) {
- if (mAddressedPlayer != null && mAddressedPlayer.getCurrentTrack() != null) {
- MediaMetadata mmd = mAddressedPlayer.getCurrentTrack().getMediaMetaData();
- if (DBG) {
- Log.d(TAG, "getCurrentMetaData mmd " + mmd);
- }
- }
- return EMPTY_MEDIA_METADATA;
- }
- }
-
- PlaybackState getCurrentPlayBackState() {
- return getCurrentPlayBackState(true);
- }
-
- PlaybackState getCurrentPlayBackState(boolean cached) {
- if (cached) {
- synchronized (mLock) {
- if (mAddressedPlayer == null) {
- return new PlaybackState.Builder().setState(PlaybackState.STATE_ERROR,
- PlaybackState.PLAYBACK_POSITION_UNKNOWN, 0).build();
- }
- return mAddressedPlayer.getPlaybackState();
- }
- } else {
- // Issue a native request, we return NULL since this is only for PTS.
- AvrcpControllerService.getPlaybackStateNative(mRemoteDevice.getBluetoothAddress());
- return null;
- }
- }
-
- // Entry point to the state machine where the services should call to fetch children
- // for a specific node. It checks if the currently browsed node is the same as the one being
- // asked for, in that case it returns the currently cached children. This saves bandwidth and
- // also if we are already fetching elements for a current folder (since we need to batch
- // fetches) then we should not submit another request but simply return what we have fetched
- // until now.
- //
- // It handles fetches to all VFS, Now Playing and Media Player lists.
- void getChildren(String parentMediaId, int start, int items) {
- BrowseTree.BrowseNode bn = mBrowseTree.findBrowseNodeByID(parentMediaId);
- if (bn == null) {
- Log.e(TAG, "Invalid folder to browse " + mBrowseTree);
- broadcastFolderList(parentMediaId, EMPTY_MEDIA_ITEM_LIST);
- return;
- }
-
- if (DBG) {
- Log.d(TAG, "To Browse folder " + bn + " is cached " + bn.isCached() + " current folder "
- + mBrowseTree.getCurrentBrowsedFolder());
- }
- if (bn.equals(mBrowseTree.getCurrentBrowsedFolder()) && bn.isCached()) {
- if (DBG) {
- Log.d(TAG, "Same cached folder -- returning existing children.");
- }
- BrowseTree.BrowseNode n = mBrowseTree.findBrowseNodeByID(parentMediaId);
- ArrayList<MediaItem> childrenList = new ArrayList<MediaItem>();
- for (BrowseTree.BrowseNode cn : n.getChildren()) {
- childrenList.add(cn.getMediaItem());
- }
- broadcastFolderList(parentMediaId, childrenList);
- return;
- }
-
- Message msg = null;
- int btDirection = mBrowseTree.getDirection(parentMediaId);
- BrowseTree.BrowseNode currFol = mBrowseTree.getCurrentBrowsedFolder();
- if (DBG) {
- Log.d(TAG, "Browse direction parent " + mBrowseTree.getCurrentBrowsedFolder() + " req "
- + parentMediaId + " direction " + btDirection);
- }
- if (BrowseTree.ROOT.equals(parentMediaId)) {
- // Root contains the list of players.
- msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_PLAYER_LIST, start, items);
- } else if (bn.isPlayer() && btDirection != BrowseTree.DIRECTION_SAME) {
- // Set browsed (and addressed player) as the new player.
- // This should fetch the list of folders.
- msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_SET_BROWSED_PLAYER,
- bn.getPlayerID(), 0, bn.getID());
- } else if (bn.isNowPlaying()) {
- // Issue a request to fetch the items.
- msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_NOW_PLAYING_LIST, start,
- items, parentMediaId);
- } else {
- // Only change folder if desired. If an app refreshes a folder
- // (because it resumed etc) and current folder does not change
- // then we can simply fetch list.
-
- // We exempt two conditions from change folder:
- // a) If the new folder is the same as current folder (refresh of UI)
- // b) If the new folder is ROOT and current folder is NOW_PLAYING (or vice-versa)
- // In this condition we 'fake' child-parent hierarchy but it does not exist in
- // bluetooth world.
- boolean isNowPlayingToRoot =
- currFol.isNowPlaying() && bn.getID().equals(BrowseTree.ROOT);
- if (!isNowPlayingToRoot) {
- // Find the direction of traversal.
- int direction = -1;
- if (DBG) Log.d(TAG, "Browse direction " + currFol + " " + bn + " = " + btDirection);
- if (btDirection == BrowseTree.DIRECTION_UNKNOWN) {
- Log.w(TAG, "parent " + bn + " is not a direct "
- + "successor or predeccessor of current folder " + currFol);
- broadcastFolderList(parentMediaId, EMPTY_MEDIA_ITEM_LIST);
- return;
- }
-
- if (btDirection == BrowseTree.DIRECTION_DOWN) {
- direction = AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN;
- } else if (btDirection == BrowseTree.DIRECTION_UP) {
- direction = AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP;
- }
-
- Bundle b = new Bundle();
- b.putString(AvrcpControllerService.EXTRA_FOLDER_ID, bn.getID());
- b.putString(AvrcpControllerService.EXTRA_FOLDER_BT_ID, bn.getFolderUID());
- msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_CHANGE_FOLDER_PATH,
- direction, 0, b);
- } else {
- // Fetch the listing without changing paths.
- msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_FOLDER_LIST, start,
- items, bn.getFolderUID());
- }
- }
-
- if (msg != null) {
- sendMessage(msg);
- }
- }
-
- public void fetchAttrAndPlayItem(String uid) {
- BrowseTree.BrowseNode currItem = mBrowseTree.findFolderByIDLocked(uid);
- BrowseTree.BrowseNode currFolder = mBrowseTree.getCurrentBrowsedFolder();
- if (DBG) Log.d(TAG, "fetchAttrAndPlayItem mediaId=" + uid + " node=" + currItem);
- if (currItem != null) {
- int scope = currFolder.isNowPlaying() ? AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING
- : AvrcpControllerService.BROWSE_SCOPE_VFS;
- Message msg =
- obtainMessage(AvrcpControllerStateMachine.MESSAGE_FETCH_ATTR_AND_PLAY_ITEM,
- scope, 0, currItem.getFolderUID());
- sendMessage(msg);
- }
- }
-
- private void broadcastMetaDataChanged(MediaMetadata metadata) {
- Intent intent = new Intent(AvrcpControllerService.ACTION_TRACK_EVENT);
- intent.putExtra(AvrcpControllerService.EXTRA_METADATA, metadata);
- if (VDBG) {
- Log.d(TAG, " broadcastMetaDataChanged = " + metadata.getDescription());
- }
- mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
- }
-
- private void broadcastFolderList(String id, ArrayList<MediaItem> items) {
- Intent intent = new Intent(AvrcpControllerService.ACTION_FOLDER_LIST);
- if (VDBG) Log.d(TAG, "broadcastFolderList id " + id + " items " + items);
- intent.putExtra(AvrcpControllerService.EXTRA_FOLDER_ID, id);
- intent.putParcelableArrayListExtra(AvrcpControllerService.EXTRA_FOLDER_LIST, items);
- mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
- }
-
- private void broadcastPlayBackStateChanged(PlaybackState state) {
- Intent intent = new Intent(AvrcpControllerService.ACTION_TRACK_EVENT);
- intent.putExtra(AvrcpControllerService.EXTRA_PLAYBACK, state);
- if (DBG) {
- Log.d(TAG, " broadcastPlayBackStateChanged = " + state.toString());
- }
- mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
- }
-
- private void setAbsVolume(int absVol, int label) {
- int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
- int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
- // Ignore first volume command since phone may not know difference between stream volume
- // and amplifier volume.
- if (mRemoteDevice.getFirstAbsVolCmdRecvd()) {
- int newIndex = (maxVolume * absVol) / ABS_VOL_BASE;
- if (DBG) {
- 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) {
- mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newIndex,
- AudioManager.FLAG_SHOW_UI);
- }
- } else {
- mRemoteDevice.setFirstAbsVolCmdRecvd();
- absVol = (currIndex * ABS_VOL_BASE) / maxVolume;
- if (DBG) Log.d(TAG, " SetAbsVol recvd for first time, respond with " + absVol);
- }
- AvrcpControllerService.sendAbsVolRspNative(mRemoteDevice.getBluetoothAddress(), absVol,
- label);
- }
-
- private int getVolumePercentage() {
- int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
- int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
- int percentageVol = ((currIndex * ABS_VOL_BASE) / maxVolume);
- return percentageVol;
- }
-
- private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ protected class Disconnecting extends State {
@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) {
- sendMessage(MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION);
- }
+ public void enter() {
+ onBrowsingDisconnected();
+ broadcastConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTING);
+ transitionTo(mDisconnected);
+ }
+ }
+
+ /**
+ * Handle a request to align our local volume with the volume of a remote device. If
+ * we're assuming the source volume is fixed then a response of ABS_VOL_MAX will always be
+ * sent and no volume adjustment action will be taken on the sink side.
+ *
+ * @param absVol A volume level based on a domain of [0, ABS_VOL_MAX]
+ * @param label Volume notification label
+ */
+ private void handleAbsVolumeRequest(int absVol, int label) {
+ logD("handleAbsVolumeRequest: absVol = " + absVol + ", label = " + label);
+ if (mIsVolumeFixed) {
+ logD("Source volume is assumed to be fixed, responding with max volume");
+ absVol = ABS_VOL_BASE;
+ } else {
+ mVolumeChangedNotificationsToIgnore++;
+ removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
+ sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT,
+ ABS_VOL_TIMEOUT_MILLIS);
+ setAbsVolume(absVol);
+ }
+ mService.sendAbsVolRspNative(mDeviceAddress, absVol, label);
+ }
+
+ /**
+ * Align our volume with a requested absolute volume level
+ *
+ * @param absVol A volume level based on a domain of [0, ABS_VOL_MAX]
+ */
+ private void setAbsVolume(int absVol) {
+ int maxLocalVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+ int curLocalVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+ int reqLocalVolume = (maxLocalVolume * absVol) / ABS_VOL_BASE;
+ logD("setAbsVolme: absVol = " + absVol + ", reqLocal = " + reqLocalVolume
+ + ", curLocal = " + curLocalVolume + ", maxLocal = " + maxLocalVolume);
+
+ /*
+ * 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 (reqLocalVolume != curLocalVolume) {
+ mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, reqLocalVolume,
+ AudioManager.FLAG_SHOW_UI);
+ }
+ }
+
+ private int getAbsVolume() {
+ if (mIsVolumeFixed) {
+ return ABS_VOL_BASE;
+ }
+ int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+ int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+ int newIndex = (currIndex * ABS_VOL_BASE) / maxVolume;
+ return newIndex;
+ }
+
+ MediaSessionCompat.Callback mSessionCallbacks = new MediaSessionCompat.Callback() {
+ @Override
+ public void onPlay() {
+ logD("onPlay");
+ onPrepare();
+ sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PLAY);
+ }
+
+ @Override
+ public void onPause() {
+ logD("onPause");
+ sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE);
+ }
+
+ @Override
+ public void onSkipToNext() {
+ logD("onSkipToNext");
+ onPrepare();
+ sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD);
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ logD("onSkipToPrevious");
+ onPrepare();
+ sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD);
+ }
+
+ @Override
+ public void onSkipToQueueItem(long id) {
+ logD("onSkipToQueueItem" + id);
+ onPrepare();
+ BrowseTree.BrowseNode node = mBrowseTree.getTrackFromNowPlayingList((int) id);
+ if (node != null) {
+ sendMessage(MESSAGE_PLAY_ITEM, node);
}
}
+
+ @Override
+ public void onStop() {
+ logD("onStop");
+ sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_STOP);
+ }
+
+ @Override
+ public void onPrepare() {
+ logD("onPrepare");
+ A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
+ if (a2dpSinkService != null) {
+ a2dpSinkService.requestAudioFocus(mDevice, true);
+ }
+ }
+
+ @Override
+ public void onRewind() {
+ logD("onRewind");
+ sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_REWIND);
+ }
+
+ @Override
+ public void onFastForward() {
+ logD("onFastForward");
+ sendMessage(MSG_AVRCP_PASSTHRU, AvrcpControllerService.PASS_THRU_CMD_ID_FF);
+ }
+
+ @Override
+ public void onPlayFromMediaId(String mediaId, Bundle extras) {
+ logD("onPlayFromMediaId");
+ // Play the item if possible.
+ onPrepare();
+ BrowseTree.BrowseNode node = mBrowseTree.findBrowseNodeByID(mediaId);
+ sendMessage(MESSAGE_PLAY_ITEM, node);
+ }
+
+ @Override
+ public void onSetRepeatMode(int repeatMode) {
+ logD("onSetRepeatMode");
+ sendMessage(MSG_AVRCP_SET_REPEAT, repeatMode);
+ }
+
+ @Override
+ public void onSetShuffleMode(int shuffleMode) {
+ logD("onSetShuffleMode");
+ sendMessage(MSG_AVRCP_SET_SHUFFLE, shuffleMode);
+
+ }
};
- public static String dumpMessageString(int message) {
- String str = "UNKNOWN";
- switch (message) {
- case MESSAGE_SEND_PASS_THROUGH_CMD:
- str = "REQ_PASS_THROUGH_CMD";
- break;
- case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
- str = "REQ_GRP_NAV_CMD";
- break;
- case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
- str = "CB_SET_ABS_VOL_CMD";
- break;
- case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
- str = "CB_REGISTER_ABS_VOL";
- break;
- case MESSAGE_PROCESS_TRACK_CHANGED:
- str = "CB_TRACK_CHANGED";
- break;
- case MESSAGE_PROCESS_PLAY_POS_CHANGED:
- str = "CB_PLAY_POS_CHANGED";
- break;
- case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
- str = "CB_PLAY_STATUS_CHANGED";
- break;
- case MESSAGE_PROCESS_RC_FEATURES:
- str = "CB_RC_FEATURES";
- break;
- case MESSAGE_PROCESS_CONNECTION_CHANGE:
- str = "CB_CONN_CHANGED";
- break;
- default:
- str = Integer.toString(message);
- break;
+ protected void broadcastConnectionStateChanged(int currentState) {
+ if (mMostRecentState == currentState) {
+ return;
}
- return str;
+ if (currentState == BluetoothProfile.STATE_CONNECTED) {
+ MetricsLogger.logProfileConnectionEvent(
+ BluetoothMetricsProto.ProfileId.AVRCP_CONTROLLER);
+ }
+ logD("Connection state " + mDevice + ": " + mMostRecentState + "->" + currentState);
+ Intent intent = new Intent(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
+ intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, mMostRecentState);
+ intent.putExtra(BluetoothProfile.EXTRA_STATE, currentState);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ mMostRecentState = currentState;
+ mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
}
- public static String displayBluetoothAvrcpSettings(BluetoothAvrcpPlayerSettings mSett) {
- StringBuffer sb = new StringBuffer();
- int supportedSetting = mSett.getSettings();
- if (VDBG) {
- Log.d(TAG, " setting: " + supportedSetting);
- }
- if ((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER) != 0) {
- sb.append(" EQ : ");
- sb.append(Integer.toString(mSett.getSettingValue(
- BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER)));
- }
- if ((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_REPEAT) != 0) {
- sb.append(" REPEAT : ");
- sb.append(Integer.toString(mSett.getSettingValue(
- BluetoothAvrcpPlayerSettings.SETTING_REPEAT)));
- }
- if ((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE) != 0) {
- sb.append(" SHUFFLE : ");
- sb.append(Integer.toString(mSett.getSettingValue(
- BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE)));
- }
- if ((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_SCAN) != 0) {
- sb.append(" SCAN : ");
- sb.append(Integer.toString(mSett.getSettingValue(
- BluetoothAvrcpPlayerSettings.SETTING_SCAN)));
- }
- return sb.toString();
+ private boolean shouldRequestFocus() {
+ return mService.getResources()
+ .getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus);
}
}
diff --git a/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java b/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java
index d9e4924..2ef3c14 100644
--- a/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java
+++ b/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayer.java
@@ -16,33 +16,69 @@
package com.android.bluetooth.avrcpcontroller;
-import android.media.session.PlaybackState;
+import android.media.MediaMetadata;
+import android.os.SystemClock;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
+import java.util.Arrays;
+
/*
* Contains information about remote player
*/
class AvrcpPlayer {
private static final String TAG = "AvrcpPlayer";
- private static final boolean DBG = true;
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
public static final int INVALID_ID = -1;
- private int mPlayStatus = PlaybackState.STATE_NONE;
- private long mPlayTime = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
+ public static final int FEATURE_PLAY = 40;
+ public static final int FEATURE_STOP = 41;
+ public static final int FEATURE_PAUSE = 42;
+ public static final int FEATURE_REWIND = 44;
+ public static final int FEATURE_FAST_FORWARD = 45;
+ public static final int FEATURE_FORWARD = 47;
+ public static final int FEATURE_PREVIOUS = 48;
+ public static final int FEATURE_BROWSING = 59;
+
+ private int mPlayStatus = PlaybackStateCompat.STATE_NONE;
+ private long mPlayTime = PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN;
+ private long mPlayTimeUpdate = 0;
+ private float mPlaySpeed = 1;
private int mId;
private String mName = "";
private int mPlayerType;
- private TrackInfo mCurrentTrack = new TrackInfo();
+ private byte[] mPlayerFeatures = new byte[16];
+ private long mAvailableActions;
+ private MediaMetadata mCurrentTrack;
+ private PlaybackStateCompat mPlaybackStateCompat;
+ private PlayerApplicationSettings mSupportedPlayerApplicationSettings =
+ new PlayerApplicationSettings();
+ private PlayerApplicationSettings mCurrentPlayerApplicationSettings;
AvrcpPlayer() {
mId = INVALID_ID;
+ //Set Default Actions in case Player data isn't available.
+ mAvailableActions = PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+ | PlaybackStateCompat.ACTION_STOP;
+ PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder()
+ .setActions(mAvailableActions);
+ mPlaybackStateCompat = playbackStateBuilder.build();
}
- AvrcpPlayer(int id, String name, int transportFlags, int playStatus, int playerType) {
+ AvrcpPlayer(int id, String name, byte[] playerFeatures, int playStatus, int playerType) {
mId = id;
mName = name;
+ mPlayStatus = playStatus;
mPlayerType = playerType;
+ mPlayerFeatures = Arrays.copyOf(playerFeatures, playerFeatures.length);
+ PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder()
+ .setActions(mAvailableActions);
+ mPlaybackStateCompat = playbackStateBuilder.build();
+ updateAvailableActions();
}
public int getId() {
@@ -55,6 +91,10 @@
public void setPlayTime(int playTime) {
mPlayTime = playTime;
+ mPlayTimeUpdate = SystemClock.elapsedRealtime();
+ mPlaybackStateCompat = new PlaybackStateCompat.Builder(mPlaybackStateCompat).setState(
+ mPlayStatus, mPlayTime,
+ mPlaySpeed).build();
}
public long getPlayTime() {
@@ -62,39 +102,117 @@
}
public void setPlayStatus(int playStatus) {
+ mPlayTime += mPlaySpeed * (SystemClock.elapsedRealtime()
+ - mPlaybackStateCompat.getLastPositionUpdateTime());
mPlayStatus = playStatus;
+ switch (mPlayStatus) {
+ case PlaybackStateCompat.STATE_STOPPED:
+ mPlaySpeed = 0;
+ break;
+ case PlaybackStateCompat.STATE_PLAYING:
+ mPlaySpeed = 1;
+ break;
+ case PlaybackStateCompat.STATE_PAUSED:
+ mPlaySpeed = 0;
+ break;
+ case PlaybackStateCompat.STATE_FAST_FORWARDING:
+ mPlaySpeed = 3;
+ break;
+ case PlaybackStateCompat.STATE_REWINDING:
+ mPlaySpeed = -3;
+ break;
+ }
+
+ mPlaybackStateCompat = new PlaybackStateCompat.Builder(mPlaybackStateCompat).setState(
+ mPlayStatus, mPlayTime,
+ mPlaySpeed).build();
}
- public PlaybackState getPlaybackState() {
+ public void setSupportedPlayerApplicationSettings(
+ PlayerApplicationSettings playerApplicationSettings) {
+ mSupportedPlayerApplicationSettings = playerApplicationSettings;
+ updateAvailableActions();
+ }
+
+ public void setCurrentPlayerApplicationSettings(
+ PlayerApplicationSettings playerApplicationSettings) {
+ Log.d(TAG, "Settings changed");
+ mCurrentPlayerApplicationSettings = playerApplicationSettings;
+ MediaSessionCompat session = BluetoothMediaBrowserService.getSession();
+ session.setRepeatMode(mCurrentPlayerApplicationSettings.getSetting(
+ PlayerApplicationSettings.REPEAT_STATUS));
+ session.setShuffleMode(mCurrentPlayerApplicationSettings.getSetting(
+ PlayerApplicationSettings.SHUFFLE_STATUS));
+ }
+
+ public int getPlayStatus() {
+ return mPlayStatus;
+ }
+
+ public boolean supportsFeature(int featureId) {
+ int byteNumber = featureId / 8;
+ byte bitMask = (byte) (1 << (featureId % 8));
+ return (mPlayerFeatures[byteNumber] & bitMask) == bitMask;
+ }
+
+ public boolean supportsSetting(int settingType, int settingValue) {
+ return mSupportedPlayerApplicationSettings.supportsSetting(settingType, settingValue);
+ }
+
+ public PlaybackStateCompat getPlaybackState() {
if (DBG) {
Log.d(TAG, "getPlayBackState state " + mPlayStatus + " time " + mPlayTime);
}
-
- long position = mPlayTime;
- float speed = 1;
- switch (mPlayStatus) {
- case PlaybackState.STATE_STOPPED:
- position = 0;
- speed = 0;
- break;
- case PlaybackState.STATE_PAUSED:
- speed = 0;
- break;
- case PlaybackState.STATE_FAST_FORWARDING:
- speed = 3;
- break;
- case PlaybackState.STATE_REWINDING:
- speed = -3;
- break;
- }
- return new PlaybackState.Builder().setState(mPlayStatus, position, speed).build();
+ return mPlaybackStateCompat;
}
- public synchronized void updateCurrentTrack(TrackInfo update) {
+ public synchronized void updateCurrentTrack(MediaMetadata update) {
+ if (update != null) {
+ long trackNumber = update.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER);
+ mPlaybackStateCompat = new PlaybackStateCompat.Builder(
+ mPlaybackStateCompat).setActiveQueueItemId(
+ trackNumber - 1).build();
+ }
mCurrentTrack = update;
}
- public synchronized TrackInfo getCurrentTrack() {
+ public synchronized MediaMetadata getCurrentTrack() {
return mCurrentTrack;
}
+
+ private void updateAvailableActions() {
+ if (supportsFeature(FEATURE_PLAY)) {
+ mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_PLAY;
+ }
+ if (supportsFeature(FEATURE_STOP)) {
+ mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_STOP;
+ }
+ if (supportsFeature(FEATURE_PAUSE)) {
+ mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_PAUSE;
+ }
+ if (supportsFeature(FEATURE_REWIND)) {
+ mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_REWIND;
+ }
+ if (supportsFeature(FEATURE_FAST_FORWARD)) {
+ mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_FAST_FORWARD;
+ }
+ if (supportsFeature(FEATURE_FORWARD)) {
+ mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
+ }
+ if (supportsFeature(FEATURE_PREVIOUS)) {
+ mAvailableActions = mAvailableActions | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
+ }
+ if (mSupportedPlayerApplicationSettings.supportsSetting(
+ PlayerApplicationSettings.REPEAT_STATUS)) {
+ mAvailableActions |= PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
+ }
+ if (mSupportedPlayerApplicationSettings.supportsSetting(
+ PlayerApplicationSettings.SHUFFLE_STATUS)) {
+ mAvailableActions |= PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
+ }
+ mPlaybackStateCompat = new PlaybackStateCompat.Builder(mPlaybackStateCompat)
+ .setActions(mAvailableActions).build();
+
+ if (DBG) Log.d(TAG, "Supported Actions = " + mAvailableActions);
+ }
}
diff --git a/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java b/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java
new file mode 100644
index 0000000..774f953
--- /dev/null
+++ b/src/com/android/bluetooth/avrcpcontroller/BluetoothMediaBrowserService.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.avrcpcontroller;
+
+import android.media.MediaMetadata;
+import android.media.browse.MediaBrowser.MediaItem;
+import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import androidx.media.MediaBrowserServiceCompat;
+
+import com.android.bluetooth.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implements the MediaBrowserService interface to AVRCP and A2DP
+ *
+ * This service provides a means for external applications to access A2DP and AVRCP.
+ * The applications are expected to use MediaBrowser (see API) and all the music
+ * browsing/playback/metadata can be controlled via MediaBrowser and MediaController.
+ *
+ * The current behavior of MediaSessionCompat exposed by this service is as follows:
+ * 1. MediaSessionCompat is active (i.e. SystemUI and other overview UIs can see updates) when
+ * device is connected and first starts playing. Before it starts playing we do not activate the
+ * session.
+ * 1.1 The session is active throughout the duration of connection.
+ * 2. The session is de-activated when the device disconnects. It will be connected again when (1)
+ * happens.
+ */
+public class BluetoothMediaBrowserService extends MediaBrowserServiceCompat {
+ private static final String TAG = "BluetoothMediaBrowserService";
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static BluetoothMediaBrowserService sBluetoothMediaBrowserService;
+
+ private MediaSessionCompat mSession;
+
+ // Browsing related structures.
+ private List<MediaSessionCompat.QueueItem> mMediaQueue = new ArrayList<>();
+
+ /**
+ * Initialize this BluetoothMediaBrowserService, creating our MediaSessionCompat, MediaPlayer
+ * and MediaMetaData, and setting up mechanisms to talk with the AvrcpControllerService.
+ */
+ @Override
+ public void onCreate() {
+ if (DBG) Log.d(TAG, "onCreate");
+ super.onCreate();
+
+ // Create and configure the MediaSessionCompat
+ mSession = new MediaSessionCompat(this, TAG);
+ setSessionToken(mSession.getSessionToken());
+ mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
+ | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ mSession.setQueueTitle(getString(R.string.bluetooth_a2dp_sink_queue_name));
+ mSession.setQueue(mMediaQueue);
+ PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
+ playbackStateBuilder.setState(PlaybackStateCompat.STATE_ERROR,
+ PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f).setActions(0);
+ playbackStateBuilder.setErrorMessage(getString(R.string.bluetooth_disconnected));
+ mSession.setPlaybackState(playbackStateBuilder.build());
+ sBluetoothMediaBrowserService = this;
+ }
+
+ List<MediaItem> getContents(final String parentMediaId) {
+ AvrcpControllerService avrcpControllerService =
+ AvrcpControllerService.getAvrcpControllerService();
+ if (avrcpControllerService == null) {
+ return new ArrayList(0);
+ } else {
+ return avrcpControllerService.getContents(parentMediaId);
+ }
+ }
+
+ @Override
+ public synchronized void onLoadChildren(final String parentMediaId,
+ final Result<List<MediaBrowserCompat.MediaItem>> result) {
+ if (DBG) Log.d(TAG, "onLoadChildren parentMediaId=" + parentMediaId);
+ List<MediaBrowserCompat.MediaItem> contents =
+ MediaBrowserCompat.MediaItem.fromMediaItemList(getContents(parentMediaId));
+ if (contents == null) {
+ result.detach();
+ } else {
+ result.sendResult(contents);
+ }
+ }
+
+ @Override
+ public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+ if (DBG) Log.d(TAG, "onGetRoot");
+ return new BrowserRoot(BrowseTree.ROOT, null);
+ }
+
+ private void updateNowPlayingQueue(BrowseTree.BrowseNode node) {
+ List<MediaItem> songList = node.getContents();
+ mMediaQueue.clear();
+ if (songList != null) {
+ for (MediaItem song : songList) {
+ mMediaQueue.add(new MediaSessionCompat.QueueItem(
+ MediaDescriptionCompat.fromMediaDescription(song.getDescription()),
+ mMediaQueue.size()));
+ }
+ }
+ mSession.setQueue(mMediaQueue);
+ }
+
+ static synchronized void notifyChanged(BrowseTree.BrowseNode node) {
+ if (sBluetoothMediaBrowserService != null) {
+ if (node.getScope() == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
+ sBluetoothMediaBrowserService.updateNowPlayingQueue(node);
+ } else {
+ sBluetoothMediaBrowserService.notifyChildrenChanged(node.getID());
+ }
+ }
+ }
+
+ static synchronized void addressedPlayerChanged(MediaSessionCompat.Callback callback) {
+ if (sBluetoothMediaBrowserService != null) {
+ sBluetoothMediaBrowserService.mSession.setCallback(callback);
+ } else {
+ Log.w(TAG, "addressedPlayerChanged Unavailable");
+ }
+ }
+
+ static synchronized void trackChanged(MediaMetadata mediaMetadata) {
+ if (sBluetoothMediaBrowserService != null) {
+ sBluetoothMediaBrowserService.mSession.setMetadata(
+ MediaMetadataCompat.fromMediaMetadata(mediaMetadata));
+ } else {
+ Log.w(TAG, "trackChanged Unavailable");
+ }
+ }
+
+ static synchronized void notifyChanged(PlaybackStateCompat playbackState) {
+ Log.d(TAG, "notifyChanged PlaybackState" + playbackState);
+ if (sBluetoothMediaBrowserService != null) {
+ sBluetoothMediaBrowserService.mSession.setPlaybackState(playbackState);
+ } else {
+ Log.w(TAG, "notifyChanged Unavailable");
+ }
+ }
+
+ /**
+ * Send AVRCP Play command
+ */
+ public static synchronized void play() {
+ if (sBluetoothMediaBrowserService != null) {
+ sBluetoothMediaBrowserService.mSession.getController().getTransportControls().play();
+ } else {
+ Log.w(TAG, "play Unavailable");
+ }
+ }
+
+ /**
+ * Send AVRCP Pause command
+ */
+ public static synchronized void pause() {
+ if (sBluetoothMediaBrowserService != null) {
+ sBluetoothMediaBrowserService.mSession.getController().getTransportControls().pause();
+ } else {
+ Log.w(TAG, "pause Unavailable");
+ }
+ }
+
+ /**
+ * Get object for controlling playback
+ */
+ public static synchronized MediaControllerCompat.TransportControls getTransportControls() {
+ if (sBluetoothMediaBrowserService != null) {
+ return sBluetoothMediaBrowserService.mSession.getController().getTransportControls();
+ } else {
+ Log.w(TAG, "transportControls Unavailable");
+ return null;
+ }
+ }
+
+ /**
+ * Set Media session active whenever we have Focus of any kind
+ */
+ public static synchronized void setActive(boolean active) {
+ if (sBluetoothMediaBrowserService != null) {
+ sBluetoothMediaBrowserService.mSession.setActive(active);
+ } else {
+ Log.w(TAG, "setActive Unavailable");
+ }
+ }
+
+ /**
+ * Get Media session for updating state
+ */
+ public static synchronized MediaSessionCompat getSession() {
+ if (sBluetoothMediaBrowserService != null) {
+ return sBluetoothMediaBrowserService.mSession;
+ } else {
+ Log.w(TAG, "getSession Unavailable");
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java b/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java
index 4482bd7..accea2a 100644
--- a/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java
+++ b/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java
@@ -16,16 +16,17 @@
package com.android.bluetooth.avrcpcontroller;
+import android.bluetooth.BluetoothDevice;
import android.media.MediaDescription;
import android.media.browse.MediaBrowser;
import android.media.browse.MediaBrowser.MediaItem;
import android.os.Bundle;
-import android.service.media.MediaBrowserService.Result;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.UUID;
// Browsing hierarchy.
// Root:
@@ -40,15 +41,11 @@
// ....
public class BrowseTree {
private static final String TAG = "BrowseTree";
- private static final boolean DBG = false;
- private static final boolean VDBG = false;
-
- public static final int DIRECTION_DOWN = 0;
- public static final int DIRECTION_UP = 1;
- public static final int DIRECTION_SAME = 2;
- public static final int DIRECTION_UNKNOWN = -1;
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
public static final String ROOT = "__ROOT__";
+ public static final String UP = "__UP__";
public static final String NOW_PLAYING_PREFIX = "NOW_PLAYING";
public static final String PLAYER_PREFIX = "PLAYER";
@@ -57,19 +54,39 @@
private BrowseNode mCurrentBrowseNode;
private BrowseNode mCurrentBrowsedPlayer;
private BrowseNode mCurrentAddressedPlayer;
+ private int mDepth = 0;
+ final BrowseNode mRootNode;
+ final BrowseNode mNavigateUpNode;
+ final BrowseNode mNowPlayingNode;
- BrowseTree() {
- }
+ BrowseTree(BluetoothDevice device) {
+ if (device == null) {
+ mRootNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
+ .setMediaId(ROOT).setTitle(ROOT).build(), MediaItem.FLAG_BROWSABLE));
+ mRootNode.setCached(true);
+ } else {
+ mRootNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
+ .setMediaId(ROOT + device.getAddress().toString()).setTitle(
+ device.getName()).build(), MediaItem.FLAG_BROWSABLE));
+ mRootNode.mDevice = device;
- public void init() {
- MediaDescription.Builder mdb = new MediaDescription.Builder();
- mdb.setMediaId(ROOT);
- mdb.setTitle(ROOT);
- Bundle mdBundle = new Bundle();
- mdBundle.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY, ROOT);
- mdb.setExtras(mdBundle);
- mBrowseMap.put(ROOT, new BrowseNode(new MediaItem(mdb.build(), MediaItem.FLAG_BROWSABLE)));
- mCurrentBrowseNode = mBrowseMap.get(ROOT);
+ }
+ mRootNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_PLAYER_LIST;
+ mRootNode.setExpectedChildren(255);
+
+ mNavigateUpNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
+ .setMediaId(UP).setTitle(UP).build(),
+ MediaItem.FLAG_BROWSABLE));
+
+ mNowPlayingNode = new BrowseNode(new MediaItem(new MediaDescription.Builder()
+ .setMediaId(NOW_PLAYING_PREFIX)
+ .setTitle(NOW_PLAYING_PREFIX).build(), MediaItem.FLAG_BROWSABLE));
+ mNowPlayingNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING;
+ mNowPlayingNode.setExpectedChildren(255);
+ mBrowseMap.put(ROOT, mRootNode);
+ mBrowseMap.put(NOW_PLAYING_PREFIX, mNowPlayingNode);
+
+ mCurrentBrowseNode = mRootNode;
}
public void clear() {
@@ -77,11 +94,23 @@
mBrowseMap.clear();
}
+ void onConnected(BluetoothDevice device) {
+ BrowseNode browseNode = new BrowseNode(device);
+ mRootNode.addChild(browseNode);
+ }
+
+ BrowseNode getTrackFromNowPlayingList(int trackNumber) {
+ return mNowPlayingNode.mChildren.get(trackNumber);
+ }
+
// Each node of the tree is represented by Folder ID, Folder Name and the children.
class BrowseNode {
// MediaItem to store the media related details.
MediaItem mItem;
+ BluetoothDevice mDevice;
+ long mBluetoothId;
+
// Type of this browse node.
// Since Media APIs do not define the player separately we define that
// distinction here.
@@ -91,15 +120,19 @@
// without doing another fetch.
boolean mCached = false;
- // Result object if this node is not loaded yet. This result object will be used
- // once loading is finished.
- Result<List<MediaItem>> mResult = null;
+ byte mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS;
// List of children.
- final List<BrowseNode> mChildren = new ArrayList<BrowseNode>();
+ private BrowseNode mParent;
+ private final List<BrowseNode> mChildren = new ArrayList<BrowseNode>();
+ private int mExpectedChildrenCount;
BrowseNode(MediaItem item) {
mItem = item;
+ Bundle extras = mItem.getDescription().getExtras();
+ if (extras != null) {
+ mBluetoothId = extras.getLong(AvrcpControllerService.MEDIA_ITEM_UID_KEY);
+ }
}
BrowseNode(AvrcpPlayer player) {
@@ -107,34 +140,120 @@
// Transform the player into a item.
MediaDescription.Builder mdb = new MediaDescription.Builder();
- Bundle mdExtra = new Bundle();
String playerKey = PLAYER_PREFIX + player.getId();
- mdExtra.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY, playerKey);
- mdb.setExtras(mdExtra);
- mdb.setMediaId(playerKey);
+ mBluetoothId = player.getId();
+
+ mdb.setMediaId(UUID.randomUUID().toString());
mdb.setTitle(player.getName());
+ int mediaItemFlags = player.supportsFeature(AvrcpPlayer.FEATURE_BROWSING)
+ ? MediaBrowser.MediaItem.FLAG_BROWSABLE : 0;
+ mItem = new MediaBrowser.MediaItem(mdb.build(), mediaItemFlags);
+ }
+
+ BrowseNode(BluetoothDevice device) {
+ boolean mIsPlayer = true;
+ mDevice = device;
+ MediaDescription.Builder mdb = new MediaDescription.Builder();
+ String playerKey = PLAYER_PREFIX + device.getAddress().toString();
+ mdb.setMediaId(playerKey);
+ mdb.setTitle(device.getName());
+ int mediaItemFlags = MediaBrowser.MediaItem.FLAG_BROWSABLE;
+ mItem = new MediaBrowser.MediaItem(mdb.build(), mediaItemFlags);
+ }
+
+ private BrowseNode(String name) {
+ MediaDescription.Builder mdb = new MediaDescription.Builder();
+ mdb.setMediaId(name);
+ mdb.setTitle(name);
mItem = new MediaBrowser.MediaItem(mdb.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
}
+ synchronized void setExpectedChildren(int count) {
+ mExpectedChildrenCount = count;
+ }
+
+ synchronized int getExpectedChildren() {
+ return mExpectedChildrenCount;
+ }
+
+ synchronized <E> int addChildren(List<E> newChildren) {
+ for (E child : newChildren) {
+ BrowseNode currentNode = null;
+ if (child instanceof MediaItem) {
+ currentNode = new BrowseNode((MediaItem) child);
+ } else if (child instanceof AvrcpPlayer) {
+ currentNode = new BrowseNode((AvrcpPlayer) child);
+ }
+ addChild(currentNode);
+ }
+ return newChildren.size();
+ }
+
+ synchronized boolean addChild(BrowseNode node) {
+ if (node != null) {
+ node.mParent = this;
+ if (this.mBrowseScope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
+ node.mBrowseScope = this.mBrowseScope;
+ }
+ if (node.mDevice == null) {
+ node.mDevice = this.mDevice;
+ }
+ mChildren.add(node);
+ mBrowseMap.put(node.getID(), node);
+ return true;
+ }
+ return false;
+ }
+
+ synchronized void removeChild(BrowseNode node) {
+ mChildren.remove(node);
+ mBrowseMap.remove(node.getID());
+ }
+
+ synchronized int getChildrenCount() {
+ return mChildren.size();
+ }
+
synchronized List<BrowseNode> getChildren() {
return mChildren;
}
- synchronized boolean isChild(BrowseNode node) {
- for (BrowseNode bn : mChildren) {
- if (bn.equals(node)) {
- return true;
+ synchronized BrowseNode getParent() {
+ return mParent;
+ }
+
+ synchronized List<MediaItem> getContents() {
+ if (mChildren.size() > 0 || mCached) {
+ List<MediaItem> contents = new ArrayList<MediaItem>(mChildren.size());
+ for (BrowseNode child : mChildren) {
+ contents.add(child.getMediaItem());
}
+ return contents;
}
- return false;
+ return null;
+ }
+
+ synchronized boolean isChild(BrowseNode node) {
+ return mChildren.contains(node);
}
synchronized boolean isCached() {
return mCached;
}
+ synchronized boolean isBrowsable() {
+ return mItem.isBrowsable();
+ }
+
synchronized void setCached(boolean cached) {
+ if (DBG) Log.d(TAG, "Set Cache" + cached + "Node" + toString());
mCached = cached;
+ if (!cached) {
+ for (BrowseNode child : mChildren) {
+ mBrowseMap.remove(child.getID());
+ }
+ mChildren.clear();
+ }
}
// Fetch the Unique UID for this item, this is unique across all elements in the tree.
@@ -147,13 +266,19 @@
return Integer.parseInt(getID().replace(PLAYER_PREFIX, ""));
}
+ synchronized byte getScope() {
+ return mBrowseScope;
+ }
+
// Fetch the Folder UID that can be used to fetch folder listing via bluetooth.
// This may not be unique hence this combined with direction will define the
// browsing here.
synchronized String getFolderUID() {
- return mItem.getDescription()
- .getExtras()
- .getString(AvrcpControllerService.MEDIA_ITEM_UID_KEY);
+ return getID();
+ }
+
+ synchronized long getBluetoothID() {
+ return mBluetoothId;
}
synchronized MediaItem getMediaItem() {
@@ -178,52 +303,24 @@
}
@Override
- public String toString() {
+ public synchronized String toString() {
if (VDBG) {
- return "ID: " + getID() + " desc: " + mItem;
+ String serialized = "[ Name: " + mItem.getDescription().getTitle()
+ + " Scope:" + mBrowseScope + " expected Children: "
+ + mExpectedChildrenCount + "] ";
+ for (BrowseNode node : mChildren) {
+ serialized += node.toString();
+ }
+ return serialized;
} else {
return "ID: " + getID();
}
}
- }
- synchronized <E> void refreshChildren(String parentID, List<E> children) {
- BrowseNode parent = findFolderByIDLocked(parentID);
- if (parent == null) {
- Log.w(TAG, "parent not found for parentID " + parentID);
- return;
+ // Returns true if target is a descendant of this.
+ synchronized boolean isDescendant(BrowseNode target) {
+ return getEldestChild(this, target) == null ? false : true;
}
- refreshChildren(parent, children);
- }
-
- synchronized <E> void refreshChildren(BrowseNode parent, List<E> children) {
- if (children == null) {
- Log.e(TAG, "children cannot be null ");
- return;
- }
-
- List<BrowseNode> bnList = new ArrayList<BrowseNode>();
- for (E child : children) {
- if (child instanceof MediaItem) {
- bnList.add(new BrowseNode((MediaItem) child));
- } else if (child instanceof AvrcpPlayer) {
- bnList.add(new BrowseNode((AvrcpPlayer) child));
- }
- }
-
- String parentID = parent.getID();
- // Make sure that the child list is clean.
- if (VDBG) {
- Log.d(TAG, "parent " + parentID + " child list " + parent.getChildren());
- }
-
- addChildrenLocked(parent, bnList);
- List<MediaItem> childrenList = new ArrayList<MediaItem>();
- for (BrowseNode bn : parent.getChildren()) {
- childrenList.add(bn.getMediaItem());
- }
-
- parent.setCached(true);
}
synchronized BrowseNode findBrowseNodeByID(String parentID) {
@@ -233,50 +330,13 @@
return null;
}
if (VDBG) {
- Log.d(TAG, "Browse map: " + mBrowseMap);
+ Log.d(TAG, "Size" + mBrowseMap.size());
}
return bn;
}
- BrowseNode findFolderByIDLocked(String parentID) {
- return mBrowseMap.get(parentID);
- }
-
- void addChildrenLocked(BrowseNode parent, List<BrowseNode> items) {
- // Remove existing children and then add the new children.
- for (BrowseNode c : parent.getChildren()) {
- mBrowseMap.remove(c.getID());
- }
- parent.getChildren().clear();
-
- for (BrowseNode bn : items) {
- parent.getChildren().add(bn);
- mBrowseMap.put(bn.getID(), bn);
- }
- }
-
- synchronized int getDirection(String toUID) {
- BrowseNode fromFolder = mCurrentBrowseNode;
- BrowseNode toFolder = findFolderByIDLocked(toUID);
- if (fromFolder == null || toFolder == null) {
- Log.e(TAG, "from folder " + mCurrentBrowseNode + " or to folder " + toUID + " null!");
- }
-
- // Check the relationship.
- if (fromFolder.isChild(toFolder)) {
- return DIRECTION_DOWN;
- } else if (toFolder.isChild(fromFolder)) {
- return DIRECTION_UP;
- } else if (fromFolder.equals(toFolder)) {
- return DIRECTION_SAME;
- } else {
- Log.w(TAG, "from folder " + mCurrentBrowseNode + "to folder " + toUID);
- return DIRECTION_UNKNOWN;
- }
- }
-
synchronized boolean setCurrentBrowsedFolder(String uid) {
- BrowseNode bn = findFolderByIDLocked(uid);
+ BrowseNode bn = mBrowseMap.get(uid);
if (bn == null) {
Log.e(TAG, "Setting an unknown browsed folder, ignoring bn " + uid);
return false;
@@ -284,10 +344,8 @@
// Set the previous folder as not cached so that we fetch the contents again.
if (!bn.equals(mCurrentBrowseNode)) {
- Log.d(TAG, "Set cache false " + bn + " curr " + mCurrentBrowseNode);
- mCurrentBrowseNode.setCached(false);
+ Log.d(TAG, "Set cache " + bn + " curr " + mCurrentBrowseNode);
}
-
mCurrentBrowseNode = bn;
return true;
}
@@ -296,13 +354,22 @@
return mCurrentBrowseNode;
}
- synchronized boolean setCurrentBrowsedPlayer(String uid) {
- BrowseNode bn = findFolderByIDLocked(uid);
+ synchronized boolean setCurrentBrowsedPlayer(String uid, int items, int depth) {
+ BrowseNode bn = mBrowseMap.get(uid);
if (bn == null) {
Log.e(TAG, "Setting an unknown browsed player, ignoring bn " + uid);
return false;
}
mCurrentBrowsedPlayer = bn;
+ mCurrentBrowseNode = mCurrentBrowsedPlayer;
+ for (Integer level = 0; level < depth; level++) {
+ BrowseNode dummyNode = new BrowseNode(level.toString());
+ dummyNode.mParent = mCurrentBrowseNode;
+ dummyNode.mBrowseScope = AvrcpControllerService.BROWSE_SCOPE_VFS;
+ mCurrentBrowseNode = dummyNode;
+ }
+ mCurrentBrowseNode.setExpectedChildren(items);
+ mDepth = depth;
return true;
}
@@ -311,9 +378,12 @@
}
synchronized boolean setCurrentAddressedPlayer(String uid) {
- BrowseNode bn = findFolderByIDLocked(uid);
+ BrowseNode bn = mBrowseMap.get(uid);
if (bn == null) {
- Log.e(TAG, "Setting an unknown addressed player, ignoring bn " + uid);
+ if (DBG) Log.d(TAG, "Setting an unknown addressed player, ignoring bn " + uid);
+ mRootNode.setCached(false);
+ mRootNode.mChildren.add(mNowPlayingNode);
+ mBrowseMap.put(NOW_PLAYING_PREFIX, mNowPlayingNode);
return false;
}
mCurrentAddressedPlayer = bn;
@@ -326,6 +396,58 @@
@Override
public String toString() {
- return mBrowseMap.toString();
+ String serialized = "Size: " + mBrowseMap.size();
+ if (VDBG) {
+ serialized += mRootNode.toString();
+ }
+ return serialized;
+ }
+
+ // Calculates the path to target node.
+ // Returns: UP node to go up
+ // Returns: target node if there
+ // Returns: named node to go down
+ // Returns: null node if unknown
+ BrowseNode getNextStepToFolder(BrowseNode target) {
+ if (target == null) {
+ return null;
+ } else if (target.equals(mCurrentBrowseNode)
+ || target.equals(mNowPlayingNode)
+ || target.equals(mRootNode)) {
+ return target;
+ } else if (target.isPlayer()) {
+ if (mDepth > 0) {
+ mDepth--;
+ return mNavigateUpNode;
+ } else {
+ return target;
+ }
+ } else if (mBrowseMap.get(target.getID()) == null) {
+ return null;
+ } else {
+ BrowseNode nextChild = getEldestChild(mCurrentBrowseNode, target);
+ if (nextChild == null) {
+ return mNavigateUpNode;
+ } else {
+ return nextChild;
+ }
+ }
+ }
+
+ static BrowseNode getEldestChild(BrowseNode ancestor, BrowseNode target) {
+ // ancestor is an ancestor of target
+ BrowseNode descendant = target;
+ if (DBG) {
+ Log.d(TAG, "NAVIGATING ancestor" + ancestor.toString() + "Target"
+ + target.toString());
+ }
+ while (!ancestor.equals(descendant.mParent)) {
+ descendant = descendant.mParent;
+ if (descendant == null) {
+ return null;
+ }
+ }
+ if (DBG) Log.d(TAG, "NAVIGATING Descendant" + descendant.toString());
+ return descendant;
}
}
diff --git a/src/com/android/bluetooth/avrcpcontroller/PlayerApplicationSettings.java b/src/com/android/bluetooth/avrcpcontroller/PlayerApplicationSettings.java
index c34a2d7..362548e 100644
--- a/src/com/android/bluetooth/avrcpcontroller/PlayerApplicationSettings.java
+++ b/src/com/android/bluetooth/avrcpcontroller/PlayerApplicationSettings.java
@@ -16,12 +16,11 @@
package com.android.bluetooth.avrcpcontroller;
-import android.bluetooth.BluetoothAvrcpPlayerSettings;
+import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
+import android.util.SparseArray;
import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
/*
* Contains information Player Application Setting extended from BluetootAvrcpPlayerSettings
@@ -32,10 +31,10 @@
/*
* Values for SetPlayerApplicationSettings from AVRCP Spec V1.6 Appendix F.
*/
- private static final byte JNI_ATTRIB_EQUALIZER_STATUS = 0x01;
- private static final byte JNI_ATTRIB_REPEAT_STATUS = 0x02;
- private static final byte JNI_ATTRIB_SHUFFLE_STATUS = 0x03;
- private static final byte JNI_ATTRIB_SCAN_STATUS = 0x04;
+ static final byte EQUALIZER_STATUS = 0x01;
+ static final byte REPEAT_STATUS = 0x02;
+ static final byte SHUFFLE_STATUS = 0x03;
+ static final byte SCAN_STATUS = 0x04;
private static final byte JNI_EQUALIZER_STATUS_OFF = 0x01;
private static final byte JNI_EQUALIZER_STATUS_ON = 0x02;
@@ -55,18 +54,17 @@
private static final byte JNI_STATUS_INVALID = -1;
-
/*
* Hash map of current settings.
*/
- private Map<Integer, Integer> mSettings = new HashMap<Integer, Integer>();
+ private SparseArray<Integer> mSettings = new SparseArray<>();
/*
* Hash map of supported values, a setting should be supported by the remote in order to enable
* in mSettings.
*/
- private Map<Integer, ArrayList<Integer>> mSupportedValues =
- new HashMap<Integer, ArrayList<Integer>>();
+ private SparseArray<ArrayList<Integer>> mSupportedValues =
+ new SparseArray<ArrayList<Integer>>();
/* Convert from JNI array to Java classes. */
static PlayerApplicationSettings makeSupportedSettings(byte[] btAvrcpAttributeList) {
@@ -82,8 +80,7 @@
supportedValues.add(
mapAttribIdValtoAvrcpPlayerSetting(attrId, btAvrcpAttributeList[i++]));
}
- newObj.mSupportedValues.put(mapBTAttribIdToAvrcpPlayerSettings(attrId),
- supportedValues);
+ newObj.mSupportedValues.put(attrId, supportedValues);
}
} catch (ArrayIndexOutOfBoundsException exception) {
Log.e(TAG, "makeSupportedSettings attributeList index error.");
@@ -91,25 +88,13 @@
return newObj;
}
- public BluetoothAvrcpPlayerSettings getAvrcpSettings() {
- int supportedSettings = 0;
- for (Integer setting : mSettings.keySet()) {
- supportedSettings |= setting;
- }
- BluetoothAvrcpPlayerSettings result = new BluetoothAvrcpPlayerSettings(supportedSettings);
- for (Integer setting : mSettings.keySet()) {
- result.addSettingValue(setting, mSettings.get(setting));
- }
- return result;
- }
-
static PlayerApplicationSettings makeSettings(byte[] btAvrcpAttributeList) {
PlayerApplicationSettings newObj = new PlayerApplicationSettings();
try {
for (int i = 0; i < btAvrcpAttributeList.length; ) {
byte attrId = btAvrcpAttributeList[i++];
- newObj.mSettings.put(mapBTAttribIdToAvrcpPlayerSettings(attrId),
+ newObj.mSettings.put(attrId,
mapAttribIdValtoAvrcpPlayerSetting(attrId, btAvrcpAttributeList[i++]));
}
} catch (ArrayIndexOutOfBoundsException exception) {
@@ -123,177 +108,69 @@
mSupportedValues = updates.mSupportedValues;
}
- public void setValues(BluetoothAvrcpPlayerSettings updates) {
- int supportedSettings = updates.getSettings();
- for (int i = 1; i <= BluetoothAvrcpPlayerSettings.SETTING_SCAN; i++) {
- if ((i & supportedSettings) > 0) {
- mSettings.put(i, updates.getSettingValue(i));
- }
- }
+ public boolean supportsSetting(int settingType, int settingValue) {
+ if (null == mSupportedValues.get(settingType)) return false;
+ return mSupportedValues.valueAt(settingType).contains(settingValue);
}
- /*
- * Check through all settings to ensure that they are all available to be set and then check
- * that the desired value is in fact supported by our remote player.
- */
- public boolean supportsSettings(BluetoothAvrcpPlayerSettings settingsToCheck) {
- int settingSubset = settingsToCheck.getSettings();
- int supportedSettings = 0;
- for (Integer setting : mSupportedValues.keySet()) {
- supportedSettings |= setting;
- }
- try {
- if ((supportedSettings & settingSubset) == settingSubset) {
- for (Integer settingId : mSettings.keySet()) {
- // The setting is in both settings to check and supported settings but the
- // value is not supported.
- if ((settingId & settingSubset) == settingId && (!mSupportedValues.get(
- settingId).contains(settingsToCheck.getSettingValue(settingId)))) {
- return false;
- }
- }
- return true;
- }
- } catch (NullPointerException e) {
- Log.e(TAG,
- "supportsSettings received a supported setting that has no supported values.");
- }
- return false;
+ public boolean supportsSetting(int settingType) {
+ return (null != mSupportedValues.get(settingType));
}
- // Convert currently desired settings into an attribute array to pass to the native layer to
- // enable them.
- public ArrayList<Byte> getNativeSettings() {
- int i = 0;
- ArrayList<Byte> attribArray = new ArrayList<Byte>();
- for (Integer settingId : mSettings.keySet()) {
- switch (settingId) {
- case BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER:
- attribArray.add(JNI_ATTRIB_EQUALIZER_STATUS);
- attribArray.add(mapAvrcpPlayerSettingstoBTattribVal(settingId,
- mSettings.get(settingId)));
- break;
- case BluetoothAvrcpPlayerSettings.SETTING_REPEAT:
- attribArray.add(JNI_ATTRIB_REPEAT_STATUS);
- attribArray.add(mapAvrcpPlayerSettingstoBTattribVal(settingId,
- mSettings.get(settingId)));
- break;
- case BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE:
- attribArray.add(JNI_ATTRIB_SHUFFLE_STATUS);
- attribArray.add(mapAvrcpPlayerSettingstoBTattribVal(settingId,
- mSettings.get(settingId)));
- break;
- case BluetoothAvrcpPlayerSettings.SETTING_SCAN:
- attribArray.add(JNI_ATTRIB_SCAN_STATUS);
- attribArray.add(mapAvrcpPlayerSettingstoBTattribVal(settingId,
- mSettings.get(settingId)));
- break;
- default:
- Log.w(TAG, "Unknown setting found in getNativeSettings: " + settingId);
- }
- }
- return attribArray;
+ public int getSetting(int settingType) {
+ if (null == mSettings.get(settingType)) return -1;
+ return mSettings.get(settingType);
}
// Convert a native Attribute Id/Value pair into the AVRCP equivalent value.
private static int mapAttribIdValtoAvrcpPlayerSetting(byte attribId, byte attribVal) {
- if (attribId == JNI_ATTRIB_EQUALIZER_STATUS) {
- switch (attribVal) {
- case JNI_EQUALIZER_STATUS_OFF:
- return BluetoothAvrcpPlayerSettings.STATE_OFF;
- case JNI_EQUALIZER_STATUS_ON:
- return BluetoothAvrcpPlayerSettings.STATE_ON;
- }
- } else if (attribId == JNI_ATTRIB_REPEAT_STATUS) {
+ if (attribId == REPEAT_STATUS) {
switch (attribVal) {
case JNI_REPEAT_STATUS_ALL_TRACK_REPEAT:
- return BluetoothAvrcpPlayerSettings.STATE_ALL_TRACK;
+ return PlaybackStateCompat.REPEAT_MODE_ALL;
case JNI_REPEAT_STATUS_GROUP_REPEAT:
- return BluetoothAvrcpPlayerSettings.STATE_GROUP;
+ return PlaybackStateCompat.REPEAT_MODE_GROUP;
case JNI_REPEAT_STATUS_OFF:
- return BluetoothAvrcpPlayerSettings.STATE_OFF;
+ return PlaybackStateCompat.REPEAT_MODE_NONE;
case JNI_REPEAT_STATUS_SINGLE_TRACK_REPEAT:
- return BluetoothAvrcpPlayerSettings.STATE_SINGLE_TRACK;
+ return PlaybackStateCompat.REPEAT_MODE_ONE;
}
- } else if (attribId == JNI_ATTRIB_SCAN_STATUS) {
- switch (attribVal) {
- case JNI_SCAN_STATUS_ALL_TRACK_SCAN:
- return BluetoothAvrcpPlayerSettings.STATE_ALL_TRACK;
- case JNI_SCAN_STATUS_GROUP_SCAN:
- return BluetoothAvrcpPlayerSettings.STATE_GROUP;
- case JNI_SCAN_STATUS_OFF:
- return BluetoothAvrcpPlayerSettings.STATE_OFF;
- }
- } else if (attribId == JNI_ATTRIB_SHUFFLE_STATUS) {
+ } else if (attribId == SHUFFLE_STATUS) {
switch (attribVal) {
case JNI_SHUFFLE_STATUS_ALL_TRACK_SHUFFLE:
- return BluetoothAvrcpPlayerSettings.STATE_ALL_TRACK;
+ return PlaybackStateCompat.SHUFFLE_MODE_ALL;
case JNI_SHUFFLE_STATUS_GROUP_SHUFFLE:
- return BluetoothAvrcpPlayerSettings.STATE_GROUP;
+ return PlaybackStateCompat.SHUFFLE_MODE_GROUP;
case JNI_SHUFFLE_STATUS_OFF:
- return BluetoothAvrcpPlayerSettings.STATE_OFF;
- }
- }
- return BluetoothAvrcpPlayerSettings.STATE_INVALID;
- }
-
- // Convert an AVRCP Setting/Value pair into the native equivalent value;
- private static byte mapAvrcpPlayerSettingstoBTattribVal(int mSetting, int mSettingVal) {
- if (mSetting == BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER) {
- switch (mSettingVal) {
- case BluetoothAvrcpPlayerSettings.STATE_OFF:
- return JNI_EQUALIZER_STATUS_OFF;
- case BluetoothAvrcpPlayerSettings.STATE_ON:
- return JNI_EQUALIZER_STATUS_ON;
- }
- } else if (mSetting == BluetoothAvrcpPlayerSettings.SETTING_REPEAT) {
- switch (mSettingVal) {
- case BluetoothAvrcpPlayerSettings.STATE_OFF:
- return JNI_REPEAT_STATUS_OFF;
- case BluetoothAvrcpPlayerSettings.STATE_SINGLE_TRACK:
- return JNI_REPEAT_STATUS_SINGLE_TRACK_REPEAT;
- case BluetoothAvrcpPlayerSettings.STATE_ALL_TRACK:
- return JNI_REPEAT_STATUS_ALL_TRACK_REPEAT;
- case BluetoothAvrcpPlayerSettings.STATE_GROUP:
- return JNI_REPEAT_STATUS_GROUP_REPEAT;
- }
- } else if (mSetting == BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE) {
- switch (mSettingVal) {
- case BluetoothAvrcpPlayerSettings.STATE_OFF:
- return JNI_SHUFFLE_STATUS_OFF;
- case BluetoothAvrcpPlayerSettings.STATE_ALL_TRACK:
- return JNI_SHUFFLE_STATUS_ALL_TRACK_SHUFFLE;
- case BluetoothAvrcpPlayerSettings.STATE_GROUP:
- return JNI_SHUFFLE_STATUS_GROUP_SHUFFLE;
- }
- } else if (mSetting == BluetoothAvrcpPlayerSettings.SETTING_SCAN) {
- switch (mSettingVal) {
- case BluetoothAvrcpPlayerSettings.STATE_OFF:
- return JNI_SCAN_STATUS_OFF;
- case BluetoothAvrcpPlayerSettings.STATE_ALL_TRACK:
- return JNI_SCAN_STATUS_ALL_TRACK_SCAN;
- case BluetoothAvrcpPlayerSettings.STATE_GROUP:
- return JNI_SCAN_STATUS_GROUP_SCAN;
+ return PlaybackStateCompat.SHUFFLE_MODE_NONE;
}
}
return JNI_STATUS_INVALID;
}
- // convert a native Attribute Id into the AVRCP Setting equivalent value;
- private static int mapBTAttribIdToAvrcpPlayerSettings(byte attribId) {
- switch (attribId) {
- case JNI_ATTRIB_EQUALIZER_STATUS:
- return BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER;
- case JNI_ATTRIB_REPEAT_STATUS:
- return BluetoothAvrcpPlayerSettings.SETTING_REPEAT;
- case JNI_ATTRIB_SHUFFLE_STATUS:
- return BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE;
- case JNI_ATTRIB_SCAN_STATUS:
- return BluetoothAvrcpPlayerSettings.SETTING_SCAN;
- default:
- return BluetoothAvrcpPlayerSettings.STATE_INVALID;
+ // Convert an AVRCP Setting/Value pair into the native equivalent value;
+ static byte mapAvrcpPlayerSettingstoBTattribVal(int mSetting, int mSettingVal) {
+ if (mSetting == REPEAT_STATUS) {
+ switch (mSettingVal) {
+ case PlaybackStateCompat.REPEAT_MODE_NONE:
+ return JNI_REPEAT_STATUS_OFF;
+ case PlaybackStateCompat.REPEAT_MODE_ONE:
+ return JNI_REPEAT_STATUS_SINGLE_TRACK_REPEAT;
+ case PlaybackStateCompat.REPEAT_MODE_ALL:
+ return JNI_REPEAT_STATUS_ALL_TRACK_REPEAT;
+ case PlaybackStateCompat.REPEAT_MODE_GROUP:
+ return JNI_REPEAT_STATUS_GROUP_REPEAT;
+ }
+ } else if (mSetting == SHUFFLE_STATUS) {
+ switch (mSettingVal) {
+ case PlaybackStateCompat.SHUFFLE_MODE_NONE:
+ return JNI_SHUFFLE_STATUS_OFF;
+ case PlaybackStateCompat.SHUFFLE_MODE_ALL:
+ return JNI_SHUFFLE_STATUS_ALL_TRACK_SHUFFLE;
+ case PlaybackStateCompat.SHUFFLE_MODE_GROUP:
+ return JNI_SHUFFLE_STATUS_GROUP_SHUFFLE;
+ }
}
+ return JNI_STATUS_INVALID;
}
-
}
-
diff --git a/src/com/android/bluetooth/avrcpcontroller/RemoteDevice.java b/src/com/android/bluetooth/avrcpcontroller/RemoteDevice.java
deleted file mode 100644
index 7946747..0000000
--- a/src/com/android/bluetooth/avrcpcontroller/RemoteDevice.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.bluetooth.avrcpcontroller;
-
-import android.bluetooth.BluetoothDevice;
-
-import com.android.bluetooth.Utils;
-
-/*
- * Contains information about remote device specifically the player and features enabled on it along
- * with an encapsulation of the current track and playlist information.
- */
-class RemoteDevice {
-
- /*
- * Remote features from JNI
- */
- private static final int FEAT_NONE = 0;
- private static final int FEAT_METADATA = 1;
- private static final int FEAT_ABSOLUTE_VOLUME = 2;
- private static final int FEAT_BROWSE = 4;
-
- private static final int VOLUME_LABEL_UNDEFINED = -1;
-
- final BluetoothDevice mBTDevice;
- private int mRemoteFeatures;
- private boolean mAbsVolNotificationRequested;
- private boolean mFirstAbsVolCmdRecvd;
- private int mNotificationLabel;
-
- RemoteDevice(BluetoothDevice mDevice) {
- mBTDevice = mDevice;
- mRemoteFeatures = FEAT_NONE;
- mAbsVolNotificationRequested = false;
- mNotificationLabel = VOLUME_LABEL_UNDEFINED;
- mFirstAbsVolCmdRecvd = false;
- }
-
- synchronized void setRemoteFeatures(int remoteFeatures) {
- mRemoteFeatures = remoteFeatures;
- }
-
- public synchronized byte[] getBluetoothAddress() {
- return Utils.getByteAddress(mBTDevice);
- }
-
- public synchronized void setNotificationLabel(int label) {
- mNotificationLabel = label;
- }
-
- public synchronized int getNotificationLabel() {
- return mNotificationLabel;
- }
-
- public synchronized void setAbsVolNotificationRequested(boolean request) {
- mAbsVolNotificationRequested = request;
- }
-
- public synchronized boolean getAbsVolNotificationRequested() {
- return mAbsVolNotificationRequested;
- }
-
- public synchronized void setFirstAbsVolCmdRecvd() {
- mFirstAbsVolCmdRecvd = true;
- }
-
- public synchronized boolean getFirstAbsVolCmdRecvd() {
- return mFirstAbsVolCmdRecvd;
- }
-}
diff --git a/src/com/android/bluetooth/avrcpcontroller/StackEvent.java b/src/com/android/bluetooth/avrcpcontroller/StackEvent.java
new file mode 100644
index 0000000..1f96bd2
--- /dev/null
+++ b/src/com/android/bluetooth/avrcpcontroller/StackEvent.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.bluetooth.avrcpcontroller;
+
+final class StackEvent {
+ // Event types for STACK_EVENT message
+ static final int EVENT_TYPE_NONE = 0;
+ static final int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1;
+ static final int EVENT_TYPE_RC_FEATURES = 2;
+
+ int mType = EVENT_TYPE_NONE;
+ boolean mRemoteControlConnected;
+ boolean mBrowsingConnected;
+ int mFeatures;
+
+ private StackEvent(int type) {
+ this.mType = type;
+ }
+
+ @Override
+ public String toString() {
+ switch (mType) {
+ case EVENT_TYPE_CONNECTION_STATE_CHANGED:
+ return "EVENT_TYPE_CONNECTION_STATE_CHANGED " + mRemoteControlConnected;
+ case EVENT_TYPE_RC_FEATURES:
+ return "EVENT_TYPE_RC_FEATURES";
+ default:
+ return "Unknown";
+ }
+ }
+
+ static StackEvent connectionStateChanged(boolean remoteControlConnected,
+ boolean browsingConnected) {
+ StackEvent event = new StackEvent(EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ event.mRemoteControlConnected = remoteControlConnected;
+ event.mBrowsingConnected = browsingConnected;
+ return event;
+ }
+
+ static StackEvent rcFeatures(int features) {
+ StackEvent event = new StackEvent(EVENT_TYPE_RC_FEATURES);
+ event.mFeatures = features;
+ return event;
+ }
+}
diff --git a/src/com/android/bluetooth/avrcpcontroller/TrackInfo.java b/src/com/android/bluetooth/avrcpcontroller/TrackInfo.java
index e89fb4c..fd1b784 100644
--- a/src/com/android/bluetooth/avrcpcontroller/TrackInfo.java
+++ b/src/com/android/bluetooth/avrcpcontroller/TrackInfo.java
@@ -14,34 +14,11 @@
* limitations under the License.
*/
-
package com.android.bluetooth.avrcpcontroller;
import android.media.MediaMetadata;
-import android.util.Log;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/*
- * Contains information about tracks that either currently playing or maintained in playlist
- * This is used as a local repository for information that will be passed on as MediaMetadata to the
- * MediaSessionServicve
- */
-class TrackInfo {
- private static final String TAG = "AvrcpTrackInfo";
- private static final boolean VDBG = false;
-
- /*
- * Default values for each of the items from JNI
- */
- private static final int TRACK_NUM_INVALID = -1;
- private static final int TOTAL_TRACKS_INVALID = -1;
- private static final int TOTAL_TRACK_TIME_INVALID = -1;
- private static final String UNPOPULATED_ATTRIBUTE = "";
-
+final class TrackInfo {
/*
*Element Id Values for GetMetaData from JNI
*/
@@ -53,95 +30,49 @@
private static final int MEDIA_ATTRIBUTE_GENRE = 0x06;
private static final int MEDIA_ATTRIBUTE_PLAYING_TIME = 0x07;
-
- private final String mArtistName;
- private final String mTrackTitle;
- private final String mAlbumTitle;
- private final String mGenre;
- private final long mTrackNum; // number of audio file on original recording.
- private final long mTotalTracks; // total number of tracks on original recording
- private final long mTrackLen; // full length of AudioFile.
-
- TrackInfo() {
- this(new ArrayList<Integer>(), new ArrayList<String>());
- }
-
- TrackInfo(List<Integer> attrIds, List<String> attrMap) {
- Map<Integer, String> attributeMap = new HashMap<>();
- for (int i = 0; i < attrIds.size(); i++) {
- attributeMap.put(attrIds.get(i), attrMap.get(i));
+ static MediaMetadata getMetadata(int[] attrIds, String[] attrMap) {
+ MediaMetadata.Builder metaDataBuilder = new MediaMetadata.Builder();
+ int attributeCount = Math.max(attrIds.length, attrMap.length);
+ for (int i = 0; i < attributeCount; i++) {
+ switch (attrIds[i]) {
+ case MEDIA_ATTRIBUTE_TITLE:
+ metaDataBuilder.putString(MediaMetadata.METADATA_KEY_TITLE, attrMap[i]);
+ break;
+ case MEDIA_ATTRIBUTE_ARTIST_NAME:
+ metaDataBuilder.putString(MediaMetadata.METADATA_KEY_ARTIST, attrMap[i]);
+ break;
+ case MEDIA_ATTRIBUTE_ALBUM_NAME:
+ metaDataBuilder.putString(MediaMetadata.METADATA_KEY_ALBUM, attrMap[i]);
+ break;
+ case MEDIA_ATTRIBUTE_TRACK_NUMBER:
+ try {
+ metaDataBuilder.putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER,
+ Long.valueOf(attrMap[i]));
+ } catch (java.lang.NumberFormatException e) {
+ // If Track Number doesn't parse, leave it unset
+ }
+ break;
+ case MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER:
+ try {
+ metaDataBuilder.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS,
+ Long.valueOf(attrMap[i]));
+ } catch (java.lang.NumberFormatException e) {
+ // If Total Track Number doesn't parse, leave it unset
+ }
+ break;
+ case MEDIA_ATTRIBUTE_GENRE:
+ metaDataBuilder.putString(MediaMetadata.METADATA_KEY_GENRE, attrMap[i]);
+ break;
+ case MEDIA_ATTRIBUTE_PLAYING_TIME:
+ try {
+ metaDataBuilder.putLong(MediaMetadata.METADATA_KEY_DURATION,
+ Long.valueOf(attrMap[i]));
+ } catch (java.lang.NumberFormatException e) {
+ // If Playing Time doesn't parse, leave it unset
+ }
+ break;
+ }
}
-
- String attribute;
- mTrackTitle = attributeMap.getOrDefault(MEDIA_ATTRIBUTE_TITLE, UNPOPULATED_ATTRIBUTE);
-
- mArtistName = attributeMap.getOrDefault(MEDIA_ATTRIBUTE_ARTIST_NAME, UNPOPULATED_ATTRIBUTE);
-
- mAlbumTitle = attributeMap.getOrDefault(MEDIA_ATTRIBUTE_ALBUM_NAME, UNPOPULATED_ATTRIBUTE);
-
- attribute = attributeMap.get(MEDIA_ATTRIBUTE_TRACK_NUMBER);
- mTrackNum = (attribute != null && !attribute.isEmpty()) ? Long.valueOf(attribute)
- : TRACK_NUM_INVALID;
-
- attribute = attributeMap.get(MEDIA_ATTRIBUTE_TOTAL_TRACK_NUMBER);
- mTotalTracks = (attribute != null && !attribute.isEmpty()) ? Long.valueOf(attribute)
- : TOTAL_TRACKS_INVALID;
-
- mGenre = attributeMap.getOrDefault(MEDIA_ATTRIBUTE_GENRE, UNPOPULATED_ATTRIBUTE);
-
- attribute = attributeMap.get(MEDIA_ATTRIBUTE_PLAYING_TIME);
- mTrackLen = (attribute != null && !attribute.isEmpty()) ? Long.valueOf(attribute)
- : TOTAL_TRACK_TIME_INVALID;
- }
-
- @Override
- public String toString() {
- return "Metadata [artist=" + mArtistName + " trackTitle= " + mTrackTitle + " albumTitle= "
- + mAlbumTitle + " genre= " + mGenre + " trackNum= " + Long.toString(mTrackNum)
- + " track_len : " + Long.toString(mTrackLen) + " TotalTracks " + Long.toString(
- mTotalTracks) + "]";
- }
-
- public MediaMetadata getMediaMetaData() {
- if (VDBG) {
- Log.d(TAG, " TrackInfo " + toString());
- }
- MediaMetadata.Builder mMetaDataBuilder = new MediaMetadata.Builder();
- mMetaDataBuilder.putString(MediaMetadata.METADATA_KEY_ARTIST, mArtistName);
- mMetaDataBuilder.putString(MediaMetadata.METADATA_KEY_TITLE, mTrackTitle);
- mMetaDataBuilder.putString(MediaMetadata.METADATA_KEY_ALBUM, mAlbumTitle);
- mMetaDataBuilder.putString(MediaMetadata.METADATA_KEY_GENRE, mGenre);
- mMetaDataBuilder.putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, mTrackNum);
- mMetaDataBuilder.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, mTotalTracks);
- mMetaDataBuilder.putLong(MediaMetadata.METADATA_KEY_DURATION, mTrackLen);
- return mMetaDataBuilder.build();
- }
-
-
- public String displayMetaData() {
- MediaMetadata metaData = getMediaMetaData();
- StringBuffer sb = new StringBuffer();
- /* getDescription only contains artist, title and album */
- sb.append(metaData.getDescription().toString() + " ");
- if (metaData.containsKey(MediaMetadata.METADATA_KEY_GENRE)) {
- sb.append(metaData.getString(MediaMetadata.METADATA_KEY_GENRE) + " ");
- }
- if (metaData.containsKey(MediaMetadata.METADATA_KEY_MEDIA_ID)) {
- sb.append(metaData.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) + " ");
- }
- if (metaData.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) {
- sb.append(
- Long.toString(metaData.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) + " ");
- }
- if (metaData.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) {
- sb.append(Long.toString(metaData.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS)) + " ");
- }
- if (metaData.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) {
- sb.append(Long.toString(metaData.getLong(MediaMetadata.METADATA_KEY_DURATION)) + " ");
- }
- if (metaData.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) {
- sb.append(Long.toString(metaData.getLong(MediaMetadata.METADATA_KEY_DURATION)) + " ");
- }
- return sb.toString();
+ return metaDataBuilder.build();
}
}
diff --git a/src/com/android/bluetooth/btservice/AbstractionLayer.java b/src/com/android/bluetooth/btservice/AbstractionLayer.java
index 6a2e636..e15104d 100644
--- a/src/com/android/bluetooth/btservice/AbstractionLayer.java
+++ b/src/com/android/bluetooth/btservice/AbstractionLayer.java
@@ -52,6 +52,9 @@
public static final int BT_DEVICE_TYPE_BLE = 0x02;
public static final int BT_DEVICE_TYPE_DUAL = 0x03;
+ static final int BT_PROPERTY_LOCAL_IO_CAPS = 0x0e;
+ static final int BT_PROPERTY_LOCAL_IO_CAPS_BLE = 0x0f;
+
static final int BT_BOND_STATE_NONE = 0x00;
static final int BT_BOND_STATE_BONDING = 0x01;
static final int BT_BOND_STATE_BONDED = 0x02;
diff --git a/src/com/android/bluetooth/btservice/ActiveDeviceManager.java b/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
index e1f999d..69381c5 100644
--- a/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
+++ b/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
@@ -33,12 +33,12 @@
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.hearingaid.HearingAidService;
import com.android.bluetooth.hfp.HeadsetService;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.LinkedList;
import java.util.List;
diff --git a/src/com/android/bluetooth/btservice/AdapterProperties.java b/src/com/android/bluetooth/btservice/AdapterProperties.java
index 981a45a..021f0a9 100644
--- a/src/com/android/bluetooth/btservice/AdapterProperties.java
+++ b/src/com/android/bluetooth/btservice/AdapterProperties.java
@@ -24,6 +24,7 @@
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothHidDevice;
import android.bluetooth.BluetoothHidHost;
import android.bluetooth.BluetoothMap;
@@ -41,11 +42,12 @@
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings.Secure;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.Pair;
import android.util.StatsLog;
+import androidx.annotation.VisibleForTesting;
+
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.RemoteDevices.DeviceProperties;
@@ -77,6 +79,9 @@
private volatile int mScanMode;
private volatile int mDiscoverableTimeout;
private volatile ParcelUuid[] mUuids;
+ private volatile int mLocalIOCapability = BluetoothAdapter.IO_CAPABILITY_UNKNOWN;
+ private volatile int mLocalIOCapabilityBLE = BluetoothAdapter.IO_CAPABILITY_UNKNOWN;
+
private CopyOnWriteArrayList<BluetoothDevice> mBondedDevices =
new CopyOnWriteArrayList<BluetoothDevice>();
@@ -130,6 +135,9 @@
case BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED:
sendConnectionStateChange(BluetoothProfile.HEADSET_CLIENT, intent);
break;
+ case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED:
+ sendConnectionStateChange(BluetoothProfile.HEARING_AID, intent);
+ break;
case BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED:
sendConnectionStateChange(BluetoothProfile.A2DP_SINK, intent);
break;
@@ -203,6 +211,7 @@
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+ filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED);
@@ -287,6 +296,45 @@
}
}
+ boolean setIoCapability(int capability) {
+ synchronized (mObject) {
+ boolean result = mService.setAdapterPropertyNative(
+ AbstractionLayer.BT_PROPERTY_LOCAL_IO_CAPS, Utils.intToByteArray(capability));
+
+ if (result) {
+ mLocalIOCapability = capability;
+ }
+
+ return result;
+ }
+ }
+
+ int getIoCapability() {
+ synchronized (mObject) {
+ return mLocalIOCapability;
+ }
+ }
+
+ boolean setLeIoCapability(int capability) {
+ synchronized (mObject) {
+ boolean result = mService.setAdapterPropertyNative(
+ AbstractionLayer.BT_PROPERTY_LOCAL_IO_CAPS_BLE,
+ Utils.intToByteArray(capability));
+
+ if (result) {
+ mLocalIOCapabilityBLE = capability;
+ }
+
+ return result;
+ }
+ }
+
+ int getLeIoCapability() {
+ synchronized (mObject) {
+ return mLocalIOCapabilityBLE;
+ }
+ }
+
/**
* @return the mScanMode
*/
@@ -533,11 +581,9 @@
Log.d(TAG,
"PROFILE_CONNECTION_STATE_CHANGE: profile=" + profile + ", device=" + device + ", "
+ prevState + " -> " + state);
- String ssaid = Secure.getString(mService.getContentResolver(), Secure.ANDROID_ID);
- String combined = ssaid + device.getAddress();
- int obfuscated_id = combined.hashCode() & 0xFFFF; // Last two bytes only
- StatsLog.write(StatsLog.BLUETOOTH_CONNECTION_STATE_CHANGED,
- state, obfuscated_id, profile);
+ StatsLog.write(StatsLog.BLUETOOTH_CONNECTION_STATE_CHANGED, state, 0 /* deprecated */,
+ profile, mService.obfuscateAddress(device));
+
if (!isNormalStateTransition(prevState, state)) {
Log.w(TAG,
"PROFILE_CONNECTION_STATE_CHANGE: unexpected transition for profile=" + profile
@@ -794,6 +840,16 @@
updateFeatureSupport(val);
break;
+ case AbstractionLayer.BT_PROPERTY_LOCAL_IO_CAPS:
+ mLocalIOCapability = Utils.byteArrayToInt(val);
+ debugLog("mLocalIOCapability set to " + mLocalIOCapability);
+ break;
+
+ case AbstractionLayer.BT_PROPERTY_LOCAL_IO_CAPS_BLE:
+ mLocalIOCapabilityBLE = Utils.byteArrayToInt(val);
+ debugLog("mLocalIOCapabilityBLE set to " + mLocalIOCapabilityBLE);
+ break;
+
default:
errorLog("Property change not handled in Java land:" + type);
}
@@ -876,6 +932,7 @@
Intent intent;
if (state == AbstractionLayer.BT_DISCOVERY_STOPPED) {
mDiscovering = false;
+ mService.clearDiscoveringPackages();
mDiscoveryEndMs = System.currentTimeMillis();
intent = new Intent(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
mService.sendBroadcast(intent, AdapterService.BLUETOOTH_PERM);
diff --git a/src/com/android/bluetooth/btservice/AdapterService.java b/src/com/android/bluetooth/btservice/AdapterService.java
index 9d8cde9..c671efd 100644
--- a/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/src/com/android/bluetooth/btservice/AdapterService.java
@@ -18,6 +18,7 @@
import android.app.ActivityManager;
import android.app.AlarmManager;
+import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothActivityEnergyInfo;
@@ -25,8 +26,10 @@
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProtoEnums;
import android.bluetooth.IBluetooth;
import android.bluetooth.IBluetoothCallback;
+import android.bluetooth.IBluetoothMetadataListener;
import android.bluetooth.IBluetoothSocketManager;
import android.bluetooth.OobData;
import android.bluetooth.UidTraffic;
@@ -66,6 +69,8 @@
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.RemoteDevices.DeviceProperties;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
+import com.android.bluetooth.btservice.storage.MetadataDatabase;
import com.android.bluetooth.gatt.GattService;
import com.android.bluetooth.sdp.SdpManager;
import com.android.internal.R;
@@ -129,6 +134,8 @@
private static final int CONTROLLER_ENERGY_UPDATE_TIMEOUT_MILLIS = 30;
+ private final ArrayList<DiscoveringPackage> mDiscoveringPackages = new ArrayList<>();
+
static {
classInitNative();
}
@@ -165,6 +172,8 @@
private boolean mNativeAvailable;
private boolean mCleaningUp;
+ private final HashMap<BluetoothDevice, ArrayList<IBluetoothMetadataListener>>
+ mMetadataListeners = new HashMap<>();
private final HashMap<String, Integer> mProfileServicesState = new HashMap<String, Integer>();
//Only BluetoothManagerService should be registered
private RemoteCallbackList<IBluetoothCallback> mCallbacks;
@@ -182,6 +191,9 @@
private ProfileObserver mProfileObserver;
private PhonePolicy mPhonePolicy;
private ActiveDeviceManager mActiveDeviceManager;
+ private DatabaseManager mDatabaseManager;
+ private SilenceDeviceManager mSilenceDeviceManager;
+ private AppOpsManager mAppOps;
/**
* Register a {@link ProfileService} with AdapterService.
@@ -224,19 +236,19 @@
class AdapterServiceHandler extends Handler {
@Override
public void handleMessage(Message msg) {
- debugLog("handleMessage() - Message: " + msg.what);
+ verboseLog("handleMessage() - Message: " + msg.what);
switch (msg.what) {
case MESSAGE_PROFILE_SERVICE_STATE_CHANGED:
- debugLog("handleMessage() - MESSAGE_PROFILE_SERVICE_STATE_CHANGED");
+ verboseLog("handleMessage() - MESSAGE_PROFILE_SERVICE_STATE_CHANGED");
processProfileServiceStateChanged((ProfileService) msg.obj, msg.arg1);
break;
case MESSAGE_PROFILE_SERVICE_REGISTERED:
- debugLog("handleMessage() - MESSAGE_PROFILE_SERVICE_REGISTERED");
+ verboseLog("handleMessage() - MESSAGE_PROFILE_SERVICE_REGISTERED");
registerProfileService((ProfileService) msg.obj);
break;
case MESSAGE_PROFILE_SERVICE_UNREGISTERED:
- debugLog("handleMessage() - MESSAGE_PROFILE_SERVICE_UNREGISTERED");
+ verboseLog("handleMessage() - MESSAGE_PROFILE_SERVICE_UNREGISTERED");
unregisterProfileService((ProfileService) msg.obj);
break;
}
@@ -252,7 +264,7 @@
private void unregisterProfileService(ProfileService profile) {
if (!mRegisteredProfiles.contains(profile)) {
- Log.e(TAG, profile.getName() + " not registered (UNREGISTERED).");
+ Log.e(TAG, profile.getName() + " not registered (UNREGISTER).");
return;
}
mRegisteredProfiles.remove(profile);
@@ -277,6 +289,8 @@
mAdapterProperties.onBluetoothReady();
updateUuids();
setBluetoothClassFromConfig();
+ getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_LOCAL_IO_CAPS);
+ getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_LOCAL_IO_CAPS_BLE);
mAdapterStateMachine.sendMessage(AdapterState.BREDR_STARTED);
}
break;
@@ -296,7 +310,6 @@
mAdapterStateMachine.sendMessage(AdapterState.BREDR_STOPPED);
} else if (mRunningProfiles.size() == 0) {
disableNative();
- mAdapterStateMachine.sendMessage(AdapterState.BLE_STOPPED);
}
break;
default:
@@ -372,6 +385,7 @@
debugLog("onCreate()");
mRemoteDevices = new RemoteDevices(this, Looper.getMainLooper());
mRemoteDevices.init();
+ clearDiscoveringPackages();
mBinder = new AdapterServiceBinder(this);
mAdapterProperties = new AdapterProperties(this);
mAdapterStateMachine = AdapterState.make(this);
@@ -379,6 +393,7 @@
initNative();
mNativeAvailable = true;
mCallbacks = new RemoteCallbackList<IBluetoothCallback>();
+ mAppOps = getSystemService(AppOpsManager.class);
//Load the name and address
getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_BDADDR);
getAdapterPropertyNative(AbstractionLayer.BT_PROPERTY_BDNAME);
@@ -407,6 +422,13 @@
mActiveDeviceManager = new ActiveDeviceManager(this, new ServiceFactory());
mActiveDeviceManager.start();
+ mDatabaseManager = new DatabaseManager(this);
+ mDatabaseManager.start(MetadataDatabase.createDatabase(this));
+
+ mSilenceDeviceManager = new SilenceDeviceManager(this, new ServiceFactory(),
+ Looper.getMainLooper());
+ mSilenceDeviceManager.start();
+
setAdapterService(this);
// First call to getSharedPreferences will result in a file read into
@@ -522,6 +544,7 @@
void stateChangeCallback(int status) {
if (status == AbstractionLayer.BT_STATE_OFF) {
debugLog("stateChangeCallback: disableNative() completed");
+ mAdapterStateMachine.sendMessage(AdapterState.BLE_STOPPED);
} else if (status == AbstractionLayer.BT_STATE_ON) {
mAdapterStateMachine.sendMessage(AdapterState.BLE_STARTED);
} else {
@@ -648,6 +671,10 @@
}
}
+ if (mDatabaseManager != null) {
+ mDatabaseManager.cleanup();
+ }
+
if (mAdapterStateMachine != null) {
mAdapterStateMachine.doQuit();
}
@@ -683,6 +710,10 @@
mPhonePolicy.cleanup();
}
+ if (mSilenceDeviceManager != null) {
+ mSilenceDeviceManager.cleanup();
+ }
+
if (mActiveDeviceManager != null) {
mActiveDeviceManager.cleanup();
}
@@ -873,6 +904,7 @@
return service.setName(name);
}
+ @Override
public BluetoothClass getBluetoothClass() {
if (!Utils.checkCaller()) {
Log.w(TAG, "getBluetoothClass() - Not allowed for non-active user");
@@ -884,6 +916,7 @@
return service.getBluetoothClass();
}
+ @Override
public boolean setBluetoothClass(BluetoothClass bluetoothClass) {
if (!Utils.checkCaller()) {
Log.w(TAG, "setBluetoothClass() - Not allowed for non-active user");
@@ -898,6 +931,54 @@
}
@Override
+ public int getIoCapability() {
+ if (!Utils.checkCaller()) {
+ Log.w(TAG, "setBluetoothClass() - Not allowed for non-active user");
+ return BluetoothAdapter.IO_CAPABILITY_UNKNOWN;
+ }
+
+ AdapterService service = getService();
+ if (service == null) return BluetoothAdapter.IO_CAPABILITY_UNKNOWN;
+ return service.getIoCapability();
+ }
+
+ @Override
+ public boolean setIoCapability(int capability) {
+ if (!Utils.checkCaller()) {
+ Log.w(TAG, "setBluetoothClass() - Not allowed for non-active user");
+ return false;
+ }
+
+ AdapterService service = getService();
+ if (service == null) return false;
+ return service.setIoCapability(capability);
+ }
+
+ @Override
+ public int getLeIoCapability() {
+ if (!Utils.checkCaller()) {
+ Log.w(TAG, "setBluetoothClass() - Not allowed for non-active user");
+ return BluetoothAdapter.IO_CAPABILITY_UNKNOWN;
+ }
+
+ AdapterService service = getService();
+ if (service == null) return BluetoothAdapter.IO_CAPABILITY_UNKNOWN;
+ return service.getLeIoCapability();
+ }
+
+ @Override
+ public boolean setLeIoCapability(int capability) {
+ if (!Utils.checkCaller()) {
+ Log.w(TAG, "setBluetoothClass() - Not allowed for non-active user");
+ return false;
+ }
+
+ AdapterService service = getService();
+ if (service == null) return false;
+ return service.setLeIoCapability(capability);
+ }
+
+ @Override
public int getScanMode() {
if (!Utils.checkCallerAllowManagedProfiles(mService)) {
Log.w(TAG, "getScanMode() - Not allowed for non-active user");
@@ -954,7 +1035,7 @@
}
@Override
- public boolean startDiscovery() {
+ public boolean startDiscovery(String callingPackage) {
if (!Utils.checkCaller()) {
Log.w(TAG, "startDiscovery() - Not allowed for non-active user");
return false;
@@ -964,7 +1045,7 @@
if (service == null) {
return false;
}
- return service.startDiscovery();
+ return service.startDiscovery(callingPackage);
}
@Override
@@ -1293,6 +1374,34 @@
}
@Override
+ public boolean setSilenceMode(BluetoothDevice device, boolean silence) {
+ if (!Utils.checkCaller()) {
+ Log.w(TAG, "setSilenceMode() - Not allowed for non-active user");
+ return false;
+ }
+
+ AdapterService service = getService();
+ if (service == null) {
+ return false;
+ }
+ return service.setSilenceMode(device, silence);
+ }
+
+ @Override
+ public boolean getSilenceMode(BluetoothDevice device) {
+ if (!Utils.checkCaller()) {
+ Log.w(TAG, "getSilenceMode() - Not allowed for non-active user");
+ return false;
+ }
+
+ AdapterService service = getService();
+ if (service == null) {
+ return false;
+ }
+ return service.getSilenceMode(device);
+ }
+
+ @Override
public boolean setPhonebookAccessPermission(BluetoothDevice device, int value) {
if (!Utils.checkCaller()) {
Log.w(TAG, "setPhonebookAccessPermission() - Not allowed for non-active user");
@@ -1551,6 +1660,43 @@
}
@Override
+ public boolean registerMetadataListener(IBluetoothMetadataListener listener,
+ BluetoothDevice device) {
+ AdapterService service = getService();
+ if (service == null) {
+ return false;
+ }
+ return service.registerMetadataListener(listener, device);
+ }
+
+ @Override
+ public boolean unregisterMetadataListener(BluetoothDevice device) {
+ AdapterService service = getService();
+ if (service == null) {
+ return false;
+ }
+ return service.unregisterMetadataListener(device);
+ }
+
+ @Override
+ public boolean setMetadata(BluetoothDevice device, int key, byte[] value) {
+ AdapterService service = getService();
+ if (service == null) {
+ return false;
+ }
+ return service.setMetadata(device, key, value);
+ }
+
+ @Override
+ public byte[] getMetadata(BluetoothDevice device, int key) {
+ AdapterService service = getService();
+ if (service == null) {
+ return null;
+ }
+ return service.getMetadata(device, key);
+ }
+
+ @Override
public void requestActivityInfo(ResultReceiver result) {
Bundle bundle = new Bundle();
bundle.putParcelable(BatteryStats.RESULT_RECEIVER_CONTROLLER_KEY, reportActivityInfo());
@@ -1690,6 +1836,47 @@
return result && storeBluetoothClassConfig(bluetoothClass.getClassOfDevice());
}
+ private boolean validateInputOutputCapability(int capability) {
+ if (capability < 0 || capability >= BluetoothAdapter.IO_CAPABILITY_MAX) {
+ Log.e(TAG, "Invalid IO capability value - " + capability);
+ return false;
+ }
+
+ return true;
+ }
+
+ int getIoCapability() {
+ enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
+
+ return mAdapterProperties.getIoCapability();
+ }
+
+ boolean setIoCapability(int capability) {
+ enforceCallingOrSelfPermission(
+ BLUETOOTH_PRIVILEGED, "Need BLUETOOTH PRIVILEGED permission");
+ if (!validateInputOutputCapability(capability)) {
+ return false;
+ }
+
+ return mAdapterProperties.setIoCapability(capability);
+ }
+
+ int getLeIoCapability() {
+ enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
+
+ return mAdapterProperties.getLeIoCapability();
+ }
+
+ boolean setLeIoCapability(int capability) {
+ enforceCallingOrSelfPermission(
+ BLUETOOTH_PRIVILEGED, "Need BLUETOOTH PRIVILEGED permission");
+ if (!validateInputOutputCapability(capability)) {
+ return false;
+ }
+
+ return mAdapterProperties.setLeIoCapability(capability);
+ }
+
int getScanMode() {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
@@ -1717,10 +1904,42 @@
return mAdapterProperties.setDiscoverableTimeout(timeout);
}
- boolean startDiscovery() {
+ ArrayList<DiscoveringPackage> getDiscoveringPackages() {
+ return mDiscoveringPackages;
+ }
+
+ void clearDiscoveringPackages() {
+ synchronized (mDiscoveringPackages) {
+ mDiscoveringPackages.clear();
+ }
+ }
+
+ boolean startDiscovery(String callingPackage) {
+ UserHandle callingUser = UserHandle.of(UserHandle.getCallingUserId());
debugLog("startDiscovery");
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
+ mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
+ boolean isQApp = Utils.isQApp(this, callingPackage);
+ String permission = null;
+ if (Utils.checkCallerHasNetworkSettingsPermission(this)) {
+ permission = android.Manifest.permission.NETWORK_SETTINGS;
+ } else if (Utils.checkCallerHasNetworkSetupWizardPermission(this)) {
+ permission = android.Manifest.permission.NETWORK_SETUP_WIZARD;
+ } else if (isQApp) {
+ if (!Utils.checkCallerHasFineLocation(this, mAppOps, callingPackage, callingUser)) {
+ return false;
+ }
+ permission = android.Manifest.permission.ACCESS_FINE_LOCATION;
+ } else {
+ if (!Utils.checkCallerHasCoarseLocation(this, mAppOps, callingPackage, callingUser)) {
+ return false;
+ }
+ permission = android.Manifest.permission.ACCESS_COARSE_LOCATION;
+ }
+ synchronized (mDiscoveringPackages) {
+ mDiscoveringPackages.add(new DiscoveringPackage(callingPackage, permission));
+ }
return startDiscoveryNative();
}
@@ -1753,6 +1972,16 @@
return mAdapterProperties.getBondedDevices();
}
+ /**
+ * Get the database manager to access Bluetooth storage
+ *
+ * @return {@link DatabaseManager} or null on error
+ */
+ @VisibleForTesting
+ public DatabaseManager getDatabase() {
+ return mDatabaseManager;
+ }
+
int getAdapterConnectionState() {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
return mAdapterProperties.getConnectionState();
@@ -1989,6 +2218,11 @@
return false;
}
+ StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
+ obfuscateAddress(device), 0, device.getType(),
+ BluetoothDevice.BOND_BONDING,
+ BluetoothProtoEnums.BOND_SUB_STATE_LOCAL_PIN_REPLIED,
+ accept ? 0 : BluetoothDevice.UNBOND_REASON_AUTH_REJECTED);
byte[] addr = Utils.getBytesFromAddress(device.getAddress());
return pinReplyNative(addr, accept, len, pinCode);
}
@@ -2000,6 +2234,11 @@
return false;
}
+ StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
+ obfuscateAddress(device), 0, device.getType(),
+ BluetoothDevice.BOND_BONDING,
+ BluetoothProtoEnums.BOND_SUB_STATE_LOCAL_SSP_REPLIED,
+ accept ? 0 : BluetoothDevice.UNBOND_REASON_AUTH_REJECTED);
byte[] addr = Utils.getBytesFromAddress(device.getAddress());
return sspReplyNative(addr, AbstractionLayer.BT_SSP_VARIANT_PASSKEY_ENTRY, accept,
Utils.byteArrayToInt(passkey));
@@ -2013,6 +2252,11 @@
return false;
}
+ StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
+ obfuscateAddress(device), 0, device.getType(),
+ BluetoothDevice.BOND_BONDING,
+ BluetoothProtoEnums.BOND_SUB_STATE_LOCAL_SSP_REPLIED,
+ accept ? 0 : BluetoothDevice.UNBOND_REASON_AUTH_REJECTED);
byte[] addr = Utils.getBytesFromAddress(device.getAddress());
return sspReplyNative(addr, AbstractionLayer.BT_SSP_VARIANT_PASSKEY_CONFIRMATION, accept,
0);
@@ -2029,9 +2273,20 @@
: BluetoothDevice.ACCESS_REJECTED;
}
- boolean setPhonebookAccessPermission(BluetoothDevice device, int value) {
+ boolean setSilenceMode(BluetoothDevice device, boolean silence) {
enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
"Need BLUETOOTH PRIVILEGED permission");
+ mSilenceDeviceManager.setSilenceMode(device, silence);
+ return true;
+ }
+
+ boolean getSilenceMode(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH PRIVILEGED permission");
+ return mSilenceDeviceManager.getSilenceMode(device);
+ }
+
+ boolean setPhonebookAccessPermission(BluetoothDevice device, int value) {
SharedPreferences pref = getSharedPreferences(PHONEBOOK_ACCESS_PERMISSION_PREFERENCE_FILE,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = pref.edit();
@@ -2118,6 +2373,9 @@
boolean factoryReset() {
enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, "Need BLUETOOTH permission");
+ if (mDatabaseManager != null) {
+ mDatabaseManager.factoryReset();
+ }
return factoryResetNative();
}
@@ -2417,6 +2675,64 @@
+ ctrlState + "traffic = " + Arrays.toString(data));
}
+ boolean registerMetadataListener(IBluetoothMetadataListener listener,
+ BluetoothDevice device) {
+ if (mMetadataListeners == null) {
+ return false;
+ }
+
+ ArrayList<IBluetoothMetadataListener> list = mMetadataListeners.get(device);
+ if (list == null) {
+ list = new ArrayList<>();
+ } else if (list.contains(listener)) {
+ // The device is already registered with this listener
+ return true;
+ }
+ list.add(listener);
+ mMetadataListeners.put(device, list);
+ return true;
+ }
+
+ boolean unregisterMetadataListener(BluetoothDevice device) {
+ if (mMetadataListeners == null) {
+ return false;
+ }
+ if (mMetadataListeners.containsKey(device)) {
+ mMetadataListeners.remove(device);
+ }
+ return true;
+ }
+
+ boolean setMetadata(BluetoothDevice device, int key, byte[] value) {
+ if (value.length > BluetoothDevice.METADATA_MAX_LENGTH) {
+ Log.e(TAG, "setMetadata: value length too long " + value.length);
+ return false;
+ }
+ return mDatabaseManager.setCustomMeta(device, key, value);
+ }
+
+ byte[] getMetadata(BluetoothDevice device, int key) {
+ return mDatabaseManager.getCustomMeta(device, key);
+ }
+
+ /**
+ * Update metadata change to registered listeners
+ */
+ @VisibleForTesting
+ public void metadataChanged(String address, int key, byte[] value) {
+ BluetoothDevice device = mRemoteDevices.getDevice(Utils.getBytesFromAddress(address));
+ if (mMetadataListeners.containsKey(device)) {
+ ArrayList<IBluetoothMetadataListener> list = mMetadataListeners.get(device);
+ for (IBluetoothMetadataListener listener : list) {
+ try {
+ listener.onMetadataChanged(device, key, value);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException when onMetadataChanged");
+ }
+ }
+ }
+ }
+
private int getIdleCurrentMa() {
return getResources().getInteger(R.integer.config_bluetooth_idle_cur_ma);
}
@@ -2460,6 +2776,7 @@
for (ProfileService profile : mRegisteredProfiles) {
profile.dump(sb);
}
+ mSilenceDeviceManager.dump(fd, writer, args);
writer.write(sb.toString());
writer.flush();
@@ -2527,6 +2844,20 @@
}
}
+ /**
+ * Obfuscate Bluetooth MAC address into a PII free ID string
+ *
+ * @param device Bluetooth device whose MAC address will be obfuscated
+ * @return a byte array that is unique to this MAC address on this device,
+ * or empty byte array when either device is null or obfuscateAddressNative fails
+ */
+ public byte[] obfuscateAddress(BluetoothDevice device) {
+ if (device == null) {
+ return new byte[0];
+ }
+ return obfuscateAddressNative(Utils.getByteAddress(device));
+ }
+
static native void classInitNative();
native boolean initNative();
@@ -2610,6 +2941,8 @@
private native void interopDatabaseAddNative(int feature, byte[] address, int length);
+ private native byte[] obfuscateAddressNative(byte[] address);
+
// Returns if this is a mock object. This is currently used in testing so that we may not call
// System.exit() while finalizing the object. Otherwise GC of mock objects unfortunately ends up
// calling finalize() which in turn calls System.exit() and the process crashes.
diff --git a/src/com/android/bluetooth/btservice/BondStateMachine.java b/src/com/android/bluetooth/btservice/BondStateMachine.java
index 81e5aae..3fa0d31 100644
--- a/src/com/android/bluetooth/btservice/BondStateMachine.java
+++ b/src/com/android/bluetooth/btservice/BondStateMachine.java
@@ -20,11 +20,13 @@
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProtoEnums;
import android.bluetooth.OobData;
import android.content.Intent;
import android.os.Message;
import android.os.UserHandle;
import android.util.Log;
+import android.util.StatsLog;
import com.android.bluetooth.Utils;
import com.android.bluetooth.a2dp.A2dpService;
@@ -39,6 +41,7 @@
import com.android.internal.util.StateMachine;
import java.util.ArrayList;
+import java.util.Objects;
import java.util.HashSet;
import java.util.Set;
@@ -204,6 +207,11 @@
case BONDING_STATE_CHANGE:
int newState = msg.arg1;
int reason = getUnbondReasonFromHALCode(msg.arg2);
+ // Bond is explicitly removed if we are in pending command state
+ if (newState == BluetoothDevice.BOND_NONE
+ && reason == BluetoothDevice.BOND_SUCCESS) {
+ reason = BluetoothDevice.UNBOND_REASON_REMOVED;
+ }
sendIntent(dev, newState, reason);
if (newState != BluetoothDevice.BOND_BONDING) {
/* this is either none/bonded, remove and transition */
@@ -315,8 +323,18 @@
} else {
result = mAdapterService.createBondNative(addr, transport);
}
-
+ StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
+ mAdapterService.obfuscateAddress(dev), transport, dev.getType(),
+ BluetoothDevice.BOND_BONDING,
+ oobData == null ? BluetoothProtoEnums.BOND_SUB_STATE_UNKNOWN
+ : BluetoothProtoEnums.BOND_SUB_STATE_LOCAL_OOB_DATA_PROVIDED,
+ BluetoothProtoEnums.UNBOND_REASON_UNKNOWN);
if (!result) {
+ StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
+ mAdapterService.obfuscateAddress(dev), transport, dev.getType(),
+ BluetoothDevice.BOND_NONE, BluetoothProtoEnums.BOND_SUB_STATE_UNKNOWN,
+ BluetoothDevice.UNBOND_REASON_REPEATED_ATTEMPTS);
+ // Using UNBOND_REASON_REMOVED for legacy reason
sendIntent(dev, BluetoothDevice.BOND_NONE, BluetoothDevice.UNBOND_REASON_REMOVED);
return false;
} else if (transition) {
@@ -337,7 +355,7 @@
intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
// Workaround for Android Auto until pre-accepting pairing requests is added.
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
- mAdapterService.sendOrderedBroadcast(intent, mAdapterService.BLUETOOTH_ADMIN_PERM);
+ mAdapterService.sendOrderedBroadcast(intent, AdapterService.BLUETOOTH_ADMIN_PERM);
}
@VisibleForTesting
@@ -368,7 +386,9 @@
if (oldState == newState) {
return;
}
-
+ StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
+ mAdapterService.obfuscateAddress(device), 0, device.getType(),
+ newState, BluetoothProtoEnums.BOND_SUB_STATE_UNKNOWN, reason);
mAdapterProperties.onBondStateChanged(device, newState);
if (devProp != null && ((devProp.getDeviceType() == BluetoothDevice.DEVICE_TYPE_CLASSIC
@@ -464,9 +484,14 @@
if (device == null) {
warnLog("Device is not known for:" + Utils.getAddressStringFromByte(address));
mRemoteDevices.addDeviceProperties(address);
- device = mRemoteDevices.getDevice(address);
+ device = Objects.requireNonNull(mRemoteDevices.getDevice(address));
}
+ StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
+ mAdapterService.obfuscateAddress(device), 0, device.getType(),
+ BluetoothDevice.BOND_BONDING,
+ BluetoothProtoEnums.BOND_SUB_STATE_LOCAL_SSP_REQUESTED, 0);
+
Message msg = obtainMessage(SSP_REQUEST);
msg.obj = device;
if (displayPasskey) {
@@ -482,7 +507,14 @@
BluetoothDevice bdDevice = mRemoteDevices.getDevice(address);
if (bdDevice == null) {
mRemoteDevices.addDeviceProperties(address);
+ bdDevice = Objects.requireNonNull(mRemoteDevices.getDevice(address));
}
+
+ StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
+ mAdapterService.obfuscateAddress(bdDevice), 0, bdDevice.getType(),
+ BluetoothDevice.BOND_BONDING,
+ BluetoothProtoEnums.BOND_SUB_STATE_LOCAL_PIN_REQUESTED, 0);
+
infoLog("pinRequestCallback: " + address + " name:" + name + " cod:" + cod);
Message msg = obtainMessage(PIN_REQUEST);
@@ -518,11 +550,6 @@
if (pbapClientService != null) {
pbapClientService.setPriority(device, BluetoothProfile.PRIORITY_UNDEFINED);
}
-
- // Clear Absolute Volume black list
- if (a2dpService != null) {
- a2dpService.resetAvrcpBlacklist(device);
- }
}
private String state2str(int state) {
diff --git a/src/com/android/bluetooth/btservice/Config.java b/src/com/android/bluetooth/btservice/Config.java
index 8a9c0a1..2b1f46c 100644
--- a/src/com/android/bluetooth/btservice/Config.java
+++ b/src/com/android/bluetooth/btservice/Config.java
@@ -30,7 +30,6 @@
import com.android.bluetooth.avrcp.AvrcpTargetService;
import com.android.bluetooth.avrcpcontroller.AvrcpControllerService;
import com.android.bluetooth.gatt.GattService;
-import com.android.bluetooth.hdp.HealthService;
import com.android.bluetooth.hearingaid.HearingAidService;
import com.android.bluetooth.hfp.HeadsetService;
import com.android.bluetooth.hfpclient.HeadsetClientService;
@@ -73,8 +72,6 @@
(1 << BluetoothProfile.A2DP_SINK)),
new ProfileConfig(HidHostService.class, R.bool.profile_supported_hid_host,
(1 << BluetoothProfile.HID_HOST)),
- new ProfileConfig(HealthService.class, R.bool.profile_supported_hdp,
- (1 << BluetoothProfile.HEALTH)),
new ProfileConfig(PanService.class, R.bool.profile_supported_pan,
(1 << BluetoothProfile.PAN)),
new ProfileConfig(GattService.class, R.bool.profile_supported_gatt,
@@ -100,7 +97,8 @@
(1 << BluetoothProfile.OPP)),
new ProfileConfig(BluetoothPbapService.class, R.bool.profile_supported_pbap,
(1 << BluetoothProfile.PBAP)),
- new ProfileConfig(HearingAidService.class, R.bool.profile_supported_hearing_aid,
+ new ProfileConfig(HearingAidService.class,
+ com.android.internal.R.bool.config_hearing_aid_profile_supported,
(1 << BluetoothProfile.HEARING_AID))
};
diff --git a/src/com/android/bluetooth/btservice/DiscoveringPackage.java b/src/com/android/bluetooth/btservice/DiscoveringPackage.java
new file mode 100644
index 0000000..baa2b24
--- /dev/null
+++ b/src/com/android/bluetooth/btservice/DiscoveringPackage.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.btservice;
+
+final class DiscoveringPackage {
+ private String mPackageName;
+ private String mPermission;
+
+ DiscoveringPackage(String packageName, String permission) {
+ mPackageName = packageName;
+ mPermission = permission;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public String getPermission() {
+ return mPermission;
+ }
+}
diff --git a/src/com/android/bluetooth/btservice/PhonePolicy.java b/src/com/android/bluetooth/btservice/PhonePolicy.java
index a0ad490..39b1252 100644
--- a/src/com/android/bluetooth/btservice/PhonePolicy.java
+++ b/src/com/android/bluetooth/btservice/PhonePolicy.java
@@ -31,7 +31,6 @@
import android.os.Message;
import android.os.ParcelUuid;
import android.os.Parcelable;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.bluetooth.a2dp.A2dpService;
@@ -40,6 +39,7 @@
import com.android.bluetooth.hid.HidHostService;
import com.android.bluetooth.pan.PanService;
import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.HashSet;
import java.util.List;
@@ -284,20 +284,23 @@
}
connectOtherProfile(device);
}
- if (prevState == BluetoothProfile.STATE_CONNECTING
- && nextState == BluetoothProfile.STATE_DISCONNECTED) {
- HeadsetService hsService = mFactory.getHeadsetService();
- boolean hsDisconnected = hsService == null || hsService.getConnectionState(device)
- == BluetoothProfile.STATE_DISCONNECTED;
- A2dpService a2dpService = mFactory.getA2dpService();
- boolean a2dpDisconnected = a2dpService == null
- || a2dpService.getConnectionState(device)
- == BluetoothProfile.STATE_DISCONNECTED;
- debugLog("processProfileStateChanged, device=" + device + ", a2dpDisconnected="
- + a2dpDisconnected + ", hsDisconnected=" + hsDisconnected);
- if (hsDisconnected && a2dpDisconnected) {
- removeAutoConnectFromA2dpSink(device);
- removeAutoConnectFromHeadset(device);
+ if (nextState == BluetoothProfile.STATE_DISCONNECTED) {
+ handleAllProfilesDisconnected(device);
+ if (prevState == BluetoothProfile.STATE_CONNECTING) {
+ HeadsetService hsService = mFactory.getHeadsetService();
+ boolean hsDisconnected = hsService == null
+ || hsService.getConnectionState(device)
+ == BluetoothProfile.STATE_DISCONNECTED;
+ A2dpService a2dpService = mFactory.getA2dpService();
+ boolean a2dpDisconnected = a2dpService == null
+ || a2dpService.getConnectionState(device)
+ == BluetoothProfile.STATE_DISCONNECTED;
+ debugLog("processProfileStateChanged, device=" + device + ", a2dpDisconnected="
+ + a2dpDisconnected + ", hsDisconnected=" + hsDisconnected);
+ if (hsDisconnected && a2dpDisconnected) {
+ removeAutoConnectFromA2dpSink(device);
+ removeAutoConnectFromHeadset(device);
+ }
}
}
}
@@ -326,6 +329,45 @@
}
}
+ private boolean handleAllProfilesDisconnected(BluetoothDevice device) {
+ boolean atLeastOneProfileConnectedForDevice = false;
+ boolean allProfilesEmpty = true;
+ HeadsetService hsService = mFactory.getHeadsetService();
+ A2dpService a2dpService = mFactory.getA2dpService();
+ PanService panService = mFactory.getPanService();
+
+ if (hsService != null) {
+ List<BluetoothDevice> hsConnDevList = hsService.getConnectedDevices();
+ allProfilesEmpty &= hsConnDevList.isEmpty();
+ atLeastOneProfileConnectedForDevice |= hsConnDevList.contains(device);
+ }
+ if (a2dpService != null) {
+ List<BluetoothDevice> a2dpConnDevList = a2dpService.getConnectedDevices();
+ allProfilesEmpty &= a2dpConnDevList.isEmpty();
+ atLeastOneProfileConnectedForDevice |= a2dpConnDevList.contains(device);
+ }
+ if (panService != null) {
+ List<BluetoothDevice> panConnDevList = panService.getConnectedDevices();
+ allProfilesEmpty &= panConnDevList.isEmpty();
+ atLeastOneProfileConnectedForDevice |= panConnDevList.contains(device);
+ }
+
+ if (!atLeastOneProfileConnectedForDevice) {
+ // Consider this device as fully disconnected, don't bother connecting others
+ debugLog("handleAllProfilesDisconnected: all profiles disconnected for " + device);
+ mHeadsetRetrySet.remove(device);
+ mA2dpRetrySet.remove(device);
+ if (allProfilesEmpty) {
+ debugLog("handleAllProfilesDisconnected: all profiles disconnected for all"
+ + " devices");
+ // reset retry status so that in the next round we can start retrying connections
+ resetStates();
+ }
+ return true;
+ }
+ return false;
+ }
+
private void resetStates() {
mHeadsetRetrySet.clear();
mA2dpRetrySet.clear();
@@ -410,45 +452,15 @@
warnLog("processConnectOtherProfiles, adapter is not ON " + mAdapterService.getState());
return;
}
+ if (handleAllProfilesDisconnected(device)) {
+ debugLog("processConnectOtherProfiles: all profiles disconnected for " + device);
+ return;
+ }
+
HeadsetService hsService = mFactory.getHeadsetService();
A2dpService a2dpService = mFactory.getA2dpService();
PanService panService = mFactory.getPanService();
- boolean atLeastOneProfileConnectedForDevice = false;
- boolean allProfilesEmpty = true;
- List<BluetoothDevice> a2dpConnDevList = null;
- List<BluetoothDevice> hsConnDevList = null;
- List<BluetoothDevice> panConnDevList = null;
-
- if (hsService != null) {
- hsConnDevList = hsService.getConnectedDevices();
- allProfilesEmpty &= hsConnDevList.isEmpty();
- atLeastOneProfileConnectedForDevice |= hsConnDevList.contains(device);
- }
- if (a2dpService != null) {
- a2dpConnDevList = a2dpService.getConnectedDevices();
- allProfilesEmpty &= a2dpConnDevList.isEmpty();
- atLeastOneProfileConnectedForDevice |= a2dpConnDevList.contains(device);
- }
- if (panService != null) {
- panConnDevList = panService.getConnectedDevices();
- allProfilesEmpty &= panConnDevList.isEmpty();
- atLeastOneProfileConnectedForDevice |= panConnDevList.contains(device);
- }
-
- if (!atLeastOneProfileConnectedForDevice) {
- // Consider this device as fully disconnected, don't bother connecting others
- debugLog("processConnectOtherProfiles, all profiles disconnected for " + device);
- mHeadsetRetrySet.remove(device);
- mA2dpRetrySet.remove(device);
- if (allProfilesEmpty) {
- debugLog("processConnectOtherProfiles, all profiles disconnected for all devices");
- // reset retry status so that in the next round we can start retrying connections
- resetStates();
- }
- return;
- }
-
if (hsService != null) {
if (!mHeadsetRetrySet.contains(device) && (hsService.getPriority(device)
>= BluetoothProfile.PRIORITY_ON) && (hsService.getConnectionState(device)
@@ -468,6 +480,7 @@
}
}
if (panService != null) {
+ List<BluetoothDevice> panConnDevList = panService.getConnectedDevices();
// TODO: the panConnDevList.isEmpty() check below should be removed once
// Multi-PAN is supported.
if (panConnDevList.isEmpty() && (panService.getPriority(device)
diff --git a/src/com/android/bluetooth/btservice/RemoteDevices.java b/src/com/android/bluetooth/btservice/RemoteDevices.java
index 011177e..56b8b9c 100644
--- a/src/com/android/bluetooth/btservice/RemoteDevices.java
+++ b/src/com/android/bluetooth/btservice/RemoteDevices.java
@@ -30,12 +30,13 @@
import android.os.Looper;
import android.os.Message;
import android.os.ParcelUuid;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
+import android.util.StatsLog;
import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
import com.android.bluetooth.hfp.HeadsetHalConstants;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashMap;
@@ -79,7 +80,8 @@
case MESSAGE_UUID_INTENT:
BluetoothDevice device = (BluetoothDevice) msg.obj;
if (device != null) {
- sendUuidIntent(device);
+ DeviceProperties prop = getDeviceProperties(device);
+ sendUuidIntent(device, prop);
}
break;
}
@@ -273,7 +275,6 @@
return mRssi;
}
}
-
/**
* @return mDeviceType
*/
@@ -319,6 +320,7 @@
without waiting for the ACTION_UUID intent.
This was resulting in multiple calls to connect().*/
mUuids = null;
+ mAlias = null;
}
}
}
@@ -366,8 +368,7 @@
}
}
- private void sendUuidIntent(BluetoothDevice device) {
- DeviceProperties prop = getDeviceProperties(device);
+ private void sendUuidIntent(BluetoothDevice device, DeviceProperties prop) {
Intent intent = new Intent(BluetoothDevice.ACTION_UUID);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothDevice.EXTRA_UUID, prop == null ? null : prop.mUuids);
@@ -455,6 +456,7 @@
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothDevice.EXTRA_BATTERY_LEVEL, batteryLevel);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
sAdapterService.sendBroadcast(intent, AdapterService.BLUETOOTH_PERM);
}
@@ -547,7 +549,7 @@
device.mUuids = newUuids;
if (sAdapterService.getState() == BluetoothAdapter.STATE_ON) {
sAdapterService.deviceUuidUpdated(bdDevice);
- sendUuidIntent(bdDevice);
+ sendUuidIntent(bdDevice, device);
}
break;
case AbstractionLayer.BT_PROPERTY_TYPE_OF_DEVICE:
@@ -583,9 +585,15 @@
intent.putExtra(BluetoothDevice.EXTRA_RSSI, deviceProp.mRssi);
intent.putExtra(BluetoothDevice.EXTRA_NAME, deviceProp.mName);
- sAdapterService.sendBroadcastMultiplePermissions(intent, new String[]{
- AdapterService.BLUETOOTH_PERM, android.Manifest.permission.ACCESS_COARSE_LOCATION
- });
+ final ArrayList<DiscoveringPackage> packages = sAdapterService.getDiscoveringPackages();
+ synchronized (packages) {
+ for (DiscoveringPackage pkg : packages) {
+ intent.setPackage(pkg.getPackageName());
+ sAdapterService.sendBroadcastMultiplePermissions(intent, new String[]{
+ AdapterService.BLUETOOTH_PERM, pkg.getPermission()
+ });
+ }
+ }
}
void aclStateChangeCallback(int status, byte[] address, int newState) {
@@ -632,6 +640,11 @@
+ " Disconnected: " + device);
}
+ int connectionState = newState == AbstractionLayer.BT_ACL_STATE_CONNECTED
+ ? BluetoothAdapter.STATE_CONNECTED : BluetoothAdapter.STATE_DISCONNECTED;
+ StatsLog.write(StatsLog.BLUETOOTH_ACL_CONNECTION_STATE_CHANGED,
+ sAdapterService.obfuscateAddress(device), connectionState);
+
if (intent != null) {
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
@@ -831,13 +844,13 @@
}
int batteryLevel = (Integer) args[1];
int numberOfLevels = (Integer) args[2];
- if (batteryLevel < 0 || numberOfLevels < 0 || batteryLevel > numberOfLevels) {
+ if (batteryLevel < 0 || numberOfLevels <= 1 || batteryLevel > numberOfLevels) {
Log.w(TAG, "getBatteryLevelFromXEventVsc() wrong event value, batteryLevel="
+ String.valueOf(batteryLevel) + ", numberOfLevels=" + String.valueOf(
numberOfLevels));
return BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
}
- return batteryLevel * 100 / numberOfLevels;
+ return batteryLevel * 100 / (numberOfLevels - 1);
}
private static void errorLog(String msg) {
diff --git a/src/com/android/bluetooth/btservice/ServiceFactory.java b/src/com/android/bluetooth/btservice/ServiceFactory.java
index 2bd7ab5..fe79a5c 100644
--- a/src/com/android/bluetooth/btservice/ServiceFactory.java
+++ b/src/com/android/bluetooth/btservice/ServiceFactory.java
@@ -17,6 +17,7 @@
package com.android.bluetooth.btservice;
import com.android.bluetooth.a2dp.A2dpService;
+import com.android.bluetooth.avrcp.AvrcpTargetService;
import com.android.bluetooth.hearingaid.HearingAidService;
import com.android.bluetooth.hfp.HeadsetService;
import com.android.bluetooth.hid.HidDeviceService;
@@ -48,4 +49,8 @@
public HearingAidService getHearingAidService() {
return HearingAidService.getHearingAidService();
}
+
+ public AvrcpTargetService getAvrcpTargetService() {
+ return AvrcpTargetService.get();
+ }
}
diff --git a/src/com/android/bluetooth/btservice/SilenceDeviceManager.java b/src/com/android/bluetooth/btservice/SilenceDeviceManager.java
new file mode 100644
index 0000000..e8ec235
--- /dev/null
+++ b/src/com/android/bluetooth/btservice/SilenceDeviceManager.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.btservice;
+
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.bluetooth.a2dp.A2dpService;
+import com.android.bluetooth.hfp.HeadsetService;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The silence device manager controls silence mode for A2DP, HFP, and AVRCP.
+ *
+ * 1) If an active device (for A2DP or HFP) enters silence mode, the active device
+ * for that profile will be set to null.
+ * 2) If a device exits silence mode while the A2DP or HFP active device is null,
+ * the device will be set as the active device for that profile.
+ * 3) If a device is disconnected, it exits silence mode.
+ * 4) If a device is set as the active device for A2DP or HFP, while silence mode
+ * is enabled, then the device will exit silence mode.
+ * 5) If a device is in silence mode, AVRCP position change event and HFP AG indicators
+ * will be disabled.
+ * 6) If a device is not connected with A2DP or HFP, it cannot enter silence mode.
+ */
+public class SilenceDeviceManager {
+ private static final boolean DBG = true;
+ private static final boolean VERBOSE = false;
+ private static final String TAG = "SilenceDeviceManager";
+
+ private final AdapterService mAdapterService;
+ private final ServiceFactory mFactory;
+ private Handler mHandler = null;
+ private Looper mLooper = null;
+
+ private final Map<BluetoothDevice, Boolean> mSilenceDevices = new HashMap<>();
+ private final List<BluetoothDevice> mA2dpConnectedDevices = new ArrayList<>();
+ private final List<BluetoothDevice> mHfpConnectedDevices = new ArrayList<>();
+
+ private static final int MSG_SILENCE_DEVICE_STATE_CHANGED = 1;
+ private static final int MSG_A2DP_CONNECTION_STATE_CHANGED = 10;
+ private static final int MSG_HFP_CONNECTION_STATE_CHANGED = 11;
+ private static final int MSG_A2DP_ACTIVE_DEIVCE_CHANGED = 20;
+ private static final int MSG_HFP_ACTIVE_DEVICE_CHANGED = 21;
+ private static final int ENABLE_SILENCE = 0;
+ private static final int DISABLE_SILENCE = 1;
+
+ // Broadcast receiver for all changes
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action == null) {
+ Log.e(TAG, "Received intent with null action");
+ return;
+ }
+ switch (action) {
+ case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
+ mHandler.obtainMessage(MSG_A2DP_CONNECTION_STATE_CHANGED,
+ intent).sendToTarget();
+ break;
+ case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:
+ mHandler.obtainMessage(MSG_HFP_CONNECTION_STATE_CHANGED,
+ intent).sendToTarget();
+ break;
+ case BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED:
+ mHandler.obtainMessage(MSG_A2DP_ACTIVE_DEIVCE_CHANGED,
+ intent).sendToTarget();
+ break;
+ case BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED:
+ mHandler.obtainMessage(MSG_HFP_ACTIVE_DEVICE_CHANGED,
+ intent).sendToTarget();
+ break;
+ default:
+ Log.e(TAG, "Received unexpected intent, action=" + action);
+ break;
+ }
+ }
+ };
+
+ class SilenceDeviceManagerHandler extends Handler {
+ SilenceDeviceManagerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (VERBOSE) {
+ Log.d(TAG, "handleMessage: " + msg.what);
+ }
+ switch (msg.what) {
+ case MSG_SILENCE_DEVICE_STATE_CHANGED: {
+ BluetoothDevice device = (BluetoothDevice) msg.obj;
+ boolean state = (msg.arg1 == ENABLE_SILENCE);
+ handleSilenceDeviceStateChanged(device, state);
+ }
+ break;
+
+ case MSG_A2DP_CONNECTION_STATE_CHANGED: {
+ Intent intent = (Intent) msg.obj;
+ BluetoothDevice device =
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
+ int nextState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+
+ if (nextState == BluetoothProfile.STATE_CONNECTED) {
+ // enter connected state
+ addConnectedDevice(device, BluetoothProfile.A2DP);
+ if (!mSilenceDevices.containsKey(device)) {
+ mSilenceDevices.put(device, false);
+ }
+ } else if (prevState == BluetoothProfile.STATE_CONNECTED) {
+ // exiting from connected state
+ removeConnectedDevice(device, BluetoothProfile.A2DP);
+ if (!isBluetoothAudioConnected(device)) {
+ handleSilenceDeviceStateChanged(device, false);
+ mSilenceDevices.remove(device);
+ }
+ }
+ }
+ break;
+
+ case MSG_HFP_CONNECTION_STATE_CHANGED: {
+ Intent intent = (Intent) msg.obj;
+ BluetoothDevice device =
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
+ int nextState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+
+ if (nextState == BluetoothProfile.STATE_CONNECTED) {
+ // enter connected state
+ addConnectedDevice(device, BluetoothProfile.HEADSET);
+ if (!mSilenceDevices.containsKey(device)) {
+ mSilenceDevices.put(device, false);
+ }
+ } else if (prevState == BluetoothProfile.STATE_CONNECTED) {
+ // exiting from connected state
+ removeConnectedDevice(device, BluetoothProfile.HEADSET);
+ if (!isBluetoothAudioConnected(device)) {
+ handleSilenceDeviceStateChanged(device, false);
+ mSilenceDevices.remove(device);
+ }
+ }
+ }
+ break;
+
+ case MSG_A2DP_ACTIVE_DEIVCE_CHANGED: {
+ Intent intent = (Intent) msg.obj;
+ BluetoothDevice a2dpActiveDevice =
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ if (getSilenceMode(a2dpActiveDevice)) {
+ // Resume the device from silence mode.
+ setSilenceMode(a2dpActiveDevice, false);
+ }
+ }
+ break;
+
+ case MSG_HFP_ACTIVE_DEVICE_CHANGED: {
+ Intent intent = (Intent) msg.obj;
+ BluetoothDevice hfpActiveDevice =
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ if (getSilenceMode(hfpActiveDevice)) {
+ // Resume the device from silence mode.
+ setSilenceMode(hfpActiveDevice, false);
+ }
+ }
+ break;
+
+ default: {
+ Log.e(TAG, "Unknown message: " + msg.what);
+ }
+ break;
+ }
+ }
+ };
+
+ SilenceDeviceManager(AdapterService service, ServiceFactory factory, Looper looper) {
+ mAdapterService = service;
+ mFactory = factory;
+ mLooper = looper;
+ }
+
+ void start() {
+ if (VERBOSE) {
+ Log.v(TAG, "start()");
+ }
+ mHandler = new SilenceDeviceManagerHandler(mLooper);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+ filter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
+ filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+ filter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
+ mAdapterService.registerReceiver(mReceiver, filter);
+ }
+
+ void cleanup() {
+ if (VERBOSE) {
+ Log.v(TAG, "cleanup()");
+ }
+ mSilenceDevices.clear();
+ mAdapterService.unregisterReceiver(mReceiver);
+ }
+
+ @VisibleForTesting
+ boolean setSilenceMode(BluetoothDevice device, boolean silence) {
+ if (mHandler == null) {
+ Log.e(TAG, "setSilenceMode() mHandler is null!");
+ return false;
+ }
+ Log.d(TAG, "setSilenceMode: " + device.getAddress() + ", " + silence);
+ Message message = mHandler.obtainMessage(MSG_SILENCE_DEVICE_STATE_CHANGED,
+ silence ? ENABLE_SILENCE : DISABLE_SILENCE, 0, device);
+ mHandler.sendMessage(message);
+ return true;
+ }
+
+ void handleSilenceDeviceStateChanged(BluetoothDevice device, boolean state) {
+ boolean oldState = getSilenceMode(device);
+ if (oldState == state) {
+ return;
+ }
+ if (!isBluetoothAudioConnected(device)) {
+ if (oldState) {
+ // Device is disconnected, resume all silenced profiles.
+ state = false;
+ } else {
+ Log.d(TAG, "Deivce is not connected to any Bluetooth audio.");
+ return;
+ }
+ }
+ mSilenceDevices.replace(device, state);
+
+ A2dpService a2dpService = mFactory.getA2dpService();
+ if (a2dpService != null) {
+ a2dpService.setSilenceMode(device, state);
+ }
+ HeadsetService headsetService = mFactory.getHeadsetService();
+ if (headsetService != null) {
+ headsetService.setSilenceMode(device, state);
+ }
+ Log.i(TAG, "Silence mode change " + device.getAddress() + ": " + oldState + " -> "
+ + state);
+ broadcastSilenceStateChange(device, state);
+ }
+
+ void broadcastSilenceStateChange(BluetoothDevice device, boolean state) {
+ Intent intent = new Intent(BluetoothDevice.ACTION_SILENCE_MODE_CHANGED);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ mAdapterService.sendBroadcastAsUser(intent, UserHandle.ALL, AdapterService.BLUETOOTH_PERM);
+
+ }
+
+ @VisibleForTesting
+ boolean getSilenceMode(BluetoothDevice device) {
+ boolean state = false;
+ if (mSilenceDevices.containsKey(device)) {
+ state = mSilenceDevices.get(device);
+ }
+ return state;
+ }
+
+ void addConnectedDevice(BluetoothDevice device, int profile) {
+ if (VERBOSE) {
+ Log.d(TAG, "addConnectedDevice: " + device.getAddress() + ", profile:" + profile);
+ }
+ switch (profile) {
+ case BluetoothProfile.A2DP:
+ if (!mA2dpConnectedDevices.contains(device)) {
+ mA2dpConnectedDevices.add(device);
+ }
+ break;
+ case BluetoothProfile.HEADSET:
+ if (!mHfpConnectedDevices.contains(device)) {
+ mHfpConnectedDevices.add(device);
+ }
+ break;
+ }
+ }
+
+ void removeConnectedDevice(BluetoothDevice device, int profile) {
+ if (VERBOSE) {
+ Log.d(TAG, "removeConnectedDevice: " + device.getAddress() + ", profile:" + profile);
+ }
+ switch (profile) {
+ case BluetoothProfile.A2DP:
+ if (mA2dpConnectedDevices.contains(device)) {
+ mA2dpConnectedDevices.remove(device);
+ }
+ break;
+ case BluetoothProfile.HEADSET:
+ if (mHfpConnectedDevices.contains(device)) {
+ mHfpConnectedDevices.remove(device);
+ }
+ break;
+ }
+ }
+
+ boolean isBluetoothAudioConnected(BluetoothDevice device) {
+ return (mA2dpConnectedDevices.contains(device) || mHfpConnectedDevices.contains(device));
+ }
+
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.println("\nSilenceDeviceManager:");
+ writer.println(" Address | Is silenced?");
+ for (BluetoothDevice device : mSilenceDevices.keySet()) {
+ writer.println(" " + device.getAddress() + " | " + getSilenceMode(device));
+ }
+ }
+
+ @VisibleForTesting
+ BroadcastReceiver getBroadcastReceiver() {
+ return mReceiver;
+ }
+}
diff --git a/src/com/android/bluetooth/btservice/storage/CustomizedMetadataEntity.java b/src/com/android/bluetooth/btservice/storage/CustomizedMetadataEntity.java
new file mode 100644
index 0000000..d0bddf1
--- /dev/null
+++ b/src/com/android/bluetooth/btservice/storage/CustomizedMetadataEntity.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.btservice.storage;
+
+import androidx.room.Entity;
+
+@Entity
+class CustomizedMetadataEntity {
+ public byte[] manufacturer_name;
+ public byte[] model_name;
+ public byte[] software_version;
+ public byte[] hardware_version;
+ public byte[] companion_app;
+ public byte[] main_icon;
+ public byte[] is_untethered_headset;
+ public byte[] untethered_left_icon;
+ public byte[] untethered_right_icon;
+ public byte[] untethered_case_icon;
+ public byte[] untethered_left_battery;
+ public byte[] untethered_right_battery;
+ public byte[] untethered_case_battery;
+ public byte[] untethered_left_charging;
+ public byte[] untethered_right_charging;
+ public byte[] untethered_case_charging;
+ public byte[] enhanced_settings_ui_uri;
+}
diff --git a/src/com/android/bluetooth/btservice/storage/DatabaseManager.java b/src/com/android/bluetooth/btservice/storage/DatabaseManager.java
new file mode 100644
index 0000000..a0e3c5a
--- /dev/null
+++ b/src/com/android/bluetooth/btservice/storage/DatabaseManager.java
@@ -0,0 +1,796 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.btservice.storage;
+
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProtoEnums;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.Settings;
+import android.util.Log;
+import android.util.StatsLog;
+
+import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The active device manager is responsible to handle a Room database
+ * for Bluetooth persistent data.
+ */
+public class DatabaseManager {
+ private static final boolean DBG = true;
+ private static final boolean VERBOSE = true;
+ private static final String TAG = "BluetoothDatabase";
+
+ private AdapterService mAdapterService = null;
+ private HandlerThread mHandlerThread = null;
+ private Handler mHandler = null;
+ private MetadataDatabase mDatabase = null;
+ private boolean mMigratedFromSettingsGlobal = false;
+
+ @VisibleForTesting
+ final Map<String, Metadata> mMetadataCache = new HashMap<>();
+ private final Semaphore mSemaphore = new Semaphore(1);
+
+ private static final int LOAD_DATABASE_TIMEOUT = 500; // milliseconds
+ private static final int MSG_LOAD_DATABASE = 0;
+ private static final int MSG_UPDATE_DATABASE = 1;
+ private static final int MSG_DELETE_DATABASE = 2;
+ private static final int MSG_CLEAR_DATABASE = 100;
+ private static final String LOCAL_STORAGE = "LocalStorage";
+
+ /**
+ * Constructor of the DatabaseManager
+ */
+ public DatabaseManager(AdapterService service) {
+ mAdapterService = service;
+ }
+
+ class DatabaseHandler extends Handler {
+ DatabaseHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_LOAD_DATABASE: {
+ synchronized (mDatabase) {
+ List<Metadata> list;
+ try {
+ list = mDatabase.load();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Unable to open database: " + e);
+ mDatabase = MetadataDatabase
+ .createDatabaseWithoutMigration(mAdapterService);
+ list = mDatabase.load();
+ }
+ cacheMetadata(list);
+ }
+ break;
+ }
+ case MSG_UPDATE_DATABASE: {
+ Metadata data = (Metadata) msg.obj;
+ synchronized (mDatabase) {
+ mDatabase.insert(data);
+ }
+ break;
+ }
+ case MSG_DELETE_DATABASE: {
+ String address = (String) msg.obj;
+ synchronized (mDatabase) {
+ mDatabase.delete(address);
+ }
+ break;
+ }
+ case MSG_CLEAR_DATABASE: {
+ synchronized (mDatabase) {
+ mDatabase.deleteAll();
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action == null) {
+ Log.e(TAG, "Received intent with null action");
+ return;
+ }
+ switch (action) {
+ case BluetoothDevice.ACTION_BOND_STATE_CHANGED: {
+ int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ BluetoothDevice.ERROR);
+ BluetoothDevice device =
+ intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ Objects.requireNonNull(device,
+ "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
+ bondStateChanged(device, state);
+ break;
+ }
+ case BluetoothAdapter.ACTION_STATE_CHANGED: {
+ int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
+ BluetoothAdapter.STATE_OFF);
+ if (!mMigratedFromSettingsGlobal
+ && state == BluetoothAdapter.STATE_TURNING_ON) {
+ migrateSettingsGlobal();
+ }
+ break;
+ }
+ }
+ }
+ };
+
+ void bondStateChanged(BluetoothDevice device, int state) {
+ synchronized (mMetadataCache) {
+ String address = device.getAddress();
+ if (state != BluetoothDevice.BOND_NONE) {
+ if (mMetadataCache.containsKey(address)) {
+ return;
+ }
+ createMetadata(address);
+ } else {
+ Metadata metadata = mMetadataCache.get(address);
+ if (metadata != null) {
+ mMetadataCache.remove(address);
+ deleteDatabase(metadata);
+ }
+ }
+ }
+ }
+
+ boolean isValidMetaKey(int key) {
+ switch (key) {
+ case BluetoothDevice.METADATA_MANUFACTURER_NAME:
+ case BluetoothDevice.METADATA_MODEL_NAME:
+ case BluetoothDevice.METADATA_SOFTWARE_VERSION:
+ case BluetoothDevice.METADATA_HARDWARE_VERSION:
+ case BluetoothDevice.METADATA_COMPANION_APP:
+ case BluetoothDevice.METADATA_MAIN_ICON:
+ case BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET:
+ case BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON:
+ case BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON:
+ case BluetoothDevice.METADATA_UNTETHERED_CASE_ICON:
+ case BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY:
+ case BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY:
+ case BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY:
+ case BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING:
+ case BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING:
+ case BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING:
+ case BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI:
+ return true;
+ }
+ Log.w(TAG, "Invalid metadata key " + key);
+ return false;
+ }
+
+ /**
+ * Set customized metadata to database with requested key
+ */
+ @VisibleForTesting
+ public boolean setCustomMeta(BluetoothDevice device, int key, byte[] newValue) {
+ synchronized (mMetadataCache) {
+ if (device == null) {
+ Log.e(TAG, "setCustomMeta: device is null");
+ return false;
+ }
+ if (!isValidMetaKey(key)) {
+ Log.e(TAG, "setCustomMeta: meta key invalid " + key);
+ return false;
+ }
+
+ String address = device.getAddress();
+ if (VERBOSE) {
+ Log.d(TAG, "setCustomMeta: " + address + ", key=" + key);
+ }
+ if (!mMetadataCache.containsKey(address)) {
+ createMetadata(address);
+ }
+ Metadata data = mMetadataCache.get(address);
+ byte[] oldValue = data.getCustomizedMeta(key);
+ if (oldValue != null && Arrays.equals(oldValue, newValue)) {
+ if (VERBOSE) {
+ Log.d(TAG, "setCustomMeta: metadata not changed.");
+ }
+ return true;
+ }
+ logManufacturerInfo(device, key, newValue);
+ data.setCustomizedMeta(key, newValue);
+
+ updateDatabase(data);
+ mAdapterService.metadataChanged(address, key, newValue);
+ return true;
+ }
+ }
+
+ /**
+ * Get customized metadata from database with requested key
+ */
+ @VisibleForTesting
+ public byte[] getCustomMeta(BluetoothDevice device, int key) {
+ synchronized (mMetadataCache) {
+ if (device == null) {
+ Log.e(TAG, "getCustomMeta: device is null");
+ return null;
+ }
+ if (!isValidMetaKey(key)) {
+ Log.e(TAG, "getCustomMeta: meta key invalid " + key);
+ return null;
+ }
+
+ String address = device.getAddress();
+
+ if (!mMetadataCache.containsKey(address)) {
+ Log.e(TAG, "getCustomMeta: device " + address + " is not in cache");
+ return null;
+ }
+
+ Metadata data = mMetadataCache.get(address);
+ return data.getCustomizedMeta(key);
+ }
+ }
+
+ /**
+ * Set the device profile prioirty
+ *
+ * @param device {@link BluetoothDevice} wish to set
+ * @param profile The Bluetooth profile; one of {@link BluetoothProfile#HEADSET},
+ * {@link BluetoothProfile#HEADSET_CLIENT}, {@link BluetoothProfile#A2DP},
+ * {@link BluetoothProfile#A2DP_SINK}, {@link BluetoothProfile#HID_HOST},
+ * {@link BluetoothProfile#PAN}, {@link BluetoothProfile#PBAP},
+ * {@link BluetoothProfile#PBAP_CLIENT}, {@link BluetoothProfile#MAP},
+ * {@link BluetoothProfile#MAP_CLIENT}, {@link BluetoothProfile#SAP},
+ * {@link BluetoothProfile#HEARING_AID}
+ * @param newPriority the priority to set; one of
+ * {@link BluetoothProfile#PRIORITY_UNDEFINED},
+ * {@link BluetoothProfile#PRIORITY_OFF},
+ * {@link BluetoothProfile#PRIORITY_ON},
+ * {@link BluetoothProfile#PRIORITY_AUTO_CONNECT}
+ */
+ @VisibleForTesting
+ public boolean setProfilePriority(BluetoothDevice device, int profile, int newPriority) {
+ synchronized (mMetadataCache) {
+ if (device == null) {
+ Log.e(TAG, "setProfilePriority: device is null");
+ return false;
+ }
+
+ if (newPriority != BluetoothProfile.PRIORITY_UNDEFINED
+ && newPriority != BluetoothProfile.PRIORITY_OFF
+ && newPriority != BluetoothProfile.PRIORITY_ON
+ && newPriority != BluetoothProfile.PRIORITY_AUTO_CONNECT) {
+ Log.e(TAG, "setProfilePriority: invalid priority " + newPriority);
+ return false;
+ }
+
+ String address = device.getAddress();
+ if (VERBOSE) {
+ Log.v(TAG, "setProfilePriority: " + address + ", profile=" + profile
+ + ", priority = " + newPriority);
+ }
+ if (!mMetadataCache.containsKey(address)) {
+ if (newPriority == BluetoothProfile.PRIORITY_UNDEFINED) {
+ return true;
+ }
+ createMetadata(address);
+ }
+ Metadata data = mMetadataCache.get(address);
+ int oldPriority = data.getProfilePriority(profile);
+ if (oldPriority == newPriority) {
+ if (VERBOSE) {
+ Log.v(TAG, "setProfilePriority priority not changed.");
+ }
+ return true;
+ }
+
+ data.setProfilePriority(profile, newPriority);
+ updateDatabase(data);
+ return true;
+ }
+ }
+
+ /**
+ * Get the device profile prioirty
+ *
+ * @param device {@link BluetoothDevice} wish to get
+ * @param profile The Bluetooth profile; one of {@link BluetoothProfile#HEADSET},
+ * {@link BluetoothProfile#HEADSET_CLIENT}, {@link BluetoothProfile#A2DP},
+ * {@link BluetoothProfile#A2DP_SINK}, {@link BluetoothProfile#HID_HOST},
+ * {@link BluetoothProfile#PAN}, {@link BluetoothProfile#PBAP},
+ * {@link BluetoothProfile#PBAP_CLIENT}, {@link BluetoothProfile#MAP},
+ * {@link BluetoothProfile#MAP_CLIENT}, {@link BluetoothProfile#SAP},
+ * {@link BluetoothProfile#HEARING_AID}
+ * @return the profile priority of the device; one of
+ * {@link BluetoothProfile#PRIORITY_UNDEFINED},
+ * {@link BluetoothProfile#PRIORITY_OFF},
+ * {@link BluetoothProfile#PRIORITY_ON},
+ * {@link BluetoothProfile#PRIORITY_AUTO_CONNECT}
+ */
+ @VisibleForTesting
+ public int getProfilePriority(BluetoothDevice device, int profile) {
+ synchronized (mMetadataCache) {
+ if (device == null) {
+ Log.e(TAG, "getProfilePriority: device is null");
+ return BluetoothProfile.PRIORITY_UNDEFINED;
+ }
+
+ String address = device.getAddress();
+
+ if (!mMetadataCache.containsKey(address)) {
+ Log.e(TAG, "getProfilePriority: device " + address + " is not in cache");
+ return BluetoothProfile.PRIORITY_UNDEFINED;
+ }
+
+ Metadata data = mMetadataCache.get(address);
+ int priority = data.getProfilePriority(profile);
+ if (VERBOSE) {
+ Log.v(TAG, "getProfilePriority: " + address + ", profile=" + profile
+ + ", priority = " + priority);
+ }
+ return priority;
+ }
+ }
+
+ /**
+ * Set the A2DP optional coedc support value
+ *
+ * @param device {@link BluetoothDevice} wish to set
+ * @param newValue the new A2DP optional coedc support value, one of
+ * {@link BluetoothA2dp#OPTIONAL_CODECS_SUPPORT_UNKNOWN},
+ * {@link BluetoothA2dp#OPTIONAL_CODECS_NOT_SUPPORTED},
+ * {@link BluetoothA2dp#OPTIONAL_CODECS_SUPPORTED}
+ */
+ @VisibleForTesting
+ public void setA2dpSupportsOptionalCodecs(BluetoothDevice device, int newValue) {
+ synchronized (mMetadataCache) {
+ if (device == null) {
+ Log.e(TAG, "setA2dpOptionalCodec: device is null");
+ return;
+ }
+ if (newValue != BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN
+ && newValue != BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED
+ && newValue != BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED) {
+ Log.e(TAG, "setA2dpSupportsOptionalCodecs: invalid value " + newValue);
+ return;
+ }
+
+ String address = device.getAddress();
+
+ if (!mMetadataCache.containsKey(address)) {
+ return;
+ }
+ Metadata data = mMetadataCache.get(address);
+ int oldValue = data.a2dpSupportsOptionalCodecs;
+ if (oldValue == newValue) {
+ return;
+ }
+
+ data.a2dpSupportsOptionalCodecs = newValue;
+ updateDatabase(data);
+ }
+ }
+
+ /**
+ * Get the A2DP optional coedc support value
+ *
+ * @param device {@link BluetoothDevice} wish to get
+ * @return the A2DP optional coedc support value, one of
+ * {@link BluetoothA2dp#OPTIONAL_CODECS_SUPPORT_UNKNOWN},
+ * {@link BluetoothA2dp#OPTIONAL_CODECS_NOT_SUPPORTED},
+ * {@link BluetoothA2dp#OPTIONAL_CODECS_SUPPORTED},
+ */
+ @VisibleForTesting
+ public int getA2dpSupportsOptionalCodecs(BluetoothDevice device) {
+ synchronized (mMetadataCache) {
+ if (device == null) {
+ Log.e(TAG, "setA2dpOptionalCodec: device is null");
+ return BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN;
+ }
+
+ String address = device.getAddress();
+
+ if (!mMetadataCache.containsKey(address)) {
+ Log.e(TAG, "getA2dpOptionalCodec: device " + address + " is not in cache");
+ return BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN;
+ }
+
+ Metadata data = mMetadataCache.get(address);
+ return data.a2dpSupportsOptionalCodecs;
+ }
+ }
+
+ /**
+ * Set the A2DP optional coedc enabled value
+ *
+ * @param device {@link BluetoothDevice} wish to set
+ * @param newValue the new A2DP optional coedc enabled value, one of
+ * {@link BluetoothA2dp#OPTIONAL_CODECS_PREF_UNKNOWN},
+ * {@link BluetoothA2dp#OPTIONAL_CODECS_PREF_DISABLED},
+ * {@link BluetoothA2dp#OPTIONAL_CODECS_PREF_ENABLED}
+ */
+ @VisibleForTesting
+ public void setA2dpOptionalCodecsEnabled(BluetoothDevice device, int newValue) {
+ synchronized (mMetadataCache) {
+ if (device == null) {
+ Log.e(TAG, "setA2dpOptionalCodecEnabled: device is null");
+ return;
+ }
+ if (newValue != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN
+ && newValue != BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED
+ && newValue != BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED) {
+ Log.e(TAG, "setA2dpOptionalCodecsEnabled: invalid value " + newValue);
+ return;
+ }
+
+ String address = device.getAddress();
+
+ if (!mMetadataCache.containsKey(address)) {
+ return;
+ }
+ Metadata data = mMetadataCache.get(address);
+ int oldValue = data.a2dpOptionalCodecsEnabled;
+ if (oldValue == newValue) {
+ return;
+ }
+
+ data.a2dpOptionalCodecsEnabled = newValue;
+ updateDatabase(data);
+ }
+ }
+
+ /**
+ * Get the A2DP optional coedc enabled value
+ *
+ * @param device {@link BluetoothDevice} wish to get
+ * @return the A2DP optional coedc enabled value, one of
+ * {@link BluetoothA2dp#OPTIONAL_CODECS_PREF_UNKNOWN},
+ * {@link BluetoothA2dp#OPTIONAL_CODECS_PREF_DISABLED},
+ * {@link BluetoothA2dp#OPTIONAL_CODECS_PREF_ENABLED}
+ */
+ @VisibleForTesting
+ public int getA2dpOptionalCodecsEnabled(BluetoothDevice device) {
+ synchronized (mMetadataCache) {
+ if (device == null) {
+ Log.e(TAG, "getA2dpOptionalCodecEnabled: device is null");
+ return BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN;
+ }
+
+ String address = device.getAddress();
+
+ if (!mMetadataCache.containsKey(address)) {
+ Log.e(TAG, "getA2dpOptionalCodecEnabled: device " + address + " is not in cache");
+ return BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN;
+ }
+
+ Metadata data = mMetadataCache.get(address);
+ return data.a2dpOptionalCodecsEnabled;
+ }
+ }
+
+ /**
+ * Get the {@link Looper} for the handler thread. This is used in testing and helper
+ * objects
+ *
+ * @return {@link Looper} for the handler thread
+ */
+ @VisibleForTesting
+ public Looper getHandlerLooper() {
+ if (mHandlerThread == null) {
+ return null;
+ }
+ return mHandlerThread.getLooper();
+ }
+
+ /**
+ * Start and initialize the DatabaseManager
+ *
+ * @param database the Bluetooth storage {@link MetadataDatabase}
+ */
+ public void start(MetadataDatabase database) {
+ if (DBG) {
+ Log.d(TAG, "start()");
+ }
+
+ if (mAdapterService == null) {
+ Log.e(TAG, "stat failed, mAdapterService is null.");
+ return;
+ }
+
+ if (database == null) {
+ Log.e(TAG, "stat failed, database is null.");
+ return;
+ }
+
+ mDatabase = database;
+
+ mHandlerThread = new HandlerThread("BluetoothDatabaseManager");
+ mHandlerThread.start();
+ mHandler = new DatabaseHandler(mHandlerThread.getLooper());
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+ mAdapterService.registerReceiver(mReceiver, filter);
+
+ loadDatabase();
+ }
+
+ String getDatabaseAbsolutePath() {
+ //TODO backup database when Bluetooth turn off and FOTA?
+ return mAdapterService.getDatabasePath(MetadataDatabase.DATABASE_NAME)
+ .getAbsolutePath();
+ }
+
+ /**
+ * Clear all persistence data in database
+ */
+ public void factoryReset() {
+ Log.w(TAG, "factoryReset");
+ Message message = mHandler.obtainMessage(MSG_CLEAR_DATABASE);
+ mHandler.sendMessage(message);
+ }
+
+ /**
+ * Close and de-init the DatabaseManager
+ */
+ public void cleanup() {
+ removeUnusedMetadata();
+ mAdapterService.unregisterReceiver(mReceiver);
+ if (mHandlerThread != null) {
+ mHandlerThread.quit();
+ mHandlerThread = null;
+ }
+ mMetadataCache.clear();
+ }
+
+ void createMetadata(String address) {
+ if (VERBOSE) {
+ Log.v(TAG, "createMetadata " + address);
+ }
+ Metadata data = new Metadata(address);
+ mMetadataCache.put(address, data);
+ updateDatabase(data);
+ }
+
+ @VisibleForTesting
+ void removeUnusedMetadata() {
+ BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
+ synchronized (mMetadataCache) {
+ mMetadataCache.forEach((address, metadata) -> {
+ if (!address.equals(LOCAL_STORAGE)
+ && !Arrays.asList(bondedDevices).stream().anyMatch(device ->
+ address.equals(device.getAddress()))) {
+ List<Integer> list = metadata.getChangedCustomizedMeta();
+ for (int key : list) {
+ mAdapterService.metadataChanged(address, key, null);
+ }
+ Log.i(TAG, "remove unpaired device from database " + address);
+ deleteDatabase(mMetadataCache.get(address));
+ }
+ });
+ }
+ }
+
+ void cacheMetadata(List<Metadata> list) {
+ synchronized (mMetadataCache) {
+ Log.i(TAG, "cacheMetadata");
+ // Unlock the main thread.
+ mSemaphore.release();
+
+ if (!isMigrated(list)) {
+ // Wait for data migrate from Settings Global
+ mMigratedFromSettingsGlobal = false;
+ return;
+ }
+ mMigratedFromSettingsGlobal = true;
+ for (Metadata data : list) {
+ String address = data.getAddress();
+ if (VERBOSE) {
+ Log.v(TAG, "cacheMetadata: found device " + address);
+ }
+ mMetadataCache.put(address, data);
+ }
+ if (VERBOSE) {
+ Log.v(TAG, "cacheMetadata: Database is ready");
+ }
+ }
+ }
+
+ boolean isMigrated(List<Metadata> list) {
+ for (Metadata data : list) {
+ String address = data.getAddress();
+ if (address.equals(LOCAL_STORAGE) && data.migrated) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void migrateSettingsGlobal() {
+ Log.i(TAG, "migrateSettingGlobal");
+
+ BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
+ ContentResolver contentResolver = mAdapterService.getContentResolver();
+
+ for (BluetoothDevice device : bondedDevices) {
+ int a2dpPriority = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothA2dpSinkPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ int a2dpSinkPriority = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothA2dpSrcPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ int hearingaidPriority = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothHearingAidPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ int headsetPriority = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothHeadsetPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ int headsetClientPriority = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothHeadsetPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ int hidHostPriority = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothHidHostPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ int mapPriority = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothMapPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ int mapClientPriority = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothMapClientPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ int panPriority = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothPanPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ int pbapPriority = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothPbapClientPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ int pbapClientPriority = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothPbapClientPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ int sapPriority = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothSapPriorityKey(device.getAddress()),
+ BluetoothProfile.PRIORITY_UNDEFINED);
+ int a2dpSupportsOptionalCodec = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothA2dpSupportsOptionalCodecsKey(device.getAddress()),
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN);
+ int a2dpOptionalCodecEnabled = Settings.Global.getInt(contentResolver,
+ Settings.Global.getBluetoothA2dpOptionalCodecsEnabledKey(device.getAddress()),
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN);
+
+ String address = device.getAddress();
+ Metadata data = new Metadata(address);
+ data.setProfilePriority(BluetoothProfile.A2DP, a2dpPriority);
+ data.setProfilePriority(BluetoothProfile.A2DP_SINK, a2dpSinkPriority);
+ data.setProfilePriority(BluetoothProfile.HEADSET, headsetPriority);
+ data.setProfilePriority(BluetoothProfile.HEADSET_CLIENT, headsetClientPriority);
+ data.setProfilePriority(BluetoothProfile.HID_HOST, hidHostPriority);
+ data.setProfilePriority(BluetoothProfile.PAN, panPriority);
+ data.setProfilePriority(BluetoothProfile.PBAP, pbapPriority);
+ data.setProfilePriority(BluetoothProfile.PBAP_CLIENT, pbapClientPriority);
+ data.setProfilePriority(BluetoothProfile.MAP, mapPriority);
+ data.setProfilePriority(BluetoothProfile.MAP_CLIENT, mapClientPriority);
+ data.setProfilePriority(BluetoothProfile.SAP, sapPriority);
+ data.setProfilePriority(BluetoothProfile.HEARING_AID, hearingaidPriority);
+ data.a2dpSupportsOptionalCodecs = a2dpSupportsOptionalCodec;
+ data.a2dpOptionalCodecsEnabled = a2dpOptionalCodecEnabled;
+ mMetadataCache.put(address, data);
+ updateDatabase(data);
+ }
+
+ // Mark database migrated from Settings Global
+ Metadata localData = new Metadata(LOCAL_STORAGE);
+ localData.migrated = true;
+ mMetadataCache.put(LOCAL_STORAGE, localData);
+ updateDatabase(localData);
+
+ // Reload database after migration is completed
+ loadDatabase();
+
+ }
+
+ private void loadDatabase() {
+ if (DBG) {
+ Log.d(TAG, "Load Database");
+ }
+ Message message = mHandler.obtainMessage(MSG_LOAD_DATABASE);
+ mHandler.sendMessage(message);
+ try {
+ // Lock the thread until handler thread finish loading database.
+ mSemaphore.tryAcquire(LOAD_DATABASE_TIMEOUT, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "loadDatabase: semaphore acquire failed");
+ }
+ }
+
+ private void updateDatabase(Metadata data) {
+ if (data.getAddress() == null) {
+ Log.e(TAG, "updateDatabase: address is null");
+ return;
+ }
+ if (DBG) {
+ Log.d(TAG, "updateDatabase " + data.getAddress());
+ }
+ Message message = mHandler.obtainMessage(MSG_UPDATE_DATABASE);
+ message.obj = data;
+ mHandler.sendMessage(message);
+ }
+
+ private void deleteDatabase(Metadata data) {
+ if (data.getAddress() == null) {
+ Log.e(TAG, "deleteDatabase: address is null");
+ return;
+ }
+ Log.d(TAG, "deleteDatabase: " + data.getAddress());
+ Message message = mHandler.obtainMessage(MSG_DELETE_DATABASE);
+ message.obj = data.getAddress();
+ mHandler.sendMessage(message);
+ }
+
+ private void logManufacturerInfo(BluetoothDevice device, int key, byte[] bytesValue) {
+ String callingApp = mAdapterService.getPackageManager().getNameForUid(
+ Binder.getCallingUid());
+ String manufacturerName = "";
+ String modelName = "";
+ String hardwareVersion = "";
+ String softwareVersion = "";
+ String value = Utils.byteArrayToUtf8String(bytesValue);
+ switch (key) {
+ case BluetoothDevice.METADATA_MANUFACTURER_NAME:
+ manufacturerName = value;
+ break;
+ case BluetoothDevice.METADATA_MODEL_NAME:
+ modelName = value;
+ break;
+ case BluetoothDevice.METADATA_HARDWARE_VERSION:
+ hardwareVersion = value;
+ break;
+ case BluetoothDevice.METADATA_SOFTWARE_VERSION:
+ softwareVersion = value;
+ break;
+ default:
+ // Do not log anything if metadata doesn't fall into above categories
+ return;
+ }
+ StatsLog.write(StatsLog.BLUETOOTH_DEVICE_INFO_REPORTED,
+ mAdapterService.obfuscateAddress(device),
+ BluetoothProtoEnums.DEVICE_INFO_EXTERNAL, callingApp, manufacturerName, modelName,
+ hardwareVersion, softwareVersion);
+ }
+}
diff --git a/src/com/android/bluetooth/btservice/storage/Metadata.java b/src/com/android/bluetooth/btservice/storage/Metadata.java
new file mode 100644
index 0000000..b39333f
--- /dev/null
+++ b/src/com/android/bluetooth/btservice/storage/Metadata.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.btservice.storage;
+
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+
+import androidx.annotation.NonNull;
+import androidx.room.Embedded;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Entity(tableName = "metadata")
+class Metadata {
+ @PrimaryKey
+ @NonNull
+ private String address;
+
+ public boolean migrated;
+
+ @Embedded
+ public ProfilePrioritiesEntity profilePriorities;
+
+ @Embedded
+ @NonNull
+ public CustomizedMetadataEntity publicMetadata;
+
+ public int a2dpSupportsOptionalCodecs;
+ public int a2dpOptionalCodecsEnabled;
+
+ Metadata(String address) {
+ this.address = address;
+ migrated = false;
+ profilePriorities = new ProfilePrioritiesEntity();
+ publicMetadata = new CustomizedMetadataEntity();
+ a2dpSupportsOptionalCodecs = BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN;
+ a2dpOptionalCodecsEnabled = BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN;
+ }
+
+ String getAddress() {
+ return address;
+ }
+
+ void setProfilePriority(int profile, int priority) {
+ switch (profile) {
+ case BluetoothProfile.A2DP:
+ profilePriorities.a2dp_priority = priority;
+ break;
+ case BluetoothProfile.A2DP_SINK:
+ profilePriorities.a2dp_sink_priority = priority;
+ break;
+ case BluetoothProfile.HEADSET:
+ profilePriorities.hfp_priority = priority;
+ break;
+ case BluetoothProfile.HEADSET_CLIENT:
+ profilePriorities.hfp_client_priority = priority;
+ break;
+ case BluetoothProfile.HID_HOST:
+ profilePriorities.hid_host_priority = priority;
+ break;
+ case BluetoothProfile.PAN:
+ profilePriorities.pan_priority = priority;
+ break;
+ case BluetoothProfile.PBAP:
+ profilePriorities.pbap_priority = priority;
+ break;
+ case BluetoothProfile.PBAP_CLIENT:
+ profilePriorities.pbap_client_priority = priority;
+ break;
+ case BluetoothProfile.MAP:
+ profilePriorities.map_priority = priority;
+ break;
+ case BluetoothProfile.MAP_CLIENT:
+ profilePriorities.map_client_priority = priority;
+ break;
+ case BluetoothProfile.SAP:
+ profilePriorities.sap_priority = priority;
+ break;
+ case BluetoothProfile.HEARING_AID:
+ profilePriorities.hearing_aid_priority = priority;
+ break;
+ default:
+ throw new IllegalArgumentException("invalid profile " + profile);
+ }
+ }
+
+ int getProfilePriority(int profile) {
+ switch (profile) {
+ case BluetoothProfile.A2DP:
+ return profilePriorities.a2dp_priority;
+ case BluetoothProfile.A2DP_SINK:
+ return profilePriorities.a2dp_sink_priority;
+ case BluetoothProfile.HEADSET:
+ return profilePriorities.hfp_priority;
+ case BluetoothProfile.HEADSET_CLIENT:
+ return profilePriorities.hfp_client_priority;
+ case BluetoothProfile.HID_HOST:
+ return profilePriorities.hid_host_priority;
+ case BluetoothProfile.PAN:
+ return profilePriorities.pan_priority;
+ case BluetoothProfile.PBAP:
+ return profilePriorities.pbap_priority;
+ case BluetoothProfile.PBAP_CLIENT:
+ return profilePriorities.pbap_client_priority;
+ case BluetoothProfile.MAP:
+ return profilePriorities.map_priority;
+ case BluetoothProfile.MAP_CLIENT:
+ return profilePriorities.map_client_priority;
+ case BluetoothProfile.SAP:
+ return profilePriorities.sap_priority;
+ case BluetoothProfile.HEARING_AID:
+ return profilePriorities.hearing_aid_priority;
+ }
+ return BluetoothProfile.PRIORITY_UNDEFINED;
+ }
+
+ void setCustomizedMeta(int key, byte[] value) {
+ switch (key) {
+ case BluetoothDevice.METADATA_MANUFACTURER_NAME:
+ publicMetadata.manufacturer_name = value;
+ break;
+ case BluetoothDevice.METADATA_MODEL_NAME:
+ publicMetadata.model_name = value;
+ break;
+ case BluetoothDevice.METADATA_SOFTWARE_VERSION:
+ publicMetadata.software_version = value;
+ break;
+ case BluetoothDevice.METADATA_HARDWARE_VERSION:
+ publicMetadata.hardware_version = value;
+ break;
+ case BluetoothDevice.METADATA_COMPANION_APP:
+ publicMetadata.companion_app = value;
+ break;
+ case BluetoothDevice.METADATA_MAIN_ICON:
+ publicMetadata.main_icon = value;
+ break;
+ case BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET:
+ publicMetadata.is_untethered_headset = value;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON:
+ publicMetadata.untethered_left_icon = value;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON:
+ publicMetadata.untethered_right_icon = value;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_CASE_ICON:
+ publicMetadata.untethered_case_icon = value;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY:
+ publicMetadata.untethered_left_battery = value;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY:
+ publicMetadata.untethered_right_battery = value;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY:
+ publicMetadata.untethered_case_battery = value;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING:
+ publicMetadata.untethered_left_charging = value;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING:
+ publicMetadata.untethered_right_charging = value;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING:
+ publicMetadata.untethered_case_charging = value;
+ break;
+ case BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI:
+ publicMetadata.enhanced_settings_ui_uri = value;
+ break;
+ }
+ }
+
+ byte[] getCustomizedMeta(int key) {
+ byte[] value = null;
+ switch (key) {
+ case BluetoothDevice.METADATA_MANUFACTURER_NAME:
+ value = publicMetadata.manufacturer_name;
+ break;
+ case BluetoothDevice.METADATA_MODEL_NAME:
+ value = publicMetadata.model_name;
+ break;
+ case BluetoothDevice.METADATA_SOFTWARE_VERSION:
+ value = publicMetadata.software_version;
+ break;
+ case BluetoothDevice.METADATA_HARDWARE_VERSION:
+ value = publicMetadata.hardware_version;
+ break;
+ case BluetoothDevice.METADATA_COMPANION_APP:
+ value = publicMetadata.companion_app;
+ break;
+ case BluetoothDevice.METADATA_MAIN_ICON:
+ value = publicMetadata.main_icon;
+ break;
+ case BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET:
+ value = publicMetadata.is_untethered_headset;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON:
+ value = publicMetadata.untethered_left_icon;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON:
+ value = publicMetadata.untethered_right_icon;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_CASE_ICON:
+ value = publicMetadata.untethered_case_icon;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY:
+ value = publicMetadata.untethered_left_battery;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY:
+ value = publicMetadata.untethered_right_battery;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY:
+ value = publicMetadata.untethered_case_battery;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING:
+ value = publicMetadata.untethered_left_charging;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING:
+ value = publicMetadata.untethered_right_charging;
+ break;
+ case BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING:
+ value = publicMetadata.untethered_case_charging;
+ break;
+ case BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI:
+ value = publicMetadata.enhanced_settings_ui_uri;
+ break;
+ }
+ return value;
+ }
+
+ List<Integer> getChangedCustomizedMeta() {
+ List<Integer> list = new ArrayList<>();
+ if (publicMetadata.manufacturer_name != null) {
+ list.add(BluetoothDevice.METADATA_MANUFACTURER_NAME);
+ }
+ if (publicMetadata.model_name != null) {
+ list.add(BluetoothDevice.METADATA_MODEL_NAME);
+ }
+ if (publicMetadata.software_version != null) {
+ list.add(BluetoothDevice.METADATA_SOFTWARE_VERSION);
+ }
+ if (publicMetadata.hardware_version != null) {
+ list.add(BluetoothDevice.METADATA_HARDWARE_VERSION);
+ }
+ if (publicMetadata.companion_app != null) {
+ list.add(BluetoothDevice.METADATA_COMPANION_APP);
+ }
+ if (publicMetadata.main_icon != null) {
+ list.add(BluetoothDevice.METADATA_MAIN_ICON);
+ }
+ if (publicMetadata.is_untethered_headset != null) {
+ list.add(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET);
+ }
+ if (publicMetadata.untethered_left_icon != null) {
+ list.add(BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON);
+ }
+ if (publicMetadata.untethered_right_icon != null) {
+ list.add(BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON);
+ }
+ if (publicMetadata.untethered_case_icon != null) {
+ list.add(BluetoothDevice.METADATA_UNTETHERED_CASE_ICON);
+ }
+ if (publicMetadata.untethered_left_battery != null) {
+ list.add(BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY);
+ }
+ if (publicMetadata.untethered_right_battery != null) {
+ list.add(BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY);
+ }
+ if (publicMetadata.untethered_case_battery != null) {
+ list.add(BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY);
+ }
+ if (publicMetadata.untethered_left_charging != null) {
+ list.add(BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING);
+ }
+ if (publicMetadata.untethered_right_charging != null) {
+ list.add(BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING);
+ }
+ if (publicMetadata.untethered_case_charging != null) {
+ list.add(BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING);
+ }
+ if (publicMetadata.enhanced_settings_ui_uri != null) {
+ list.add(BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI);
+ }
+ return list;
+ }
+}
diff --git a/src/com/android/bluetooth/btservice/storage/MetadataDao.java b/src/com/android/bluetooth/btservice/storage/MetadataDao.java
new file mode 100644
index 0000000..7c5f440
--- /dev/null
+++ b/src/com/android/bluetooth/btservice/storage/MetadataDao.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.btservice.storage;
+
+import androidx.room.Dao;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+
+import java.util.List;
+
+@Dao
+interface MetadataDao {
+ /**
+ * Load all items in the database
+ */
+ @Query("SELECT * FROM metadata")
+ List<Metadata> load();
+
+ /**
+ * Create or update a Metadata in the database
+ */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ void insert(Metadata... metadata);
+
+ /**
+ * Delete a Metadata in the database
+ */
+ @Query("DELETE FROM metadata WHERE address = :address")
+ void delete(String address);
+
+ /**
+ * Delete all Metadatas in the database
+ */
+ @Query("DELETE FROM metadata")
+ void deleteAll();
+}
diff --git a/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java b/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
new file mode 100644
index 0000000..fe56e3a
--- /dev/null
+++ b/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.btservice.storage;
+
+import android.content.Context;
+
+import androidx.room.Database;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.List;
+
+/**
+ * MetadataDatabase is a Room database stores Bluetooth persistence data
+ */
+@Database(entities = {Metadata.class}, version = 102)
+public abstract class MetadataDatabase extends RoomDatabase {
+ /**
+ * The database file name
+ */
+ public static final String DATABASE_NAME = "bluetooth_db";
+
+ protected abstract MetadataDao mMetadataDao();
+
+ /**
+ * Create a {@link MetadataDatabase} database with migrations
+ *
+ * @param context the Context to create database
+ * @return the created {@link MetadataDatabase}
+ */
+ public static MetadataDatabase createDatabase(Context context) {
+ return Room.databaseBuilder(context,
+ MetadataDatabase.class, DATABASE_NAME)
+ .addMigrations(MIGRATION_100_101)
+ .addMigrations(MIGRATION_101_102)
+ .build();
+ }
+
+ /**
+ * Create a {@link MetadataDatabase} database without migration, database
+ * would be reset if any load failure happens
+ *
+ * @param context the Context to create database
+ * @return the created {@link MetadataDatabase}
+ */
+ public static MetadataDatabase createDatabaseWithoutMigration(Context context) {
+ return Room.databaseBuilder(context,
+ MetadataDatabase.class, DATABASE_NAME)
+ .fallbackToDestructiveMigration()
+ .build();
+ }
+
+ /**
+ * Insert a {@link Metadata} to database
+ *
+ * @param metadata the data wish to put into storage
+ */
+ public void insert(Metadata... metadata) {
+ mMetadataDao().insert(metadata);
+ }
+
+ /**
+ * Load all data from database as a {@link List} of {@link Metadata}
+ *
+ * @return a {@link List} of {@link Metadata}
+ */
+ public List<Metadata> load() {
+ return mMetadataDao().load();
+ }
+
+ /**
+ * Delete one of the {@link Metadata} contains in database
+ *
+ * @param address the address of Metadata to delete
+ */
+ public void delete(String address) {
+ mMetadataDao().delete(address);
+ }
+
+ /**
+ * Clear database.
+ */
+ public void deleteAll() {
+ mMetadataDao().deleteAll();
+ }
+
+ @VisibleForTesting
+ static final Migration MIGRATION_100_101 = new Migration(100, 101) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ database.execSQL("ALTER TABLE metadata ADD COLUMN `pbap_client_priority` INTEGER");
+ }
+ };
+
+ @VisibleForTesting
+ static final Migration MIGRATION_101_102 = new Migration(101, 102) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS `metadata_tmp` ("
+ + "`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, "
+ + "`a2dpSupportsOptionalCodecs` INTEGER NOT NULL, "
+ + "`a2dpOptionalCodecsEnabled` INTEGER NOT NULL, "
+ + "`a2dp_priority` INTEGER, `a2dp_sink_priority` INTEGER, "
+ + "`hfp_priority` INTEGER, `hfp_client_priority` INTEGER, "
+ + "`hid_host_priority` INTEGER, `pan_priority` INTEGER, "
+ + "`pbap_priority` INTEGER, `pbap_client_priority` INTEGER, "
+ + "`map_priority` INTEGER, `sap_priority` INTEGER, "
+ + "`hearing_aid_priority` INTEGER, `map_client_priority` INTEGER, "
+ + "`manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, "
+ + "`hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, "
+ + "`is_untethered_headset` BLOB, `untethered_left_icon` BLOB, "
+ + "`untethered_right_icon` BLOB, `untethered_case_icon` BLOB, "
+ + "`untethered_left_battery` BLOB, `untethered_right_battery` BLOB, "
+ + "`untethered_case_battery` BLOB, `untethered_left_charging` BLOB, "
+ + "`untethered_right_charging` BLOB, `untethered_case_charging` BLOB, "
+ + "`enhanced_settings_ui_uri` BLOB, PRIMARY KEY(`address`))");
+
+ database.execSQL("INSERT INTO metadata_tmp ("
+ + "address, migrated, a2dpSupportsOptionalCodecs, a2dpOptionalCodecsEnabled, "
+ + "a2dp_priority, a2dp_sink_priority, hfp_priority, hfp_client_priority, "
+ + "hid_host_priority, pan_priority, pbap_priority, pbap_client_priority, "
+ + "map_priority, sap_priority, hearing_aid_priority, map_client_priority, "
+ + "manufacturer_name, model_name, software_version, hardware_version, "
+ + "companion_app, main_icon, is_untethered_headset, untethered_left_icon, "
+ + "untethered_right_icon, untethered_case_icon, untethered_left_battery, "
+ + "untethered_right_battery, untethered_case_battery, "
+ + "untethered_left_charging, untethered_right_charging, "
+ + "untethered_case_charging, enhanced_settings_ui_uri) "
+ + "SELECT "
+ + "address, migrated, a2dpSupportsOptionalCodecs, a2dpOptionalCodecsEnabled, "
+ + "a2dp_priority, a2dp_sink_priority, hfp_priority, hfp_client_priority, "
+ + "hid_host_priority, pan_priority, pbap_priority, pbap_client_priority, "
+ + "map_priority, sap_priority, hearing_aid_priority, map_client_priority, "
+ + "CAST (manufacturer_name AS BLOB), "
+ + "CAST (model_name AS BLOB), "
+ + "CAST (software_version AS BLOB), "
+ + "CAST (hardware_version AS BLOB), "
+ + "CAST (companion_app AS BLOB), "
+ + "CAST (main_icon AS BLOB), "
+ + "CAST (is_unthethered_headset AS BLOB), "
+ + "CAST (unthethered_left_icon AS BLOB), "
+ + "CAST (unthethered_right_icon AS BLOB), "
+ + "CAST (unthethered_case_icon AS BLOB), "
+ + "CAST (unthethered_left_battery AS BLOB), "
+ + "CAST (unthethered_right_battery AS BLOB), "
+ + "CAST (unthethered_case_battery AS BLOB), "
+ + "CAST (unthethered_left_charging AS BLOB), "
+ + "CAST (unthethered_right_charging AS BLOB), "
+ + "CAST (unthethered_case_charging AS BLOB), "
+ + "CAST (enhanced_settings_ui_uri AS BLOB)"
+ + "FROM metadata");
+
+ database.execSQL("DROP TABLE `metadata`");
+ database.execSQL("ALTER TABLE `metadata_tmp` RENAME TO `metadata`");
+ }
+ };
+}
diff --git a/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java b/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java
new file mode 100644
index 0000000..f4ea719
--- /dev/null
+++ b/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.btservice.storage;
+
+import android.bluetooth.BluetoothProfile;
+
+import androidx.room.Entity;
+
+@Entity
+class ProfilePrioritiesEntity {
+ /* Bluetooth profile priorities*/
+ public int a2dp_priority;
+ public int a2dp_sink_priority;
+ public int hfp_priority;
+ public int hfp_client_priority;
+ public int hid_host_priority;
+ public int pan_priority;
+ public int pbap_priority;
+ public int pbap_client_priority;
+ public int map_priority;
+ public int sap_priority;
+ public int hearing_aid_priority;
+ public int map_client_priority;
+
+ ProfilePrioritiesEntity() {
+ a2dp_priority = BluetoothProfile.PRIORITY_UNDEFINED;
+ a2dp_sink_priority = BluetoothProfile.PRIORITY_UNDEFINED;
+ hfp_priority = BluetoothProfile.PRIORITY_UNDEFINED;
+ hfp_client_priority = BluetoothProfile.PRIORITY_UNDEFINED;
+ hid_host_priority = BluetoothProfile.PRIORITY_UNDEFINED;
+ pan_priority = BluetoothProfile.PRIORITY_UNDEFINED;
+ pbap_priority = BluetoothProfile.PRIORITY_UNDEFINED;
+ pbap_client_priority = BluetoothProfile.PRIORITY_UNDEFINED;
+ map_priority = BluetoothProfile.PRIORITY_UNDEFINED;
+ sap_priority = BluetoothProfile.PRIORITY_UNDEFINED;
+ hearing_aid_priority = BluetoothProfile.PRIORITY_UNDEFINED;
+ map_client_priority = BluetoothProfile.PRIORITY_UNDEFINED;
+ }
+}
diff --git a/src/com/android/bluetooth/gatt/AppScanStats.java b/src/com/android/bluetooth/gatt/AppScanStats.java
index dd499f2..ec8b1f8 100644
--- a/src/com/android/bluetooth/gatt/AppScanStats.java
+++ b/src/com/android/bluetooth/gatt/AppScanStats.java
@@ -122,16 +122,17 @@
synchronized void addResult(int scannerId) {
LastScan scan = getScanFromScannerId(scannerId);
if (scan != null) {
- int batteryStatsResults = ++scan.results;
+ scan.results++;
// Only update battery stats after receiving 100 new results in order
// to lower the cost of the binder transaction
- if (batteryStatsResults % 100 == 0) {
+ if (scan.results % 100 == 0) {
try {
mBatteryStats.noteBleScanResults(mWorkSource, 100);
} catch (RemoteException e) {
/* ignore */
}
+ StatsLog.write(StatsLog.BLE_SCAN_RESULT_RECEIVED, mWorkSource, 100);
}
}
@@ -178,7 +179,9 @@
} catch (RemoteException e) {
/* ignore */
}
- writeToStatsLog(scan, StatsLog.BLE_SCAN_STATE_CHANGED__STATE__ON);
+ StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED, mWorkSource,
+ StatsLog.BLE_SCAN_STATE_CHANGED__STATE__ON,
+ scan.filtered, scan.background, scan.opportunistic);
mOngoingScans.put(scannerId, scan);
}
@@ -221,15 +224,17 @@
}
try {
- // Inform battery stats of any results it might be missing on
- // scan stop
+ // Inform battery stats of any results it might be missing on scan stop
boolean isUnoptimized = !(scan.filtered || scan.background || scan.opportunistic);
mBatteryStats.noteBleScanResults(mWorkSource, scan.results % 100);
mBatteryStats.noteBleScanStopped(mWorkSource, isUnoptimized);
} catch (RemoteException e) {
/* ignore */
}
- writeToStatsLog(scan, StatsLog.BLE_SCAN_STATE_CHANGED__STATE__OFF);
+ StatsLog.write(StatsLog.BLE_SCAN_RESULT_RECEIVED, mWorkSource, scan.results % 100);
+ StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED, mWorkSource,
+ StatsLog.BLE_SCAN_STATE_CHANGED__STATE__OFF,
+ scan.filtered, scan.background, scan.opportunistic);
}
synchronized void recordScanSuspend(int scannerId) {
@@ -254,24 +259,6 @@
mTotalSuspendTime += suspendDuration;
}
- private void writeToStatsLog(LastScan scan, int statsLogState) {
- for (int i = 0; i < mWorkSource.size(); i++) {
- StatsLog.write_non_chained(StatsLog.BLE_SCAN_STATE_CHANGED,
- mWorkSource.get(i), null,
- statsLogState, scan.filtered, scan.background, scan.opportunistic);
- }
-
- final List<WorkSource.WorkChain> workChains = mWorkSource.getWorkChains();
- if (workChains != null) {
- for (int i = 0; i < workChains.size(); ++i) {
- WorkSource.WorkChain workChain = workChains.get(i);
- StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED,
- workChain.getUids(), workChain.getTags(),
- statsLogState, scan.filtered, scan.background, scan.opportunistic);
- }
- }
- }
-
synchronized void setScanTimeout(int scannerId) {
if (!isScanning()) {
return;
diff --git a/src/com/android/bluetooth/gatt/ContextMap.java b/src/com/android/bluetooth/gatt/ContextMap.java
index af59262..d54a25a 100644
--- a/src/com/android/bluetooth/gatt/ContextMap.java
+++ b/src/com/android/bluetooth/gatt/ContextMap.java
@@ -20,6 +20,7 @@
import android.os.IInterface;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.UserHandle;
import android.os.WorkSource;
import android.util.Log;
@@ -86,10 +87,22 @@
public Boolean isCongested = false;
/** Whether the calling app has location permission */
- boolean hasLocationPermisson;
+ boolean hasLocationPermission;
- /** Whether the calling app has peers mac address permission */
- boolean hasPeersMacAddressPermission;
+ /** Whether the calling app has bluetooth privileged permission */
+ boolean hasBluetoothPrivilegedPermission;
+
+ /** The user handle of the app that started the scan */
+ UserHandle mUserHandle;
+
+ /** Whether the calling app is targeting Q or better */
+ boolean mIsQApp;
+
+ /** Whether the calling app has the network settings permission */
+ boolean mHasNetworkSettingsPermission;
+
+ /** Whether the calling app has the network setup wizard permission */
+ boolean mHasNetworkSetupWizardPermission;
/** Internal callback info queue, waiting to be send on congestion clear */
private List<CallbackInfo> mCongestionQueue = new ArrayList<CallbackInfo>();
diff --git a/src/com/android/bluetooth/gatt/GattService.java b/src/com/android/bluetooth/gatt/GattService.java
index fd8551a..8a3a1aa 100644
--- a/src/com/android/bluetooth/gatt/GattService.java
+++ b/src/com/android/bluetooth/gatt/GattService.java
@@ -50,8 +50,8 @@
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.UserHandle;
import android.os.WorkSource;
-import android.provider.Settings;
import android.util.Log;
import com.android.bluetooth.BluetoothMetricsProto;
@@ -63,6 +63,7 @@
import com.android.bluetooth.util.NumberUtils;
import com.android.internal.annotations.VisibleForTesting;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -98,6 +99,9 @@
private static final int ET_LEGACY_MASK = 0x10;
+ private static final UUID HID_SERVICE_UUID =
+ UUID.fromString("00001812-0000-1000-8000-00805F9B34FB");
+
private static final UUID[] HID_UUIDS = {
UUID.fromString("00002A4A-0000-1000-8000-00805F9B34FB"),
UUID.fromString("00002A4B-0000-1000-8000-00805F9B34FB"),
@@ -105,9 +109,11 @@
UUID.fromString("00002A4D-0000-1000-8000-00805F9B34FB")
};
- private static final UUID[] FIDO_UUIDS = {
- UUID.fromString("0000FFFD-0000-1000-8000-00805F9B34FB") // U2F
- };
+ private static final UUID ANDROID_TV_REMOTE_SERVICE_UUID =
+ UUID.fromString("AB5E0001-5A21-4F05-BC7D-AF01F617B664");
+
+ private static final UUID FIDO_SERVICE_UUID =
+ UUID.fromString("0000FFFD-0000-1000-8000-00805F9B34FB"); // U2F
/**
* Keep the arguments passed in for the PendingIntent.
@@ -157,13 +163,17 @@
private int mMaxScanFilters;
private static final int NUM_SCAN_EVENTS_KEPT = 20;
+
/**
* Internal list of scan events to use with the proto
*/
- private final ArrayList<BluetoothMetricsProto.ScanEvent> mScanEvents =
- new ArrayList<>(NUM_SCAN_EVENTS_KEPT);
+ private final ArrayDeque<BluetoothMetricsProto.ScanEvent> mScanEvents =
+ new ArrayDeque<>(NUM_SCAN_EVENTS_KEPT);
- private final Map<Integer, List<BluetoothGattService>> mGattClientDatabases = new HashMap<>();
+ /**
+ * Set of restricted (which require a BLUETOOTH_PRIVILEGED permission) handles per connectionId.
+ */
+ private final Map<Integer, Set<Integer>> mRestrictedHandles = new HashMap<>();
private BluetoothAdapter mAdapter;
private AdvertiseManager mAdvertiseManager;
@@ -274,36 +284,34 @@
sGattService = instance;
}
- boolean permissionCheck(UUID uuid) {
- return !(isRestrictedCharUuid(uuid) && (0 != checkCallingOrSelfPermission(
- BLUETOOTH_PRIVILEGED)));
+ private boolean permissionCheck(UUID characteristicUuid) {
+ return !isHidCharUuid(characteristicUuid)
+ || (checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)
+ == PERMISSION_GRANTED);
}
- boolean permissionCheck(int connId, int handle) {
- List<BluetoothGattService> db = mGattClientDatabases.get(connId);
- if (db == null) {
+ private boolean permissionCheck(int connId, int handle) {
+ Set<Integer> restrictedHandles = mRestrictedHandles.get(connId);
+ if (restrictedHandles == null || !restrictedHandles.contains(handle)) {
return true;
}
- for (BluetoothGattService service : db) {
- for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
- if (handle == characteristic.getInstanceId()) {
- return !((isRestrictedCharUuid(characteristic.getUuid())
- || isRestrictedSrvcUuid(service.getUuid()))
- && (0 != checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)));
- }
+ return (checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)
+ == PERMISSION_GRANTED);
+ }
- for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) {
- if (handle == descriptor.getInstanceId()) {
- return !((isRestrictedCharUuid(characteristic.getUuid())
- || isRestrictedSrvcUuid(service.getUuid())) && (0
- != checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)));
- }
- }
- }
+ private boolean permissionCheck(ClientMap.App app, int connId, int handle) {
+ Set<Integer> restrictedHandles = mRestrictedHandles.get(connId);
+ if (restrictedHandles == null || !restrictedHandles.contains(handle)) {
+ return true;
}
- return true;
+ if (!app.hasBluetoothPrivilegedPermission
+ && checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED)== PERMISSION_GRANTED) {
+ app.hasBluetoothPrivilegedPermission = true;
+ }
+
+ return app.hasBluetoothPrivilegedPermission;
}
@Override
@@ -1000,8 +1008,7 @@
txPower, rssi, periodicAdvInt,
ScanRecord.parseFromBytes(scanRecordData),
SystemClock.elapsedRealtimeNanos());
- // Do no report if location mode is OFF or the client has no location permission
- // PEERS_MAC_ADDRESS permission holders always get results
+ // Do not report if location mode is OFF or the client has no location permission
if (!hasScanResultPermission(client) || !matchesFilters(client, result)) {
continue;
}
@@ -1088,15 +1095,10 @@
/** Determines if the given scan client has the appropriate permissions to receive callbacks. */
private boolean hasScanResultPermission(final ScanClient client) {
- final boolean requiresLocationEnabled =
- getResources().getBoolean(R.bool.strict_location_check);
- final boolean locationEnabledSetting =
- Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE,
- Settings.Secure.LOCATION_MODE_OFF) != Settings.Secure.LOCATION_MODE_OFF;
- final boolean locationEnabled =
- !requiresLocationEnabled || locationEnabledSetting || client.legacyForegroundApp;
- return (client.hasPeersMacAddressPermission || (client.hasLocationPermission
- && locationEnabled));
+ if (client.hasNetworkSettingsPermission || client.hasNetworkSetupWizardPermission) {
+ return true;
+ }
+ return client.hasLocationPermission && !Utils.blockedByLocationOff(this, client.userHandle);
}
// Check if a scan record matches a specific filters.
@@ -1312,9 +1314,13 @@
}
List<BluetoothGattService> dbOut = new ArrayList<BluetoothGattService>();
+ Set<Integer> restrictedIds = new HashSet<>();
BluetoothGattService currSrvc = null;
BluetoothGattCharacteristic currChar = null;
+ boolean isRestrictedSrvc = false;
+ boolean isHidSrvc = false;
+ boolean isRestrictedChar = false;
for (GattDbElement el : db) {
switch (el.type) {
@@ -1326,6 +1332,12 @@
currSrvc = new BluetoothGattService(el.uuid, el.id, el.type);
dbOut.add(currSrvc);
+ isRestrictedSrvc =
+ isFidoSrvcUuid(el.uuid) || isAndroidTvRemoteSrvcUuid(el.uuid);
+ isHidSrvc = isHidSrvcUuid(el.uuid);
+ if (isRestrictedSrvc) {
+ restrictedIds.add(el.id);
+ }
break;
case GattDbElement.TYPE_CHARACTERISTIC:
@@ -1335,6 +1347,10 @@
currChar = new BluetoothGattCharacteristic(el.uuid, el.id, el.properties, 0);
currSrvc.addCharacteristic(currChar);
+ isRestrictedChar = isRestrictedSrvc || (isHidSrvc && isHidCharUuid(el.uuid));
+ if (isRestrictedChar) {
+ restrictedIds.add(el.id);
+ }
break;
case GattDbElement.TYPE_DESCRIPTOR:
@@ -1343,6 +1359,9 @@
}
currChar.addDescriptor(new BluetoothGattDescriptor(el.uuid, el.id, 0));
+ if (isRestrictedChar) {
+ restrictedIds.add(el.id);
+ }
break;
case GattDbElement.TYPE_INCLUDED_SERVICE:
@@ -1361,8 +1380,10 @@
}
}
+ if (!restrictedIds.isEmpty()) {
+ mRestrictedHandles.put(connId, restrictedIds);
+ }
// Search is complete when there was error, or nothing more to process
- mGattClientDatabases.put(connId, dbOut);
app.callback.onSearchComplete(address, dbOut, 0 /* status */);
}
@@ -1383,13 +1404,12 @@
+ data.length);
}
- if (!permissionCheck(connId, handle)) {
- Log.w(TAG, "onNotify() - permission check failed!");
- return;
- }
-
ClientMap.App app = mClientMap.getByConnId(connId);
if (app != null) {
+ if (!permissionCheck(app, connId, handle)) {
+ Log.w(TAG, "onNotify() - permission check failed!");
+ return;
+ }
app.callback.onNotify(address, handle, data);
}
}
@@ -1917,16 +1937,28 @@
if (DBG) {
Log.d(TAG, "start scan with filters");
}
+ UserHandle callingUser = UserHandle.of(UserHandle.getCallingUserId());
enforceAdminPermission();
if (needsPrivilegedPermissionForScan(settings)) {
enforcePrivilegedPermission();
}
final ScanClient scanClient = new ScanClient(scannerId, settings, filters, storages);
- scanClient.hasLocationPermission =
- Utils.checkCallerHasLocationPermission(this, mAppOps, callingPackage);
- scanClient.hasPeersMacAddressPermission =
- Utils.checkCallerHasPeersMacAddressPermission(this);
- scanClient.legacyForegroundApp = Utils.isLegacyForegroundApp(this, callingPackage);
+ scanClient.userHandle = UserHandle.of(UserHandle.getCallingUserId());
+ mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
+ scanClient.isQApp = Utils.isQApp(this, callingPackage);
+ if (scanClient.isQApp) {
+ scanClient.hasLocationPermission =
+ Utils.checkCallerHasFineLocation(
+ this, mAppOps, callingPackage, scanClient.userHandle);
+ } else {
+ scanClient.hasLocationPermission =
+ Utils.checkCallerHasCoarseOrFineLocation(
+ this, mAppOps, callingPackage, scanClient.userHandle);
+ }
+ scanClient.hasNetworkSettingsPermission =
+ Utils.checkCallerHasNetworkSettingsPermission(this);
+ scanClient.hasNetworkSetupWizardPermission =
+ Utils.checkCallerHasNetworkSetupWizardPermission(this);
AppScanStats app = mScannerMap.getAppScanStatsById(scannerId);
if (app != null) {
@@ -1958,19 +1990,25 @@
piInfo.filters = filters;
piInfo.callingPackage = callingPackage;
ScannerMap.App app = mScannerMap.add(uuid, null, null, piInfo, this);
+ app.mUserHandle = UserHandle.of(UserHandle.getCallingUserId());
+ mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
+ app.mIsQApp = Utils.isQApp(this, callingPackage);
try {
- app.hasLocationPermisson =
- Utils.checkCallerHasLocationPermission(this, mAppOps, callingPackage);
+ if (app.mIsQApp) {
+ app.hasLocationPermission = Utils.checkCallerHasFineLocation(
+ this, mAppOps, callingPackage, app.mUserHandle);
+ } else {
+ app.hasLocationPermission = Utils.checkCallerHasCoarseOrFineLocation(
+ this, mAppOps, callingPackage, app.mUserHandle);
+ }
} catch (SecurityException se) {
// No need to throw here. Just mark as not granted.
- app.hasLocationPermisson = false;
+ app.hasLocationPermission = false;
}
- try {
- app.hasPeersMacAddressPermission = Utils.checkCallerHasPeersMacAddressPermission(this);
- } catch (SecurityException se) {
- // No need to throw here. Just mark as not granted.
- app.hasPeersMacAddressPermission = false;
- }
+ app.mHasNetworkSettingsPermission =
+ Utils.checkCallerHasNetworkSettingsPermission(this);
+ app.mHasNetworkSetupWizardPermission =
+ Utils.checkCallerHasNetworkSetupWizardPermission(this);
mScanManager.registerScanner(uuid);
}
@@ -1978,9 +2016,11 @@
final PendingIntentInfo piInfo = app.info;
final ScanClient scanClient =
new ScanClient(scannerId, piInfo.settings, piInfo.filters, null);
- scanClient.hasLocationPermission = app.hasLocationPermisson;
- scanClient.hasPeersMacAddressPermission = app.hasPeersMacAddressPermission;
- scanClient.legacyForegroundApp = Utils.isLegacyForegroundApp(this, piInfo.callingPackage);
+ scanClient.hasLocationPermission = app.hasLocationPermission;
+ scanClient.userHandle = app.mUserHandle;
+ scanClient.isQApp = app.mIsQApp;
+ scanClient.hasNetworkSettingsPermission = app.mHasNetworkSettingsPermission;
+ scanClient.hasNetworkSetupWizardPermission = app.mHasNetworkSetupWizardPermission;
AppScanStats scanStats = mScannerMap.getAppScanStatsById(scannerId);
if (scanStats != null) {
@@ -2457,7 +2497,7 @@
int latency;
// Link supervision timeout is measured in N * 10ms
- int timeout = 2000; // 20s
+ int timeout = 500; // 5s
switch (connectionPriority) {
case BluetoothGatt.CONNECTION_PRIORITY_HIGH:
@@ -2978,15 +3018,11 @@
* Private functions
*************************************************************************/
- private boolean isRestrictedCharUuid(final UUID charUuid) {
- return isHidUuid(charUuid);
+ private boolean isHidSrvcUuid(final UUID uuid) {
+ return HID_SERVICE_UUID.equals(uuid);
}
- private boolean isRestrictedSrvcUuid(final UUID srvcUuid) {
- return isFidoUUID(srvcUuid);
- }
-
- private boolean isHidUuid(final UUID uuid) {
+ private boolean isHidCharUuid(final UUID uuid) {
for (UUID hidUuid : HID_UUIDS) {
if (hidUuid.equals(uuid)) {
return true;
@@ -2995,13 +3031,12 @@
return false;
}
- private boolean isFidoUUID(final UUID uuid) {
- for (UUID fidoUuid : FIDO_UUIDS) {
- if (fidoUuid.equals(uuid)) {
- return true;
- }
- }
- return false;
+ private boolean isAndroidTvRemoteSrvcUuid(final UUID uuid) {
+ return ANDROID_TV_REMOTE_SERVICE_UUID.equals(uuid);
+ }
+
+ private boolean isFidoSrvcUuid(final UUID uuid) {
+ return FIDO_SERVICE_UUID.equals(uuid);
}
private int getDeviceType(BluetoothDevice device) {
@@ -3153,7 +3188,7 @@
void addScanEvent(BluetoothMetricsProto.ScanEvent event) {
synchronized (mScanEvents) {
if (mScanEvents.size() == NUM_SCAN_EVENTS_KEPT) {
- mScanEvents.remove(0);
+ mScanEvents.remove();
}
mScanEvents.add(event);
}
diff --git a/src/com/android/bluetooth/gatt/ScanClient.java b/src/com/android/bluetooth/gatt/ScanClient.java
index f774347..83a034b 100644
--- a/src/com/android/bluetooth/gatt/ScanClient.java
+++ b/src/com/android/bluetooth/gatt/ScanClient.java
@@ -20,6 +20,7 @@
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanSettings;
import android.os.Binder;
+import android.os.UserHandle;
import java.util.List;
import java.util.Objects;
@@ -41,9 +42,10 @@
// App associated with the scan client died.
public boolean appDied;
public boolean hasLocationPermission;
- public boolean hasPeersMacAddressPermission;
- // Pre-M apps are allowed to get scan results even if location is disabled
- public boolean legacyForegroundApp;
+ public UserHandle userHandle;
+ public boolean isQApp;
+ public boolean hasNetworkSettingsPermission;
+ public boolean hasNetworkSetupWizardPermission;
public AppScanStats stats = null;
diff --git a/src/com/android/bluetooth/gatt/ScanFilterQueue.java b/src/com/android/bluetooth/gatt/ScanFilterQueue.java
index 15233e4..6c47711 100644
--- a/src/com/android/bluetooth/gatt/ScanFilterQueue.java
+++ b/src/com/android/bluetooth/gatt/ScanFilterQueue.java
@@ -95,6 +95,15 @@
Entry entry = new Entry();
entry.type = TYPE_SOLICIT_UUID;
entry.uuid = uuid;
+ entry.uuid_mask = new UUID(0, 0);
+ mEntries.add(entry);
+ }
+
+ void addSolicitUuid(UUID uuid, UUID uuidMask) {
+ Entry entry = new Entry();
+ entry.type = TYPE_SOLICIT_UUID;
+ entry.uuid = uuid;
+ entry.uuid_mask = uuidMask;
mEntries.add(entry);
}
@@ -179,6 +188,14 @@
addUuid(filter.getServiceUuid().getUuid(), filter.getServiceUuidMask().getUuid());
}
}
+ if (filter.getServiceSolicitationUuid() != null) {
+ if (filter.getServiceSolicitationUuidMask() == null) {
+ addSolicitUuid(filter.getServiceSolicitationUuid().getUuid());
+ } else {
+ addSolicitUuid(filter.getServiceSolicitationUuid().getUuid(),
+ filter.getServiceSolicitationUuidMask().getUuid());
+ }
+ }
if (filter.getManufacturerData() != null) {
if (filter.getManufacturerDataMask() == null) {
addManufacturerData(filter.getManufacturerId(), filter.getManufacturerData());
diff --git a/src/com/android/bluetooth/gatt/ScanManager.java b/src/com/android/bluetooth/gatt/ScanManager.java
index 5207c69..0736978 100644
--- a/src/com/android/bluetooth/gatt/ScanManager.java
+++ b/src/com/android/bluetooth/gatt/ScanManager.java
@@ -323,7 +323,7 @@
}
final boolean locationEnabled = mLocationManager.isLocationEnabled();
- if (!locationEnabled && !isFiltered && !client.legacyForegroundApp) {
+ if (!locationEnabled && !isFiltered) {
Log.i(TAG, "Cannot start unfiltered scan in location-off. This scan will be"
+ " resumed when location is on: " + client.scannerId);
mSuspendedScanClients.add(client);
@@ -416,7 +416,7 @@
void handleSuspendScans() {
for (ScanClient client : mRegularScanClients) {
if (!mScanNative.isOpportunisticScanClient(client) && (client.filters == null
- || client.filters.isEmpty()) && !client.legacyForegroundApp) {
+ || client.filters.isEmpty())) {
/*Suspend unfiltered scans*/
if (client.stats != null) {
client.stats.recordScanSuspend(client.scannerId);
diff --git a/src/com/android/bluetooth/hdp/HealthService.java b/src/com/android/bluetooth/hdp/HealthService.java
deleted file mode 100644
index 6a4af04..0000000
--- a/src/com/android/bluetooth/hdp/HealthService.java
+++ /dev/null
@@ -1,1014 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.bluetooth.hdp;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothHealth;
-import android.bluetooth.BluetoothHealthAppConfiguration;
-import android.bluetooth.IBluetoothHealth;
-import android.bluetooth.IBluetoothHealthCallback;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.ParcelFileDescriptor;
-import android.os.RemoteException;
-import android.support.annotation.VisibleForTesting;
-import android.util.Log;
-
-import com.android.bluetooth.BluetoothMetricsProto;
-import com.android.bluetooth.Utils;
-import com.android.bluetooth.btservice.MetricsLogger;
-import com.android.bluetooth.btservice.ProfileService;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.NoSuchElementException;
-
-
-/**
- * Provides Bluetooth Health Device profile, as a service in
- * the Bluetooth application.
- * @hide
- */
-public class HealthService extends ProfileService {
- private static final boolean DBG = true;
- private static final boolean VDBG = false;
- private static final String TAG = "HealthService";
-
- private List<HealthChannel> mHealthChannels;
- private Map<BluetoothHealthAppConfiguration, AppInfo> mApps;
- private Map<BluetoothDevice, Integer> mHealthDevices;
- private boolean mNativeAvailable;
- private HealthServiceMessageHandler mHandler;
- private static final int MESSAGE_REGISTER_APPLICATION = 1;
- private static final int MESSAGE_UNREGISTER_APPLICATION = 2;
- private static final int MESSAGE_CONNECT_CHANNEL = 3;
- private static final int MESSAGE_DISCONNECT_CHANNEL = 4;
- private static final int MESSAGE_APP_REGISTRATION_CALLBACK = 11;
- private static final int MESSAGE_CHANNEL_STATE_CALLBACK = 12;
-
- private static HealthService sHealthService;
-
- static {
- classInitNative();
- }
-
- @Override
- protected IProfileServiceBinder initBinder() {
- return new BluetoothHealthBinder(this);
- }
-
- @Override
- protected boolean start() {
- mHealthChannels = Collections.synchronizedList(new ArrayList<HealthChannel>());
- mApps = Collections.synchronizedMap(
- new HashMap<BluetoothHealthAppConfiguration, AppInfo>());
- mHealthDevices = Collections.synchronizedMap(new HashMap<BluetoothDevice, Integer>());
-
- HandlerThread thread = new HandlerThread("BluetoothHdpHandler");
- thread.start();
- Looper looper = thread.getLooper();
- mHandler = new HealthServiceMessageHandler(looper);
- initializeNative();
- mNativeAvailable = true;
- setHealthService(this);
- return true;
- }
-
- @Override
- protected boolean stop() {
- setHealthService(null);
- if (mHandler != null) {
- mHandler.removeCallbacksAndMessages(null);
- Looper looper = mHandler.getLooper();
- if (looper != null) {
- looper.quit();
- }
- }
- cleanupApps();
- return true;
- }
-
- private void cleanupApps() {
- if (mApps != null) {
- Iterator<Map.Entry<BluetoothHealthAppConfiguration, AppInfo>> it =
- mApps.entrySet().iterator();
- while (it.hasNext()) {
- Map.Entry<BluetoothHealthAppConfiguration, AppInfo> entry = it.next();
- AppInfo appInfo = entry.getValue();
- if (appInfo != null) {
- appInfo.cleanup();
- }
- it.remove();
- }
- }
- }
-
- @Override
- protected void cleanup() {
- mHandler = null;
- //Cleanup native
- if (mNativeAvailable) {
- cleanupNative();
- mNativeAvailable = false;
- }
- if (mHealthChannels != null) {
- mHealthChannels.clear();
- }
- if (mHealthDevices != null) {
- mHealthDevices.clear();
- }
- if (mApps != null) {
- mApps.clear();
- }
- }
-
- /**
- * Get a static reference to the current health service instance
- *
- * @return current health service instance
- */
- @VisibleForTesting
- public static synchronized HealthService getHealthService() {
- if (sHealthService == null) {
- Log.w(TAG, "getHealthService(): service is null");
- return null;
- }
- if (!sHealthService.isAvailable()) {
- Log.w(TAG, "getHealthService(): service is not available");
- return null;
- }
- return sHealthService;
- }
-
- private static synchronized void setHealthService(HealthService instance) {
- if (DBG) {
- Log.d(TAG, "setHealthService(): set to: " + instance);
- }
- sHealthService = instance;
- }
-
- private final class HealthServiceMessageHandler extends Handler {
- private HealthServiceMessageHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- if (DBG) {
- Log.d(TAG, "HealthService Handler msg: " + msg.what);
- }
- switch (msg.what) {
- case MESSAGE_REGISTER_APPLICATION: {
- BluetoothHealthAppConfiguration appConfig =
- (BluetoothHealthAppConfiguration) msg.obj;
- AppInfo appInfo = mApps.get(appConfig);
- if (appInfo == null) {
- break;
- }
- int halRole = convertRoleToHal(appConfig.getRole());
- int halChannelType = convertChannelTypeToHal(appConfig.getChannelType());
- if (VDBG) {
- Log.d(TAG, "register datatype: " + appConfig.getDataType() + " role: "
- + halRole + " name: " + appConfig.getName() + " channeltype: "
- + halChannelType);
- }
- int appId = registerHealthAppNative(appConfig.getDataType(), halRole,
- appConfig.getName(), halChannelType);
- if (appId == -1) {
- callStatusCallback(appConfig,
- BluetoothHealth.APP_CONFIG_REGISTRATION_FAILURE);
- appInfo.cleanup();
- mApps.remove(appConfig);
- } else {
- //link to death with a recipient object to implement binderDead()
- appInfo.mRcpObj =
- new BluetoothHealthDeathRecipient(HealthService.this, appConfig);
- IBinder binder = appInfo.mCallback.asBinder();
- try {
- binder.linkToDeath(appInfo.mRcpObj, 0);
- } catch (RemoteException e) {
- Log.e(TAG, "LinktoDeath Exception:" + e);
- }
- appInfo.mAppId = appId;
- callStatusCallback(appConfig,
- BluetoothHealth.APP_CONFIG_REGISTRATION_SUCCESS);
- }
- }
- break;
- case MESSAGE_UNREGISTER_APPLICATION: {
- BluetoothHealthAppConfiguration appConfig =
- (BluetoothHealthAppConfiguration) msg.obj;
- int appId = (mApps.get(appConfig)).mAppId;
- if (!unregisterHealthAppNative(appId)) {
- Log.e(TAG, "Failed to unregister application: id: " + appId);
- callStatusCallback(appConfig,
- BluetoothHealth.APP_CONFIG_UNREGISTRATION_FAILURE);
- }
- }
- break;
- case MESSAGE_CONNECT_CHANNEL: {
- HealthChannel chan = (HealthChannel) msg.obj;
- byte[] devAddr = Utils.getByteAddress(chan.mDevice);
- int appId = (mApps.get(chan.mConfig)).mAppId;
- chan.mChannelId = connectChannelNative(devAddr, appId);
- if (chan.mChannelId == -1) {
- callHealthChannelCallback(chan.mConfig, chan.mDevice,
- BluetoothHealth.STATE_CHANNEL_DISCONNECTING,
- BluetoothHealth.STATE_CHANNEL_DISCONNECTED, chan.mChannelFd,
- chan.mChannelId);
- callHealthChannelCallback(chan.mConfig, chan.mDevice,
- BluetoothHealth.STATE_CHANNEL_DISCONNECTED,
- BluetoothHealth.STATE_CHANNEL_DISCONNECTING, chan.mChannelFd,
- chan.mChannelId);
- }
- }
- break;
- case MESSAGE_DISCONNECT_CHANNEL: {
- HealthChannel chan = (HealthChannel) msg.obj;
- if (!disconnectChannelNative(chan.mChannelId)) {
- callHealthChannelCallback(chan.mConfig, chan.mDevice,
- BluetoothHealth.STATE_CHANNEL_DISCONNECTING,
- BluetoothHealth.STATE_CHANNEL_CONNECTED, chan.mChannelFd,
- chan.mChannelId);
- callHealthChannelCallback(chan.mConfig, chan.mDevice,
- BluetoothHealth.STATE_CHANNEL_CONNECTED,
- BluetoothHealth.STATE_CHANNEL_DISCONNECTING, chan.mChannelFd,
- chan.mChannelId);
- }
- }
- break;
- case MESSAGE_APP_REGISTRATION_CALLBACK: {
- BluetoothHealthAppConfiguration appConfig = findAppConfigByAppId(msg.arg1);
- if (appConfig == null) {
- break;
- }
-
- int regStatus = convertHalRegStatus(msg.arg2);
- callStatusCallback(appConfig, regStatus);
- if (regStatus == BluetoothHealth.APP_CONFIG_REGISTRATION_FAILURE
- || regStatus == BluetoothHealth.APP_CONFIG_UNREGISTRATION_SUCCESS) {
- //unlink to death once app is unregistered
- AppInfo appInfo = mApps.get(appConfig);
- appInfo.cleanup();
- mApps.remove(appConfig);
- }
- }
- break;
- case MESSAGE_CHANNEL_STATE_CALLBACK: {
- ChannelStateEvent channelStateEvent = (ChannelStateEvent) msg.obj;
- HealthChannel chan = findChannelById(channelStateEvent.mChannelId);
- BluetoothHealthAppConfiguration appConfig =
- findAppConfigByAppId(channelStateEvent.mAppId);
- int newState;
- newState = convertHalChannelState(channelStateEvent.mState);
- if (newState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED
- && appConfig == null) {
- Log.e(TAG, "Disconnected for non existing app");
- break;
- }
- if (chan == null) {
- // incoming connection
-
- BluetoothDevice device = getDevice(channelStateEvent.mAddr);
- chan = new HealthChannel(device, appConfig, appConfig.getChannelType());
- chan.mChannelId = channelStateEvent.mChannelId;
- mHealthChannels.add(chan);
- }
- newState = convertHalChannelState(channelStateEvent.mState);
- if (newState == BluetoothHealth.STATE_CHANNEL_CONNECTED) {
- try {
- chan.mChannelFd = ParcelFileDescriptor.dup(channelStateEvent.mFd);
- } catch (IOException e) {
- Log.e(TAG, "failed to dup ParcelFileDescriptor");
- break;
- }
- } else {
- /*set the channel fd to null if channel state isnot equal to connected*/
- chan.mChannelFd = null;
- }
- callHealthChannelCallback(chan.mConfig, chan.mDevice, newState, chan.mState,
- chan.mChannelFd, chan.mChannelId);
- chan.mState = newState;
- if (channelStateEvent.mState == CONN_STATE_DESTROYED) {
- mHealthChannels.remove(chan);
- }
- }
- break;
- }
- }
- }
-
- //Handler for DeathReceipient
- private static class BluetoothHealthDeathRecipient implements IBinder.DeathRecipient {
- private BluetoothHealthAppConfiguration mConfig;
- private HealthService mService;
-
- BluetoothHealthDeathRecipient(HealthService service,
- BluetoothHealthAppConfiguration config) {
- mService = service;
- mConfig = config;
- }
-
- @Override
- public void binderDied() {
- if (DBG) {
- Log.d(TAG, "Binder is dead.");
- }
- mService.unregisterAppConfiguration(mConfig);
- }
-
- public void cleanup() {
- mService = null;
- mConfig = null;
- }
- }
-
- /**
- * Handlers for incoming service calls
- */
- private static class BluetoothHealthBinder extends IBluetoothHealth.Stub
- implements IProfileServiceBinder {
- private HealthService mService;
-
- BluetoothHealthBinder(HealthService svc) {
- mService = svc;
- }
-
- @Override
- public void cleanup() {
- mService = null;
- }
-
- private HealthService getService() {
- if (!Utils.checkCaller()) {
- Log.w(TAG, "Health call not allowed for non-active user");
- return null;
- }
-
- if (mService != null && mService.isAvailable()) {
- return mService;
- }
- return null;
- }
-
- @Override
- public boolean registerAppConfiguration(BluetoothHealthAppConfiguration config,
- IBluetoothHealthCallback callback) {
- HealthService service = getService();
- if (service == null) {
- return false;
- }
- return service.registerAppConfiguration(config, callback);
- }
-
- @Override
- public boolean unregisterAppConfiguration(BluetoothHealthAppConfiguration config) {
- HealthService service = getService();
- if (service == null) {
- return false;
- }
- return service.unregisterAppConfiguration(config);
- }
-
- @Override
- public boolean connectChannelToSource(BluetoothDevice device,
- BluetoothHealthAppConfiguration config) {
- HealthService service = getService();
- if (service == null) {
- return false;
- }
- return service.connectChannelToSource(device, config);
- }
-
- @Override
- public boolean connectChannelToSink(BluetoothDevice device,
- BluetoothHealthAppConfiguration config, int channelType) {
- HealthService service = getService();
- if (service == null) {
- return false;
- }
- return service.connectChannelToSink(device, config, channelType);
- }
-
- @Override
- public boolean disconnectChannel(BluetoothDevice device,
- BluetoothHealthAppConfiguration config, int channelId) {
- HealthService service = getService();
- if (service == null) {
- return false;
- }
- return service.disconnectChannel(device, config, channelId);
- }
-
- @Override
- public ParcelFileDescriptor getMainChannelFd(BluetoothDevice device,
- BluetoothHealthAppConfiguration config) {
- HealthService service = getService();
- if (service == null) {
- return null;
- }
- return service.getMainChannelFd(device, config);
- }
-
- @Override
- public int getHealthDeviceConnectionState(BluetoothDevice device) {
- HealthService service = getService();
- if (service == null) {
- return BluetoothHealth.STATE_DISCONNECTED;
- }
- return service.getHealthDeviceConnectionState(device);
- }
-
- @Override
- public List<BluetoothDevice> getConnectedHealthDevices() {
- HealthService service = getService();
- if (service == null) {
- return new ArrayList<BluetoothDevice>(0);
- }
- return service.getConnectedHealthDevices();
- }
-
- @Override
- public List<BluetoothDevice> getHealthDevicesMatchingConnectionStates(int[] states) {
- HealthService service = getService();
- if (service == null) {
- return new ArrayList<BluetoothDevice>(0);
- }
- return service.getHealthDevicesMatchingConnectionStates(states);
- }
- }
-
- ;
-
- boolean registerAppConfiguration(BluetoothHealthAppConfiguration config,
- IBluetoothHealthCallback callback) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
-
- if (config == null) {
- Log.e(TAG, "Trying to use a null config for registration");
- return false;
- }
-
- if (mApps.get(config) != null) {
- if (DBG) {
- Log.d(TAG, "Config has already been registered");
- }
- return false;
- }
- mApps.put(config, new AppInfo(callback));
- Message msg = mHandler.obtainMessage(MESSAGE_REGISTER_APPLICATION, config);
- mHandler.sendMessage(msg);
- return true;
- }
-
- boolean unregisterAppConfiguration(BluetoothHealthAppConfiguration config) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- if (mApps.get(config) == null) {
- if (DBG) {
- Log.d(TAG, "unregisterAppConfiguration: no app found");
- }
- return false;
- }
- Message msg = mHandler.obtainMessage(MESSAGE_UNREGISTER_APPLICATION, config);
- mHandler.sendMessage(msg);
- return true;
- }
-
- boolean connectChannelToSource(BluetoothDevice device, BluetoothHealthAppConfiguration config) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- return connectChannel(device, config, BluetoothHealth.CHANNEL_TYPE_ANY);
- }
-
- boolean connectChannelToSink(BluetoothDevice device, BluetoothHealthAppConfiguration config,
- int channelType) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- return connectChannel(device, config, channelType);
- }
-
- boolean disconnectChannel(BluetoothDevice device, BluetoothHealthAppConfiguration config,
- int channelId) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- HealthChannel chan = findChannelById(channelId);
- if (chan == null) {
- if (DBG) {
- Log.d(TAG, "disconnectChannel: no channel found");
- }
- return false;
- }
- Message msg = mHandler.obtainMessage(MESSAGE_DISCONNECT_CHANNEL, chan);
- mHandler.sendMessage(msg);
- return true;
- }
-
- ParcelFileDescriptor getMainChannelFd(BluetoothDevice device,
- BluetoothHealthAppConfiguration config) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- HealthChannel healthChan = null;
- for (HealthChannel chan : mHealthChannels) {
- if (chan.mDevice.equals(device) && chan.mConfig.equals(config)) {
- healthChan = chan;
- }
- }
- if (healthChan == null) {
- Log.e(TAG, "No channel found for device: " + device + " config: " + config);
- return null;
- }
- return healthChan.mChannelFd;
- }
-
- int getHealthDeviceConnectionState(BluetoothDevice device) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- return getConnectionState(device);
- }
-
- List<BluetoothDevice> getConnectedHealthDevices() {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- List<BluetoothDevice> devices =
- lookupHealthDevicesMatchingStates(new int[]{BluetoothHealth.STATE_CONNECTED});
- return devices;
- }
-
- List<BluetoothDevice> getHealthDevicesMatchingConnectionStates(int[] states) {
- enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- List<BluetoothDevice> devices = lookupHealthDevicesMatchingStates(states);
- return devices;
- }
-
- private void onAppRegistrationState(int appId, int state) {
- Message msg = mHandler.obtainMessage(MESSAGE_APP_REGISTRATION_CALLBACK);
- msg.arg1 = appId;
- msg.arg2 = state;
- mHandler.sendMessage(msg);
- }
-
- private void onChannelStateChanged(int appId, byte[] addr, int cfgIndex, int channelId,
- int state, FileDescriptor pfd) {
- Message msg = mHandler.obtainMessage(MESSAGE_CHANNEL_STATE_CALLBACK);
- ChannelStateEvent channelStateEvent =
- new ChannelStateEvent(appId, addr, cfgIndex, channelId, state, pfd);
- msg.obj = channelStateEvent;
- mHandler.sendMessage(msg);
- }
-
- private String getStringChannelType(int type) {
- if (type == BluetoothHealth.CHANNEL_TYPE_RELIABLE) {
- return "Reliable";
- } else if (type == BluetoothHealth.CHANNEL_TYPE_STREAMING) {
- return "Streaming";
- } else {
- return "Any";
- }
- }
-
- private void callStatusCallback(BluetoothHealthAppConfiguration config, int status) {
- if (VDBG) {
- Log.d(TAG, "Health Device Application: " + config + " State Change: status:" + status);
- }
- IBluetoothHealthCallback callback = (mApps.get(config)).mCallback;
- if (callback == null) {
- Log.e(TAG, "Callback object null");
- }
-
- try {
- callback.onHealthAppConfigurationStatusChange(config, status);
- } catch (RemoteException e) {
- Log.e(TAG, "Remote Exception:" + e);
- }
- }
-
- private BluetoothHealthAppConfiguration findAppConfigByAppId(int appId) {
- BluetoothHealthAppConfiguration appConfig = null;
- for (Entry<BluetoothHealthAppConfiguration, AppInfo> e : mApps.entrySet()) {
- if (appId == (e.getValue()).mAppId) {
- appConfig = e.getKey();
- break;
- }
- }
- if (appConfig == null) {
- Log.e(TAG, "No appConfig found for " + appId);
- }
- return appConfig;
- }
-
- private int convertHalRegStatus(int halRegStatus) {
- switch (halRegStatus) {
- case APP_REG_STATE_REG_SUCCESS:
- return BluetoothHealth.APP_CONFIG_REGISTRATION_SUCCESS;
- case APP_REG_STATE_REG_FAILED:
- return BluetoothHealth.APP_CONFIG_REGISTRATION_FAILURE;
- case APP_REG_STATE_DEREG_SUCCESS:
- return BluetoothHealth.APP_CONFIG_UNREGISTRATION_SUCCESS;
- case APP_REG_STATE_DEREG_FAILED:
- return BluetoothHealth.APP_CONFIG_UNREGISTRATION_FAILURE;
- }
- Log.e(TAG, "Unexpected App Registration state: " + halRegStatus);
- return BluetoothHealth.APP_CONFIG_REGISTRATION_FAILURE;
- }
-
- private int convertHalChannelState(int halChannelState) {
- switch (halChannelState) {
- case CONN_STATE_CONNECTED:
- return BluetoothHealth.STATE_CHANNEL_CONNECTED;
- case CONN_STATE_CONNECTING:
- return BluetoothHealth.STATE_CHANNEL_CONNECTING;
- case CONN_STATE_DISCONNECTING:
- return BluetoothHealth.STATE_CHANNEL_DISCONNECTING;
- case CONN_STATE_DISCONNECTED:
- return BluetoothHealth.STATE_CHANNEL_DISCONNECTED;
- case CONN_STATE_DESTROYED:
- // TODO(BT) add BluetoothHealth.STATE_CHANNEL_DESTROYED;
- return BluetoothHealth.STATE_CHANNEL_DISCONNECTED;
- default:
- Log.e(TAG, "Unexpected channel state: " + halChannelState);
- return BluetoothHealth.STATE_CHANNEL_DISCONNECTED;
- }
- }
-
- private boolean connectChannel(BluetoothDevice device, BluetoothHealthAppConfiguration config,
- int channelType) {
- if (mApps.get(config) == null) {
- Log.e(TAG, "connectChannel fail to get a app id from config");
- return false;
- }
-
- HealthChannel chan = new HealthChannel(device, config, channelType);
-
- Message msg = mHandler.obtainMessage(MESSAGE_CONNECT_CHANNEL);
- msg.obj = chan;
- mHandler.sendMessage(msg);
-
- return true;
- }
-
- private void callHealthChannelCallback(BluetoothHealthAppConfiguration config,
- BluetoothDevice device, int state, int prevState, ParcelFileDescriptor fd, int id) {
- broadcastHealthDeviceStateChange(device, state);
-
- Log.d(TAG,
- "Health Device Callback: " + device + " State Change: " + prevState + "->" + state);
-
- ParcelFileDescriptor dupedFd = null;
- if (fd != null) {
- try {
- dupedFd = fd.dup();
- } catch (IOException e) {
- dupedFd = null;
- Log.e(TAG, "Exception while duping: " + e);
- }
- }
-
- IBluetoothHealthCallback callback = (mApps.get(config)).mCallback;
- if (callback == null) {
- Log.e(TAG, "No callback found for config: " + config);
- return;
- }
-
- try {
- callback.onHealthChannelStateChange(config, device, prevState, state, dupedFd, id);
- } catch (RemoteException e) {
- Log.e(TAG, "Remote Exception:" + e);
- }
- }
-
- /**
- * This function sends the intent for the updates on the connection status to the remote device.
- * Note that multiple channels can be connected to the remote device by multiple applications.
- * This sends an intent for the update to the device connection status and not the channel
- * connection status. Only the following state transitions are possible:
- *
- * {@link BluetoothHealth#STATE_DISCONNECTED} to {@link BluetoothHealth#STATE_CONNECTING}
- * {@link BluetoothHealth#STATE_CONNECTING} to {@link BluetoothHealth#STATE_CONNECTED}
- * {@link BluetoothHealth#STATE_CONNECTED} to {@link BluetoothHealth#STATE_DISCONNECTING}
- * {@link BluetoothHealth#STATE_DISCONNECTING} to {@link BluetoothHealth#STATE_DISCONNECTED}
- * {@link BluetoothHealth#STATE_DISCONNECTED} to {@link BluetoothHealth#STATE_CONNECTED}
- * {@link BluetoothHealth#STATE_CONNECTED} to {@link BluetoothHealth#STATE_DISCONNECTED}
- * {@link BluetoothHealth#STATE_CONNECTING} to {{@link BluetoothHealth#STATE_DISCONNECTED}
- *
- * @param device
- * @param prevChannelState
- * @param newChannelState
- * @hide
- */
- private void broadcastHealthDeviceStateChange(BluetoothDevice device, int newChannelState) {
- if (mHealthDevices.get(device) == null) {
- mHealthDevices.put(device, BluetoothHealth.STATE_DISCONNECTED);
- }
-
- int currDeviceState = mHealthDevices.get(device);
- int newDeviceState = convertState(newChannelState);
-
- if (currDeviceState == newDeviceState) {
- return;
- }
-
- boolean sendIntent = false;
- List<HealthChannel> chan;
- switch (currDeviceState) {
- case BluetoothHealth.STATE_DISCONNECTED:
- // there was no connection or connect/disconnect attemp with the remote device
- sendIntent = true;
- break;
- case BluetoothHealth.STATE_CONNECTING:
- // there was no connection, there was a connecting attempt going on
-
- // Channel got connected.
- if (newDeviceState == BluetoothHealth.STATE_CONNECTED) {
- sendIntent = true;
- } else {
- // Channel got disconnected
- chan = findChannelByStates(device, new int[]{
- BluetoothHealth.STATE_CHANNEL_CONNECTING,
- BluetoothHealth.STATE_CHANNEL_DISCONNECTING
- });
- if (chan.isEmpty()) {
- sendIntent = true;
- }
- }
- break;
- case BluetoothHealth.STATE_CONNECTED:
- // there was at least one connection
-
- // Channel got disconnected or is in disconnecting state.
- chan = findChannelByStates(device, new int[]{
- BluetoothHealth.STATE_CHANNEL_CONNECTING,
- BluetoothHealth.STATE_CHANNEL_CONNECTED
- });
- if (chan.isEmpty()) {
- sendIntent = true;
- }
- break;
- case BluetoothHealth.STATE_DISCONNECTING:
- // there was no connected channel with the remote device
- // We were disconnecting all the channels with the remote device
-
- // Channel got disconnected.
- chan = findChannelByStates(device, new int[]{
- BluetoothHealth.STATE_CHANNEL_CONNECTING,
- BluetoothHealth.STATE_CHANNEL_DISCONNECTING
- });
- if (chan.isEmpty()) {
- updateAndSendIntent(device, newDeviceState, currDeviceState);
- }
- break;
- }
- if (sendIntent) {
- updateAndSendIntent(device, newDeviceState, currDeviceState);
- }
- }
-
- private void updateAndSendIntent(BluetoothDevice device, int newDeviceState,
- int prevDeviceState) {
- if (newDeviceState == BluetoothHealth.STATE_DISCONNECTED) {
- mHealthDevices.remove(device);
- } else {
- mHealthDevices.put(device, newDeviceState);
- }
- if (newDeviceState != prevDeviceState
- && newDeviceState == BluetoothHealth.STATE_CONNECTED) {
- MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.HEALTH);
- }
- }
-
- /**
- * This function converts the channel connection state to device connection state.
- *
- * @param state
- * @return
- */
- private int convertState(int state) {
- switch (state) {
- case BluetoothHealth.STATE_CHANNEL_CONNECTED:
- return BluetoothHealth.STATE_CONNECTED;
- case BluetoothHealth.STATE_CHANNEL_CONNECTING:
- return BluetoothHealth.STATE_CONNECTING;
- case BluetoothHealth.STATE_CHANNEL_DISCONNECTING:
- return BluetoothHealth.STATE_DISCONNECTING;
- case BluetoothHealth.STATE_CHANNEL_DISCONNECTED:
- return BluetoothHealth.STATE_DISCONNECTED;
- }
- Log.e(TAG, "Mismatch in Channel and Health Device State: " + state);
- return BluetoothHealth.STATE_DISCONNECTED;
- }
-
- private int convertRoleToHal(int role) {
- if (role == BluetoothHealth.SOURCE_ROLE) {
- return MDEP_ROLE_SOURCE;
- }
- if (role == BluetoothHealth.SINK_ROLE) {
- return MDEP_ROLE_SINK;
- }
- Log.e(TAG, "unkonw role: " + role);
- return MDEP_ROLE_SINK;
- }
-
- private int convertChannelTypeToHal(int channelType) {
- if (channelType == BluetoothHealth.CHANNEL_TYPE_RELIABLE) {
- return CHANNEL_TYPE_RELIABLE;
- }
- if (channelType == BluetoothHealth.CHANNEL_TYPE_STREAMING) {
- return CHANNEL_TYPE_STREAMING;
- }
- if (channelType == BluetoothHealth.CHANNEL_TYPE_ANY) {
- return CHANNEL_TYPE_ANY;
- }
- Log.e(TAG, "unkonw channel type: " + channelType);
- return CHANNEL_TYPE_ANY;
- }
-
- private HealthChannel findChannelById(int id) {
- for (HealthChannel chan : mHealthChannels) {
- if (chan.mChannelId == id) {
- return chan;
- }
- }
- Log.e(TAG, "No channel found by id: " + id);
- return null;
- }
-
- private List<HealthChannel> findChannelByStates(BluetoothDevice device, int[] states) {
- List<HealthChannel> channels = new ArrayList<HealthChannel>();
- for (HealthChannel chan : mHealthChannels) {
- if (chan.mDevice.equals(device)) {
- for (int state : states) {
- if (chan.mState == state) {
- channels.add(chan);
- }
- }
- }
- }
- return channels;
- }
-
- private int getConnectionState(BluetoothDevice device) {
- if (mHealthDevices.get(device) == null) {
- return BluetoothHealth.STATE_DISCONNECTED;
- }
- return mHealthDevices.get(device);
- }
-
- List<BluetoothDevice> lookupHealthDevicesMatchingStates(int[] states) {
- List<BluetoothDevice> healthDevices = new ArrayList<BluetoothDevice>();
-
- for (BluetoothDevice device : mHealthDevices.keySet()) {
- int healthDeviceState = getConnectionState(device);
- for (int state : states) {
- if (state == healthDeviceState) {
- healthDevices.add(device);
- break;
- }
- }
- }
- return healthDevices;
- }
-
- @Override
- public void dump(StringBuilder sb) {
- super.dump(sb);
- println(sb, "mHealthChannels:");
- for (HealthChannel channel : mHealthChannels) {
- println(sb, " " + channel);
- }
- println(sb, "mApps:");
- for (BluetoothHealthAppConfiguration conf : mApps.keySet()) {
- println(sb, " " + conf + " : " + mApps.get(conf));
- }
- println(sb, "mHealthDevices:");
- for (BluetoothDevice device : mHealthDevices.keySet()) {
- println(sb, " " + device + " : " + mHealthDevices.get(device));
- }
- }
-
- private static class AppInfo {
- private IBluetoothHealthCallback mCallback;
- private BluetoothHealthDeathRecipient mRcpObj;
- private int mAppId;
-
- private AppInfo(IBluetoothHealthCallback callback) {
- mCallback = callback;
- mRcpObj = null;
- mAppId = -1;
- }
-
- private void cleanup() {
- if (mCallback != null) {
- if (mRcpObj != null) {
- IBinder binder = mCallback.asBinder();
- try {
- binder.unlinkToDeath(mRcpObj, 0);
- } catch (NoSuchElementException e) {
- Log.e(TAG, "No death recipient registered" + e);
- }
- mRcpObj.cleanup();
- mRcpObj = null;
- }
- mCallback = null;
- } else if (mRcpObj != null) {
- mRcpObj.cleanup();
- mRcpObj = null;
- }
- }
- }
-
- private class HealthChannel {
- private ParcelFileDescriptor mChannelFd;
- private BluetoothDevice mDevice;
- private BluetoothHealthAppConfiguration mConfig;
- // BluetoothHealth channel state
- private int mState;
- private int mChannelType;
- private int mChannelId;
-
- private HealthChannel(BluetoothDevice device, BluetoothHealthAppConfiguration config,
- int channelType) {
- mChannelFd = null;
- mDevice = device;
- mConfig = config;
- mState = BluetoothHealth.STATE_CHANNEL_DISCONNECTED;
- mChannelType = channelType;
- mChannelId = -1;
- }
- }
-
- // Channel state event from Hal
- private class ChannelStateEvent {
- int mAppId;
- byte[] mAddr;
- int mCfgIndex;
- int mChannelId;
- int mState;
- FileDescriptor mFd;
-
- private ChannelStateEvent(int appId, byte[] addr, int cfgIndex, int channelId, int state,
- FileDescriptor fileDescriptor) {
- mAppId = appId;
- mAddr = addr;
- mCfgIndex = cfgIndex;
- mState = state;
- mChannelId = channelId;
- mFd = fileDescriptor;
- }
- }
-
- // Constants matching Hal header file bt_hl.h
- // bthl_app_reg_state_t
- private static final int APP_REG_STATE_REG_SUCCESS = 0;
- private static final int APP_REG_STATE_REG_FAILED = 1;
- private static final int APP_REG_STATE_DEREG_SUCCESS = 2;
- private static final int APP_REG_STATE_DEREG_FAILED = 3;
-
- // bthl_channel_state_t
- private static final int CONN_STATE_CONNECTING = 0;
- private static final int CONN_STATE_CONNECTED = 1;
- private static final int CONN_STATE_DISCONNECTING = 2;
- private static final int CONN_STATE_DISCONNECTED = 3;
- private static final int CONN_STATE_DESTROYED = 4;
-
- // bthl_mdep_role_t
- private static final int MDEP_ROLE_SOURCE = 0;
- private static final int MDEP_ROLE_SINK = 1;
-
- // bthl_channel_type_t
- private static final int CHANNEL_TYPE_RELIABLE = 0;
- private static final int CHANNEL_TYPE_STREAMING = 1;
- private static final int CHANNEL_TYPE_ANY = 2;
-
- private static native void classInitNative();
-
- private native void initializeNative();
-
- private native void cleanupNative();
-
- private native int registerHealthAppNative(int dataType, int role, String name,
- int channelType);
-
- private native boolean unregisterHealthAppNative(int appId);
-
- private native int connectChannelNative(byte[] btAddress, int appId);
-
- private native boolean disconnectChannelNative(int channelId);
-
-}
diff --git a/src/com/android/bluetooth/hearingaid/HearingAidNativeInterface.java b/src/com/android/bluetooth/hearingaid/HearingAidNativeInterface.java
index e735a88..b32d304 100644
--- a/src/com/android/bluetooth/hearingaid/HearingAidNativeInterface.java
+++ b/src/com/android/bluetooth/hearingaid/HearingAidNativeInterface.java
@@ -23,11 +23,11 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.bluetooth.Utils;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
/**
* HearingAid Native Interface to/from JNI.
@@ -69,7 +69,7 @@
*
* priorities to configure.
*/
- @VisibleForTesting (otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void init() {
initNative();
}
@@ -77,7 +77,7 @@
/**
* Cleanup the native interface.
*/
- @VisibleForTesting (otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void cleanup() {
cleanupNative();
}
@@ -88,7 +88,7 @@
* @param device the remote device
* @return true on success, otherwise false.
*/
- @VisibleForTesting (otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public boolean connectHearingAid(BluetoothDevice device) {
return connectHearingAidNative(getByteAddress(device));
}
@@ -99,7 +99,7 @@
* @param device the remote device
* @return true on success, otherwise false.
*/
- @VisibleForTesting (otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public boolean disconnectHearingAid(BluetoothDevice device) {
return disconnectHearingAidNative(getByteAddress(device));
}
@@ -110,27 +110,16 @@
* @param device the remote device
* @return true on success, otherwise false.
*/
- @VisibleForTesting (otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public boolean addToWhiteList(BluetoothDevice device) {
return addToWhiteListNative(getByteAddress(device));
}
/**
- * Remove a hearing aid device from white list.
- *
- * @param device the remote device
- * @return true on success, otherwise false.
- */
- @VisibleForTesting (otherwise = VisibleForTesting.PACKAGE_PRIVATE)
- public boolean removeFromWhiteList(BluetoothDevice device) {
- return removeFromWhiteListNative(getByteAddress(device));
- }
-
- /**
* Sets the HearingAid volume
* @param volume
*/
- @VisibleForTesting (otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void setVolume(int volume) {
setVolumeNative(volume);
}
@@ -191,6 +180,5 @@
private native boolean connectHearingAidNative(byte[] address);
private native boolean disconnectHearingAidNative(byte[] address);
private native boolean addToWhiteListNative(byte[] address);
- private native boolean removeFromWhiteListNative(byte[] address);
private native void setVolumeNative(int volume);
}
diff --git a/src/com/android/bluetooth/hearingaid/HearingAidService.java b/src/com/android/bluetooth/hearingaid/HearingAidService.java
index 704a3d5..f29d9b9 100644
--- a/src/com/android/bluetooth/hearingaid/HearingAidService.java
+++ b/src/com/android/bluetooth/hearingaid/HearingAidService.java
@@ -28,15 +28,17 @@
import android.media.AudioManager;
import android.os.HandlerThread;
import android.os.ParcelUuid;
-import android.provider.Settings;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
+import android.util.StatsLog;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.Utils;
+import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.bluetooth.btservice.ServiceFactory;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashMap;
@@ -50,16 +52,12 @@
* @hide
*/
public class HearingAidService extends ProfileService {
- private static final boolean DBG = false;
+ private static final boolean DBG = true;
private static final String TAG = "HearingAidService";
// Upper limit of all HearingAid devices: Bonded or Connected
private static final int MAX_HEARING_AID_STATE_MACHINES = 10;
private static HearingAidService sHearingAidService;
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static int sConnectTimeoutForEachSideMs = 8000;
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static int sCheckWhitelistTimeoutMs = 16000;
private AdapterService mAdapterService;
private HandlerThread mStateMachinesThread;
@@ -74,11 +72,14 @@
new HashMap<>();
private final Map<BluetoothDevice, Long> mDeviceHiSyncIdMap = new ConcurrentHashMap<>();
private final Map<BluetoothDevice, Integer> mDeviceCapabilitiesMap = new HashMap<>();
+ private final Map<Long, Boolean> mHiSyncIdConnectedMap = new HashMap<>();
private long mActiveDeviceHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
private BroadcastReceiver mBondStateChangedReceiver;
private BroadcastReceiver mConnectionStateChangedReceiver;
+ private final ServiceFactory mFactory = new ServiceFactory();
+
@Override
protected IProfileServiceBinder initBinder() {
return new BluetoothHearingAidBinder(this);
@@ -115,9 +116,10 @@
mStateMachinesThread = new HandlerThread("HearingAidService.StateMachines");
mStateMachinesThread.start();
- // Clear HiSyncId map and capabilities map
+ // Clear HiSyncId map, capabilities map and HiSyncId Connected map
mDeviceHiSyncIdMap.clear();
mDeviceCapabilitiesMap.clear();
+ mHiSyncIdConnectedMap.clear();
// Setup broadcast receivers
IntentFilter filter = new IntentFilter();
@@ -170,9 +172,10 @@
mStateMachines.clear();
}
- // Clear HiSyncId map and capabilities map
+ // Clear HiSyncId map, capabilities map and HiSyncId Connected map
mDeviceHiSyncIdMap.clear();
mDeviceCapabilitiesMap.clear();
+ mHiSyncIdConnectedMap.clear();
if (mStateMachinesThread != null) {
mStateMachinesThread.quitSafely();
@@ -247,6 +250,14 @@
}
}
+ synchronized (mStateMachines) {
+ HearingAidStateMachine smConnect = getOrCreateStateMachine(device);
+ if (smConnect == null) {
+ Log.e(TAG, "Cannot connect to " + device + " : no state machine");
+ }
+ smConnect.sendMessage(HearingAidStateMachine.CONNECT);
+ }
+
for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) {
if (device.equals(storedDevice)) {
continue;
@@ -259,27 +270,14 @@
Log.e(TAG, "Ignored connect request for " + device + " : no state machine");
continue;
}
- sm.sendMessage(HearingAidStateMachine.CONNECT,
- sConnectTimeoutForEachSideMs);
- sm.sendMessageDelayed(HearingAidStateMachine.CHECK_WHITELIST_CONNECTION,
- sCheckWhitelistTimeoutMs);
+ sm.sendMessage(HearingAidStateMachine.CONNECT);
}
- break;
+ if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
+ && !device.equals(storedDevice)) {
+ break;
+ }
}
}
-
- synchronized (mStateMachines) {
- HearingAidStateMachine smConnect = getOrCreateStateMachine(device);
- if (smConnect == null) {
- Log.e(TAG, "Cannot connect to " + device + " : no state machine");
- } else {
- smConnect.sendMessage(HearingAidStateMachine.CONNECT,
- sConnectTimeoutForEachSideMs * 2);
- smConnect.sendMessageDelayed(HearingAidStateMachine.CHECK_WHITELIST_CONNECTION,
- sCheckWhitelistTimeoutMs);
- }
- }
-
return true;
}
@@ -329,13 +327,28 @@
}
/**
+ * Check any peer device is connected.
+ * The check considers any peer device is connected.
+ *
+ * @param device the peer device to connect to
+ * @return true if there are any peer device connected.
+ */
+ public boolean isConnectedPeerDevices(BluetoothDevice device) {
+ long hiSyncId = getHiSyncId(device);
+ if (getConnectedPeerDevices(hiSyncId).isEmpty()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
* Check whether can connect to a peer device.
* The check considers a number of factors during the evaluation.
*
* @param device the peer device to connect to
* @return true if connection is allowed, otherwise false
*/
- @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public boolean okToConnect(BluetoothDevice device) {
// Check if this is an incoming connection in Quiet mode.
if (mAdapterService.isQuietModeEnabled()) {
@@ -438,20 +451,18 @@
*/
public boolean setPriority(BluetoothDevice device, int priority) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothHearingAidPriorityKey(device.getAddress()), priority);
if (DBG) {
Log.d(TAG, "Saved priority " + device + " = " + priority);
}
+ mAdapterService.getDatabase()
+ .setProfilePriority(device, BluetoothProfile.HEARING_AID, priority);
return true;
}
public int getPriority(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- int priority = Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothHearingAidPriorityKey(device.getAddress()),
- BluetoothProfile.PRIORITY_UNDEFINED);
- return priority;
+ return mAdapterService.getDatabase()
+ .getProfilePriority(device, BluetoothProfile.HEARING_AID);
}
void setVolume(int volume) {
@@ -494,6 +505,15 @@
Long deviceHiSyncId = mDeviceHiSyncIdMap.getOrDefault(device,
BluetoothHearingAid.HI_SYNC_ID_INVALID);
if (deviceHiSyncId != mActiveDeviceHiSyncId) {
+ // Give an early notification to A2DP that active device is being switched
+ // to Hearing Aids before the Audio Service.
+ final A2dpService a2dpService = mFactory.getA2dpService();
+ if (a2dpService != null) {
+ if (DBG) {
+ Log.d(TAG, "earlyNotifyHearingAidActive for " + device);
+ }
+ a2dpService.earlyNotifyHearingAidActive();
+ }
mActiveDeviceHiSyncId = deviceHiSyncId;
reportActiveDevice(device);
}
@@ -508,7 +528,7 @@
* device; the second element is the right active device. If either or both side
* is not active, it will be null on that position
*/
- List<BluetoothDevice> getActiveDevices() {
+ public List<BluetoothDevice> getActiveDevices() {
if (DBG) {
Log.d(TAG, "getActiveDevices");
}
@@ -611,6 +631,9 @@
Log.d(TAG, "reportActiveDevice(" + device + ")");
}
+ StatsLog.write(StatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED, BluetoothProfile.HEARING_AID,
+ mAdapterService.obfuscateAddress(device));
+
Intent intent = new Intent(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
@@ -731,10 +754,15 @@
MetricsLogger.logProfileConnectionEvent(
BluetoothMetricsProto.ProfileId.HEARING_AID);
}
- setActiveDevice(device);
+ if (!mHiSyncIdConnectedMap.getOrDefault(myHiSyncId, false)) {
+ setActiveDevice(device);
+ mHiSyncIdConnectedMap.put(myHiSyncId, true);
+ }
}
if (fromState == BluetoothProfile.STATE_CONNECTED && getConnectedDevices().isEmpty()) {
setActiveDevice(null);
+ long myHiSyncId = getHiSyncId(device);
+ mHiSyncIdConnectedMap.put(myHiSyncId, false);
}
// Check if the device is disconnected - if unbond, remove the state machine
if (toState == BluetoothProfile.STATE_DISCONNECTED) {
diff --git a/src/com/android/bluetooth/hearingaid/HearingAidStateMachine.java b/src/com/android/bluetooth/hearingaid/HearingAidStateMachine.java
index 3e0d617..5b8d798 100644
--- a/src/com/android/bluetooth/hearingaid/HearingAidStateMachine.java
+++ b/src/com/android/bluetooth/hearingaid/HearingAidStateMachine.java
@@ -51,10 +51,10 @@
import android.content.Intent;
import android.os.Looper;
import android.os.Message;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
@@ -69,21 +69,18 @@
static final int CONNECT = 1;
static final int DISCONNECT = 2;
- static final int CHECK_WHITELIST_CONNECTION = 3;
@VisibleForTesting
static final int STACK_EVENT = 101;
private static final int CONNECT_TIMEOUT = 201;
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static int sConnectTimeoutMs = 16000;
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static int sDisconnectTimeoutMs = 16000;
+ // NOTE: the value is not "final" - it is modified in the unit tests
+ @VisibleForTesting
+ static int sConnectTimeoutMs = 30000; // 30s
private Disconnected mDisconnected;
private Connecting mConnecting;
private Disconnecting mDisconnecting;
private Connected mConnected;
- private int mConnectionState = BluetoothProfile.STATE_DISCONNECTED;
private int mLastConnectionState = -1;
private HearingAidService mService;
@@ -135,13 +132,13 @@
public void enter() {
Log.i(TAG, "Enter Disconnected(" + mDevice + "): " + messageWhatToString(
getCurrentMessage().what));
- mConnectionState = BluetoothProfile.STATE_DISCONNECTED;
removeDeferredMessages(DISCONNECT);
if (mLastConnectionState != -1) {
// Don't broadcast during startup
- broadcastConnectionState(mConnectionState, mLastConnectionState);
+ broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTED,
+ mLastConnectionState);
}
}
@@ -172,13 +169,8 @@
}
break;
case DISCONNECT:
- Log.w(TAG, "Disconnected: DISCONNECT ignored: " + mDevice);
- break;
- case CHECK_WHITELIST_CONNECTION:
- if (mService.getConnectedDevices().isEmpty()) {
- log("No device connected, remove this device from white list");
- mNativeInterface.removeFromWhiteList(mDevice);
- }
+ Log.d(TAG, "Disconnected: DISCONNECT: call native disconnect for " + mDevice);
+ mNativeInterface.disconnectHearingAid(mDevice);
break;
case STACK_EVENT:
HearingAidStackEvent event = (HearingAidStackEvent) message.obj;
@@ -246,11 +238,8 @@
public void enter() {
Log.i(TAG, "Enter Connecting(" + mDevice + "): "
+ messageWhatToString(getCurrentMessage().what));
- int timeout = getCurrentMessage().arg1 != 0
- ? getCurrentMessage().arg1 : sConnectTimeoutMs;
- sendMessageDelayed(CONNECT_TIMEOUT, timeout);
- mConnectionState = BluetoothProfile.STATE_CONNECTING;
- broadcastConnectionState(mConnectionState, mLastConnectionState);
+ sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
+ broadcastConnectionState(BluetoothProfile.STATE_CONNECTING, mLastConnectionState);
}
@Override
@@ -271,13 +260,18 @@
deferMessage(message);
break;
case CONNECT_TIMEOUT:
- Log.w(TAG, "Connecting connection timeout: " + mDevice + ". Try whitelist");
+ Log.w(TAG, "Connecting connection timeout: " + mDevice);
mNativeInterface.disconnectHearingAid(mDevice);
- mNativeInterface.addToWhiteList(mDevice);
- transitionTo(mDisconnected);
- break;
- case CHECK_WHITELIST_CONNECTION:
- deferMessage(message);
+ if (mService.isConnectedPeerDevices(mDevice)) {
+ Log.w(TAG, "One side connection timeout: " + mDevice + ". Try whitelist");
+ mNativeInterface.addToWhiteList(mDevice);
+ }
+ HearingAidStackEvent disconnectEvent =
+ new HearingAidStackEvent(
+ HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ disconnectEvent.device = mDevice;
+ disconnectEvent.valueInt1 = HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED;
+ sendMessage(STACK_EVENT, disconnectEvent);
break;
case DISCONNECT:
log("Connecting: connection canceled to " + mDevice);
@@ -334,9 +328,8 @@
public void enter() {
Log.i(TAG, "Enter Disconnecting(" + mDevice + "): "
+ messageWhatToString(getCurrentMessage().what));
- sendMessageDelayed(CONNECT_TIMEOUT, sDisconnectTimeoutMs);
- mConnectionState = BluetoothProfile.STATE_DISCONNECTING;
- broadcastConnectionState(mConnectionState, mLastConnectionState);
+ sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
+ broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTING, mLastConnectionState);
}
@Override
@@ -433,9 +426,8 @@
public void enter() {
Log.i(TAG, "Enter Connected(" + mDevice + "): "
+ messageWhatToString(getCurrentMessage().what));
- mConnectionState = BluetoothProfile.STATE_CONNECTED;
removeDeferredMessages(CONNECT);
- broadcastConnectionState(mConnectionState, mLastConnectionState);
+ broadcastConnectionState(BluetoothProfile.STATE_CONNECTED, mLastConnectionState);
}
@Override
@@ -489,7 +481,7 @@
private void processConnectionEvent(int state) {
switch (state) {
case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTED:
- Log.i(TAG, "Disconnected from " + mDevice);
+ Log.i(TAG, "Disconnected from " + mDevice + " but still in Whitelist");
transitionTo(mDisconnected);
break;
case HearingAidStackEvent.CONNECTION_STATE_DISCONNECTING:
@@ -504,7 +496,20 @@
}
int getConnectionState() {
- return mConnectionState;
+ String currentState = getCurrentState().getName();
+ switch (currentState) {
+ case "Disconnected":
+ return BluetoothProfile.STATE_DISCONNECTED;
+ case "Connecting":
+ return BluetoothProfile.STATE_CONNECTING;
+ case "Connected":
+ return BluetoothProfile.STATE_CONNECTED;
+ case "Disconnecting":
+ return BluetoothProfile.STATE_DISCONNECTING;
+ default:
+ Log.e(TAG, "Bad currentState: " + currentState);
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
}
BluetoothDevice getDevice() {
diff --git a/src/com/android/bluetooth/hfp/HeadsetCallState.java b/src/com/android/bluetooth/hfp/HeadsetCallState.java
index 3afd3c4..df45e75 100644
--- a/src/com/android/bluetooth/hfp/HeadsetCallState.java
+++ b/src/com/android/bluetooth/hfp/HeadsetCallState.java
@@ -42,13 +42,19 @@
* Phone number type
*/
int mType;
+ /**
+ * Caller display name
+ */
+ String mName;
- HeadsetCallState(int numActive, int numHeld, int callState, String number, int type) {
+ HeadsetCallState(int numActive, int numHeld, int callState, String number, int type,
+ String name) {
mNumActive = numActive;
mNumHeld = numHeld;
mCallState = callState;
mNumber = number;
mType = type;
+ mName = name;
}
@Override
@@ -69,7 +75,13 @@
} else {
builder.append("***");
}
- builder.append(mNumber).append(", type=").append(mType).append("]");
+ builder.append(", type=").append(mType).append(", name=");
+ if (mName == null) {
+ builder.append("null");
+ } else {
+ builder.append("***");
+ }
+ builder.append("]");
}
@Override
@@ -83,11 +95,11 @@
HeadsetCallState that = (HeadsetCallState) object;
return mNumActive == that.mNumActive && mNumHeld == that.mNumHeld
&& mCallState == that.mCallState && Objects.equals(mNumber, that.mNumber)
- && mType == that.mType;
+ && mType == that.mType && Objects.equals(mName, that.mName);
}
@Override
public int hashCode() {
- return Objects.hash(mNumActive, mNumHeld, mCallState, mNumber, mType);
+ return Objects.hash(mNumActive, mNumHeld, mCallState, mNumber, mType, mName);
}
}
diff --git a/src/com/android/bluetooth/hfp/HeadsetNativeInterface.java b/src/com/android/bluetooth/hfp/HeadsetNativeInterface.java
index b236d51..61c1163 100644
--- a/src/com/android/bluetooth/hfp/HeadsetNativeInterface.java
+++ b/src/com/android/bluetooth/hfp/HeadsetNativeInterface.java
@@ -18,10 +18,10 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.bluetooth.Utils;
+import com.android.internal.annotations.VisibleForTesting;
/**
* Defines native calls that are used by state machine/service to either send or receive
@@ -127,9 +127,9 @@
sendMessageToService(event);
}
- private void onNoiceReductionEnable(boolean enable, byte[] address) {
+ private void onNoiseReductionEnable(boolean enable, byte[] address) {
HeadsetStackEvent event =
- new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_NOICE_REDUCTION, enable ? 1 : 0,
+ new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_NOISE_REDUCTION, enable ? 1 : 0,
getDevice(address));
sendMessageToService(event);
}
@@ -419,7 +419,7 @@
@VisibleForTesting
public boolean phoneStateChange(BluetoothDevice device, HeadsetCallState callState) {
return phoneStateChangeNative(callState.mNumActive, callState.mNumHeld,
- callState.mCallState, callState.mNumber, callState.mType,
+ callState.mCallState, callState.mNumber, callState.mType, callState.mName,
Utils.getByteAddress(device));
}
@@ -493,7 +493,7 @@
private native boolean copsResponseNative(String operatorName, byte[] address);
private native boolean phoneStateChangeNative(int numActive, int numHeld, int callState,
- String number, int type, byte[] address);
+ String number, int type, String name, byte[] address);
private native boolean setScoAllowedNative(boolean value);
diff --git a/src/com/android/bluetooth/hfp/HeadsetPhoneState.java b/src/com/android/bluetooth/hfp/HeadsetPhoneState.java
index bcf123c..941ec14 100644
--- a/src/com/android/bluetooth/hfp/HeadsetPhoneState.java
+++ b/src/com/android/bluetooth/hfp/HeadsetPhoneState.java
@@ -22,7 +22,6 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Looper;
-import android.support.annotation.VisibleForTesting;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
@@ -31,6 +30,7 @@
import android.telephony.TelephonyManager;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.IccCardConstants;
import com.android.internal.telephony.TelephonyIntents;
@@ -160,7 +160,7 @@
return;
}
Log.i(TAG, "startListenForPhoneState(), subId=" + subId + ", enabled_events=" + events);
- mPhoneStateListener = new HeadsetPhoneStateListener(subId,
+ mPhoneStateListener = new HeadsetPhoneStateListener(
mHeadsetService.getStateMachinesThreadLooper());
mTelephonyManager.listen(mPhoneStateListener, events);
if ((events & PhoneStateListener.LISTEN_SIGNAL_STRENGTHS) != 0) {
@@ -192,7 +192,7 @@
return mNumActive;
}
- @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void setNumActiveCall(int numActive) {
mNumActive = numActive;
}
@@ -201,7 +201,7 @@
return mCallState;
}
- @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void setCallState(int callState) {
mCallState = callState;
}
@@ -210,7 +210,7 @@
return mNumHeld;
}
- @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void setNumHeldCall(int numHeldCall) {
mNumHeld = numHeldCall;
}
@@ -228,7 +228,7 @@
*
* @param batteryLevel battery level value
*/
- @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void setCindBatteryCharge(int batteryLevel) {
if (mCindBatteryCharge != batteryLevel) {
mCindBatteryCharge = batteryLevel;
@@ -274,8 +274,8 @@
}
private class HeadsetPhoneStateListener extends PhoneStateListener {
- HeadsetPhoneStateListener(Integer subId, Looper looper) {
- super(subId, looper);
+ HeadsetPhoneStateListener(Looper looper) {
+ super(looper);
}
@Override
diff --git a/src/com/android/bluetooth/hfp/HeadsetService.java b/src/com/android/bluetooth/hfp/HeadsetService.java
index 6d16a6a..4b3da51 100644
--- a/src/com/android/bluetooth/hfp/HeadsetService.java
+++ b/src/com/android/bluetooth/hfp/HeadsetService.java
@@ -39,9 +39,9 @@
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.UserHandle;
-import android.provider.Settings;
import android.telecom.PhoneAccount;
import android.util.Log;
+import android.util.StatsLog;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.Utils;
@@ -61,7 +61,8 @@
* Provides Bluetooth Headset and Handsfree profile, as a service in the Bluetooth application.
*
* Three modes for SCO audio:
- * Mode 1: Telecom call through {@link #phoneStateChanged(int, int, int, String, int, boolean)}
+ * Mode 1: Telecom call through {@link #phoneStateChanged(int, int, int, String, int, String,
+ * boolean)}
* Mode 2: Virtual call through {@link #startScoUsingVirtualVoiceCall()}
* Mode 3: Voice recognition through {@link #startVoiceRecognition(BluetoothDevice)}
*
@@ -219,7 +220,9 @@
mStateMachinesThread.quitSafely();
mStateMachinesThread = null;
// Step 1: Clear
- mAdapterService = null;
+ synchronized (mStateMachines) {
+ mAdapterService = null;
+ }
return true;
}
@@ -600,12 +603,12 @@
@Override
public void phoneStateChanged(int numActive, int numHeld, int callState, String number,
- int type) {
+ int type, String name) {
HeadsetService service = getService();
if (service == null) {
return;
}
- service.phoneStateChanged(numActive, numHeld, callState, number, type, false);
+ service.phoneStateChanged(numActive, numHeld, callState, number, type, name, false);
}
@Override
@@ -770,14 +773,14 @@
public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
ArrayList<BluetoothDevice> devices = new ArrayList<>();
- if (states == null) {
- return devices;
- }
- final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
- if (bondedDevices == null) {
- return devices;
- }
synchronized (mStateMachines) {
+ if (states == null || mAdapterService == null) {
+ return devices;
+ }
+ final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
+ if (bondedDevices == null) {
+ return devices;
+ }
for (BluetoothDevice device : bondedDevices) {
final ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
if (!BluetoothUuid.containsAnyUuid(featureUuids, HEADSET_UUIDS)) {
@@ -808,18 +811,17 @@
public boolean setPriority(BluetoothDevice device, int priority) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothHeadsetPriorityKey(device.getAddress()), priority);
Log.i(TAG, "setPriority: device=" + device + ", priority=" + priority + ", "
+ Utils.getUidPidString());
+ mAdapterService.getDatabase()
+ .setProfilePriority(device, BluetoothProfile.HEADSET, priority);
return true;
}
public int getPriority(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
- return Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothHeadsetPriorityKey(device.getAddress()),
- BluetoothProfile.PRIORITY_UNDEFINED);
+ return mAdapterService.getDatabase()
+ .getProfilePriority(device, BluetoothProfile.HEADSET);
}
boolean startVoiceRecognition(BluetoothDevice device) {
@@ -1007,6 +1009,36 @@
}
/**
+ * Process a change in the silence mode for a {@link BluetoothDevice}.
+ *
+ * @param device the device to change silence mode
+ * @param silence true to enable silence mode, false to disable.
+ * @return true on success, false on error
+ */
+ @VisibleForTesting
+ public boolean setSilenceMode(BluetoothDevice device, boolean silence) {
+ Log.d(TAG, "setSilenceMode(" + device + "): " + silence);
+
+ if (silence && Objects.equals(mActiveDevice, device)) {
+ setActiveDevice(null);
+ } else if (!silence && mActiveDevice == null) {
+ // Set the device as the active device if currently no active device.
+ setActiveDevice(device);
+ }
+ synchronized (mStateMachines) {
+ final HeadsetStateMachine stateMachine = mStateMachines.get(device);
+ if (stateMachine == null) {
+ Log.w(TAG, "setSilenceMode: device " + device
+ + " was never connected/connecting");
+ return false;
+ }
+ stateMachine.setSilenceDevice(silence);
+ }
+
+ return true;
+ }
+
+ /**
* Set the active device.
*
* @param device the active device
@@ -1221,9 +1253,9 @@
}
mVirtualCallStarted = true;
// Send virtual phone state changed to initialize SCO
- phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_DIALING, "", 0, true);
- phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_ALERTING, "", 0, true);
- phoneStateChanged(1, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0, true);
+ phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_DIALING, "", 0, "", true);
+ phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_ALERTING, "", 0, "", true);
+ phoneStateChanged(1, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0, "", true);
return true;
}
}
@@ -1239,7 +1271,7 @@
}
mVirtualCallStarted = false;
// 2. Send virtual phone state changed to close SCO
- phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0, true);
+ phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0, "", true);
}
return true;
}
@@ -1452,7 +1484,7 @@
}
private void phoneStateChanged(int numActive, int numHeld, int callState, String number,
- int type, boolean isVirtualCall) {
+ int type, String name, boolean isVirtualCall) {
enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, "Need MODIFY_PHONE_STATE permission");
synchronized (mStateMachines) {
// Should stop all other audio mode in this case
@@ -1498,7 +1530,7 @@
});
doForEachConnectedStateMachine(
stateMachine -> stateMachine.sendMessage(HeadsetStateMachine.CALL_STATE_CHANGED,
- new HeadsetCallState(numActive, numHeld, callState, number, type)));
+ new HeadsetCallState(numActive, numHeld, callState, number, type, name)));
mStateMachinesThread.getThreadHandler().post(() -> {
if (callState == HeadsetHalConstants.CALL_STATE_IDLE
&& mSystemInterface.isCallIdle() && !isAudioOn()) {
@@ -1668,6 +1700,8 @@
private void broadcastActiveDevice(BluetoothDevice device) {
logD("broadcastActiveDevice: " + device);
+ StatsLog.write(StatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED, BluetoothProfile.HEADSET,
+ mAdapterService.obfuscateAddress(device));
Intent intent = new Intent(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
diff --git a/src/com/android/bluetooth/hfp/HeadsetStackEvent.java b/src/com/android/bluetooth/hfp/HeadsetStackEvent.java
index 200fb92..328ac08 100644
--- a/src/com/android/bluetooth/hfp/HeadsetStackEvent.java
+++ b/src/com/android/bluetooth/hfp/HeadsetStackEvent.java
@@ -33,7 +33,7 @@
public static final int EVENT_TYPE_VOLUME_CHANGED = 6;
public static final int EVENT_TYPE_DIAL_CALL = 7;
public static final int EVENT_TYPE_SEND_DTMF = 8;
- public static final int EVENT_TYPE_NOICE_REDUCTION = 9;
+ public static final int EVENT_TYPE_NOISE_REDUCTION = 9;
public static final int EVENT_TYPE_AT_CHLD = 10;
public static final int EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST = 11;
public static final int EVENT_TYPE_AT_CIND = 12;
@@ -153,8 +153,8 @@
return "EVENT_TYPE_DIAL_CALL";
case EVENT_TYPE_SEND_DTMF:
return "EVENT_TYPE_SEND_DTMF";
- case EVENT_TYPE_NOICE_REDUCTION:
- return "EVENT_TYPE_NOICE_REDUCTION";
+ case EVENT_TYPE_NOISE_REDUCTION:
+ return "EVENT_TYPE_NOISE_REDUCTION";
case EVENT_TYPE_AT_CHLD:
return "EVENT_TYPE_AT_CHLD";
case EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST:
diff --git a/src/com/android/bluetooth/hfp/HeadsetStateMachine.java b/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
index d25d9ed..c5485f9 100644
--- a/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
+++ b/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
@@ -16,23 +16,28 @@
package com.android.bluetooth.hfp;
+import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAssignedNumbers;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProtoEnums;
+import android.bluetooth.hfp.BluetoothHfpProtoEnums;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.os.UserHandle;
-import android.support.annotation.VisibleForTesting;
import android.telephony.PhoneNumberUtils;
import android.telephony.PhoneStateListener;
+import android.text.TextUtils;
import android.util.Log;
+import android.util.StatsLog;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
@@ -128,6 +133,7 @@
// Runtime states
private int mSpeakerVolume;
private int mMicVolume;
+ private boolean mDeviceSilenced;
private HeadsetAgIndicatorEnableState mAgIndicatorEnableState;
// The timestamp when the device entered connecting/connected state
private long mConnectingTimestampMs = Long.MIN_VALUE;
@@ -170,6 +176,7 @@
mSystemInterface =
Objects.requireNonNull(systemInterface, "systemInterface cannot be null");
mAdapterService = Objects.requireNonNull(adapterService, "AdapterService cannot be null");
+ mDeviceSilenced = false;
// Create phonebook helper
mPhonebook = new AtPhonebook(mHeadsetService, mNativeInterface);
// Initialize state machine
@@ -299,6 +306,12 @@
// Should not be called from enter() method
void broadcastAudioState(BluetoothDevice device, int fromState, int toState) {
stateLogD("broadcastAudioState: " + device + ": " + fromState + "->" + toState);
+ StatsLog.write(StatsLog.BLUETOOTH_SCO_CONNECTION_STATE_CHANGED,
+ mAdapterService.obfuscateAddress(device),
+ getConnectionStateFromAudioState(toState),
+ TextUtils.equals(mAudioParams.get(HEADSET_WBS), HEADSET_AUDIO_FEATURE_ON)
+ ? BluetoothHfpProtoEnums.SCO_CODEC_MSBC
+ : BluetoothHfpProtoEnums.SCO_CODEC_CVSD);
mHeadsetService.onAudioStateChangedFromStateMachine(device, fromState, toState);
Intent intent = new Intent(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, fromState);
@@ -527,8 +540,9 @@
}
// Per HFP 1.7.1 spec page 23/144, Pending state needs to handle
- // AT+BRSF, AT+CIND, AT+CMER, AT+BIND, +CHLD
+ // AT+BRSF, AT+CIND, AT+CMER, AT+BIND, AT+CHLD
// commands during SLC establishment
+ // AT+CHLD=? will be handled by statck directly
class Connecting extends HeadsetStateBase {
@Override
int getConnectionStateInt() {
@@ -585,9 +599,6 @@
case HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
processConnectionEvent(message, event.valueInt);
break;
- case HeadsetStackEvent.EVENT_TYPE_AT_CHLD:
- processAtChld(event.valueInt, event.device);
- break;
case HeadsetStackEvent.EVENT_TYPE_AT_CIND:
processAtCind(event.device);
break;
@@ -830,6 +841,8 @@
break;
}
case CALL_STATE_CHANGED: {
+ if (mDeviceSilenced) break;
+
HeadsetCallState callState = (HeadsetCallState) message.obj;
if (!mNativeInterface.phoneStateChange(mDevice, callState)) {
stateLogW("processCallState: failed to update call state " + callState);
@@ -921,7 +934,7 @@
case HeadsetStackEvent.EVENT_TYPE_SEND_DTMF:
mSystemInterface.sendDtmf(event.valueInt, event.device);
break;
- case HeadsetStackEvent.EVENT_TYPE_NOICE_REDUCTION:
+ case HeadsetStackEvent.EVENT_TYPE_NOISE_REDUCTION:
processNoiseReductionEvent(event.valueInt == 1);
break;
case HeadsetStackEvent.EVENT_TYPE_WBS:
@@ -1011,9 +1024,6 @@
@Override
public void enter() {
super.enter();
- if (mConnectingTimestampMs == Long.MIN_VALUE) {
- mConnectingTimestampMs = SystemClock.uptimeMillis();
- }
if (mPrevState == mConnecting) {
// Reset AG indicator subscriptions, HF can set this later using AT+BIA command
updateAgIndicatorEnableState(DEFAULT_AG_INDICATOR_ENABLE_STATE);
@@ -1427,6 +1437,27 @@
return mConnectingTimestampMs;
}
+ /**
+ * Set the silence mode status of this state machine
+ *
+ * @param silence true to enter silence mode, false on exit
+ * @return true on success, false on error
+ */
+ @VisibleForTesting
+ public boolean setSilenceDevice(boolean silence) {
+ if (silence == mDeviceSilenced) {
+ return false;
+ }
+ if (silence) {
+ mSystemInterface.getHeadsetPhoneState().listenForPhoneState(mDevice,
+ PhoneStateListener.LISTEN_NONE);
+ } else {
+ updateAgIndicatorEnableState(mAgIndicatorEnableState);
+ }
+ mDeviceSilenced = silence;
+ return true;
+ }
+
/*
* Put the AT command, company ID, arguments, and device in an Intent and broadcast it.
*/
@@ -1803,6 +1834,18 @@
Log.w(TAG, "processAtXapl() argument types not match");
return;
}
+ String[] deviceInfo = ((String) args[0]).split("-");
+ if (deviceInfo.length != 3) {
+ Log.w(TAG, "processAtXapl() deviceInfo length " + deviceInfo.length + " is wrong");
+ return;
+ }
+ String vendorId = deviceInfo[0];
+ String productId = deviceInfo[1];
+ String version = deviceInfo[2];
+ StatsLog.write(StatsLog.BLUETOOTH_DEVICE_INFO_REPORTED,
+ mAdapterService.obfuscateAddress(device), BluetoothProtoEnums.DEVICE_INFO_INTERNAL,
+ BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XAPL, vendorId, productId, version,
+ null);
// feature = 2 indicates that we support battery level reporting only
mNativeInterface.atResponseString(device, "+XAPL=iPhone," + String.valueOf(2));
}
@@ -1880,19 +1923,15 @@
private void processAtBind(String atString, BluetoothDevice device) {
log("processAtBind: " + atString);
- // Parse the AT String to find the Indicator Ids that are supported
- int indId = 0;
- int iter = 0;
- int iter1 = 0;
+ for (String id : atString.split(",")) {
- while (iter < atString.length()) {
- iter1 = findChar(',', atString, iter);
- String id = atString.substring(iter, iter1);
+ int indId;
try {
- indId = Integer.valueOf(id);
+ indId = Integer.parseInt(id);
} catch (NumberFormatException e) {
Log.e(TAG, Log.getStackTraceString(new Throwable()));
+ continue;
}
switch (indId) {
@@ -1908,8 +1947,6 @@
log("Invalid HF Indicator Received");
break;
}
-
- iter = iter1 + 1; // move past comma
}
}
@@ -1947,7 +1984,8 @@
private void updateAgIndicatorEnableState(
HeadsetAgIndicatorEnableState agIndicatorEnableState) {
- if (Objects.equals(mAgIndicatorEnableState, agIndicatorEnableState)) {
+ if (!mDeviceSilenced
+ && Objects.equals(mAgIndicatorEnableState, agIndicatorEnableState)) {
Log.i(TAG, "updateAgIndicatorEnableState, no change in indicator state "
+ mAgIndicatorEnableState);
return;
@@ -2024,6 +2062,18 @@
}
}
+ private static int getConnectionStateFromAudioState(int audioState) {
+ switch (audioState) {
+ case BluetoothHeadset.STATE_AUDIO_CONNECTED:
+ return BluetoothAdapter.STATE_CONNECTED;
+ case BluetoothHeadset.STATE_AUDIO_CONNECTING:
+ return BluetoothAdapter.STATE_CONNECTING;
+ case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
+ return BluetoothAdapter.STATE_DISCONNECTED;
+ }
+ return BluetoothAdapter.STATE_DISCONNECTED;
+ }
+
private static String getMessageName(int what) {
switch (what) {
case CONNECT:
diff --git a/src/com/android/bluetooth/hfpclient/HeadsetClientHalConstants.java b/src/com/android/bluetooth/hfpclient/HeadsetClientHalConstants.java
index 88ae579..d7b5067 100644
--- a/src/com/android/bluetooth/hfpclient/HeadsetClientHalConstants.java
+++ b/src/com/android/bluetooth/hfpclient/HeadsetClientHalConstants.java
@@ -170,6 +170,7 @@
// used for sending vendor specific AT cmds to AG.
static final int HANDSFREECLIENT_AT_CMD_NREC = 15;
+ static final int HANDSFREECLIENT_AT_CMD_VENDOR_SPECIFIC_CMD = 16;
// Flag to check for local NREC support
static final boolean HANDSFREECLIENT_NREC_SUPPORTED = true;
diff --git a/src/com/android/bluetooth/hfpclient/HeadsetClientService.java b/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
index 46c017c..474d332 100644
--- a/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
+++ b/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
@@ -29,10 +29,10 @@
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Message;
-import android.provider.Settings;
import android.util.Log;
import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService;
@@ -64,10 +64,6 @@
public static final String HFP_CLIENT_STOP_TAG = "hfp_client_stop_tag";
- static {
- NativeInterface.classInitNative();
- }
-
@Override
public IProfileServiceBinder initBinder() {
return new BluetoothHeadsetClientBinder(this);
@@ -78,8 +74,15 @@
if (DBG) {
Log.d(TAG, "start()");
}
+ if (sHeadsetClientService != null) {
+ Log.w(TAG, "start(): start called without stop");
+ return false;
+ }
+
// Setup the JNI service
- NativeInterface.initializeNative();
+ mNativeInterface = NativeInterface.getInstance();
+ mNativeInterface.initialize();
+
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
if (mAudioManager == null) {
Log.e(TAG, "AudioManager service doesn't exist?");
@@ -94,8 +97,6 @@
IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
registerReceiver(mBroadcastReceiver, filter);
- mNativeInterface = new NativeInterface();
-
// Start the HfpClientConnectionService to create connection with telecom when HFP
// connection is available.
Intent startIntent = new Intent(this, HfpClientConnectionService.class);
@@ -115,6 +116,11 @@
Log.w(TAG, "stop() called without start()");
return false;
}
+
+ // Stop the HfpClientConnectionService.
+ Intent stopIntent = new Intent(this, HfpClientConnectionService.class);
+ sHeadsetClientService.stopService(stopIntent);
+
setHeadsetClientService(null);
unregisterReceiver(mBroadcastReceiver);
@@ -127,17 +133,12 @@
it.remove();
}
- // Stop the HfpClientConnectionService.
- Intent stopIntent = new Intent(this, HfpClientConnectionService.class);
- stopIntent.putExtra(HFP_CLIENT_STOP_TAG, true);
- startService(stopIntent);
- mNativeInterface = null;
-
// Stop the handler thread
mSmThread.quit();
mSmThread = null;
- NativeInterface.cleanupNative();
+ mNativeInterface.cleanup();
+ mNativeInterface = null;
return true;
}
@@ -430,6 +431,15 @@
}
@Override
+ public boolean sendVendorAtCommand(BluetoothDevice device, int vendorId, String atCommand) {
+ HeadsetClientService service = getService();
+ if (service == null) {
+ return false;
+ }
+ return service.sendVendorAtCommand(device, vendorId, atCommand);
+ }
+
+ @Override
public Bundle getCurrentAgFeatures(BluetoothDevice device) {
HeadsetClientService service = getService();
if (service == null) {
@@ -537,20 +547,18 @@
public boolean setPriority(BluetoothDevice device, int priority) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothHeadsetPriorityKey(device.getAddress()), priority);
if (DBG) {
Log.d(TAG, "Saved priority " + device + " = " + priority);
}
+ AdapterService.getAdapterService().getDatabase()
+ .setProfilePriority(device, BluetoothProfile.HEADSET_CLIENT, priority);
return true;
}
public int getPriority(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- int priority = Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothHeadsetPriorityKey(device.getAddress()),
- BluetoothProfile.PRIORITY_UNDEFINED);
- return priority;
+ return AdapterService.getAdapterService().getDatabase()
+ .getProfilePriority(device, BluetoothProfile.HEADSET_CLIENT);
}
boolean startVoiceRecognition(BluetoothDevice device) {
@@ -821,6 +829,26 @@
return true;
}
+ /** Send vendor AT command. */
+ public boolean sendVendorAtCommand(BluetoothDevice device, int vendorId, String atCommand) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+ HeadsetClientStateMachine sm = getStateMachine(device);
+ if (sm == null) {
+ Log.e(TAG, "Cannot allocate SM for device " + device);
+ return false;
+ }
+
+ int connectionState = sm.getConnectionState(device);
+ if (connectionState != BluetoothProfile.STATE_CONNECTED) {
+ return false;
+ }
+
+ Message msg = sm.obtainMessage(HeadsetClientStateMachine.SEND_VENDOR_AT_COMMAND,
+ vendorId, 0, atCommand);
+ sm.sendMessage(msg);
+ return true;
+ }
+
public Bundle getCurrentAgEvents(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
HeadsetClientStateMachine sm = getStateMachine(device);
@@ -886,7 +914,7 @@
// Allocate a new SM
Log.d(TAG, "Creating a new state machine");
- sm = mSmFactory.make(this, mSmThread);
+ sm = mSmFactory.make(this, mSmThread, mNativeInterface);
mStateMachineMap.put(device, sm);
return sm;
}
@@ -914,8 +942,6 @@
super.dump(sb);
for (HeadsetClientStateMachine sm : mStateMachineMap.values()) {
if (sm != null) {
- println(sb, "State machine:");
- println(sb, "=============");
sm.dump(sb);
}
}
@@ -930,7 +956,7 @@
mSmFactory = factory;
}
- AudioManager getAudioManager() {
+ protected AudioManager getAudioManager() {
return mAudioManager;
}
}
diff --git a/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java b/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
index 844c470..3093aaf 100644
--- a/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
+++ b/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
@@ -39,6 +39,7 @@
import android.bluetooth.BluetoothHeadsetClientCall;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
+import android.bluetooth.hfp.BluetoothHfpProtoEnums;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
@@ -48,9 +49,9 @@
import android.os.Message;
import android.os.ParcelUuid;
import android.os.SystemClock;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.Pair;
+import android.util.StatsLog;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.R;
@@ -58,6 +59,7 @@
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IState;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
@@ -73,7 +75,7 @@
public class HeadsetClientStateMachine extends StateMachine {
private static final String TAG = "HeadsetClientStateMachine";
- private static final boolean DBG = false;
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
static final int NO_ACTION = 0;
static final int IN_BAND_RING_ENABLED = 1;
@@ -97,6 +99,7 @@
public static final int SEND_DTMF = 17;
public static final int EXPLICIT_CALL_TRANSFER = 18;
public static final int DISABLE_NREC = 20;
+ public static final int SEND_VENDOR_AT_COMMAND = 21;
// internal actions
private static final int QUERY_CURRENT_CALLS = 50;
@@ -172,7 +175,9 @@
// This is returned when requesting focus from AudioManager
private AudioFocusRequest mAudioFocusRequest;
- private AudioManager mAudioManager;
+ private final AudioManager mAudioManager;
+ private final NativeInterface mNativeInterface;
+ private final VendorCommandResponseProcessor mVendorProcessor;
// Accessor for the states, useful for reusing the state machines
public IState getDisconnectedState() {
@@ -185,7 +190,9 @@
}
public void dump(StringBuilder sb) {
- ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice);
+ if (mCurrentDevice == null) return;
+ ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice.getAddress() + "("
+ + mCurrentDevice.getName() + ") " + this.toString());
ProfileService.println(sb, "mAudioState: " + mAudioState);
ProfileService.println(sb, "mAudioWbs: " + mAudioWbs);
ProfileService.println(sb, "mIndicatorNetworkState: " + mIndicatorNetworkState);
@@ -208,9 +215,6 @@
ProfileService.println(sb, " " + call);
}
}
-
- ProfileService.println(sb, "State machine stats:");
- ProfileService.println(sb, this.toString());
}
private void clearPendingAction() {
@@ -230,9 +234,7 @@
}
private BluetoothHeadsetClientCall getCall(int... states) {
- if (DBG) {
- Log.d(TAG, "getFromCallsWithStates states:" + Arrays.toString(states));
- }
+ logD("getFromCallsWithStates states:" + Arrays.toString(states));
for (BluetoothHeadsetClientCall c : mCalls.values()) {
for (int s : states) {
if (c.getState() == s) {
@@ -255,9 +257,7 @@
}
private void sendCallChangedIntent(BluetoothHeadsetClientCall c) {
- if (DBG) {
- Log.d(TAG, "sendCallChangedIntent " + c);
- }
+ logD("sendCallChangedIntent " + c);
Intent intent = new Intent(BluetoothHeadsetClient.ACTION_CALL_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.putExtra(BluetoothHeadsetClient.EXTRA_CALL, c);
@@ -265,19 +265,15 @@
}
private boolean queryCallsStart() {
- if (DBG) {
- Log.d(TAG, "queryCallsStart");
- }
+ logD("queryCallsStart");
clearPendingAction();
- NativeInterface.queryCurrentCallsNative(getByteAddress(mCurrentDevice));
+ mNativeInterface.queryCurrentCalls(getByteAddress(mCurrentDevice));
addQueuedAction(QUERY_CURRENT_CALLS, 0);
return true;
}
private void queryCallsDone() {
- if (DBG) {
- Log.d(TAG, "queryCallsDone");
- }
+ logD("queryCallsDone");
// mCalls has two types of calls:
// (a) Calls that are received from AG of a previous iteration of queryCallsStart()
// (b) Calls that are outgoing initiated from HF
@@ -324,11 +320,9 @@
callRetainedIds.addAll(currCallIdSet);
callRetainedIds.retainAll(newCallIdSet);
- if (DBG) {
- Log.d(TAG, "currCallIdSet " + mCalls.keySet() + " newCallIdSet " + newCallIdSet
- + " callAddedIds " + callAddedIds + " callRemovedIds " + callRemovedIds
- + " callRetainedIds " + callRetainedIds);
- }
+ logD("currCallIdSet " + mCalls.keySet() + " newCallIdSet " + newCallIdSet
+ + " callAddedIds " + callAddedIds + " callRemovedIds " + callRemovedIds
+ + " callRetainedIds " + callRetainedIds);
// First thing is to try to associate the outgoing HF with a valid call.
Integer hfOriginatedAssoc = -1;
@@ -336,9 +330,7 @@
BluetoothHeadsetClientCall c = mCalls.get(HF_ORIGINATED_CALL_ID);
long cCreationElapsed = c.getCreationElapsedMilli();
if (callAddedIds.size() > 0) {
- if (DBG) {
- Log.d(TAG, "Associating the first call with HF originated call");
- }
+ logD("Associating the first call with HF originated call");
hfOriginatedAssoc = (Integer) callAddedIds.toArray()[0];
mCalls.put(hfOriginatedAssoc, mCalls.get(HF_ORIGINATED_CALL_ID));
mCalls.remove(HF_ORIGINATED_CALL_ID);
@@ -366,11 +358,9 @@
}
}
- if (DBG) {
- Log.d(TAG, "ADJUST: currCallIdSet " + mCalls.keySet() + " newCallIdSet " + newCallIdSet
- + " callAddedIds " + callAddedIds + " callRemovedIds " + callRemovedIds
- + " callRetainedIds " + callRetainedIds);
- }
+ logD("ADJUST: currCallIdSet " + mCalls.keySet() + " newCallIdSet " + newCallIdSet
+ + " callAddedIds " + callAddedIds + " callRemovedIds " + callRemovedIds
+ + " callRetainedIds " + callRetainedIds);
// Terminate & remove the calls that are done.
for (Integer idx : callRemovedIds) {
@@ -405,7 +395,7 @@
sendMessageDelayed(QUERY_CURRENT_CALLS, QUERY_CURRENT_CALLS_WAIT_MILLIS);
} else {
if (getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING) != null) {
- Log.d(TAG, "Still have incoming call; polling");
+ logD("Still have incoming call; polling");
sendMessageDelayed(QUERY_CURRENT_CALLS, QUERY_CURRENT_CALLS_WAIT_MILLIS);
} else {
removeMessages(QUERY_CURRENT_CALLS);
@@ -418,9 +408,7 @@
private void queryCallsUpdate(int id, int state, String number, boolean multiParty,
boolean outgoing) {
- if (DBG) {
- Log.d(TAG, "queryCallsUpdate: " + id);
- }
+ logD("queryCallsUpdate: " + id);
mCallsUpdate.put(id,
new BluetoothHeadsetClientCall(mCurrentDevice, id, state, number, multiParty,
outgoing, mInBandRing));
@@ -429,9 +417,7 @@
private void acceptCall(int flag) {
int action = -1;
- if (DBG) {
- Log.d(TAG, "acceptCall: (" + flag + ")");
- }
+ logD("acceptCall: (" + flag + ")");
BluetoothHeadsetClientCall c = getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING,
BluetoothHeadsetClientCall.CALL_STATE_WAITING);
@@ -444,9 +430,7 @@
}
}
- if (DBG) {
- Log.d(TAG, "Call to accept: " + c);
- }
+ logD("Call to accept: " + c);
switch (c.getState()) {
case BluetoothHeadsetClientCall.CALL_STATE_INCOMING:
if (flag != BluetoothHeadsetClient.CALL_ACCEPT_NONE) {
@@ -468,14 +452,10 @@
// existing call or hold the existing call. We hold the other call by default.
if (flag == BluetoothHeadsetClient.CALL_ACCEPT_HOLD
|| flag == BluetoothHeadsetClient.CALL_ACCEPT_NONE) {
- if (DBG) {
- Log.d(TAG, "Accepting call with accept and hold");
- }
+ logD("Accepting call with accept and hold");
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_2;
} else if (flag == BluetoothHeadsetClient.CALL_ACCEPT_TERMINATE) {
- if (DBG) {
- Log.d(TAG, "Accepting call with accept and reject");
- }
+ logD("Accepting call with accept and reject");
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_1;
} else {
Log.e(TAG, "Aceept call with invalid flag: " + flag);
@@ -510,7 +490,7 @@
routeHfpAudio(true);
}
- if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), action, 0)) {
+ if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) {
addQueuedAction(ACCEPT_CALL, action);
} else {
Log.e(TAG, "ERROR: Couldn't accept a call, action:" + action);
@@ -520,18 +500,14 @@
private void rejectCall() {
int action;
- if (DBG) {
- Log.d(TAG, "rejectCall");
- }
+ logD("rejectCall");
BluetoothHeadsetClientCall c = getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING,
BluetoothHeadsetClientCall.CALL_STATE_WAITING,
BluetoothHeadsetClientCall.CALL_STATE_HELD_BY_RESPONSE_AND_HOLD,
BluetoothHeadsetClientCall.CALL_STATE_HELD);
if (c == null) {
- if (DBG) {
- Log.d(TAG, "No call to reject, returning.");
- }
+ logD("No call to reject, returning.");
return;
}
@@ -553,10 +529,8 @@
return;
}
- if (DBG) {
- Log.d(TAG, "Reject call action " + action);
- }
- if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), action, 0)) {
+ if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) {
+ logD("Reject call action " + action);
addQueuedAction(REJECT_CALL, action);
} else {
Log.e(TAG, "ERROR: Couldn't reject a call, action:" + action);
@@ -566,9 +540,7 @@
private void holdCall() {
int action;
- if (DBG) {
- Log.d(TAG, "holdCall");
- }
+ logD("holdCall");
BluetoothHeadsetClientCall c = getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING);
if (c != null) {
@@ -582,7 +554,7 @@
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_2;
}
- if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), action, 0)) {
+ if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) {
addQueuedAction(HOLD_CALL, action);
} else {
Log.e(TAG, "ERROR: Couldn't hold a call, action:" + action);
@@ -590,9 +562,7 @@
}
private void terminateCall() {
- if (DBG) {
- Log.d(TAG, "terminateCall");
- }
+ logD("terminateCall");
int action = HeadsetClientHalConstants.CALL_ACTION_CHUP;
@@ -605,7 +575,7 @@
action = HeadsetClientHalConstants.CALL_ACTION_CHLD_0;
}
if (c != null) {
- if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice), action, 0)) {
+ if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice), action, 0)) {
addQueuedAction(TERMINATE_CALL, action);
} else {
Log.e(TAG, "ERROR: Couldn't terminate outgoing call");
@@ -614,9 +584,7 @@
}
private void enterPrivateMode(int idx) {
- if (DBG) {
- Log.d(TAG, "enterPrivateMode: " + idx);
- }
+ logD("enterPrivateMode: " + idx);
BluetoothHeadsetClientCall c = mCalls.get(idx);
@@ -625,7 +593,7 @@
return;
}
- if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice),
+ if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice),
HeadsetClientHalConstants.CALL_ACTION_CHLD_2X, idx)) {
addQueuedAction(ENTER_PRIVATE_MODE, c);
} else {
@@ -634,16 +602,14 @@
}
private void explicitCallTransfer() {
- if (DBG) {
- Log.d(TAG, "explicitCallTransfer");
- }
+ logD("explicitCallTransfer");
// can't transfer call if there is not enough call parties
if (mCalls.size() < 2) {
return;
}
- if (NativeInterface.handleCallActionNative(getByteAddress(mCurrentDevice),
+ if (mNativeInterface.handleCallAction(getByteAddress(mCurrentDevice),
HeadsetClientHalConstants.CALL_ACTION_CHLD_4, -1)) {
addQueuedAction(EXPLICIT_CALL_TRANSFER);
} else {
@@ -657,6 +623,10 @@
== HeadsetClientHalConstants.PEER_FEAT_3WAY) {
b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_3WAY_CALLING, true);
}
+ if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_VREC)
+ == HeadsetClientHalConstants.PEER_FEAT_VREC) {
+ b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_VOICE_RECOGNITION, true);
+ }
if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_REJECT)
== HeadsetClientHalConstants.PEER_FEAT_REJECT) {
b.putBoolean(BluetoothHeadsetClient.EXTRA_AG_FEATURE_REJECT_CALL, true);
@@ -692,11 +662,15 @@
return b;
}
- HeadsetClientStateMachine(HeadsetClientService context, Looper looper) {
+ HeadsetClientStateMachine(HeadsetClientService context, Looper looper,
+ NativeInterface nativeInterface) {
super(TAG, looper);
mService = context;
+ mNativeInterface = nativeInterface;
mAudioManager = mService.getAudioManager();
+ mVendorProcessor = new VendorCommandResponseProcessor(mService, mNativeInterface);
+
mAdapter = BluetoothAdapter.getDefaultAdapter();
mAudioState = BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED;
mAudioWbs = false;
@@ -732,11 +706,11 @@
setInitialState(mDisconnected);
}
- static HeadsetClientStateMachine make(HeadsetClientService context, Looper l) {
- if (DBG) {
- Log.d(TAG, "make");
- }
- HeadsetClientStateMachine hfcsm = new HeadsetClientStateMachine(context, l);
+ static HeadsetClientStateMachine make(HeadsetClientService context, Looper looper,
+ NativeInterface nativeInterface) {
+ logD("make");
+ HeadsetClientStateMachine hfcsm = new HeadsetClientStateMachine(context, looper,
+ nativeInterface);
hfcsm.start();
return hfcsm;
}
@@ -746,9 +720,7 @@
Log.e(TAG, "AudioManager is null!");
return;
}
- if (DBG) {
- Log.d(TAG, "hfp_enable=" + enable);
- }
+ logD("hfp_enable=" + enable);
if (enable && !sAudioIsRouted) {
mAudioManager.setParameters("hfp_enable=true");
} else if (!enable) {
@@ -767,16 +739,17 @@
.setAudioAttributes(streamAttributes)
.build();
int focusRequestStatus = mAudioManager.requestAudioFocus(focusRequest);
- if (DBG) {
- String s = (focusRequestStatus == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
- ? "AudioFocus granted" : "AudioFocus NOT granted";
- Log.d(TAG, "AudioManager requestAudioFocus returned: " + s);
- }
+ String s = (focusRequestStatus == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
+ ? "AudioFocus granted" : "AudioFocus NOT granted";
+ logD("AudioManager requestAudioFocus returned: " + s);
return focusRequest;
}
public void doQuit() {
- Log.d(TAG, "doQuit");
+ logD("doQuit");
+ if (mCurrentDevice != null) {
+ mNativeInterface.disconnect(getByteAddress(mCurrentDevice));
+ }
routeHfpAudio(false);
returnAudioFocusIfNecessary();
quitNow();
@@ -793,7 +766,7 @@
int hfRange = MAX_HFP_SCO_VOICE_CALL_VOLUME - MIN_HFP_SCO_VOICE_CALL_VOLUME;
int amOffset = (amRange * (hfVol - MIN_HFP_SCO_VOICE_CALL_VOLUME)) / hfRange;
int amVol = sMinAmVcVol + amOffset;
- Log.d(TAG, "HF -> AM " + hfVol + " " + amVol);
+ logD("HF -> AM " + hfVol + " " + amVol);
return amVol;
}
@@ -802,14 +775,14 @@
int hfRange = MAX_HFP_SCO_VOICE_CALL_VOLUME - MIN_HFP_SCO_VOICE_CALL_VOLUME;
int hfOffset = (hfRange * (amVol - sMinAmVcVol)) / amRange;
int hfVol = MIN_HFP_SCO_VOICE_CALL_VOLUME + hfOffset;
- Log.d(TAG, "AM -> HF " + amVol + " " + hfVol);
+ logD("AM -> HF " + amVol + " " + hfVol);
return hfVol;
}
class Disconnected extends State {
@Override
public void enter() {
- Log.d(TAG, "Enter Disconnected: " + getCurrentMessage().what);
+ logD("Enter Disconnected: " + getCurrentMessage().what);
// cleanup
mIndicatorNetworkState = HeadsetClientHalConstants.NETWORK_STATE_NOT_AVAILABLE;
@@ -851,7 +824,7 @@
@Override
public synchronized boolean processMessage(Message message) {
- Log.d(TAG, "Disconnected process message: " + message.what);
+ logD("Disconnected process message: " + message.what);
if (mCurrentDevice != null) {
Log.e(TAG, "ERROR: current device not null in Disconnected");
@@ -861,7 +834,7 @@
switch (message.what) {
case CONNECT:
BluetoothDevice device = (BluetoothDevice) message.obj;
- if (!NativeInterface.connectNative(getByteAddress(device))) {
+ if (!mNativeInterface.connect(getByteAddress(device))) {
// No state transition is involved, fire broadcast immediately
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
@@ -875,15 +848,11 @@
break;
case StackEvent.STACK_EVENT:
StackEvent event = (StackEvent) message.obj;
- if (DBG) {
- Log.d(TAG, "Stack event type: " + event.type);
- }
+ logD("Stack event type: " + event.type);
switch (event.type) {
case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
- if (DBG) {
- Log.d(TAG, "Disconnected: Connection " + event.device
- + " state changed:" + event.valueInt);
- }
+ logD("Disconnected: Connection " + event.device
+ + " state changed:" + event.valueInt);
processConnectionEvent(event.valueInt, event.device);
break;
default:
@@ -911,7 +880,7 @@
+ " bondState=" + device.getBondState());
// reject the connection and stay in Disconnected state
// itself
- NativeInterface.disconnectNative(getByteAddress(device));
+ mNativeInterface.disconnect(getByteAddress(device));
// the other profile connection should be initiated
AdapterService adapterService = AdapterService.getAdapterService();
// No state transition is involved, fire broadcast immediately
@@ -930,9 +899,7 @@
@Override
public void exit() {
- if (DBG) {
- Log.d(TAG, "Exit Disconnected: " + getCurrentMessage().what);
- }
+ logD("Exit Disconnected: " + getCurrentMessage().what);
mPrevState = this;
}
}
@@ -940,9 +907,7 @@
class Connecting extends State {
@Override
public void enter() {
- if (DBG) {
- Log.d(TAG, "Enter Connecting: " + getCurrentMessage().what);
- }
+ logD("Enter Connecting: " + getCurrentMessage().what);
// This message is either consumed in processMessage or
// removed in exit. It is safe to send a CONNECTING_TIMEOUT here since
// the only transition is when connection attempt is initiated.
@@ -959,9 +924,7 @@
@Override
public synchronized boolean processMessage(Message message) {
- if (DBG) {
- Log.d(TAG, "Connecting process message: " + message.what);
- }
+ logD("Connecting process message: " + message.what);
switch (message.what) {
case CONNECT:
@@ -971,16 +934,11 @@
break;
case StackEvent.STACK_EVENT:
StackEvent event = (StackEvent) message.obj;
- if (DBG) {
- Log.d(TAG, "Connecting: event type: " + event.type);
- }
+ logD("Connecting: event type: " + event.type);
switch (event.type) {
case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
- if (DBG) {
- Log.d(TAG,
- "Connecting: Connection " + event.device + " state changed:"
- + event.valueInt);
- }
+ logD("Connecting: Connection " + event.device + " state changed:"
+ + event.valueInt);
processConnectionEvent(event.valueInt, event.valueInt2, event.valueInt3,
event.device);
break;
@@ -1029,14 +987,14 @@
break;
case HeadsetClientHalConstants.CONNECTION_STATE_SLC_CONNECTED:
- Log.d(TAG, "HFPClient Connected from Connecting state");
+ logD("HFPClient Connected from Connecting state");
mPeerFeatures = peerFeat;
mChldFeatures = chldFeat;
// We do not support devices which do not support enhanced call status (ECS).
if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_ECS) == 0) {
- NativeInterface.disconnectNative(getByteAddress(device));
+ mNativeInterface.disconnect(getByteAddress(device));
return;
}
@@ -1044,7 +1002,7 @@
if (HeadsetClientHalConstants.HANDSFREECLIENT_NREC_SUPPORTED && (
(mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_ECNR)
== HeadsetClientHalConstants.PEER_FEAT_ECNR)) {
- if (NativeInterface.sendATCmdNative(getByteAddress(mCurrentDevice),
+ if (mNativeInterface.sendATCmd(getByteAddress(mCurrentDevice),
HeadsetClientHalConstants.HANDSFREECLIENT_AT_CMD_NREC, 1, 0,
null)) {
addQueuedAction(DISABLE_NREC);
@@ -1080,9 +1038,7 @@
break;
case HeadsetClientHalConstants.CONNECTION_STATE_CONNECTING:
/* outgoing connecting started */
- if (DBG) {
- Log.d(TAG, "outgoing connection started, ignore");
- }
+ logD("outgoing connection started, ignore");
break;
case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTING:
default:
@@ -1093,9 +1049,7 @@
@Override
public void exit() {
- if (DBG) {
- Log.d(TAG, "Exit Connecting: " + getCurrentMessage().what);
- }
+ logD("Exit Connecting: " + getCurrentMessage().what);
removeMessages(CONNECTING_TIMEOUT);
mPrevState = this;
}
@@ -1106,9 +1060,7 @@
@Override
public void enter() {
- if (DBG) {
- Log.d(TAG, "Enter Connected: " + getCurrentMessage().what);
- }
+ logD("Enter Connected: " + getCurrentMessage().what);
mAudioWbs = false;
mCommandedSpeakerVolume = -1;
if (mPrevState == mConnecting) {
@@ -1125,9 +1077,7 @@
@Override
public synchronized boolean processMessage(Message message) {
- if (DBG) {
- Log.d(TAG, "Connected process message: " + message.what);
- }
+ logD("Connected process message: " + message.what);
if (DBG) {
if (mCurrentDevice == null) {
Log.e(TAG, "ERROR: mCurrentDevice is null in Connected");
@@ -1142,20 +1092,20 @@
// already connected to this device, do nothing
break;
}
- NativeInterface.connectNative(getByteAddress(device));
+ mNativeInterface.connect(getByteAddress(device));
break;
case DISCONNECT:
BluetoothDevice dev = (BluetoothDevice) message.obj;
if (!mCurrentDevice.equals(dev)) {
break;
}
- if (!NativeInterface.disconnectNative(getByteAddress(dev))) {
+ if (!mNativeInterface.disconnect(getByteAddress(dev))) {
Log.e(TAG, "disconnectNative failed for " + dev);
}
break;
case CONNECT_AUDIO:
- if (!NativeInterface.connectAudioNative(getByteAddress(mCurrentDevice))) {
+ if (!mNativeInterface.connectAudio(getByteAddress(mCurrentDevice))) {
Log.e(TAG, "ERROR: Couldn't connect Audio for device " + mCurrentDevice);
// No state transition is involved, fire broadcast immediately
broadcastAudioState(mCurrentDevice,
@@ -1167,14 +1117,14 @@
break;
case DISCONNECT_AUDIO:
- if (!NativeInterface.disconnectAudioNative(getByteAddress(mCurrentDevice))) {
+ if (!mNativeInterface.disconnectAudio(getByteAddress(mCurrentDevice))) {
Log.e(TAG, "ERROR: Couldn't disconnect Audio for device " + mCurrentDevice);
}
break;
case VOICE_RECOGNITION_START:
if (mVoiceRecognitionActive == HeadsetClientHalConstants.VR_STATE_STOPPED) {
- if (NativeInterface.startVoiceRecognitionNative(
+ if (mNativeInterface.startVoiceRecognition(
getByteAddress(mCurrentDevice))) {
addQueuedAction(VOICE_RECOGNITION_START);
} else {
@@ -1185,7 +1135,7 @@
case VOICE_RECOGNITION_STOP:
if (mVoiceRecognitionActive == HeadsetClientHalConstants.VR_STATE_STARTED) {
- if (NativeInterface.stopVoiceRecognitionNative(
+ if (mNativeInterface.stopVoiceRecognition(
getByteAddress(mCurrentDevice))) {
addQueuedAction(VOICE_RECOGNITION_STOP);
} else {
@@ -1194,6 +1144,13 @@
}
break;
+ case SEND_VENDOR_AT_COMMAND: {
+ int vendorId = message.arg1;
+ String atCommand = (String) (message.obj);
+ mVendorProcessor.sendCommand(vendorId, atCommand, mCurrentDevice);
+ break;
+ }
+
// Called only for Mute/Un-mute - Mic volume change is not allowed.
case SET_MIC_VOLUME:
break;
@@ -1202,10 +1159,10 @@
int amVol = message.arg1;
int hfVol = amToHfVol(amVol);
if (amVol != mCommandedSpeakerVolume) {
- Log.d(TAG, "Volume" + amVol + ":" + mCommandedSpeakerVolume);
+ logD("Volume" + amVol + ":" + mCommandedSpeakerVolume);
// Volume was changed by a 3rd party
mCommandedSpeakerVolume = -1;
- if (NativeInterface.setVolumeNative(getByteAddress(mCurrentDevice),
+ if (mNativeInterface.setVolume(getByteAddress(mCurrentDevice),
HeadsetClientHalConstants.VOLUME_TYPE_SPK, hfVol)) {
addQueuedAction(SET_SPEAKER_VOLUME);
}
@@ -1216,7 +1173,7 @@
BluetoothHeadsetClientCall c = (BluetoothHeadsetClientCall) message.obj;
mCalls.put(HF_ORIGINATED_CALL_ID, c);
- if (NativeInterface.dialNative(getByteAddress(mCurrentDevice), c.getNumber())) {
+ if (mNativeInterface.dial(getByteAddress(mCurrentDevice), c.getNumber())) {
addQueuedAction(DIAL_NUMBER, c.getNumber());
// Start looping on calling current calls.
sendMessage(QUERY_CURRENT_CALLS);
@@ -1248,7 +1205,7 @@
explicitCallTransfer();
break;
case SEND_DTMF:
- if (NativeInterface.sendDtmfNative(getByteAddress(mCurrentDevice),
+ if (mNativeInterface.sendDtmf(getByteAddress(mCurrentDevice),
(byte) message.arg1)) {
addQueuedAction(SEND_DTMF);
} else {
@@ -1256,7 +1213,7 @@
}
break;
case SUBSCRIBER_INFO:
- if (NativeInterface.retrieveSubscriberInfoNative(
+ if (mNativeInterface.retrieveSubscriberInfo(
getByteAddress(mCurrentDevice))) {
addQueuedAction(SUBSCRIBER_INFO);
} else {
@@ -1274,29 +1231,21 @@
case StackEvent.STACK_EVENT:
Intent intent = null;
StackEvent event = (StackEvent) message.obj;
- if (DBG) {
- Log.d(TAG, "Connected: event type: " + event.type);
- }
+ logD("Connected: event type: " + event.type);
switch (event.type) {
case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
- if (DBG) {
- Log.d(TAG, "Connected: Connection state changed: " + event.device
- + ": " + event.valueInt);
- }
+ logD("Connected: Connection state changed: " + event.device
+ + ": " + event.valueInt);
processConnectionEvent(event.valueInt, event.device);
break;
case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
- if (DBG) {
- Log.d(TAG, "Connected: Audio state changed: " + event.device + ": "
- + event.valueInt);
- }
+ logD("Connected: Audio state changed: " + event.device + ": "
+ + event.valueInt);
processAudioEvent(event.valueInt, event.device);
break;
case StackEvent.EVENT_TYPE_NETWORK_STATE:
- if (DBG) {
- Log.d(TAG, "Connected: Network state: " + event.valueInt);
- }
+ logD("Connected: Network state: " + event.valueInt);
mIndicatorNetworkState = event.valueInt;
intent = new Intent(BluetoothHeadsetClient.ACTION_AG_EVENT);
@@ -1315,7 +1264,7 @@
if (mIndicatorNetworkState
== HeadsetClientHalConstants.NETWORK_STATE_AVAILABLE) {
- if (NativeInterface.queryCurrentOperatorNameNative(
+ if (mNativeInterface.queryCurrentOperatorName(
getByteAddress(mCurrentDevice))) {
addQueuedAction(QUERY_OPERATOR_NAME);
} else {
@@ -1388,7 +1337,7 @@
case StackEvent.EVENT_TYPE_VOLUME_CHANGED:
if (event.valueInt == HeadsetClientHalConstants.VOLUME_TYPE_SPK) {
mCommandedSpeakerVolume = hfToAmVol(event.valueInt2);
- Log.d(TAG, "AM volume set to " + mCommandedSpeakerVolume);
+ logD("AM volume set to " + mCommandedSpeakerVolume);
mAudioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL,
+mCommandedSpeakerVolume, AudioManager.FLAG_SHOW_UI);
} else if (event.valueInt
@@ -1405,10 +1354,8 @@
break;
}
- if (DBG) {
- Log.d(TAG, "Connected: command result: " + event.valueInt
- + " queuedAction: " + queuedAction.first);
- }
+ logD("Connected: command result: " + event.valueInt
+ + " queuedAction: " + queuedAction.first);
switch (queuedAction.first) {
case QUERY_CURRENT_CALLS:
@@ -1447,16 +1394,19 @@
event.valueInt);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, event.device);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
- if (DBG) {
- Log.d(TAG,
- event.device.toString() + "onInBandRing" + event.valueInt);
- }
+ logD(event.device.toString() + "onInBandRing" + event.valueInt);
break;
case StackEvent.EVENT_TYPE_RING_INDICATION:
// Ringing is not handled at this indication and rather should be
// implemented (by the client of this service). Use the
// CALL_STATE_INCOMING (and similar) handle ringing.
break;
+ case StackEvent.EVENT_TYPE_UNKNOWN_EVENT:
+ if (!mVendorProcessor.processEvent(event.valueString, event.device)) {
+ Log.e(TAG, "Unknown event :" + event.valueString
+ + " for device " + event.device);
+ }
+ break;
default:
Log.e(TAG, "Unknown stack event: " + event.type);
break;
@@ -1473,9 +1423,7 @@
private void processConnectionEvent(int state, BluetoothDevice device) {
switch (state) {
case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED:
- if (DBG) {
- Log.d(TAG, "Connected disconnects.");
- }
+ logD("Connected disconnects.");
// AG disconnects
if (mCurrentDevice.equals(device)) {
transitionTo(mDisconnected);
@@ -1526,23 +1474,15 @@
final int amVol = mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL);
final int hfVol = amToHfVol(amVol);
- if (DBG) {
- Log.d(TAG, "hfp_enable=true mAudioWbs is " + mAudioWbs);
- }
+ logD("hfp_enable=true mAudioWbs is " + mAudioWbs);
if (mAudioWbs) {
- if (DBG) {
- Log.d(TAG, "Setting sampling rate as 16000");
- }
+ logD("Setting sampling rate as 16000");
mAudioManager.setParameters("hfp_set_sampling_rate=16000");
} else {
- if (DBG) {
- Log.d(TAG, "Setting sampling rate as 8000");
- }
+ logD("Setting sampling rate as 8000");
mAudioManager.setParameters("hfp_set_sampling_rate=8000");
}
- if (DBG) {
- Log.d(TAG, "hf_volume " + hfVol);
- }
+ logD("hf_volume " + hfVol);
routeHfpAudio(true);
mAudioFocusRequest = requestAudioFocus();
mAudioManager.setParameters("hfp_volume=" + hfVol);
@@ -1571,9 +1511,7 @@
@Override
public void exit() {
- if (DBG) {
- Log.d(TAG, "Exit Connected: " + getCurrentMessage().what);
- }
+ logD("Exit Connected: " + getCurrentMessage().what);
mPrevState = this;
}
}
@@ -1581,18 +1519,14 @@
class AudioOn extends State {
@Override
public void enter() {
- if (DBG) {
- Log.d(TAG, "Enter AudioOn: " + getCurrentMessage().what);
- }
+ logD("Enter AudioOn: " + getCurrentMessage().what);
broadcastAudioState(mCurrentDevice, BluetoothHeadsetClient.STATE_AUDIO_CONNECTED,
BluetoothHeadsetClient.STATE_AUDIO_CONNECTING);
}
@Override
public synchronized boolean processMessage(Message message) {
- if (DBG) {
- Log.d(TAG, "AudioOn process message: " + message.what);
- }
+ logD("AudioOn process message: " + message.what);
if (DBG) {
if (mCurrentDevice == null) {
Log.e(TAG, "ERROR: mCurrentDevice is null in Connected");
@@ -1617,7 +1551,7 @@
* StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED, that triggers State
* Machines state changing
*/
- if (NativeInterface.disconnectAudioNative(getByteAddress(mCurrentDevice))) {
+ if (mNativeInterface.disconnectAudio(getByteAddress(mCurrentDevice))) {
routeHfpAudio(false);
returnAudioFocusIfNecessary();
}
@@ -1629,22 +1563,16 @@
case StackEvent.STACK_EVENT:
StackEvent event = (StackEvent) message.obj;
- if (DBG) {
- Log.d(TAG, "AudioOn: event type: " + event.type);
- }
+ logD("AudioOn: event type: " + event.type);
switch (event.type) {
case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
- if (DBG) {
- Log.d(TAG, "AudioOn connection state changed" + event.device + ": "
- + event.valueInt);
- }
+ logD("AudioOn connection state changed" + event.device + ": "
+ + event.valueInt);
processConnectionEvent(event.valueInt, event.device);
break;
case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
- if (DBG) {
- Log.d(TAG, "AudioOn audio state changed" + event.device + ": "
- + event.valueInt);
- }
+ logD("AudioOn audio state changed" + event.device + ": "
+ + event.valueInt);
processAudioEvent(event.valueInt, event.device);
break;
default:
@@ -1703,9 +1631,7 @@
@Override
public void exit() {
- if (DBG) {
- Log.d(TAG, "Exit AudioOn: " + getCurrentMessage().what);
- }
+ logD("Exit AudioOn: " + getCurrentMessage().what);
mPrevState = this;
broadcastAudioState(mCurrentDevice, BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED,
BluetoothHeadsetClient.STATE_AUDIO_CONNECTED);
@@ -1738,6 +1664,11 @@
}
private void broadcastAudioState(BluetoothDevice device, int newState, int prevState) {
+ StatsLog.write(StatsLog.BLUETOOTH_SCO_CONNECTION_STATE_CHANGED,
+ AdapterService.getAdapterService().obfuscateAddress(device),
+ getConnectionStateFromAudioState(newState), mAudioWbs
+ ? BluetoothHfpProtoEnums.SCO_CODEC_MSBC
+ : BluetoothHfpProtoEnums.SCO_CODEC_CVSD);
Intent intent = new Intent(BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, newState);
@@ -1746,16 +1677,12 @@
}
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
- if (DBG) {
- Log.d(TAG, "Audio state " + device + ": " + prevState + "->" + newState);
- }
+ logD("Audio state " + device + ": " + prevState + "->" + newState);
}
// This method does not check for error condition (newState == prevState)
private void broadcastConnectionState(BluetoothDevice device, int newState, int prevState) {
- if (DBG) {
- Log.d(TAG, "Connection state " + device + ": " + prevState + "->" + newState);
- }
+ logD("Connection state " + device + ": " + prevState + "->" + newState);
/*
* Notifying the connection state change of the profile before sending
* the intent for connection state change, as it was causing a race
@@ -1773,6 +1700,10 @@
== HeadsetClientHalConstants.PEER_FEAT_3WAY) {
intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_3WAY_CALLING, true);
}
+ if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_VREC)
+ == HeadsetClientHalConstants.PEER_FEAT_VREC) {
+ intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_VOICE_RECOGNITION, true);
+ }
if ((mPeerFeatures & HeadsetClientHalConstants.PEER_FEAT_REJECT)
== HeadsetClientHalConstants.PEER_FEAT_REJECT) {
intent.putExtra(BluetoothHeadsetClient.EXTRA_AG_FEATURE_REJECT_CALL, true);
@@ -1890,4 +1821,22 @@
b.putString(BluetoothHeadsetClient.EXTRA_SUBSCRIBER_INFO, mSubscriberInfo);
return b;
}
+
+ private static int getConnectionStateFromAudioState(int audioState) {
+ switch (audioState) {
+ case BluetoothHeadsetClient.STATE_AUDIO_CONNECTED:
+ return BluetoothAdapter.STATE_CONNECTED;
+ case BluetoothHeadsetClient.STATE_AUDIO_CONNECTING:
+ return BluetoothAdapter.STATE_CONNECTING;
+ case BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED:
+ return BluetoothAdapter.STATE_DISCONNECTED;
+ }
+ return BluetoothAdapter.STATE_DISCONNECTED;
+ }
+
+ private static void logD(String message) {
+ if (DBG) {
+ Log.d(TAG, message);
+ }
+ }
}
diff --git a/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineFactory.java b/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineFactory.java
index 6be72d0..b0c7265 100644
--- a/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineFactory.java
+++ b/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineFactory.java
@@ -20,7 +20,12 @@
// Factory so that StateMachine objected can be mocked
public class HeadsetClientStateMachineFactory {
- public HeadsetClientStateMachine make(HeadsetClientService context, HandlerThread t) {
- return HeadsetClientStateMachine.make(context, t.getLooper());
+ /**
+ * Factory method to create state machine for headset client
+ *
+ */
+ public HeadsetClientStateMachine make(HeadsetClientService context, HandlerThread t,
+ NativeInterface nativeInterface) {
+ return HeadsetClientStateMachine.make(context, t.getLooper(), nativeInterface);
}
}
diff --git a/src/com/android/bluetooth/hfpclient/NativeInterface.java b/src/com/android/bluetooth/hfpclient/NativeInterface.java
index 77d9af7..d622e78 100644
--- a/src/com/android/bluetooth/hfpclient/NativeInterface.java
+++ b/src/com/android/bluetooth/hfpclient/NativeInterface.java
@@ -24,50 +24,280 @@
import android.bluetooth.BluetoothDevice;
import android.util.Log;
-class NativeInterface {
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Defines native calls that are used by state machine/service to either send or receive
+ * messages to/from the native stack. This file is registered for the native methods in
+ * corresponding CPP file.
+ */
+public class NativeInterface {
private static final String TAG = "NativeInterface";
private static final boolean DBG = false;
- NativeInterface() {}
+ static {
+ classInitNative();
+ }
+
+ private NativeInterface() {}
+ private static NativeInterface sInterface;
+ private static final Object INSTANCE_LOCK = new Object();
+
+ /**
+ * This class is a singleton because native library should only be loaded once
+ *
+ * @return default instance
+ */
+ public static NativeInterface getInstance() {
+ synchronized (INSTANCE_LOCK) {
+ if (sInterface == null) {
+ sInterface = new NativeInterface();
+ }
+ }
+ return sInterface;
+ }
+
+ // Native wrappers to help unit testing
+ /**
+ * Initialize native stack
+ */
+ @VisibleForTesting
+ public void initialize() {
+ initializeNative();
+ }
+
+ /**
+ * Close and clean up native stack
+ */
+ @VisibleForTesting
+ public void cleanup() {
+ cleanupNative();
+ }
+
+ /**
+ * Connect to the specified paired device
+ *
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean connect(byte[] address) {
+ return connectNative(address);
+ }
+
+ /**
+ * Disconnect from the specified paired device
+ *
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean disconnect(byte[] address) {
+ return disconnectNative(address);
+ }
+
+ /**
+ * Initiate audio connection to the specified paired device
+ *
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean connectAudio(byte[] address) {
+ return connectAudioNative(address);
+ }
+
+ /**
+ * Close audio connection from the specified paired device
+ *
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ public boolean disconnectAudio(byte[] address) {
+ return disconnectAudioNative(address);
+ }
+
+ /**
+ * Initiate voice recognition to the specified paired device
+ *
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean startVoiceRecognition(byte[] address) {
+ return startVoiceRecognitionNative(address);
+ }
+
+ /**
+ * Close voice recognition to the specified paired device
+ *
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean stopVoiceRecognition(byte[] address) {
+ return stopVoiceRecognitionNative(address);
+ }
+
+ /**
+ * Set volume to the specified paired device
+ *
+ * @param volumeType type of volume as in
+ * HeadsetClientHalConstants.VOLUME_TYPE_xxxx
+ * @param volume volume level
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean setVolume(byte[] address, int volumeType, int volume) {
+ return setVolumeNative(address, volumeType, volume);
+ }
+
+ /**
+ * dial number from the specified paired device
+ *
+ * @param number phone number to be dialed
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean dial(byte[] address, String number) {
+ return dialNative(address, number);
+ }
+
+ /**
+ * Memory dialing from the specified paired device
+ *
+ * @param location memory location
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean dialMemory(byte[] address, int location) {
+ return dialMemoryNative(address, location);
+ }
+
+ /**
+ * Apply action to call
+ *
+ * @action action (e.g. hold, terminate etc)
+ * @index call index
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean handleCallAction(byte[] address, int action, int index) {
+ return handleCallActionNative(address, action, index);
+ }
+
+ /**
+ * Query current call status from the specified paired device
+ *
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean queryCurrentCalls(byte[] address) {
+ return queryCurrentCallsNative(address);
+ }
+
+ /**
+ * Query operator name from the specified paired device
+ *
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean queryCurrentOperatorName(byte[] address) {
+ return queryCurrentOperatorNameNative(address);
+ }
+
+ /**
+ * Retrieve subscriber number from the specified paired device
+ *
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean retrieveSubscriberInfo(byte[] address) {
+ return retrieveSubscriberInfoNative(address);
+ }
+
+ /**
+ * Transmit DTMF code
+ *
+ * @param code DTMF code
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean sendDtmf(byte[] address, byte code) {
+ return sendDtmfNative(address, code);
+ }
+
+ /**
+ * Request last voice tag
+ *
+ * @param address target device's address
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean requestLastVoiceTagNumber(byte[] address) {
+ return requestLastVoiceTagNumberNative(address);
+ }
+
+ /**
+ * Send an AT command
+ *
+ * @param atCmd command code
+ * @param val1 command specific argurment1
+ * @param val2 command specific argurment2
+ * @param arg other command specific argurments
+ * @return True on success, False on failure
+ */
+ @VisibleForTesting
+ public boolean sendATCmd(byte[] address, int atCmd, int val1, int val2, String arg) {
+ return sendATCmdNative(address, atCmd, val1, val2, arg);
+ }
// Native methods that call into the JNI interface
- static native void classInitNative();
+ private static native void classInitNative();
- static native void initializeNative();
+ private native void initializeNative();
- static native void cleanupNative();
+ private native void cleanupNative();
- static native boolean connectNative(byte[] address);
+ private static native boolean connectNative(byte[] address);
- static native boolean disconnectNative(byte[] address);
+ private static native boolean disconnectNative(byte[] address);
- static native boolean connectAudioNative(byte[] address);
+ private static native boolean connectAudioNative(byte[] address);
- static native boolean disconnectAudioNative(byte[] address);
+ private static native boolean disconnectAudioNative(byte[] address);
- static native boolean startVoiceRecognitionNative(byte[] address);
+ private static native boolean startVoiceRecognitionNative(byte[] address);
- static native boolean stopVoiceRecognitionNative(byte[] address);
+ private static native boolean stopVoiceRecognitionNative(byte[] address);
- static native boolean setVolumeNative(byte[] address, int volumeType, int volume);
+ private static native boolean setVolumeNative(byte[] address, int volumeType, int volume);
- static native boolean dialNative(byte[] address, String number);
+ private static native boolean dialNative(byte[] address, String number);
- static native boolean dialMemoryNative(byte[] address, int location);
+ private static native boolean dialMemoryNative(byte[] address, int location);
- static native boolean handleCallActionNative(byte[] address, int action, int index);
+ private static native boolean handleCallActionNative(byte[] address, int action, int index);
- static native boolean queryCurrentCallsNative(byte[] address);
+ private static native boolean queryCurrentCallsNative(byte[] address);
- static native boolean queryCurrentOperatorNameNative(byte[] address);
+ private static native boolean queryCurrentOperatorNameNative(byte[] address);
- static native boolean retrieveSubscriberInfoNative(byte[] address);
+ private static native boolean retrieveSubscriberInfoNative(byte[] address);
- static native boolean sendDtmfNative(byte[] address, byte code);
+ private static native boolean sendDtmfNative(byte[] address, byte code);
- static native boolean requestLastVoiceTagNumberNative(byte[] address);
+ private static native boolean requestLastVoiceTagNumberNative(byte[] address);
- static native boolean sendATCmdNative(byte[] address, int atCmd, int val1, int val2,
+ private static native boolean sendATCmdNative(byte[] address, int atCmd, int val1, int val2,
String arg);
private BluetoothDevice getDevice(byte[] address) {
@@ -420,4 +650,21 @@
"onRingIndication: Ignoring message because service not available: " + event);
}
}
+
+ private void onUnknownEvent(String eventString, byte[] address) {
+ StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_UNKNOWN_EVENT);
+ event.device = getDevice(address);
+ event.valueString = eventString;
+ if (DBG) {
+ Log.d(TAG, "onUnknownEvent: address " + address + " event " + event);
+ }
+ HeadsetClientService service = HeadsetClientService.getHeadsetClientService();
+ if (service != null) {
+ service.messageFromNative(event);
+ } else {
+ Log.w(TAG,
+ "onUnknowEvent: Ignoring message because service not available: " + event);
+ }
+ }
+
}
diff --git a/src/com/android/bluetooth/hfpclient/StackEvent.java b/src/com/android/bluetooth/hfpclient/StackEvent.java
index de2a338..e9f7cba 100644
--- a/src/com/android/bluetooth/hfpclient/StackEvent.java
+++ b/src/com/android/bluetooth/hfpclient/StackEvent.java
@@ -47,6 +47,7 @@
public static final int EVENT_TYPE_SUBSCRIBER_INFO = 18;
public static final int EVENT_TYPE_IN_BAND_RINGTONE = 19;
public static final int EVENT_TYPE_RING_INDICATION = 21;
+ public static final int EVENT_TYPE_UNKNOWN_EVENT = 22;
public int type = EVENT_TYPE_NONE;
public int valueInt = 0;
diff --git a/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java b/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java
new file mode 100644
index 0000000..7e81d71
--- /dev/null
+++ b/src/com/android/bluetooth/hfpclient/VendorCommandResponseProcessor.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Defines utility inteface that is used by state machine/service to either send vendor specific AT
+ * command or receive vendor specific response from the native stack.
+ */
+package com.android.bluetooth.hfpclient;
+
+import android.bluetooth.BluetoothAssignedNumbers;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadsetClient;
+import android.content.Intent;
+import android.util.Log;
+
+import com.android.bluetooth.Utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class VendorCommandResponseProcessor {
+
+ private static final String TAG = "VendorCommandResponseProcessor";
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final HeadsetClientService mService;
+ private final NativeInterface mNativeInterface;
+
+ // Keys are AT commands (without payload), and values are the company IDs.
+ private static final Map<String, Integer> SUPPORTED_VENDOR_AT_COMMANDS;
+ static {
+ SUPPORTED_VENDOR_AT_COMMANDS = new HashMap<>();
+ SUPPORTED_VENDOR_AT_COMMANDS.put(
+ "+XAPL=",
+ BluetoothAssignedNumbers.APPLE);
+ SUPPORTED_VENDOR_AT_COMMANDS.put(
+ "+IPHONEACCEV=",
+ BluetoothAssignedNumbers.APPLE);
+ SUPPORTED_VENDOR_AT_COMMANDS.put(
+ "+APLSIRI?",
+ BluetoothAssignedNumbers.APPLE);
+ SUPPORTED_VENDOR_AT_COMMANDS.put(
+ "+APLEFM",
+ BluetoothAssignedNumbers.APPLE);
+ }
+
+ // Keys are AT events (without payload), and values are the company IDs.
+ private static final Map<String, Integer> SUPPORTED_VENDOR_EVENTS;
+ static {
+ SUPPORTED_VENDOR_EVENTS = new HashMap<>();
+ SUPPORTED_VENDOR_EVENTS.put(
+ "+APLSIRI:",
+ BluetoothAssignedNumbers.APPLE);
+ SUPPORTED_VENDOR_EVENTS.put(
+ "+XAPL=",
+ BluetoothAssignedNumbers.APPLE);
+ }
+
+ VendorCommandResponseProcessor(HeadsetClientService context, NativeInterface nativeInterface) {
+ mService = context;
+ mNativeInterface = nativeInterface;
+ }
+
+ public boolean sendCommand(int vendorId, String atCommand, BluetoothDevice device) {
+ if (device == null) {
+ Log.w(TAG, "processVendorCommand device is null");
+ return false;
+ }
+
+ // Do not support more than one command at one line.
+ // We simplify and say no ; allowed as well.
+ int indexOfSemicolon = atCommand.indexOf(';');
+ if (indexOfSemicolon > 0) {
+ Log.e(TAG, "Do not support ; and more than one command:"
+ + atCommand);
+ return false;
+ }
+
+ // Get command word
+ int indexOfEqual = atCommand.indexOf('=');
+ int indexOfQuestionMark = atCommand.indexOf('?');
+ String commandWord;
+ if (indexOfEqual > 0) {
+ commandWord = atCommand.substring(0, indexOfEqual + 1);
+ } else if (indexOfQuestionMark > 0) {
+ commandWord = atCommand.substring(0, indexOfQuestionMark + 1);
+ } else {
+ commandWord = atCommand;
+ }
+
+ // replace all white spaces
+ commandWord = commandWord.replaceAll("\\s+", "");
+
+ if (SUPPORTED_VENDOR_AT_COMMANDS.get(commandWord) != (Integer) (vendorId)) {
+ Log.e(TAG, "Invalid command " + atCommand + ", " + vendorId + ". Cand="
+ + commandWord);
+ return false;
+ }
+
+ if (!mNativeInterface.sendATCmd(Utils.getBytesFromAddress(device.getAddress()),
+ HeadsetClientHalConstants
+ .HANDSFREECLIENT_AT_CMD_VENDOR_SPECIFIC_CMD,
+ 0, 0, atCommand)) {
+ Log.e(TAG, "Failed to send vendor specific at command");
+ return false;
+ }
+ logD("Send vendor command: " + atCommand);
+ return true;
+ }
+
+ public boolean processEvent(String atString, BluetoothDevice device) {
+ if (device == null) {
+ Log.w(TAG, "processVendorEvent device is null");
+ return false;
+ }
+
+ // Get event code
+ int indexOfEqual = atString.indexOf('=');
+ int indexOfColon = atString.indexOf(':');
+ String eventCode;
+ if (indexOfEqual > 0) {
+ eventCode = atString.substring(0, indexOfEqual + 1);
+ } else if (indexOfColon > 0) {
+ eventCode = atString.substring(0, indexOfColon + 1);
+ } else {
+ eventCode = atString;
+ }
+
+ // replace all white spaces
+ eventCode = eventCode.replaceAll("\\s+", "");
+
+ Integer vendorId = SUPPORTED_VENDOR_EVENTS.get(eventCode);
+ if (vendorId == null) {
+ Log.e(TAG, "Invalid response: " + atString + ". " + eventCode);
+ return false;
+ }
+ broadcastVendorSpecificEventIntent(vendorId, eventCode, atString, device);
+ logD("process vendor event " + vendorId + ", " + eventCode + ", "
+ + atString + " for device" + device);
+ return true;
+ }
+
+ private void broadcastVendorSpecificEventIntent(int vendorId, String vendorEventCode,
+ String vendorResponse, BluetoothDevice device) {
+ logD("broadcastVendorSpecificEventIntent(" + vendorResponse + ")");
+ Intent intent = new Intent(BluetoothHeadsetClient
+ .ACTION_VENDOR_SPECIFIC_HEADSETCLIENT_EVENT);
+ intent.putExtra(BluetoothHeadsetClient.EXTRA_VENDOR_ID, vendorId);
+ intent.putExtra(BluetoothHeadsetClient.EXTRA_VENDOR_EVENT_CODE, vendorEventCode);
+ intent.putExtra(BluetoothHeadsetClient.EXTRA_VENDOR_EVENT_FULL_ARGS, vendorResponse);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ mService.sendBroadcast(intent, HeadsetClientService.BLUETOOTH_PERM);
+ }
+
+ private void logD(String msg) {
+ if (DBG) {
+ Log.d(TAG, msg);
+ }
+ }
+}
diff --git a/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnection.java b/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnection.java
index 1d2add4..f3cb472 100644
--- a/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnection.java
+++ b/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnection.java
@@ -80,7 +80,6 @@
return;
}
- mHeadsetProfile.connectAudio(device);
setInitializing();
setDialing();
finishInitializing();
@@ -265,7 +264,6 @@
if (!mClosed) {
mHeadsetProfile.acceptCall(mDevice, BluetoothHeadsetClient.CALL_ACCEPT_NONE);
}
- mHeadsetProfile.connectAudio(mDevice);
}
@Override
diff --git a/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnectionService.java b/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnectionService.java
index af46122..2abff9c 100644
--- a/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnectionService.java
+++ b/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnectionService.java
@@ -328,10 +328,19 @@
PhoneAccountHandle handle =
new PhoneAccountHandle(new ComponentName(context, HfpClientConnectionService.class),
device.getAddress());
+
+ int capabilities = PhoneAccount.CAPABILITY_CALL_PROVIDER;
+ if (context.getApplicationContext().getResources().getBoolean(
+ com.android.bluetooth.R.bool
+ .hfp_client_connection_service_support_emergency_call)) {
+ // Need to have an emergency call capability to place emergency call
+ capabilities |= PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS;
+ }
+
PhoneAccount account =
new PhoneAccount.Builder(handle, "HFP " + device.toString()).setAddress(addr)
.setSupportedUriSchemes(Arrays.asList(PhoneAccount.SCHEME_TEL))
- .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+ .setCapabilities(capabilities)
.build();
if (DBG) {
Log.d(TAG, "phoneaccount: " + account);
diff --git a/src/com/android/bluetooth/hid/HidDeviceNativeInterface.java b/src/com/android/bluetooth/hid/HidDeviceNativeInterface.java
index c0f7042..db72a59 100644
--- a/src/com/android/bluetooth/hid/HidDeviceNativeInterface.java
+++ b/src/com/android/bluetooth/hid/HidDeviceNativeInterface.java
@@ -24,11 +24,11 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.bluetooth.Utils;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
/**
* HID Device Native Interface to/from JNI.
diff --git a/src/com/android/bluetooth/hid/HidHostService.java b/src/com/android/bluetooth/hid/HidHostService.java
index 63f5206..7bc7216 100644
--- a/src/com/android/bluetooth/hid/HidHostService.java
+++ b/src/com/android/bluetooth/hid/HidHostService.java
@@ -26,9 +26,10 @@
import android.os.Message;
import android.os.UserHandle;
import android.provider.Settings;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
@@ -181,7 +182,7 @@
if (DBG) {
Log.d(TAG, "Incoming HID connection rejected");
}
- disconnectHidNative(Utils.getByteAddress(device));
+ virtualUnPlugNative(Utils.getByteAddress(device));
} else {
broadcastConnectionState(device, convertHalState(halState));
}
@@ -523,11 +524,11 @@
if (DBG) {
Log.d(TAG, "setPriority: " + device.getAddress());
}
- Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothHidHostPriorityKey(device.getAddress()), priority);
if (DBG) {
Log.d(TAG, "Saved priority " + device + " = " + priority);
}
+ AdapterService.getAdapterService().getDatabase()
+ .setProfilePriority(device, BluetoothProfile.HID_HOST, priority);
return true;
}
@@ -536,10 +537,8 @@
if (DBG) {
Log.d(TAG, "getPriority: " + device.getAddress());
}
- int priority = Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothHidHostPriorityKey(device.getAddress()),
- BluetoothProfile.PRIORITY_UNDEFINED);
- return priority;
+ return AdapterService.getAdapterService().getDatabase()
+ .getProfilePriority(device, BluetoothProfile.HID_HOST);
}
/* The following APIs regarding test app for compliance */
diff --git a/src/com/android/bluetooth/map/BluetoothMapAccountLoader.java b/src/com/android/bluetooth/map/BluetoothMapAccountLoader.java
index 3ad600b..1fc4af9 100644
--- a/src/com/android/bluetooth/map/BluetoothMapAccountLoader.java
+++ b/src/com/android/bluetooth/map/BluetoothMapAccountLoader.java
@@ -185,7 +185,7 @@
return children;
} finally {
if (mProviderClient != null) {
- mProviderClient.release();
+ mProviderClient.close();
}
}
diff --git a/src/com/android/bluetooth/map/BluetoothMapContentObserver.java b/src/com/android/bluetooth/map/BluetoothMapContentObserver.java
index c862619..8a73ef2 100644
--- a/src/com/android/bluetooth/map/BluetoothMapContentObserver.java
+++ b/src/com/android/bluetooth/map/BluetoothMapContentObserver.java
@@ -1054,7 +1054,7 @@
mResolver.unregisterContentObserver(mObserver);
mObserverRegistered = false;
if (mProviderClient != null) {
- mProviderClient.release();
+ mProviderClient.close();
mProviderClient = null;
}
}
diff --git a/src/com/android/bluetooth/map/BluetoothMapObexServer.java b/src/com/android/bluetooth/map/BluetoothMapObexServer.java
index afd1eb0..3b35262 100644
--- a/src/com/android/bluetooth/map/BluetoothMapObexServer.java
+++ b/src/com/android/bluetooth/map/BluetoothMapObexServer.java
@@ -974,7 +974,7 @@
}
if (mProviderClient != null) {
- mProviderClient.release();
+ mProviderClient.close();
mProviderClient = null;
}
diff --git a/src/com/android/bluetooth/map/BluetoothMapService.java b/src/com/android/bluetooth/map/BluetoothMapService.java
index bfb1d3f..cc42b60 100644
--- a/src/com/android/bluetooth/map/BluetoothMapService.java
+++ b/src/com/android/bluetooth/map/BluetoothMapService.java
@@ -36,8 +36,6 @@
import android.os.ParcelUuid;
import android.os.PowerManager;
import android.os.RemoteException;
-import android.provider.Settings;
-import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
@@ -45,8 +43,10 @@
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.util.ArrayList;
@@ -577,14 +577,14 @@
if (VERBOSE) {
Log.v(TAG, "Saved priority " + device + " = " + priority);
}
- return Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothMapPriorityKey(device.getAddress()), priority);
+ AdapterService.getAdapterService().getDatabase()
+ .setProfilePriority(device, BluetoothProfile.MAP, priority);
+ return true;
}
int getPriority(BluetoothDevice device) {
- return Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothMapPriorityKey(device.getAddress()),
- BluetoothProfile.PRIORITY_UNDEFINED);
+ return AdapterService.getAdapterService().getDatabase()
+ .getProfilePriority(device, BluetoothProfile.MAP);
}
@Override
diff --git a/src/com/android/bluetooth/map/BluetoothMnsObexClient.java b/src/com/android/bluetooth/map/BluetoothMnsObexClient.java
index eba6485..2f2af1d 100644
--- a/src/com/android/bluetooth/map/BluetoothMnsObexClient.java
+++ b/src/com/android/bluetooth/map/BluetoothMnsObexClient.java
@@ -222,7 +222,6 @@
if (looper != null) {
looper.quit();
}
- mHandler = null;
}
/* Disconnect if connected */
diff --git a/src/com/android/bluetooth/mapclient/MapClientService.java b/src/com/android/bluetooth/mapclient/MapClientService.java
index 0d77c8e..9467683 100644
--- a/src/com/android/bluetooth/mapclient/MapClientService.java
+++ b/src/com/android/bluetooth/mapclient/MapClientService.java
@@ -30,12 +30,12 @@
import android.content.IntentFilter;
import android.net.Uri;
import android.os.ParcelUuid;
-import android.provider.Settings;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.Arrays;
@@ -101,6 +101,10 @@
Log.d(TAG, "MAP connect device: " + device
+ ", InstanceMap start state: " + sb.toString());
}
+ if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) {
+ Log.w(TAG, "Connection not allowed: <" + device.getAddress() + "> is PRIORITY_OFF");
+ return false;
+ }
MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
if (mapStateMachine == null) {
// a map state machine instance doesn't exist yet, create a new one if we can.
@@ -189,20 +193,20 @@
}
public synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
- Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
+ if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
List<BluetoothDevice> deviceList = new ArrayList<>();
Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
int connectionState;
for (BluetoothDevice device : bondedDevices) {
connectionState = getConnectionState(device);
- Log.d(TAG, "Device: " + device + "State: " + connectionState);
+ if (DBG) Log.d(TAG, "Device: " + device + "State: " + connectionState);
for (int i = 0; i < states.length; i++) {
if (connectionState == states[i]) {
deviceList.add(device);
}
}
}
- Log.d(TAG, deviceList.toString());
+ if (DBG) Log.d(TAG, deviceList.toString());
return deviceList;
}
@@ -214,19 +218,17 @@
}
public boolean setPriority(BluetoothDevice device, int priority) {
- Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothMapClientPriorityKey(device.getAddress()), priority);
if (VDBG) {
Log.v(TAG, "Saved priority " + device + " = " + priority);
}
+ AdapterService.getAdapterService().getDatabase()
+ .setProfilePriority(device, BluetoothProfile.MAP_CLIENT, priority);
return true;
}
public int getPriority(BluetoothDevice device) {
- int priority = Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothMapClientPriorityKey(device.getAddress()),
- BluetoothProfile.PRIORITY_UNDEFINED);
- return priority;
+ return AdapterService.getAdapterService().getDatabase()
+ .getProfilePriority(device, BluetoothProfile.MAP_CLIENT);
}
public synchronized boolean sendMessage(BluetoothDevice device, Uri[] contacts, String message,
@@ -293,7 +295,13 @@
setMapClientService(null);
}
- void cleanupDevice(BluetoothDevice device) {
+ /**
+ * cleanupDevice removes the associated state machine from the instance map
+ *
+ * @param device BluetoothDevice address of remote device
+ */
+ @VisibleForTesting
+ public void cleanupDevice(BluetoothDevice device) {
if (DBG) {
StringBuilder sb = new StringBuilder();
dump(sb);
@@ -346,10 +354,23 @@
return mapStateMachine.getUnreadMessages();
}
+ /**
+ * Returns the SDP record's MapSupportedFeatures field (see Bluetooth MAP 1.4 spec, page 114).
+ * @param device The Bluetooth device to get this value for.
+ * @return the SDP record's MapSupportedFeatures field.
+ */
+ public synchronized int getSupportedFeatures(BluetoothDevice device) {
+ MceStateMachine mapStateMachine = mMapInstanceMap.get(device);
+ if (mapStateMachine == null) {
+ if (DBG) Log.d(TAG, "in getSupportedFeatures, returning 0");
+ return 0;
+ }
+ return mapStateMachine.getSupportedFeatures();
+ }
+
@Override
public void dump(StringBuilder sb) {
super.dump(sb);
- ProfileService.println(sb, "# Services Connected: " + mMapInstanceMap.size());
for (MceStateMachine stateMachine : mMapInstanceMap.values()) {
stateMachine.dump(sb);
}
@@ -487,7 +508,7 @@
if (service == null) {
return false;
}
- Log.d(TAG, "Checking Permission of sendMessage");
+ if (DBG) Log.d(TAG, "Checking Permission of sendMessage");
mService.enforceCallingOrSelfPermission(Manifest.permission.SEND_SMS,
"Need SEND_SMS permission");
@@ -504,6 +525,21 @@
"Need READ_SMS permission");
return service.getUnreadMessages(device);
}
+
+ @Override
+ public int getSupportedFeatures(BluetoothDevice device) {
+ MapClientService service = getService();
+ if (service == null) {
+ if (DBG) {
+ Log.d(TAG,
+ "in MapClientService getSupportedFeatures stub, returning 0");
+ }
+ return 0;
+ }
+ mService.enforceCallingOrSelfPermission(Manifest.permission.BLUETOOTH,
+ "Need BLUETOOTH permission");
+ return service.getSupportedFeatures(device);
+ }
}
private class MapBroadcastReceiver extends BroadcastReceiver {
diff --git a/src/com/android/bluetooth/mapclient/MapUtils.java b/src/com/android/bluetooth/mapclient/MapUtils.java
index 4ed26c8..b0a603b 100644
--- a/src/com/android/bluetooth/mapclient/MapUtils.java
+++ b/src/com/android/bluetooth/mapclient/MapUtils.java
@@ -15,7 +15,7 @@
*/
package com.android.bluetooth.mapclient;
-import android.support.annotation.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting;
class MapUtils {
private static MnsService sMnsService = null;
diff --git a/src/com/android/bluetooth/mapclient/MasClient.java b/src/com/android/bluetooth/mapclient/MasClient.java
index d7283bf..6991704 100644
--- a/src/com/android/bluetooth/mapclient/MasClient.java
+++ b/src/com/android/bluetooth/mapclient/MasClient.java
@@ -104,7 +104,7 @@
+ mSdpMasRecord.getRfcommCannelNumber());
}
mSocket = mRemoteDevice.createRfcommSocket(mSdpMasRecord.getRfcommCannelNumber());
- Log.d(TAG, mRemoteDevice.toString() + "Socket: " + mSocket.toString());
+ if (DBG) Log.d(TAG, mRemoteDevice.toString() + "Socket: " + mSocket.toString());
mSocket.connect();
mTransport = new BluetoothObexTransport(mSocket);
@@ -118,7 +118,7 @@
oap.addToHeaderSet(headerset);
headerset = mSession.connect(headerset);
- Log.d(TAG, "Connection results" + headerset.getResponseCode());
+ if (DBG) Log.d(TAG, "Connection results" + headerset.getResponseCode());
if (headerset.getResponseCode() == ResponseCodes.OBEX_HTTP_OK) {
if (DBG) {
@@ -190,6 +190,10 @@
NATIVE, UTF_8;
}
+ SdpMasRecord getSdpMasRecord() {
+ return mSdpMasRecord;
+ }
+
private static class MasClientHandler extends Handler {
WeakReference<MasClient> mInst;
@@ -201,22 +205,23 @@
@Override
public void handleMessage(Message msg) {
MasClient inst = mInst.get();
- if (!inst.mConnected && msg.what != CONNECT) {
- Log.w(TAG, "Cannot execute " + msg + " when not CONNECTED.");
- return;
- }
-
switch (msg.what) {
case CONNECT:
- inst.connect();
+ if (!inst.mConnected) {
+ inst.connect();
+ }
break;
case DISCONNECT:
- inst.disconnect();
+ if (inst.mConnected) {
+ inst.disconnect();
+ }
break;
case REQUEST:
- inst.executeRequest((Request) msg.obj);
+ if (inst.mConnected) {
+ inst.executeRequest((Request) msg.obj);
+ }
break;
}
}
diff --git a/src/com/android/bluetooth/mapclient/MceStateMachine.java b/src/com/android/bluetooth/mapclient/MceStateMachine.java
index ae11d2a..867ab18 100644
--- a/src/com/android/bluetooth/mapclient/MceStateMachine.java
+++ b/src/com/android/bluetooth/mapclient/MceStateMachine.java
@@ -42,7 +42,6 @@
import android.app.Activity;
import android.app.PendingIntent;
-import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothMapClient;
import android.bluetooth.BluetoothProfile;
@@ -51,6 +50,7 @@
import android.content.Intent;
import android.net.Uri;
import android.os.Message;
+import android.provider.Telephony;
import android.telecom.PhoneAccount;
import android.telephony.SmsManager;
import android.util.Log;
@@ -70,6 +70,7 @@
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
/* The MceStateMachine is responsible for setting up and maintaining a connection to a single
* specific Messaging Server Equipment endpoint. Upon connect command an SDP record is retrieved,
@@ -122,6 +123,48 @@
new HashMap<>(MAX_MESSAGES);
private Bmessage.Type mDefaultMessageType = Bmessage.Type.SMS_CDMA;
+ /**
+ * An object to hold the necessary meta-data for each message so we can broadcast it alongside
+ * the message content.
+ *
+ * This is necessary because the metadata is inferred or received separately from the actual
+ * message content.
+ *
+ * Note: In the future it may be best to use the entries from the MessageListing in full instead
+ * of this small subset.
+ */
+ private class MessageMetadata {
+ private final String mHandle;
+ private final Long mTimestamp;
+ private boolean mRead;
+
+ MessageMetadata(String handle, Long timestamp, boolean read) {
+ mHandle = handle;
+ mTimestamp = timestamp;
+ mRead = read;
+ }
+
+ public String getHandle() {
+ return mHandle;
+ }
+
+ public Long getTimestamp() {
+ return mTimestamp;
+ }
+
+ public synchronized boolean getRead() {
+ return mRead;
+ }
+
+ public synchronized void setRead(boolean read) {
+ mRead = read;
+ }
+ }
+
+ // Map each message to its metadata via the handle
+ private ConcurrentHashMap<String, MessageMetadata> mMessages =
+ new ConcurrentHashMap<String, MessageMetadata>();
+
MceStateMachine(MapClientService service, BluetoothDevice device) {
this(service, device, null);
}
@@ -184,7 +227,7 @@
public synchronized int getState() {
IState currentState = this.getCurrentState();
- if (currentState.getClass() == Disconnected.class) {
+ if (currentState == null || currentState.getClass() == Disconnected.class) {
return BluetoothProfile.STATE_DISCONNECTED;
}
if (currentState.getClass() == Connected.class) {
@@ -280,6 +323,15 @@
return false;
}
+ synchronized int getSupportedFeatures() {
+ if (this.getCurrentState() == mConnected && mMasClient != null) {
+ if (DBG) Log.d(TAG, "returning getSupportedFeatures from SDP record");
+ return mMasClient.getSdpMasRecord().getSupportedFeatures();
+ }
+ if (DBG) Log.d(TAG, "in getSupportedFeatures, returning 0");
+ return 0;
+ }
+
private String getContactURIFromPhone(String number) {
return PhoneAccount.SCHEME_TEL + ":" + number;
}
@@ -302,8 +354,8 @@
}
public void dump(StringBuilder sb) {
- ProfileService.println(sb, "mCurrentDevice: " + mDevice.getAddress() + " (name = "
- + mDevice.getName() + "), StateMachine: " + this.toString());
+ ProfileService.println(sb, "mCurrentDevice: " + mDevice.getAddress() + "("
+ + mDevice.getName() + ") " + this.toString());
}
class Disconnected extends State {
@@ -331,7 +383,6 @@
}
onConnectionStateChanged(mPreviousState, BluetoothProfile.STATE_CONNECTING);
- BluetoothAdapter.getDefaultAdapter().cancelDiscovery();
// When commanded to connect begin SDP to find the MAS server.
mDevice.sdpSearch(BluetoothUuid.MAS);
sendMessageDelayed(MSG_CONNECTING_TIMEOUT, TIMEOUT);
@@ -349,9 +400,14 @@
Log.d(TAG, "SDP Complete");
}
if (mMasClient == null) {
- mMasClient = new MasClient(mDevice, MceStateMachine.this,
- (SdpMasRecord) message.obj);
- setDefaultMessageType((SdpMasRecord) message.obj);
+ SdpMasRecord record = (SdpMasRecord) message.obj;
+ if (record == null) {
+ Log.e(TAG, "Unexpected: SDP record is null for device "
+ + mDevice.getName());
+ return NOT_HANDLED;
+ }
+ mMasClient = new MasClient(mDevice, MceStateMachine.this, record);
+ setDefaultMessageType(record);
}
break;
@@ -360,6 +416,9 @@
break;
case MSG_MAS_DISCONNECTED:
+ if (mMasClient != null) {
+ mMasClient.shutdown();
+ }
transitionTo(mDisconnected);
break;
@@ -456,8 +515,12 @@
Log.d(TAG, "Message Sent......." + messageHandle);
}
// ignore the top-order byte (converted to string) in the handle for now
- mSentMessageLog.put(messageHandle.substring(2),
- ((RequestPushMessage) message.obj).getBMsg());
+ // some test devices don't populate messageHandle field.
+ // in such cases, no need to wait up for response for such messages.
+ if (messageHandle != null && messageHandle.length() > 2) {
+ mSentMessageLog.put(messageHandle.substring(2),
+ ((RequestPushMessage) message.obj).getBMsg());
+ }
} else if (message.obj instanceof RequestGetMessagesListing) {
processMessageListing((RequestGetMessagesListing) message.obj);
}
@@ -483,6 +546,15 @@
mPreviousState = BluetoothProfile.STATE_CONNECTED;
}
+ /**
+ * Given a message notification event, will ensure message caching and updating and update
+ * interested applications.
+ *
+ * Message notifications arrive for both remote message reception and Message-Listing object
+ * updates that are triggered by the server side.
+ *
+ * @param msg - A Message object containing a EventReport object describing the remote event
+ */
private void processNotification(Message msg) {
if (DBG) {
Log.d(TAG, "Handler: msg: " + msg.what);
@@ -491,16 +563,25 @@
switch (msg.what) {
case MSG_NOTIFICATION:
EventReport ev = (EventReport) msg.obj;
- if (DBG) {
- Log.d(TAG, "Message Type = " + ev.getType());
+ if (ev == null) {
+ Log.w(TAG, "MSG_NOTIFICATION event is null");
+ return;
}
if (DBG) {
- Log.d(TAG, "Message handle = " + ev.getHandle());
+ Log.d(TAG, "Message Type = " + ev.getType()
+ + ", Message handle = " + ev.getHandle());
}
switch (ev.getType()) {
case NEW_MESSAGE:
- //mService.get().sendNewMessageNotification(ev);
+ // Infer the timestamp for this message as 'now' and read status false
+ // instead of getting the message listing data for it
+ if (!mMessages.contains(ev.getHandle())) {
+ Calendar calendar = Calendar.getInstance();
+ MessageMetadata metadata = new MessageMetadata(ev.getHandle(),
+ calendar.getTime().getTime(), false);
+ mMessages.put(ev.getHandle(), metadata);
+ }
mMasClient.makeRequest(new RequestGetMessage(ev.getHandle(),
MasClient.CharsetType.UTF_8, false));
break;
@@ -516,6 +597,8 @@
// Sets the specified message status to "read" (from "unread" status, mostly)
private void markMessageRead(RequestGetMessage request) {
if (DBG) Log.d(TAG, "markMessageRead");
+ MessageMetadata metadata = mMessages.get(request.getHandle());
+ metadata.setRead(true);
mMasClient.makeRequest(new RequestSetMessageStatus(
request.getHandle(), RequestSetMessageStatus.StatusIndicator.READ));
}
@@ -527,21 +610,44 @@
request.getHandle(), RequestSetMessageStatus.StatusIndicator.DELETED));
}
+ /**
+ * Given the result of a Message Listing request, will cache the contents of each Message in
+ * the Message Listing Object and kick off requests to retrieve message contents from the
+ * remote device.
+ *
+ * @param request - A request object that has been resolved and returned with a message list
+ */
private void processMessageListing(RequestGetMessagesListing request) {
if (DBG) {
Log.d(TAG, "processMessageListing");
}
- ArrayList<com.android.bluetooth.mapclient.Message> messageHandles = request.getList();
- if (messageHandles != null) {
- for (com.android.bluetooth.mapclient.Message handle : messageHandles) {
+ ArrayList<com.android.bluetooth.mapclient.Message> messageListing = request.getList();
+ if (messageListing != null) {
+ // Message listings by spec arrive ordered newest first but we wish to broadcast as
+ // oldest first. Iterate in reverse order so we initiate requests oldest first.
+ for (int i = messageListing.size() - 1; i >= 0; i--) {
+ com.android.bluetooth.mapclient.Message msg = messageListing.get(i);
if (DBG) {
- Log.d(TAG, "getting message ");
+ Log.d(TAG, "getting message for handle " + msg.getHandle());
}
- getMessage(handle.getHandle());
+ // A message listing coming from the server should always have up to date data
+ mMessages.put(msg.getHandle(), new MessageMetadata(msg.getHandle(),
+ msg.getDateTime().getTime(), msg.isRead()));
+ getMessage(msg.getHandle());
}
}
}
+ /**
+ * Given the response of a GetMessage request, will broadcast the bMessage contents on to
+ * all registered applications.
+ *
+ * Inbound messages arrive as bMessage objects following a GetMessage request. GetMessage
+ * uses a message handle that can arrive from both a GetMessageListing request or a Message
+ * Notification event.
+ *
+ * @param request - A request object that has been resolved and returned with message data
+ */
private void processInboundMessage(RequestGetMessage request) {
Bmessage message = request.getMessage();
if (DBG) {
@@ -570,10 +676,18 @@
Log.d(TAG, "Recipients" + message.getRecipients().toString());
}
+ // Grab the message metadata and update the cached read status from the bMessage
+ MessageMetadata metadata = mMessages.get(request.getHandle());
+ metadata.setRead(request.getMessage().getStatus() == Bmessage.Status.READ);
+
Intent intent = new Intent();
intent.setAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE, request.getHandle());
+ intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_TIMESTAMP,
+ metadata.getTimestamp());
+ intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_READ_STATUS,
+ metadata.getRead());
intent.putExtra(android.content.Intent.EXTRA_TEXT, message.getBodyContent());
VCardEntry originator = message.getOriginator();
if (originator != null) {
@@ -592,7 +706,12 @@
intent.putExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_NAME,
originator.getDisplayName());
}
- mService.sendBroadcast(intent);
+ // Only send to the current default SMS app if one exists
+ String defaultMessagingPackage = Telephony.Sms.getDefaultSmsPackage(mService);
+ if (defaultMessagingPackage != null) {
+ intent.setPackage(defaultMessagingPackage);
+ }
+ mService.sendBroadcast(intent, android.Manifest.permission.RECEIVE_SMS);
break;
case MMS:
@@ -607,6 +726,9 @@
if (DBG) {
Log.d(TAG, "got a status for " + handle + " Status = " + status);
}
+ // some test devices don't populate messageHandle field.
+ // in such cases, ignore such messages.
+ if (handle == null || handle.length() <= 2) return;
PendingIntent intentToSend = null;
// ignore the top-order byte (converted to string) in the handle for now
String shortHandle = handle.substring(2);
@@ -688,10 +810,8 @@
void receiveEvent(EventReport ev) {
if (DBG) {
- Log.d(TAG, "Message Type = " + ev.getType());
- }
- if (DBG) {
- Log.d(TAG, "Message handle = " + ev.getHandle());
+ Log.d(TAG, "Message Type = " + ev.getType()
+ + ", Message handle = " + ev.getHandle());
}
sendMessage(MSG_NOTIFICATION, ev);
}
diff --git a/src/com/android/bluetooth/mapclient/obex/ObexTime.java b/src/com/android/bluetooth/mapclient/obex/ObexTime.java
index 42a32c1..cc58a51 100644
--- a/src/com/android/bluetooth/mapclient/obex/ObexTime.java
+++ b/src/com/android/bluetooth/mapclient/obex/ObexTime.java
@@ -29,8 +29,17 @@
public ObexTime(String time) {
/*
- * match OBEX time string: YYYYMMDDTHHMMSS with optional UTF offset
- * +/-hhmm
+ * Match OBEX time string: YYYYMMDDTHHMMSS with optional UTF offset +/-hhmm
+ *
+ * Matched groups are numberes as follows:
+ *
+ * YYYY MM DD T HH MM SS + hh mm
+ * ^^^^ ^^ ^^ ^^ ^^ ^^ ^ ^^ ^^
+ * 1 2 3 4 5 6 8 9 10
+ * |---7---|
+ *
+ * All groups are guaranteed to be numeric so conversion will always succeed (except group 8
+ * which is either + or -)
*/
Pattern p = Pattern.compile(
"(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})(([+-])(\\d{2})(\\d{2})" + ")?");
@@ -39,20 +48,26 @@
if (m.matches()) {
/*
- * matched groups are numberes as follows: YYYY MM DD T HH MM SS +
- * hh mm ^^^^ ^^ ^^ ^^ ^^ ^^ ^ ^^ ^^ 1 2 3 4 5 6 8 9 10 all groups
- * are guaranteed to be numeric so conversion will always succeed
- * (except group 8 which is either + or -)
+ * MAP spec says to default to "Local Time basis" for a message listing timestamp. We'll
+ * use the system default timezone and assume it knows best what our local timezone is.
+ * The builder defaults to the default locale and timezone if none is provided.
*/
+ Calendar.Builder builder = new Calendar.Builder();
- Calendar cal = Calendar.getInstance();
- cal.set(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)) - 1,
- Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)),
- Integer.parseInt(m.group(5)), Integer.parseInt(m.group(6)));
+ /* Note that Calendar months are zero-based */
+ builder.setDate(Integer.parseInt(m.group(1)), /* year */
+ Integer.parseInt(m.group(2)) - 1, /* month */
+ Integer.parseInt(m.group(3))); /* day of month */
+
+ /* Note the MAP timestamp doesn't have milliseconds and we're explicitly setting to 0 */
+ builder.setTimeOfDay(Integer.parseInt(m.group(4)), /* hours */
+ Integer.parseInt(m.group(5)), /* minutes */
+ Integer.parseInt(m.group(6)), /* seconds */
+ 0); /* milliseconds */
/*
- * if 7th group is matched then we have UTC offset information
- * included
+ * If 7th group is matched then we're no longer using "Local Time basis" and instead
+ * have a UTC based timestamp and offset information included
*/
if (m.group(7) != null) {
int ohh = Integer.parseInt(m.group(9));
@@ -68,10 +83,10 @@
TimeZone tz = TimeZone.getTimeZone("UTC");
tz.setRawOffset(offset);
- cal.setTimeZone(tz);
+ builder.setTimeZone(tz);
}
- mDate = cal.getTime();
+ mDate = builder.build().getTime();
}
}
diff --git a/src/com/android/bluetooth/opp/BluetoothOppFileProvider.java b/src/com/android/bluetooth/opp/BluetoothOppFileProvider.java
index 166983a..53b785f 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppFileProvider.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppFileProvider.java
@@ -23,9 +23,10 @@
import android.net.Uri;
import android.os.UserHandle;
import android.os.UserManager;
-import android.support.v4.content.FileProvider;
import android.util.Log;
+import androidx.core.content.FileProvider;
+
import java.io.File;
/**
diff --git a/src/com/android/bluetooth/opp/BluetoothOppHandoverReceiver.java b/src/com/android/bluetooth/opp/BluetoothOppHandoverReceiver.java
index 6e83480..bae8cb9 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppHandoverReceiver.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppHandoverReceiver.java
@@ -32,7 +32,8 @@
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
-
+ if (D) Log.d(TAG, "Action :" + action);
+ if (action == null) return;
if (action.equals(Constants.ACTION_HANDOVER_SEND) || action.equals(
Constants.ACTION_HANDOVER_SEND_MULTIPLE)) {
final BluetoothDevice device =
diff --git a/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java b/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java
index 99e3e69..1a15adb 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppLauncherActivity.java
@@ -61,7 +61,7 @@
* selection dialog.
*/
public class BluetoothOppLauncherActivity extends Activity {
- private static final String TAG = "BluetoothLauncherActivity";
+ private static final String TAG = "BluetoothOppLauncherActivity";
private static final boolean D = Constants.DEBUG;
private static final boolean V = Constants.VERBOSE;
@@ -75,6 +75,11 @@
Intent intent = getIntent();
String action = intent.getAction();
+ if (action == null) {
+ Log.w(TAG, " Received " + intent + " with null action");
+ finish();
+ return;
+ }
if (action.equals(Intent.ACTION_SEND) || action.equals(Intent.ACTION_SEND_MULTIPLE)) {
//Check if Bluetooth is available in the beginning instead of at the end
@@ -248,7 +253,7 @@
Settings.System.getString(resolver, Settings.Global.AIRPLANE_MODE_RADIOS);
final boolean isAirplaneSensitive =
airplaneModeRadios == null || airplaneModeRadios.contains(
- Settings.System.RADIO_BLUETOOTH);
+ Settings.Global.RADIO_BLUETOOTH);
if (!isAirplaneSensitive) {
return true;
}
diff --git a/src/com/android/bluetooth/opp/BluetoothOppNotification.java b/src/com/android/bluetooth/opp/BluetoothOppNotification.java
index 41ffec3..1e92996 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppNotification.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppNotification.java
@@ -40,6 +40,7 @@
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
+import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
@@ -557,12 +558,14 @@
.setClassName(Constants.THIS_PACKAGE_NAME,
BluetoothOppReceiver.class.getName());
Notification.Action actionDecline =
- new Notification.Action.Builder(R.drawable.ic_decline,
+ new Notification.Action.Builder(Icon.createWithResource(mContext,
+ R.drawable.ic_decline),
mContext.getText(R.string.incoming_file_confirm_cancel),
PendingIntent.getBroadcast(mContext, 0,
new Intent(baseIntent).setAction(Constants.ACTION_DECLINE),
0)).build();
- Notification.Action actionAccept = new Notification.Action.Builder(R.drawable.ic_accept,
+ Notification.Action actionAccept = new Notification.Action.Builder(
+ Icon.createWithResource(mContext,R.drawable.ic_accept),
mContext.getText(R.string.incoming_file_confirm_ok),
PendingIntent.getBroadcast(mContext, 0,
new Intent(baseIntent).setAction(Constants.ACTION_ACCEPT), 0)).build();
@@ -589,7 +592,7 @@
.setStyle(new Notification.BigTextStyle().bigText(mContext.getString(
R.string.incoming_file_confirm_Notification_content,
info.mDeviceName, info.mFileName)))
- .setContentInfo(Formatter.formatFileSize(mContext, info.mTotalBytes))
+ .setSubText(Formatter.formatFileSize(mContext, info.mTotalBytes))
.setSmallIcon(R.drawable.bt_incomming_file_notification)
.setLocalOnly(true)
.build();
diff --git a/src/com/android/bluetooth/opp/BluetoothOppReceiver.java b/src/com/android/bluetooth/opp/BluetoothOppReceiver.java
index 73d0194..f072d51 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppReceiver.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppReceiver.java
@@ -58,7 +58,8 @@
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
-
+ if (D) Log.d(TAG, " action :" + action);
+ if (action == null) return;
if (action.equals(BluetoothDevicePicker.ACTION_DEVICE_SELECTED)) {
BluetoothOppManager mOppManager = BluetoothOppManager.getInstance(context);
diff --git a/src/com/android/bluetooth/opp/BluetoothOppService.java b/src/com/android/bluetooth/opp/BluetoothOppService.java
index 383a497..914b9b6 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppService.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppService.java
@@ -52,7 +52,6 @@
import android.os.Handler;
import android.os.Message;
import android.os.Process;
-import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.bluetooth.BluetoothObexTransport;
@@ -60,6 +59,7 @@
import com.android.bluetooth.ObexServerSockets;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.sdp.SdpManager;
+import com.android.internal.annotations.VisibleForTesting;
import com.google.android.collect.Lists;
@@ -115,6 +115,8 @@
private UpdateThread mUpdateThread;
+ private boolean mUpdateThreadRunning;
+
private ArrayList<BluetoothOppShareInfo> mShares;
private ArrayList<BluetoothOppBatch> mBatches;
@@ -241,6 +243,10 @@
@Override
public boolean stop() {
+ if (sBluetoothOppService == null) {
+ Log.w(TAG, "stop() called before start()");
+ return true;
+ }
setBluetoothOppService(null);
mHandler.sendMessage(mHandler.obtainMessage(STOP_LISTENER));
return true;
@@ -330,8 +336,19 @@
unregisterReceivers();
synchronized (BluetoothOppService.this) {
if (mUpdateThread != null) {
+ mUpdateThread.interrupt();
+ }
+ }
+ while (mUpdateThread != null && mUpdateThreadRunning) {
+ try {
+ Thread.sleep(50);
+ } catch (Exception e) {
+ Log.e(TAG, "Thread sleep", e);
+ }
+ }
+ synchronized (BluetoothOppService.this) {
+ if (mUpdateThread != null) {
try {
- mUpdateThread.interrupt();
mUpdateThread.join();
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted", e);
@@ -339,6 +356,7 @@
mUpdateThread = null;
}
}
+
mNotifier.cancelNotifications();
break;
case START_LISTENER:
@@ -551,6 +569,7 @@
if (mUpdateThread == null) {
mUpdateThread = new UpdateThread();
mUpdateThread.start();
+ mUpdateThreadRunning = true;
}
}
}
@@ -580,6 +599,7 @@
while (!mIsInterrupted) {
synchronized (BluetoothOppService.this) {
if (mUpdateThread != this) {
+ mUpdateThreadRunning = false;
throw new IllegalStateException(
"multiple UpdateThreads in BluetoothOppService");
}
@@ -589,6 +609,7 @@
}
if (!mPendingUpdate) {
mUpdateThread = null;
+ mUpdateThreadRunning = false;
return;
}
mPendingUpdate = false;
@@ -598,6 +619,7 @@
BluetoothShare._ID);
if (cursor == null) {
+ mUpdateThreadRunning = false;
return;
}
@@ -634,9 +656,6 @@
}
}
- if (shouldScanFile(arrayPos)) {
- scanFile(arrayPos);
- }
deleteShare(arrayPos); // this advances in the array
} else {
int id = cursor.getInt(idColumn);
@@ -660,14 +679,11 @@
Log.v(TAG,
"Array update: removing " + arrayId + " @ " + arrayPos);
}
- if (shouldScanFile(arrayPos)) {
- scanFile(arrayPos);
- }
deleteShare(arrayPos);
} else if (arrayId == id) {
// This cursor row already exists in the stored array.
updateShare(cursor, arrayPos);
-
+ scanFileIfNeeded(arrayPos);
++arrayPos;
cursor.moveToNext();
isAfterLast = cursor.isAfterLast();
@@ -691,6 +707,8 @@
cursor.close();
}
+
+ mUpdateThreadRunning = false;
}
}
@@ -1031,8 +1049,14 @@
}
}
- private boolean scanFile(int arrayPos) {
+ private void scanFileIfNeeded(int arrayPos) {
BluetoothOppShareInfo info = mShares.get(arrayPos);
+ boolean isFileReceived = BluetoothShare.isStatusSuccess(info.mStatus)
+ && info.mDirection == BluetoothShare.DIRECTION_INBOUND && !info.mMediaScanned
+ && info.mConfirm != BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED;
+ if (!isFileReceived) {
+ return;
+ }
synchronized (BluetoothOppService.this) {
if (D) {
Log.d(TAG, "Scanning file " + info.mFilename);
@@ -1040,20 +1064,10 @@
if (!mMediaScanInProgress) {
mMediaScanInProgress = true;
new MediaScannerNotifier(this, info, mHandler);
- return true;
- } else {
- return false;
}
}
}
- private boolean shouldScanFile(int arrayPos) {
- BluetoothOppShareInfo info = mShares.get(arrayPos);
- return BluetoothShare.isStatusSuccess(info.mStatus)
- && info.mDirection == BluetoothShare.DIRECTION_INBOUND && !info.mMediaScanned
- && info.mConfirm != BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED;
- }
-
// Run in a background thread at boot.
private static void trimDatabase(ContentResolver contentResolver) {
// remove the invisible/unconfirmed inbound shares
diff --git a/src/com/android/bluetooth/opp/BluetoothOppTransferActivity.java b/src/com/android/bluetooth/opp/BluetoothOppTransferActivity.java
index 74fd872..fc45d3f 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppTransferActivity.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppTransferActivity.java
@@ -309,7 +309,10 @@
} else if (mWhichDialog == DIALOG_RECEIVE_COMPLETE_FAIL) {
if (mTransInfo.mStatus == BluetoothShare.STATUS_ERROR_SDCARD_FULL) {
mLine1View = (TextView) mView.findViewById(R.id.line1_view);
- tmp = getString(R.string.bt_sm_2_1, mTransInfo.mDeviceName);
+ int id = BluetoothOppUtility.deviceHasNoSdCard()
+ ? R.string.bt_sm_2_1_nosdcard
+ : R.string.bt_sm_2_1_default;
+ tmp = getString(id);
mLine1View.setText(tmp);
mLine2View = (TextView) mView.findViewById(R.id.line2_view);
tmp = getString(R.string.download_fail_line2, mTransInfo.mFileName);
diff --git a/src/com/android/bluetooth/opp/BluetoothOppTransferAdapter.java b/src/com/android/bluetooth/opp/BluetoothOppTransferAdapter.java
index 7221c53..134b2d6 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppTransferAdapter.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppTransferAdapter.java
@@ -58,7 +58,7 @@
private Context mContext;
public BluetoothOppTransferAdapter(Context context, int layout, Cursor c) {
- super(context, layout, c);
+ super(context, layout, c, true /* autoRequery */ );
mContext = context;
}
diff --git a/src/com/android/bluetooth/opp/BluetoothOppUtility.java b/src/com/android/bluetooth/opp/BluetoothOppUtility.java
index 1b5cd59..7533d0d 100644
--- a/src/com/android/bluetooth/opp/BluetoothOppUtility.java
+++ b/src/com/android/bluetooth/opp/BluetoothOppUtility.java
@@ -45,6 +45,7 @@
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
+import android.os.SystemProperties;
import android.util.Log;
import com.android.bluetooth.R;
@@ -56,6 +57,7 @@
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@@ -66,6 +68,8 @@
private static final String TAG = "BluetoothOppUtility";
private static final boolean D = Constants.DEBUG;
private static final boolean V = Constants.VERBOSE;
+ /** Whether the device has the "nosdcard" characteristic, or null if not-yet-known. */
+ private static Boolean sNoSdCard = null;
private static final ConcurrentHashMap<Uri, BluetoothOppSendFileInfo> sSendFileMap =
new ConcurrentHashMap<Uri, BluetoothOppSendFileInfo>();
@@ -292,6 +296,17 @@
}
/**
+ * Whether the device has the "nosdcard" characteristic or not.
+ */
+ public static boolean deviceHasNoSdCard() {
+ if (sNoSdCard == null) {
+ String characteristics = SystemProperties.get("ro.build.characteristics", "");
+ sNoSdCard = Arrays.asList(characteristics).contains("nosdcard");
+ }
+ return sNoSdCard;
+ }
+
+ /**
* Get status description according to status code.
*/
public static String getStatusDescription(Context context, int statusCode, String deviceName) {
@@ -311,11 +326,15 @@
} else if (statusCode == BluetoothShare.STATUS_FILE_ERROR) {
ret = context.getString(R.string.status_file_error);
} else if (statusCode == BluetoothShare.STATUS_ERROR_NO_SDCARD) {
- ret = context.getString(R.string.status_no_sd_card);
+ int id = deviceHasNoSdCard()
+ ? R.string.status_no_sd_card_nosdcard
+ : R.string.status_no_sd_card_default;
+ ret = context.getString(id);
} else if (statusCode == BluetoothShare.STATUS_CONNECTION_ERROR) {
ret = context.getString(R.string.status_connection_error);
} else if (statusCode == BluetoothShare.STATUS_ERROR_SDCARD_FULL) {
- ret = context.getString(R.string.bt_sm_2_1, deviceName);
+ int id = deviceHasNoSdCard() ? R.string.bt_sm_2_1_nosdcard : R.string.bt_sm_2_1_default;
+ ret = context.getString(id);
} else if ((statusCode == BluetoothShare.STATUS_BAD_REQUEST) || (statusCode
== BluetoothShare.STATUS_LENGTH_REQUIRED) || (statusCode
== BluetoothShare.STATUS_PRECONDITION_FAILED) || (statusCode
diff --git a/src/com/android/bluetooth/pan/BluetoothTetheringNetworkFactory.java b/src/com/android/bluetooth/pan/BluetoothTetheringNetworkFactory.java
index f477dd9..72016f7 100644
--- a/src/com/android/bluetooth/pan/BluetoothTetheringNetworkFactory.java
+++ b/src/com/android/bluetooth/pan/BluetoothTetheringNetworkFactory.java
@@ -25,12 +25,17 @@
import android.net.NetworkFactory;
import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState;
-import android.net.ip.IpClient;
-import android.net.ip.IpClient.WaitForProvisioningCallback;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientUtil;
+import android.net.ip.IpClientUtil.WaitForProvisioningCallbacks;
+import android.net.shared.ProvisioningConfiguration;
import android.os.Looper;
+import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Slog;
+import com.android.internal.annotations.GuardedBy;
+
/**
* This class tracks the data connection associated with Bluetooth
* reverse tethering. PanService calls it when a reverse tethered
@@ -49,7 +54,9 @@
// All accesses to these must be synchronized(this).
private final NetworkInfo mNetworkInfo;
- private IpClient mIpClient;
+ private IIpClient mIpClient;
+ @GuardedBy("this")
+ private int mIpClientStartIndex = 0;
private String mInterfaceName;
private NetworkAgent mNetworkAgent;
@@ -65,13 +72,64 @@
setCapabilityFilter(mNetworkCapabilities);
}
+ private class BtIpClientCallback extends WaitForProvisioningCallbacks {
+ private final int mCurrentStartIndex;
+
+ private BtIpClientCallback(int currentStartIndex) {
+ mCurrentStartIndex = currentStartIndex;
+ }
+
+ @Override
+ public void onIpClientCreated(IIpClient ipClient) {
+ synchronized (BluetoothTetheringNetworkFactory.this) {
+ if (mCurrentStartIndex != mIpClientStartIndex) {
+ // Do not start IpClient: the current request is obsolete.
+ // IpClient will be GCed eventually as the IIpClient Binder token goes out
+ // of scope.
+ return;
+ }
+ mIpClient = ipClient;
+ try {
+ mIpClient.startProvisioning(new ProvisioningConfiguration.Builder()
+ .withoutMultinetworkPolicyTracker()
+ .withoutIpReachabilityMonitor()
+ .build().toStableParcelable());
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error starting IpClient provisioning", e);
+ }
+ }
+ }
+
+ @Override
+ public void onLinkPropertiesChange(LinkProperties newLp) {
+ synchronized (BluetoothTetheringNetworkFactory.this) {
+ if (mNetworkAgent != null && mNetworkInfo.isConnected()) {
+ mNetworkAgent.sendLinkProperties(newLp);
+ }
+ }
+ }
+ }
+
private void stopIpClientLocked() {
+ // Mark all previous start requests as obsolete
+ mIpClientStartIndex++;
if (mIpClient != null) {
- mIpClient.shutdown();
+ try {
+ mIpClient.shutdown();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error shutting down IpClient", e);
+ }
mIpClient = null;
}
}
+ private BtIpClientCallback startIpClientLocked() {
+ mIpClientStartIndex++;
+ final BtIpClientCallback callback = new BtIpClientCallback(mIpClientStartIndex);
+ IpClientUtil.makeIpClient(mContext, mInterfaceName, callback);
+ return callback;
+ }
+
// Called by NetworkFactory when PanService and NetworkFactory both desire a Bluetooth
// reverse-tether connection. A network interface for Bluetooth reverse-tethering can be
// assumed to be available because we only register our NetworkFactory when it is so.
@@ -84,16 +142,7 @@
@Override
public void run() {
LinkProperties linkProperties;
- final WaitForProvisioningCallback ipcCallback = new WaitForProvisioningCallback() {
- @Override
- public void onLinkPropertiesChange(LinkProperties newLp) {
- synchronized (BluetoothTetheringNetworkFactory.this) {
- if (mNetworkAgent != null && mNetworkInfo.isConnected()) {
- mNetworkAgent.sendLinkProperties(newLp);
- }
- }
- }
- };
+ final WaitForProvisioningCallbacks ipcCallback;
synchronized (BluetoothTetheringNetworkFactory.this) {
if (TextUtils.isEmpty(mInterfaceName)) {
@@ -102,11 +151,7 @@
}
log("ipProvisioningThread(+" + mInterfaceName + "): " + "mNetworkInfo="
+ mNetworkInfo);
- mIpClient = new IpClient(mContext, mInterfaceName, ipcCallback);
- mIpClient.startProvisioning(mIpClient.buildProvisioningConfiguration()
- .withoutMultinetworkPolicyTracker()
- .withoutIpReachabilityMonitor()
- .build());
+ ipcCallback = startIpClientLocked();
mNetworkInfo.setDetailedState(DetailedState.OBTAINING_IPADDR, null, null);
}
diff --git a/src/com/android/bluetooth/pan/PanService.java b/src/com/android/bluetooth/pan/PanService.java
index 700b395..8c52df7 100644
--- a/src/com/android/bluetooth/pan/PanService.java
+++ b/src/com/android/bluetooth/pan/PanService.java
@@ -34,11 +34,11 @@
import android.os.Message;
import android.os.ServiceManager;
import android.os.UserManager;
-import android.provider.Settings;
import android.util.Log;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
@@ -398,11 +398,11 @@
throw new IllegalArgumentException("Null device");
}
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothPanPriorityKey(device.getAddress()), priority);
if (DBG) {
Log.d(TAG, "Saved priority " + device + " = " + priority);
}
+ AdapterService.getAdapterService().getDatabase()
+ .setProfilePriority(device, BluetoothProfile.PAN, priority);
return true;
}
@@ -411,9 +411,8 @@
throw new IllegalArgumentException("Null device");
}
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- return Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothPanPriorityKey(device.getAddress()),
- BluetoothProfile.PRIORITY_UNDEFINED);
+ return AdapterService.getAdapterService().getDatabase()
+ .getProfilePriority(device, BluetoothProfile.PAN);
}
public List<BluetoothDevice> getConnectedDevices() {
diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapActivity.java b/src/com/android/bluetooth/pbap/BluetoothPbapActivity.java
index f709cd5..5468a76 100644
--- a/src/com/android/bluetooth/pbap/BluetoothPbapActivity.java
+++ b/src/com/android/bluetooth/pbap/BluetoothPbapActivity.java
@@ -109,7 +109,7 @@
Intent i = getIntent();
String action = i.getAction();
mDevice = i.getParcelableExtra(BluetoothPbapService.EXTRA_DEVICE);
- if (action.equals(BluetoothPbapService.AUTH_CHALL_ACTION)) {
+ if (action != null && action.equals(BluetoothPbapService.AUTH_CHALL_ACTION)) {
showPbapDialog(DIALOG_YES_NO_AUTH);
mCurrentDialog = DIALOG_YES_NO_AUTH;
} else {
diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java b/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java
index 805f3ea..b5fb51b 100644
--- a/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java
+++ b/src/com/android/bluetooth/pbap/BluetoothPbapCallLogComposer.java
@@ -23,7 +23,6 @@
import android.provider.CallLog;
import android.provider.CallLog.Calls;
import android.text.TextUtils;
-import android.text.format.Time;
import android.util.Log;
import com.android.bluetooth.R;
@@ -32,13 +31,15 @@
import com.android.vcard.VCardConstants;
import com.android.vcard.VCardUtils;
+import java.text.SimpleDateFormat;
import java.util.Arrays;
+import java.util.Calendar;
/**
* VCard composer especially for Call Log used in Bluetooth.
*/
public class BluetoothPbapCallLogComposer {
- private static final String TAG = "CallLogComposer";
+ private static final String TAG = "PbapCallLogComposer";
private static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
"Failed to get database information";
@@ -86,6 +87,8 @@
private String mErrorReason = NO_ERROR;
+ private final String RFC_2455_FORMAT = "yyyyMMdd'T'HHmmss";
+
public BluetoothPbapCallLogComposer(final Context context) {
mContext = context;
mContentResolver = context.getContentResolver();
@@ -198,9 +201,10 @@
* The format is: ("%Y%m%dT%H%M%S").
*/
private String toRfc2455Format(final long millSecs) {
- Time startDate = new Time();
- startDate.set(millSecs);
- return startDate.format2445();
+ Calendar cal = Calendar.getInstance();
+ cal.setTimeInMillis(millSecs);
+ SimpleDateFormat df = new SimpleDateFormat(RFC_2455_FORMAT);
+ return df.format(cal.getTime());
}
/**
diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java b/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java
old mode 100644
new mode 100755
index 247a76d..306f31d
--- a/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java
+++ b/src/com/android/bluetooth/pbap/BluetoothPbapObexServer.java
@@ -817,50 +817,65 @@
}
if (type.equals("number")) {
+ ArrayList<Integer> savedPosList = new ArrayList<>();
+ ArrayList<String> selectedNameList = new ArrayList<String>();
// query the number, to get the names
ArrayList<String> names =
mVcardManager.getContactNamesByNumber(appParamValue.searchValue);
if (mOrderBy == ORDER_BY_ALPHABETICAL) Collections.sort(names);
for (int i = 0; i < names.size(); i++) {
compareValue = names.get(i).trim();
- if (D) {
- Log.d(TAG, "compareValue=" + compareValue);
- }
- for (int pos = appParamValue.listStartOffset;
- pos < listSize && itemsFound < requestSize; pos++) {
+ if (D) Log.d(TAG, "compareValue=" + compareValue);
+ for (int pos = 0; pos < listSize; pos++) {
currentValue = nameList.get(pos);
if (V) {
Log.d(TAG, "currentValue=" + currentValue);
}
if (currentValue.equals(compareValue)) {
- itemsFound++;
if (currentValue.contains(",")) {
currentValue = currentValue.substring(0, currentValue.lastIndexOf(','));
}
- writeVCardEntry(pos, currentValue, result);
+ selectedNameList.add(currentValue);
+ savedPosList.add(pos);
}
}
- if (itemsFound >= requestSize) {
- break;
- }
}
+
+ for (int j = appParamValue.listStartOffset;
+ j < selectedNameList.size() && itemsFound < requestSize; j++) {
+ itemsFound++;
+ writeVCardEntry(savedPosList.get(j), selectedNameList.get(j), result);
+ }
+
} else {
+ ArrayList<Integer> savedPosList = new ArrayList<>();
+ ArrayList<String> selectedNameList = new ArrayList<String>();
if (appParamValue.searchValue != null) {
compareValue = appParamValue.searchValue.trim().toLowerCase();
}
- for (int pos = appParamValue.listStartOffset;
- pos < listSize && itemsFound < requestSize; pos++) {
+
+ for (int pos = 0; pos < listSize; pos++) {
currentValue = nameList.get(pos);
+
if (currentValue.contains(",")) {
currentValue = currentValue.substring(0, currentValue.lastIndexOf(','));
}
- if (appParamValue.searchValue.isEmpty() || ((currentValue.toLowerCase()).startsWith(
- compareValue))) {
- itemsFound++;
- writeVCardEntry(pos, currentValue, result);
+ if (appParamValue.searchValue != null) {
+ if (appParamValue.searchValue.isEmpty()
+ || ((currentValue.toLowerCase())
+ .startsWith(compareValue.toLowerCase()))) {
+ selectedNameList.add(currentValue);
+ savedPosList.add(pos);
+ }
}
}
+
+ for (int i = appParamValue.listStartOffset;
+ i < selectedNameList.size() && itemsFound < requestSize; i++) {
+ itemsFound++;
+ writeVCardEntry(savedPosList.get(i), selectedNameList.get(i), result);
+ }
}
return itemsFound;
}
@@ -971,6 +986,8 @@
nmnum = nmnum > 0 ? nmnum : 0;
misnum[0] = (byte) nmnum;
+ ap.addAPPHeader(ApplicationParameter.TRIPLET_TAGID.NEWMISSEDCALLS_TAGID,
+ ApplicationParameter.TRIPLET_LENGTH.NEWMISSEDCALLS_LENGTH, misnum);
if (D) {
Log.d(TAG, "handleAppParaForResponse(): mNeedNewMissedCallsNum=true, num= "
+ nmnum);
diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapService.java b/src/com/android/bluetooth/pbap/BluetoothPbapService.java
index 374b5a1..e7dba2a 100644
--- a/src/com/android/bluetooth/pbap/BluetoothPbapService.java
+++ b/src/com/android/bluetooth/pbap/BluetoothPbapService.java
@@ -49,7 +49,6 @@
import android.os.Message;
import android.os.PowerManager;
import android.os.UserManager;
-import android.support.annotation.VisibleForTesting;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
@@ -61,6 +60,7 @@
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.sdp.SdpManager;
import com.android.bluetooth.util.DevicePolicyUtils;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashMap;
@@ -127,6 +127,7 @@
static final int CONTACTS_LOADED = 5;
static final int CHECK_SECONDARY_VERSION_COUNTER = 6;
static final int ROLLOVER_COUNTERS = 7;
+ static final int GET_LOCAL_TELEPHONY_DETAILS = 8;
static final int USER_CONFIRM_TIMEOUT_VALUE = 30000;
static final int RELEASE_WAKE_LOCK_DELAY = 10000;
@@ -408,6 +409,8 @@
mPbapStateMachineMap.remove(remoteDevice);
}
break;
+ case GET_LOCAL_TELEPHONY_DETAILS:
+ getLocalTelephonyDetails();
default:
break;
}
@@ -507,18 +510,12 @@
Log.e(TAG, "Illegal state exception, content observer is already registered");
}
- TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
- if (tm != null) {
- sLocalPhoneNum = tm.getLine1Number();
- sLocalPhoneName = tm.getLine1AlphaTag();
- if (TextUtils.isEmpty(sLocalPhoneName)) {
- sLocalPhoneName = this.getString(R.string.localPhoneName);
- }
- }
+ setBluetoothPbapService(this);
+ mSessionStatusHandler.sendMessage(
+ mSessionStatusHandler.obtainMessage(GET_LOCAL_TELEPHONY_DETAILS));
mSessionStatusHandler.sendMessage(mSessionStatusHandler.obtainMessage(LOAD_CONTACTS));
mSessionStatusHandler.sendMessage(mSessionStatusHandler.obtainMessage(START_LISTENER));
- setBluetoothPbapService(this);
return true;
}
@@ -686,7 +683,7 @@
* Send the result to the state machine.
* @param stateMachine PbapStateMachine which sends the request
*/
- @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void checkOrGetPhonebookPermission(PbapStateMachine stateMachine) {
BluetoothDevice device = stateMachine.getRemoteDevice();
int permission = device.getPhonebookAccessPermission();
@@ -775,4 +772,18 @@
mThreadUpdateSecVersionCounter.start();
}
}
+
+ private void getLocalTelephonyDetails() {
+ TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm != null) {
+ sLocalPhoneNum = tm.getLine1Number();
+ sLocalPhoneName = tm.getLine1AlphaTag();
+ if (TextUtils.isEmpty(sLocalPhoneName)) {
+ sLocalPhoneName = this.getString(R.string.localPhoneName);
+ }
+ }
+ if (VERBOSE)
+ Log.v(TAG, "Local Phone Details- Number:" + sLocalPhoneNum
+ + ", Name:" + sLocalPhoneName);
+ }
}
diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java b/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
old mode 100644
new mode 100755
index e58fa2e..5ba2b4b
--- a/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
+++ b/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
@@ -332,12 +332,18 @@
contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, null, null,
Phone.CONTACT_ID);
+ ArrayList<String> contactNameIdList = new ArrayList<String>();
+ appendDistinctNameIdList(contactNameIdList,
+ mContext.getString(android.R.string.unknownName), contactCursor);
+
if (contactCursor != null) {
if (!composer.initWithCallback(contactCursor,
new EnterpriseRawContactEntitlesInfoCallback())) {
return nameList;
}
+ int i = 0;
+ contactCursor.moveToFirst();
while (!composer.isAfterLast()) {
String vcard = composer.createOneEntry();
if (vcard == null) {
@@ -362,8 +368,9 @@
if (TextUtils.isEmpty(name)) {
name = mContext.getString(android.R.string.unknownName);
}
- nameList.add(name);
+ nameList.add(contactNameIdList.get(i));
}
+ i++;
}
if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
if (V) {
diff --git a/src/com/android/bluetooth/pbapclient/Authenticator.java b/src/com/android/bluetooth/pbapclient/Authenticator.java
index 0811499..3a096f1 100644
--- a/src/com/android/bluetooth/pbapclient/Authenticator.java
+++ b/src/com/android/bluetooth/pbapclient/Authenticator.java
@@ -26,6 +26,7 @@
public class Authenticator extends AbstractAccountAuthenticator {
private static final String TAG = "PbapAuthenticator";
+ private static final boolean DBG = Utils.DBG;
public Authenticator(Context context) {
super(context);
@@ -34,7 +35,7 @@
// Editing properties is not supported
@Override
public Bundle editProperties(AccountAuthenticatorResponse r, String s) {
- Log.d(TAG, "got call", new Exception());
+ if (DBG) Log.d(TAG, "got call", new Exception());
throw new UnsupportedOperationException();
}
@@ -42,7 +43,7 @@
@Override
public Bundle addAccount(AccountAuthenticatorResponse r, String s, String s2, String[] strings,
Bundle bundle) throws NetworkErrorException {
- Log.d(TAG, "got call", new Exception());
+ if (DBG) Log.d(TAG, "got call", new Exception());
// Don't allow accounts to be added.
throw new UnsupportedOperationException();
}
@@ -51,7 +52,7 @@
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse r, Account account, Bundle bundle)
throws NetworkErrorException {
- Log.d(TAG, "got call", new Exception());
+ if (DBG) Log.d(TAG, "got call", new Exception());
return null;
}
@@ -59,14 +60,14 @@
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse r, Account account, String s,
Bundle bundle) throws NetworkErrorException {
- Log.d(TAG, "got call", new Exception());
+ if (DBG) Log.d(TAG, "got call", new Exception());
throw new UnsupportedOperationException();
}
// Getting a label for the auth token is not supported
@Override
public String getAuthTokenLabel(String s) {
- Log.d(TAG, "got call", new Exception());
+ if (DBG) Log.d(TAG, "got call", new Exception());
return null;
}
@@ -74,7 +75,7 @@
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse r, Account account, String s,
Bundle bundle) throws NetworkErrorException {
- Log.d(TAG, "got call", new Exception());
+ if (DBG) Log.d(TAG, "got call", new Exception());
return null;
}
@@ -82,7 +83,7 @@
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse r, Account account, String[] strings)
throws NetworkErrorException {
- Log.d(TAG, "got call", new Exception());
+ if (DBG) Log.d(TAG, "got call", new Exception());
final Bundle result = new Bundle();
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
diff --git a/src/com/android/bluetooth/pbapclient/BluetoothPbapObexAuthenticator.java b/src/com/android/bluetooth/pbapclient/BluetoothPbapObexAuthenticator.java
index ab8091a..f14a805 100644
--- a/src/com/android/bluetooth/pbapclient/BluetoothPbapObexAuthenticator.java
+++ b/src/com/android/bluetooth/pbapclient/BluetoothPbapObexAuthenticator.java
@@ -30,7 +30,8 @@
class BluetoothPbapObexAuthenticator implements Authenticator {
- private static final String TAG = "BluetoothPbapObexAuthenticator";
+ private static final String TAG = "BtPbapObexAuthenticator";
+ private static final boolean DBG = Utils.DBG;
//Default session key for legacy devices is 0000
private String mSessionKey = "0000";
@@ -45,13 +46,16 @@
public PasswordAuthentication onAuthenticationChallenge(String description,
boolean isUserIdRequired, boolean isFullAccess) {
PasswordAuthentication pa = null;
- Log.v(TAG, "onAuthenticationChallenge: starting");
+ if (DBG) Log.v(TAG, "onAuthenticationChallenge: starting");
if (mSessionKey != null && mSessionKey.length() != 0) {
- Log.v(TAG, "onAuthenticationChallenge: mSessionKey=" + mSessionKey);
+ if (DBG) Log.v(TAG, "onAuthenticationChallenge: mSessionKey=" + mSessionKey);
pa = new PasswordAuthentication(null, mSessionKey.getBytes());
} else {
- Log.v(TAG, "onAuthenticationChallenge: mSessionKey is empty, timeout/cancel occured");
+ if (DBG) {
+ Log.v(TAG,
+ "onAuthenticationChallenge: mSessionKey is empty, timeout/cancel occured");
+ }
}
return pa;
@@ -59,7 +63,7 @@
@Override
public byte[] onAuthenticationResponse(byte[] userName) {
- Log.v(TAG, "onAuthenticationResponse: " + userName);
+ if (DBG) Log.v(TAG, "onAuthenticationResponse: " + userName);
/* required only in case PCE challenges PSE which we don't do now */
return null;
}
diff --git a/src/com/android/bluetooth/pbapclient/BluetoothPbapRequest.java b/src/com/android/bluetooth/pbapclient/BluetoothPbapRequest.java
index 1bd71ce..8b8db79 100644
--- a/src/com/android/bluetooth/pbapclient/BluetoothPbapRequest.java
+++ b/src/com/android/bluetooth/pbapclient/BluetoothPbapRequest.java
@@ -29,6 +29,7 @@
abstract class BluetoothPbapRequest {
private static final String TAG = "BluetoothPbapRequest";
+ private static final boolean DBG = Utils.DBG;
protected static final byte OAP_TAGID_ORDER = 0x01;
protected static final byte OAP_TAGID_SEARCH_VALUE = 0x02;
@@ -58,7 +59,7 @@
}
public void execute(ClientSession session) throws IOException {
- Log.v(TAG, "execute");
+ if (DBG) Log.v(TAG, "execute");
/* in case request is aborted before can be executed */
if (mAborted) {
@@ -88,7 +89,7 @@
mResponseCode = mOp.getResponseCode();
- Log.d(TAG, "mResponseCode=" + mResponseCode);
+ if (DBG) Log.d(TAG, "mResponseCode=" + mResponseCode);
checkResponseCode(mResponseCode);
} catch (IOException e) {
@@ -112,19 +113,19 @@
}
protected void readResponse(InputStream stream) throws IOException {
- Log.v(TAG, "readResponse");
+ if (DBG) Log.v(TAG, "readResponse");
/* nothing here by default */
}
protected void readResponseHeaders(HeaderSet headerset) {
- Log.v(TAG, "readResponseHeaders");
+ if (DBG) Log.v(TAG, "readResponseHeaders");
/* nothing here by dafault */
}
protected void checkResponseCode(int responseCode) throws IOException {
- Log.v(TAG, "checkResponseCode");
+ if (DBG) Log.v(TAG, "checkResponseCode");
/* nothing here by dafault */
}
diff --git a/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBook.java b/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBook.java
index ddbf215..a5b3fbf 100644
--- a/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBook.java
+++ b/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBook.java
@@ -29,9 +29,9 @@
final class BluetoothPbapRequestPullPhoneBook extends BluetoothPbapRequest {
- private static final boolean VDBG = false;
+ private static final boolean VDBG = Utils.VDBG;
- private static final String TAG = "BluetoothPbapRequestPullPhoneBook";
+ private static final String TAG = "BtPbapReqPullPhoneBook";
private static final String TYPE = "x-bt/phonebook";
@@ -73,7 +73,7 @@
oap.add(OAP_TAGID_FORMAT, format);
/*
- * maxListCount is a special case which is handled in
+ * maxListCount == 0 is a special case which is handled in
* BluetoothPbapRequestPullPhoneBookSize
*/
if (maxListCount > 0) {
@@ -93,7 +93,7 @@
@Override
protected void readResponse(InputStream stream) throws IOException {
- Log.v(TAG, "readResponse");
+ if (VDBG) Log.v(TAG, "readResponse");
mResponse = new BluetoothPbapVcardList(mAccount, stream, mFormat);
if (VDBG) {
@@ -103,7 +103,7 @@
@Override
protected void readResponseHeaders(HeaderSet headerset) {
- Log.v(TAG, "readResponseHeaders");
+ if (VDBG) Log.v(TAG, "readResponseHeaders");
ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
diff --git a/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBookSize.java b/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBookSize.java
new file mode 100644
index 0000000..f070661
--- /dev/null
+++ b/src/com/android/bluetooth/pbapclient/BluetoothPbapRequestPullPhoneBookSize.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.pbapclient;
+
+import android.util.Log;
+
+import javax.obex.HeaderSet;
+
+final class BluetoothPbapRequestPullPhoneBookSize extends BluetoothPbapRequest {
+
+ private static final boolean VDBG = Utils.VDBG;
+
+ private static final String TAG = "BtPbapReqPullPhoneBookSize";
+
+ private static final String TYPE = "x-bt/phonebook";
+
+ private int mSize;
+
+ BluetoothPbapRequestPullPhoneBookSize(String pbName, long filter) {
+ mHeaderSet.setHeader(HeaderSet.NAME, pbName);
+
+ mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+
+ ObexAppParameters oap = new ObexAppParameters();
+ // Set MaxListCount in the request to 0 to get PhonebookSize in the response.
+ // If a vCardSelector is present in the request, then the result shall
+ // contain the number of items that satisfy the selector’s criteria.
+ // See PBAP v1.2.3, Sec. 5.1.4.5.
+ oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) 0);
+ if (filter != 0) {
+ oap.add(OAP_TAGID_FILTER, filter);
+ }
+ oap.addToHeaderSet(mHeaderSet);
+ }
+
+ @Override
+ protected void readResponseHeaders(HeaderSet headerset) {
+ if (VDBG) {
+ Log.v(TAG, "readResponseHeaders");
+ }
+
+ ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
+
+ if (oap.exists(OAP_TAGID_PHONEBOOK_SIZE)) {
+ mSize = oap.getShort(OAP_TAGID_PHONEBOOK_SIZE);
+ }
+ }
+
+ public int getSize() {
+ return mSize;
+ }
+}
diff --git a/src/com/android/bluetooth/pbapclient/CallLogPullRequest.java b/src/com/android/bluetooth/pbapclient/CallLogPullRequest.java
index 77665a5..8ab9a1a 100644
--- a/src/com/android/bluetooth/pbapclient/CallLogPullRequest.java
+++ b/src/com/android/bluetooth/pbapclient/CallLogPullRequest.java
@@ -39,8 +39,8 @@
import java.util.List;
public class CallLogPullRequest extends PullRequest {
- private static final boolean DBG = true;
- private static final boolean VDBG = false;
+ private static final boolean DBG = Utils.DBG;
+ private static final boolean VDBG = Utils.VDBG;
private static final String TAG = "PbapCallLogPullRequest";
private static final String TIMESTAMP_PROPERTY = "X-IRMC-CALL-DATETIME";
private static final String TIMESTAMP_FORMAT = "yyyyMMdd'T'HHmmss";
@@ -88,9 +88,10 @@
ContentValues values = new ContentValues();
values.put(CallLog.Calls.TYPE, type);
- values.put(Calls.PHONE_ACCOUNT_ID, mAccount.hashCode());
+ values.put(Calls.PHONE_ACCOUNT_ID, mAccount.name);
List<PhoneData> phones = vcard.getPhoneList();
- if (phones == null || phones.get(0).getNumber().equals(";")) {
+ if (phones == null || phones.get(0).getNumber().equals(";")
+ || phones.get(0).getNumber().length() == 0) {
values.put(CallLog.Calls.NUMBER, "");
} else {
String phoneNumber = phones.get(0).getNumber();
diff --git a/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java b/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java
index 913ba0e..f29f0f8 100644
--- a/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java
+++ b/src/com/android/bluetooth/pbapclient/PbapClientConnectionHandler.java
@@ -46,8 +46,18 @@
* controlling state machine.
*/
class PbapClientConnectionHandler extends Handler {
- static final String TAG = "PBAP PCE handler";
- static final boolean DBG = true;
+ // Tradeoff: larger BATCH_SIZE leads to faster download rates, while smaller
+ // BATCH_SIZE is less prone to IO Exceptions if there is a download in
+ // progress when Bluetooth stack is torn down.
+ private static final int DEFAULT_BATCH_SIZE = 250;
+
+ // Upper limit on the indices of the vcf cards/entries, inclusive,
+ // i.e., valid indices are [0, 1, ... , UPPER_LIMIT]
+ private static final int UPPER_LIMIT = 65535;
+
+ static final String TAG = "PbapClientConnHandler";
+ static final boolean DBG = Utils.DBG;
+ static final boolean VDBG = Utils.VDBG;
static final int MSG_CONNECT = 1;
static final int MSG_DISCONNECT = 2;
static final int MSG_DOWNLOAD = 3;
@@ -91,7 +101,6 @@
private static final long PBAP_REQUESTED_FIELDS =
PBAP_FILTER_VERSION | PBAP_FILTER_FN | PBAP_FILTER_N | PBAP_FILTER_PHOTO
| PBAP_FILTER_ADR | PBAP_FILTER_EMAIL | PBAP_FILTER_TEL | PBAP_FILTER_NICKNAME;
- private static final int PBAP_V1_2 = 0x0102;
private static final int L2CAP_INVALID_PSM = -1;
public static final String PB_PATH = "telecom/pb.vcf";
@@ -99,6 +108,7 @@
public static final String ICH_PATH = "telecom/ich.vcf";
public static final String OCH_PATH = "telecom/och.vcf";
+ public static final int PBAP_V1_2 = 0x0102;
public static final byte VCARD_TYPE_21 = 0;
public static final byte VCARD_TYPE_30 = 1;
@@ -194,17 +204,17 @@
}
} else {
Log.w(TAG, "Socket CONNECT Failure ");
- mPbapClientStateMachine.obtainMessage(
- PbapClientStateMachine.MSG_CONNECTION_FAILED).sendToTarget();
+ mPbapClientStateMachine.sendMessage(
+ PbapClientStateMachine.MSG_CONNECTION_FAILED);
return;
}
if (connectObexSession()) {
- mPbapClientStateMachine.obtainMessage(
- PbapClientStateMachine.MSG_CONNECTION_COMPLETE).sendToTarget();
+ mPbapClientStateMachine.sendMessage(
+ PbapClientStateMachine.MSG_CONNECTION_COMPLETE);
} else {
- mPbapClientStateMachine.obtainMessage(
- PbapClientStateMachine.MSG_CONNECTION_FAILED).sendToTarget();
+ mPbapClientStateMachine.sendMessage(
+ PbapClientStateMachine.MSG_CONNECTION_FAILED);
}
break;
@@ -234,34 +244,16 @@
removeAccount(mAccount);
removeCallLog(mAccount);
- mPbapClientStateMachine.obtainMessage(PbapClientStateMachine.MSG_CONNECTION_CLOSED)
- .sendToTarget();
+ mPbapClientStateMachine.sendMessage(PbapClientStateMachine.MSG_CONNECTION_CLOSED);
break;
case MSG_DOWNLOAD:
- try {
- mAccountCreated = addAccount(mAccount);
- if (!mAccountCreated) {
- Log.e(TAG, "Account creation failed.");
- return;
- }
- // Start at contact 1 to exclued Owner Card PBAP 1.1 sec 3.1.5.2
- BluetoothPbapRequestPullPhoneBook request =
- new BluetoothPbapRequestPullPhoneBook(PB_PATH, mAccount,
- PBAP_REQUESTED_FIELDS, VCARD_TYPE_30, 0, 1);
- request.execute(mObexSession);
- PhonebookPullRequest processor =
- new PhonebookPullRequest(mPbapClientStateMachine.getContext(),
- mAccount);
- processor.setResults(request.getList());
- processor.onPullComplete();
- HashMap<String, Integer> callCounter = new HashMap<>();
- downloadCallLog(MCH_PATH, callCounter);
- downloadCallLog(ICH_PATH, callCounter);
- downloadCallLog(OCH_PATH, callCounter);
- } catch (IOException e) {
- Log.w(TAG, "DOWNLOAD_CONTACTS Failure" + e.toString());
- }
+ downloadContacts();
+
+ HashMap<String, Integer> callCounter = new HashMap<>();
+ downloadCallLog(MCH_PATH, callCounter);
+ downloadCallLog(ICH_PATH, callCounter);
+ downloadCallLog(OCH_PATH, callCounter);
break;
default:
@@ -272,19 +264,19 @@
/* Utilize SDP, if available, to create a socket connection over L2CAP, RFCOMM specified
* channel, or RFCOMM default channel. */
- private boolean connectSocket() {
+ private synchronized boolean connectSocket() {
try {
/* Use BluetoothSocket to connect */
if (mPseRec == null) {
// BackWardCompatability: Fall back to create RFCOMM through UUID.
- Log.v(TAG, "connectSocket: UUID: " + BluetoothUuid.PBAP_PSE.getUuid());
+ if (VDBG) Log.v(TAG, "connectSocket: UUID: " + BluetoothUuid.PBAP_PSE.getUuid());
mSocket =
mDevice.createRfcommSocketToServiceRecord(BluetoothUuid.PBAP_PSE.getUuid());
} else if (mPseRec.getL2capPsm() != L2CAP_INVALID_PSM) {
- Log.v(TAG, "connectSocket: PSM: " + mPseRec.getL2capPsm());
+ if (VDBG) Log.v(TAG, "connectSocket: PSM: " + mPseRec.getL2capPsm());
mSocket = mDevice.createL2capSocket(mPseRec.getL2capPsm());
} else {
- Log.v(TAG, "connectSocket: channel: " + mPseRec.getRfcommChannelNumber());
+ if (VDBG) Log.v(TAG, "connectSocket: channel: " + mPseRec.getRfcommChannelNumber());
mSocket = mDevice.createRfcommSocket(mPseRec.getRfcommChannelNumber());
}
@@ -306,7 +298,7 @@
boolean connectionSuccessful = false;
try {
- if (DBG) {
+ if (VDBG) {
Log.v(TAG, "Start Obex Client Session");
}
BluetoothObexTransport transport = new BluetoothObexTransport(mSocket);
@@ -351,7 +343,7 @@
this.getLooper().getThread().interrupt();
}
- private void closeSocket() {
+ private synchronized void closeSocket() {
try {
if (mSocket != null) {
if (DBG) {
@@ -366,6 +358,51 @@
}
}
+ void downloadContacts() {
+ try {
+ mAccountCreated = addAccount(mAccount);
+ if (!mAccountCreated) {
+ Log.e(TAG, "Account creation failed.");
+ return;
+ }
+ PhonebookPullRequest processor =
+ new PhonebookPullRequest(mPbapClientStateMachine.getContext(),
+ mAccount);
+
+ // Download contacts in batches of size DEFAULT_BATCH_SIZE
+ BluetoothPbapRequestPullPhoneBookSize requestPbSize =
+ new BluetoothPbapRequestPullPhoneBookSize(PB_PATH,
+ PBAP_REQUESTED_FIELDS);
+ requestPbSize.execute(mObexSession);
+
+ // "-1" because Owner Card is also included in PhoneBookSize
+ int numberOfContactsRemaining = requestPbSize.getSize() - 1;
+
+ // Start at contact 1 to exclude Owner Card PBAP 1.1 sec 3.1.5.2
+ int startOffset = 1;
+ while ((numberOfContactsRemaining > 0) && (startOffset <= UPPER_LIMIT)) {
+ int numberOfContactsToDownload =
+ Math.min(Math.min(DEFAULT_BATCH_SIZE, numberOfContactsRemaining),
+ UPPER_LIMIT - startOffset + 1);
+ BluetoothPbapRequestPullPhoneBook request =
+ new BluetoothPbapRequestPullPhoneBook(PB_PATH, mAccount,
+ PBAP_REQUESTED_FIELDS, VCARD_TYPE_30,
+ numberOfContactsToDownload, startOffset);
+ request.execute(mObexSession);
+ processor.setResults(request.getList());
+ processor.onPullComplete();
+
+ startOffset += numberOfContactsToDownload;
+ numberOfContactsRemaining -= numberOfContactsToDownload;
+ }
+ if ((startOffset > UPPER_LIMIT) && (numberOfContactsRemaining > 0)) {
+ Log.w(TAG, "Download contacts incomplete, index exceeded upper limit.");
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Download contacts failure" + e.toString());
+ }
+ }
+
void downloadCallLog(String path, HashMap<String, Integer> callCounter) {
try {
BluetoothPbapRequestPullPhoneBook request =
@@ -410,8 +447,8 @@
}
return;
}
- String where = Calls.PHONE_ACCOUNT_ID + "=" + account.hashCode();
- mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI, where, null);
+ mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI,
+ Calls.PHONE_ACCOUNT_ID + "=?", new String[]{mAccount.name});
} catch (IllegalArgumentException e) {
Log.d(TAG, "Call Logs could not be deleted, they may not exist yet.");
}
diff --git a/src/com/android/bluetooth/pbapclient/PbapClientService.java b/src/com/android/bluetooth/pbapclient/PbapClientService.java
index 0a9541c..f150cdd 100644
--- a/src/com/android/bluetooth/pbapclient/PbapClientService.java
+++ b/src/com/android/bluetooth/pbapclient/PbapClientService.java
@@ -26,12 +26,12 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.provider.CallLog;
-import android.provider.Settings;
import android.util.Log;
import com.android.bluetooth.R;
-import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.bluetooth.sdp.SdpManager;
import java.util.ArrayList;
import java.util.List;
@@ -44,14 +44,18 @@
* @hide
*/
public class PbapClientService extends ProfileService {
- private static final boolean DBG = false;
+ private static final boolean DBG = Utils.DBG;
+ private static final boolean VDBG = Utils.VDBG;
+
private static final String TAG = "PbapClientService";
+ private static final String SERVICE_NAME = "Phonebook Access PCE";
// MAXIMUM_DEVICES set to 10 to prevent an excessive number of simultaneous devices.
private static final int MAXIMUM_DEVICES = 10;
private Map<BluetoothDevice, PbapClientStateMachine> mPbapClientStateMachineMap =
new ConcurrentHashMap<>();
private static PbapClientService sPbapClientService;
private PbapBroadcastReceiver mPbapBroadcastReceiver = new PbapBroadcastReceiver();
+ private int mSdpHandle = -1;
@Override
public IProfileServiceBinder initBinder() {
@@ -60,8 +64,8 @@
@Override
protected boolean start() {
- if (DBG) {
- Log.d(TAG, "onStart");
+ if (VDBG) {
+ Log.v(TAG, "onStart");
}
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
@@ -72,13 +76,17 @@
} catch (Exception e) {
Log.w(TAG, "Unable to register pbapclient receiver", e);
}
+
removeUncleanAccounts();
+ registerSdpRecord();
setPbapClientService(this);
return true;
}
@Override
protected boolean stop() {
+ setPbapClientService(null);
+ cleanUpSdpRecord();
try {
unregisterReceiver(mPbapBroadcastReceiver);
} catch (Exception e) {
@@ -87,18 +95,12 @@
for (PbapClientStateMachine pbapClientStateMachine : mPbapClientStateMachineMap.values()) {
pbapClientStateMachine.doQuit();
}
+ removeUncleanAccounts();
return true;
}
- @Override
- protected void cleanup() {
- removeUncleanAccounts();
- // TODO: Should move to stop()
- setPbapClientService(null);
- }
-
void cleanupDevice(BluetoothDevice device) {
- Log.w(TAG, "Cleanup device: " + device);
+ if (DBG) Log.d(TAG, "Cleanup device: " + device);
synchronized (mPbapClientStateMachineMap) {
PbapClientStateMachine pbapClientStateMachine = mPbapClientStateMachineMap.get(device);
if (pbapClientStateMachine != null) {
@@ -112,24 +114,54 @@
AccountManager accountManager = AccountManager.get(this);
Account[] accounts =
accountManager.getAccountsByType(getString(R.string.pbap_account_type));
- Log.w(TAG, "Found " + accounts.length + " unclean accounts");
+ if (VDBG) Log.v(TAG, "Found " + accounts.length + " unclean accounts");
for (Account acc : accounts) {
Log.w(TAG, "Deleting " + acc);
+ try {
+ getContentResolver().delete(CallLog.Calls.CONTENT_URI,
+ CallLog.Calls.PHONE_ACCOUNT_ID + "=?", new String[]{acc.name});
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Call Logs could not be deleted, they may not exist yet.");
+ }
// The device ID is the name of the account.
accountManager.removeAccountExplicitly(acc);
}
- try {
- getContentResolver().delete(CallLog.Calls.CONTENT_URI, null, null);
- } catch (IllegalArgumentException e) {
- Log.w(TAG, "Call Logs could not be deleted, they may not exist yet.");
+ }
+
+ private void registerSdpRecord() {
+ SdpManager sdpManager = SdpManager.getDefaultManager();
+ if (sdpManager == null) {
+ Log.e(TAG, "SdpManager is null");
+ return;
+ }
+ mSdpHandle = sdpManager.createPbapPceRecord(SERVICE_NAME,
+ PbapClientConnectionHandler.PBAP_V1_2);
+ }
+
+ private void cleanUpSdpRecord() {
+ if (mSdpHandle < 0) {
+ Log.e(TAG, "cleanUpSdpRecord, SDP record never created");
+ return;
+ }
+ int sdpHandle = mSdpHandle;
+ mSdpHandle = -1;
+ SdpManager sdpManager = SdpManager.getDefaultManager();
+ if (sdpManager == null) {
+ Log.e(TAG, "cleanUpSdpRecord failed, sdpManager is null, sdpHandle=" + sdpHandle);
+ return;
+ }
+ Log.i(TAG, "cleanUpSdpRecord, mSdpHandle=" + sdpHandle);
+ if (!sdpManager.removeSdpRecord(sdpHandle)) {
+ Log.e(TAG, "cleanUpSdpRecord, removeSdpRecord failed, sdpHandle=" + sdpHandle);
}
}
+
private class PbapBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
- Log.v(TAG, "onReceive" + action);
+ if (DBG) Log.v(TAG, "onReceive" + action);
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (getConnectionState(device) == BluetoothProfile.STATE_CONNECTED) {
@@ -160,7 +192,7 @@
}
private PbapClientService getService() {
- if (!Utils.checkCaller()) {
+ if (!com.android.bluetooth.Utils.checkCaller()) {
Log.w(TAG, "PbapClient call not allowed for non-active user");
return null;
}
@@ -255,8 +287,8 @@
}
private static synchronized void setPbapClientService(PbapClientService instance) {
- if (DBG) {
- Log.d(TAG, "setPbapClientService(): set to: " + instance);
+ if (VDBG) {
+ Log.v(TAG, "setPbapClientService(): set to: " + instance);
}
sPbapClientService = instance;
}
@@ -266,7 +298,7 @@
throw new IllegalArgumentException("Null device");
}
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
- Log.d(TAG, "Received request to ConnectPBAPPhonebook " + device.getAddress());
+ if (DBG) Log.d(TAG, "Received request to ConnectPBAPPhonebook " + device.getAddress());
if (getPriority(device) <= BluetoothProfile.PRIORITY_OFF) {
return false;
}
@@ -342,11 +374,11 @@
throw new IllegalArgumentException("Null device");
}
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothPbapClientPriorityKey(device.getAddress()), priority);
if (DBG) {
Log.d(TAG, "Saved priority " + device + " = " + priority);
}
+ AdapterService.getAdapterService().getDatabase()
+ .setProfilePriority(device, BluetoothProfile.PBAP_CLIENT, priority);
return true;
}
@@ -355,10 +387,8 @@
throw new IllegalArgumentException("Null device");
}
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
- int priority = Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothPbapClientPriorityKey(device.getAddress()),
- BluetoothProfile.PRIORITY_UNDEFINED);
- return priority;
+ return AdapterService.getAdapterService().getDatabase()
+ .getProfilePriority(device, BluetoothProfile.PBAP_CLIENT);
}
@Override
diff --git a/src/com/android/bluetooth/pbapclient/PbapClientStateMachine.java b/src/com/android/bluetooth/pbapclient/PbapClientStateMachine.java
index 3cc9094..2eaf8e5 100644
--- a/src/com/android/bluetooth/pbapclient/PbapClientStateMachine.java
+++ b/src/com/android/bluetooth/pbapclient/PbapClientStateMachine.java
@@ -67,7 +67,7 @@
import java.util.List;
final class PbapClientStateMachine extends StateMachine {
- private static final boolean DBG = true;
+ private static final boolean DBG = Utils.DBG;
private static final String TAG = "PbapClientStateMachine";
// Messages for handling connect/disconnect requests.
@@ -126,7 +126,7 @@
class Disconnected extends State {
@Override
public void enter() {
- Log.d(TAG, "Enter Disconnected: " + getCurrentMessage().what);
+ if (DBG) Log.d(TAG, "Enter Disconnected: " + getCurrentMessage().what);
onConnectionStateChanged(mCurrentDevice, mMostRecentState,
BluetoothProfile.STATE_DISCONNECTED);
mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
@@ -224,8 +224,6 @@
ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID);
if (DBG) {
Log.v(TAG, "Received UUID: " + uuid.toString());
- }
- if (DBG) {
Log.v(TAG, "expected UUID: " + BluetoothUuid.PBAP_PSE.toString());
}
if (uuid.equals(BluetoothUuid.PBAP_PSE)) {
@@ -250,7 +248,7 @@
class Disconnecting extends State {
@Override
public void enter() {
- Log.d(TAG, "Enter Disconnecting: " + getCurrentMessage().what);
+ if (DBG) Log.d(TAG, "Enter Disconnecting: " + getCurrentMessage().what);
onConnectionStateChanged(mCurrentDevice, mMostRecentState,
BluetoothProfile.STATE_DISCONNECTING);
mMostRecentState = BluetoothProfile.STATE_DISCONNECTING;
@@ -295,7 +293,7 @@
class Connected extends State {
@Override
public void enter() {
- Log.d(TAG, "Enter Connected: " + getCurrentMessage().what);
+ if (DBG) Log.d(TAG, "Enter Connected: " + getCurrentMessage().what);
onConnectionStateChanged(mCurrentDevice, mMostRecentState,
BluetoothProfile.STATE_CONNECTED);
mMostRecentState = BluetoothProfile.STATE_CONNECTED;
@@ -349,7 +347,7 @@
}
public void disconnect(BluetoothDevice device) {
- Log.d(TAG, "Disconnect Request " + device);
+ if (DBG) Log.d(TAG, "Disconnect Request " + device);
sendMessage(MSG_DISCONNECT, device);
}
@@ -433,7 +431,7 @@
}
public void dump(StringBuilder sb) {
- ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice);
- ProfileService.println(sb, "StateMachine: " + this.toString());
+ ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice.getAddress() + "("
+ + mCurrentDevice.getName() + ") " + this.toString());
}
}
diff --git a/src/com/android/bluetooth/pbapclient/PhonebookPullRequest.java b/src/com/android/bluetooth/pbapclient/PhonebookPullRequest.java
index 49fbd44..48ccf37 100644
--- a/src/com/android/bluetooth/pbapclient/PhonebookPullRequest.java
+++ b/src/com/android/bluetooth/pbapclient/PhonebookPullRequest.java
@@ -30,8 +30,8 @@
public class PhonebookPullRequest extends PullRequest {
private static final int MAX_OPS = 250;
- private static final boolean VDBG = false;
- private static final String TAG = "PbapPhonebookPullRequest";
+ private static final boolean VDBG = Utils.VDBG;
+ private static final String TAG = "PbapPbPullRequest";
private final Account mAccount;
private final Context mContext;
diff --git a/src/com/android/bluetooth/pbapclient/Utils.java b/src/com/android/bluetooth/pbapclient/Utils.java
new file mode 100644
index 0000000..c59d8d5
--- /dev/null
+++ b/src/com/android/bluetooth/pbapclient/Utils.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.pbapclient;
+
+class Utils {
+ static final boolean DBG = false;
+ static final boolean VDBG = false;
+}
diff --git a/src/com/android/bluetooth/sap/SapServer.java b/src/com/android/bluetooth/sap/SapServer.java
index 69f683f..3ff1fc7 100644
--- a/src/com/android/bluetooth/sap/SapServer.java
+++ b/src/com/android/bluetooth/sap/SapServer.java
@@ -11,6 +11,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.graphics.drawable.Icon;
import android.hardware.radio.V1_0.ISap;
import android.os.Handler;
import android.os.Handler.Callback;
@@ -252,16 +253,17 @@
sapDisconnectIntent.putExtra(SapServer.SAP_DISCONNECT_TYPE_EXTRA, type);
PendingIntent pIntentDisconnect =
PendingIntent.getBroadcast(mContext, type, sapDisconnectIntent, flags);
+ Notification.Action actionDisconnect =
+ new Notification.Action.Builder(Icon.createWithResource(mContext,
+ android.R.drawable.stat_sys_data_bluetooth), button, pIntentDisconnect).build();
notification =
new Notification.Builder(mContext, SAP_NOTIFICATION_CHANNEL).setOngoing(true)
- .addAction(android.R.drawable.stat_sys_data_bluetooth, button,
- pIntentDisconnect)
+ .addAction(actionDisconnect)
.setContentTitle(title)
.setTicker(ticker)
.setContentText(text)
.setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
.setAutoCancel(false)
- .setPriority(Notification.PRIORITY_MAX)
.setOnlyAlertOnce(true)
.setLocalOnly(true)
.build();
@@ -277,22 +279,24 @@
PendingIntent pIntentForceDisconnect =
PendingIntent.getBroadcast(mContext, SapMessage.DISC_IMMEDIATE,
sapForceDisconnectIntent, flags);
+ Notification.Action actionDisconnect = new Notification.Action.Builder(
+ Icon.createWithResource(mContext, android.R.drawable.stat_sys_data_bluetooth),
+ mContext.getString(R.string.bluetooth_sap_notif_disconnect_button),
+ pIntentDisconnect).build();
+ Notification.Action actionForceDisconnect =
+ new Notification.Action.Builder(Icon.createWithResource(mContext,
+ android.R.drawable.stat_sys_data_bluetooth),
+ mContext.getString(R.string.bluetooth_sap_notif_force_disconnect_button),
+ pIntentForceDisconnect).build();
notification =
new Notification.Builder(mContext, SAP_NOTIFICATION_CHANNEL).setOngoing(true)
- .addAction(android.R.drawable.stat_sys_data_bluetooth,
- mContext.getString(
- R.string.bluetooth_sap_notif_disconnect_button),
- pIntentDisconnect)
- .addAction(android.R.drawable.stat_sys_data_bluetooth,
- mContext.getString(
- R.string.bluetooth_sap_notif_force_disconnect_button),
- pIntentForceDisconnect)
+ .addAction(actionDisconnect)
+ .addAction(actionForceDisconnect)
.setContentTitle(title)
.setTicker(ticker)
.setContentText(text)
.setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
.setAutoCancel(false)
- .setPriority(Notification.PRIORITY_MAX)
.setOnlyAlertOnce(true)
.setLocalOnly(true)
.build();
@@ -491,7 +495,7 @@
if (mHandlerThread != null) {
try {
- mHandlerThread.quit();
+ mHandlerThread.quitSafely();
mHandlerThread.join();
mHandlerThread = null;
} catch (InterruptedException e) {
diff --git a/src/com/android/bluetooth/sap/SapService.java b/src/com/android/bluetooth/sap/SapService.java
index 66e0b19..995051e 100644
--- a/src/com/android/bluetooth/sap/SapService.java
+++ b/src/com/android/bluetooth/sap/SapService.java
@@ -20,17 +20,17 @@
import android.os.Message;
import android.os.ParcelUuid;
import android.os.PowerManager;
-import android.provider.Settings;
-import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.sdp.SdpManager;
+import com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.util.ArrayList;
@@ -539,7 +539,7 @@
public boolean disconnect(BluetoothDevice device) {
boolean result = false;
synchronized (SapService.this) {
- if (getRemoteDevice().equals(device)) {
+ if (mRemoteDevice != null && mRemoteDevice.equals(device)) {
switch (mState) {
case BluetoothSap.STATE_CONNECTED:
closeConnectionSocket();
@@ -596,19 +596,17 @@
}
public boolean setPriority(BluetoothDevice device, int priority) {
- Settings.Global.putInt(getContentResolver(),
- Settings.Global.getBluetoothSapPriorityKey(device.getAddress()), priority);
if (DEBUG) {
Log.d(TAG, "Saved priority " + device + " = " + priority);
}
+ AdapterService.getAdapterService().getDatabase()
+ .setProfilePriority(device, BluetoothProfile.SAP, priority);
return true;
}
public int getPriority(BluetoothDevice device) {
- int priority = Settings.Global.getInt(getContentResolver(),
- Settings.Global.getBluetoothSapPriorityKey(device.getAddress()),
- BluetoothProfile.PRIORITY_UNDEFINED);
- return priority;
+ return AdapterService.getAdapterService().getDatabase()
+ .getProfilePriority(device, BluetoothProfile.SAP);
}
@Override
@@ -784,8 +782,9 @@
if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) {
Log.v(TAG, " - Received BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY");
- if (!mIsWaitingAuthorization) {
- // this reply is not for us
+
+ int requestType = intent.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, -1);
+ if (requestType != BluetoothDevice.REQUEST_TYPE_SIM_ACCESS) {
return;
}
@@ -794,7 +793,7 @@
if (intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT,
BluetoothDevice.CONNECTION_ACCESS_NO)
== BluetoothDevice.CONNECTION_ACCESS_YES) {
- //bluetooth connection accepted by user
+ // bluetooth connection accepted by user
if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) {
boolean result = mRemoteDevice.setSimAccessPermission(
BluetoothDevice.ACCESS_ALLOWED);
@@ -802,6 +801,9 @@
Log.v(TAG, "setSimAccessPermission(ACCESS_ALLOWED) result=" + result);
}
}
+ boolean result = setPriority(mRemoteDevice, BluetoothProfile.PRIORITY_ON);
+ Log.d(TAG, "setPriority ON, result = " + result);
+
try {
if (mConnSocket != null) {
// start obex server and rfcomm connection
@@ -820,6 +822,8 @@
Log.v(TAG, "setSimAccessPermission(ACCESS_REJECTED) result=" + result);
}
}
+ boolean result = setPriority(mRemoteDevice, BluetoothProfile.PRIORITY_OFF);
+ Log.d(TAG, "setPriority OFF, result = " + result);
// Ensure proper cleanup, and prepare for new connect.
mSessionStatusHandler.sendEmptyMessage(MSG_SERVERSESSION_CLOSE);
}
diff --git a/src/com/android/bluetooth/sdp/SdpManager.java b/src/com/android/bluetooth/sdp/SdpManager.java
index d0cca25..d5e3770 100644
--- a/src/com/android/bluetooth/sdp/SdpManager.java
+++ b/src/com/android/bluetooth/sdp/SdpManager.java
@@ -85,6 +85,9 @@
private native int sdpCreateMapMnsRecordNative(String serviceName, int rfcommChannel,
int l2capPsm, int version, int features);
+ private native int sdpCreatePbapPceRecordNative(String serviceName,
+ int version);
+
private native int sdpCreatePbapPseRecordNative(String serviceName, int rfcommChannel,
int l2capPsm, int version, int repositories, int features);
@@ -537,6 +540,27 @@
return sdpCreateMapMnsRecordNative(serviceName, rfcommChannel, l2capPsm, version, features);
}
+ /**
+ * Create a Client side Phone Book Access Profile Service Record.
+ * Create the record once, and reuse it for all connections.
+ * If changes to a record is needed remove the old record using {@link removeSdpRecord}
+ * and then create a new one.
+ * @param serviceName The textual name of the service
+ * @param version The Profile version number (As specified in the Bluetooth
+ * PBAP specification)
+ * @return a handle to the record created. The record can be removed again
+ * using {@link removeSdpRecord}(). The record is not linked to the
+ * creation/destruction of BluetoothSockets, hence SDP record cleanup
+ * is a separate process.
+ */
+ public int createPbapPceRecord(String serviceName, int version) {
+ if (!sNativeAvailable) {
+ throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized");
+ }
+ return sdpCreatePbapPceRecordNative(serviceName, version);
+ }
+
+
/**
* Create a Server side Phone Book Access Profile Service Record.
* Create the record once, and reuse it for all connections.
diff --git a/tests/robotests/Android.mk b/tests/robotests/Android.mk
index 5a6c0bb..73f7706 100644
--- a/tests/robotests/Android.mk
+++ b/tests/robotests/Android.mk
@@ -12,8 +12,7 @@
LOCAL_JAVA_LIBRARIES := \
junit \
- platform-robolectric-3.6.1-prebuilt \
- sdk_vcurrent
+ platform-robolectric-3.6.2-prebuilt
LOCAL_INSTRUMENTATION_FOR := Bluetooth
LOCAL_MODULE := BluetoothRoboTests
@@ -38,4 +37,4 @@
LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src
-include prebuilts/misc/common/robolectric/3.6.1/run_robotests.mk
+include prebuilts/misc/common/robolectric/3.6.2/run_robotests.mk
diff --git a/tests/unit/Android.mk b/tests/unit/Android.mk
index ef948a6..24db70f 100644
--- a/tests/unit/Android.mk
+++ b/tests/unit/Android.mk
@@ -15,9 +15,16 @@
LOCAL_STATIC_JAVA_LIBRARIES := \
com.android.emailcommon \
- android-support-test \
+ androidx.test.rules \
mockito-target \
- espresso-intents
+ androidx.test.espresso.intents \
+ gson-prebuilt-jar \
+ bt-androidx-room-migration-nodeps \
+ bt-androidx-room-runtime-nodeps \
+ bt-androidx-room-testing-nodeps
+
+LOCAL_ASSET_DIR += \
+ $(LOCAL_PATH)/src/com/android/bluetooth/btservice/storage/schemas
# Include all test java files.
LOCAL_SRC_FILES := $(call all-java-files-under, src)
@@ -30,3 +37,14 @@
LOCAL_INSTRUMENTATION_FOR := Bluetooth
include $(BUILD_PACKAGE)
+
+include $(CLEAR_VARS)
+
+ROOM_LIBS_PATH := ../../lib/room
+
+LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
+ bt-androidx-room-migration-nodeps:$(ROOM_LIBS_PATH)/room-migration-2.0.0-beta01.jar \
+ bt-androidx-room-runtime-nodeps:$(ROOM_LIBS_PATH)/room-runtime-2.0.0-alpha1.aar \
+ bt-androidx-room-testing-nodeps:$(ROOM_LIBS_PATH)/room-testing-2.0.0-alpha1.aar
+
+include $(BUILD_MULTI_PREBUILT)
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index e344399..10c5d6d 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -54,19 +54,14 @@
<application
android:allowBackup="true" >
<uses-library android:name="android.test.runner" />
- <uses-permission android:name="android.permission.ACCESS_BLUETOOTH_SHARE" />
<uses-library android:name="org.apache.http.legacy" android:required="false" />
- <uses-permission android:name="com.android.permission.WHITELIST_BLUETOOTH_DEVICE" />
- <path-permission
- android:pathPrefix="/btopp"
- android:permission="android.permission.ACCESS_BLUETOOTH_SHARE" />
</application>
<!--
This declares that this application uses the instrumentation test runner targeting
the package of com.android.bluetooth. To run the tests use the command:
"adb shell am instrument -w com.android.bluetooth.tests/android.test.InstrumentationTestRunner"
-->
- <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.bluetooth"
android:label="Tests for com.android.bluetooth"/>
</manifest>
diff --git a/tests/unit/AndroidTest.xml b/tests/unit/AndroidTest.xml
index 230f2b1..6edd9a5 100644
--- a/tests/unit/AndroidTest.xml
+++ b/tests/unit/AndroidTest.xml
@@ -26,7 +26,9 @@
<option name="teardown-command" value="svc bluetooth enable" />
<option name="teardown-command" value="settings put global ble_scan_always_enabled 1" />
</target_preparer>
-
+ <target_preparer class="com.android.tradefed.targetprep.FolderSaver">
+ <option name="device-path" value="/data/vendor/ssrdump" />
+ </target_preparer>
<option name="test-tag" value="BluetoothInstrumentationTests" />
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.bluetooth.tests" />
diff --git a/tests/unit/src/com/android/bluetooth/FileSystemWriteTest.java b/tests/unit/src/com/android/bluetooth/FileSystemWriteTest.java
index 7841e80..a0215a8 100644
--- a/tests/unit/src/com/android/bluetooth/FileSystemWriteTest.java
+++ b/tests/unit/src/com/android/bluetooth/FileSystemWriteTest.java
@@ -15,8 +15,8 @@
*/
package com.android.bluetooth;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
diff --git a/tests/unit/src/com/android/bluetooth/TestUtils.java b/tests/unit/src/com/android/bluetooth/TestUtils.java
index d0e3324..4e6b7ba 100644
--- a/tests/unit/src/com/android/bluetooth/TestUtils.java
+++ b/tests/unit/src/com/android/bluetooth/TestUtils.java
@@ -23,8 +23,9 @@
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.rule.ServiceTestRule;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.rule.ServiceTestRule;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
@@ -33,9 +34,13 @@
import org.mockito.ArgumentCaptor;
import org.mockito.internal.util.MockUtil;
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.HashMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -271,6 +276,42 @@
}
/**
+ * Read Bluetooth adapter configuration from the filesystem
+ *
+ * @return A {@link HashMap} of Bluetooth configs in the format:
+ * section -> key1 -> value1
+ * -> key2 -> value2
+ * Assume no empty section name, no duplicate keys in the same section
+ */
+ public static HashMap<String, HashMap<String, String>> readAdapterConfig() {
+ HashMap<String, HashMap<String, String>> adapterConfig = new HashMap<>();
+ try (BufferedReader reader =
+ new BufferedReader(new FileReader("/data/misc/bluedroid/bt_config.conf"))) {
+ String section = "";
+ for (String line; (line = reader.readLine()) != null;) {
+ line = line.trim();
+ if (line.isEmpty() || line.startsWith("#")) {
+ continue;
+ }
+ if (line.startsWith("[")) {
+ if (line.charAt(line.length() - 1) != ']') {
+ return null;
+ }
+ section = line.substring(1, line.length() - 1);
+ adapterConfig.put(section, new HashMap<>());
+ } else {
+ String[] keyValue = line.split("=");
+ adapterConfig.get(section).put(keyValue[0].trim(),
+ keyValue.length == 1 ? "" : keyValue[1].trim());
+ }
+ }
+ } catch (IOException e) {
+ return null;
+ }
+ return adapterConfig;
+ }
+
+ /**
* Helper class used to run synchronously a runnable action on a looper.
*/
private static final class SyncRunnable implements Runnable {
diff --git a/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java b/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java
new file mode 100644
index 0000000..a97a5b0
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java
@@ -0,0 +1,901 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.a2dp;
+
+import static org.mockito.Mockito.*;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecStatus;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.content.res.Resources;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.R;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class A2dpCodecConfigTest {
+ private Context mTargetContext;
+ private BluetoothDevice mTestDevice;
+ private A2dpCodecConfig mA2dpCodecConfig;
+
+ @Mock private Context mMockContext;
+ @Mock private Resources mMockResources;
+ @Mock private A2dpNativeInterface mA2dpNativeInterface;
+
+ private static final int[] sOptionalCodecTypes = new int[] {
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC
+ };
+
+ // Not use the default value to make sure it reads from config
+ private static final int SBC_PRIORITY_DEFAULT = 1001;
+ private static final int AAC_PRIORITY_DEFAULT = 3001;
+ private static final int APTX_PRIORITY_DEFAULT = 5001;
+ private static final int APTX_HD_PRIORITY_DEFAULT = 7001;
+ private static final int LDAC_PRIORITY_DEFAULT = 9001;
+ private static final int PRIORITY_HIGH = 1000000;
+
+ private static final BluetoothCodecConfig[] sCodecCapabilities = new BluetoothCodecConfig[] {
+ new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ SBC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_MONO
+ | BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0), // Codec-specific fields
+ new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ AAC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0), // Codec-specific fields
+ new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX,
+ APTX_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100
+ | BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0), // Codec-specific fields
+ new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD,
+ APTX_HD_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100
+ | BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0), // Codec-specific fields
+ new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ LDAC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100
+ | BluetoothCodecConfig.SAMPLE_RATE_48000
+ | BluetoothCodecConfig.SAMPLE_RATE_88200
+ | BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16
+ | BluetoothCodecConfig.BITS_PER_SAMPLE_24
+ | BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0) // Codec-specific fields
+ };
+
+ private static final BluetoothCodecConfig[] sDefaultCodecConfigs = new BluetoothCodecConfig[] {
+ new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ SBC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0), // Codec-specific fields
+ new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ AAC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0), // Codec-specific fields
+ new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX,
+ APTX_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0), // Codec-specific fields
+ new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD,
+ APTX_HD_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0), // Codec-specific fields
+ new BluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ LDAC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0) // Codec-specific fields
+ };
+
+ @Before
+ public void setUp() throws Exception {
+ // Set up mocks and test assets
+ MockitoAnnotations.initMocks(this);
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+
+ when(mMockContext.getResources()).thenReturn(mMockResources);
+ when(mMockResources.getInteger(R.integer.a2dp_source_codec_priority_sbc))
+ .thenReturn(SBC_PRIORITY_DEFAULT);
+ when(mMockResources.getInteger(R.integer.a2dp_source_codec_priority_aac))
+ .thenReturn(AAC_PRIORITY_DEFAULT);
+ when(mMockResources.getInteger(R.integer.a2dp_source_codec_priority_aptx))
+ .thenReturn(APTX_PRIORITY_DEFAULT);
+ when(mMockResources.getInteger(R.integer.a2dp_source_codec_priority_aptx_hd))
+ .thenReturn(APTX_HD_PRIORITY_DEFAULT);
+ when(mMockResources.getInteger(R.integer.a2dp_source_codec_priority_ldac))
+ .thenReturn(LDAC_PRIORITY_DEFAULT);
+
+ mA2dpCodecConfig = new A2dpCodecConfig(mMockContext, mA2dpNativeInterface);
+ mTestDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:01:02:03:04:05");
+
+ doReturn(true).when(mA2dpNativeInterface).setCodecConfigPreference(
+ any(BluetoothDevice.class),
+ any(BluetoothCodecConfig[].class));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ @Test
+ public void testAssignCodecConfigPriorities() {
+ BluetoothCodecConfig[] codecConfigs = mA2dpCodecConfig.codecConfigPriorities();
+ for (BluetoothCodecConfig config : codecConfigs) {
+ switch(config.getCodecType()) {
+ case BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC:
+ Assert.assertEquals(config.getCodecPriority(), SBC_PRIORITY_DEFAULT);
+ break;
+ case BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC:
+ Assert.assertEquals(config.getCodecPriority(), AAC_PRIORITY_DEFAULT);
+ break;
+ case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX:
+ Assert.assertEquals(config.getCodecPriority(), APTX_PRIORITY_DEFAULT);
+ break;
+ case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD:
+ Assert.assertEquals(config.getCodecPriority(), APTX_HD_PRIORITY_DEFAULT);
+ break;
+ case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC:
+ Assert.assertEquals(config.getCodecPriority(), LDAC_PRIORITY_DEFAULT);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Test that we can fallback to default codec by lower the codec priority we changed before.
+ */
+ @Test
+ public void testSetCodecPreference_priorityHighToDefault() {
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, SBC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, AAC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, PRIORITY_HIGH,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX, APTX_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX, PRIORITY_HIGH,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD, APTX_HD_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD, PRIORITY_HIGH,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, LDAC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, PRIORITY_HIGH,
+ false);
+ }
+
+ /**
+ * Test that we can change the default codec to another by raising the codec priority.
+ * LDAC is the default highest codec, so no need to test others.
+ */
+ @Test
+ public void testSetCodecPreference_priorityDefaultToRaiseHigh() {
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, LDAC_PRIORITY_DEFAULT,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, LDAC_PRIORITY_DEFAULT,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, LDAC_PRIORITY_DEFAULT,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, LDAC_PRIORITY_DEFAULT,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, LDAC_PRIORITY_DEFAULT,
+ false);
+ }
+
+ @Test
+ public void testSetCodecPreference_prioritySbcHighToOthersHigh() {
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+ false);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+ true);
+ }
+
+ @Test
+ public void testSetCodecPreference_priorityAacHighToOthersHigh() {
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, PRIORITY_HIGH,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, PRIORITY_HIGH,
+ false);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, PRIORITY_HIGH,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, PRIORITY_HIGH,
+ true);
+ testCodecPriorityChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC, PRIORITY_HIGH,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, PRIORITY_HIGH,
+ true);
+ }
+
+ @Test
+ public void testSetCodecPreference_parametersChangedInSameCodec() {
+ // The SBC default / preferred config is Stereo
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ false);
+ // SBC Mono is mandatory
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ true);
+
+ // The LDAC default / preferred config within mDefaultCodecConfigs
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ false);
+
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ true);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ true);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ true);
+
+ // None for system default (Developer options)
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_NONE,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ false);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_NONE,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ false);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_NONE,
+ false);
+
+ int unsupportedParameter = 0xc0;
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ unsupportedParameter,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ false);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ unsupportedParameter,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ false);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ unsupportedParameter,
+ false);
+
+ int multipleSupportedParameters = 0x03;
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ multipleSupportedParameters,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ false);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ multipleSupportedParameters,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ false);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ multipleSupportedParameters,
+ false);
+ }
+
+ @Test
+ public void testSetCodecPreference_parametersChangedToAnotherCodec() {
+ // different sample rate (44.1 kHz -> 96 kHz)
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ true);
+ // different bits per channel (16 bits -> 32 bits)
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ true);
+ // change all PCM parameters
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ true);
+
+ // None for system default (Developer options)
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_NONE,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ true);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_NONE,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ true);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_NONE,
+ true);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SAMPLE_RATE_NONE,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_NONE,
+ BluetoothCodecConfig.CHANNEL_MODE_NONE,
+ true);
+
+ int unsupportedParameter = 0xc0;
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ unsupportedParameter,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ false);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ unsupportedParameter,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ false);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ unsupportedParameter,
+ false);
+
+ int multipleSupportedParameters = 0x03;
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ multipleSupportedParameters,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ false);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ multipleSupportedParameters,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ false);
+ testCodecParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ multipleSupportedParameters,
+ false);
+ }
+
+ @Test
+ public void testSetCodecPreference_ldacCodecSpecificFieldChanged() {
+ int ldacAudioQualityHigh = 1000;
+ int ldacAudioQualityABR = 1003;
+ int sbcCodecSpecificParameter = 0;
+ testCodecSpecificParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ ldacAudioQualityABR,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ ldacAudioQualityABR,
+ false);
+ testCodecSpecificParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ ldacAudioQualityHigh,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ ldacAudioQualityABR,
+ true);
+ testCodecSpecificParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ ldacAudioQualityABR,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ sbcCodecSpecificParameter,
+ true);
+
+ // Only LDAC will check the codec specific1 field, but not SBC
+ testCodecSpecificParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ sbcCodecSpecificParameter,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ ldacAudioQualityABR,
+ true);
+ testCodecSpecificParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ ldacAudioQualityABR,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ ldacAudioQualityABR,
+ true);
+ testCodecSpecificParametersChangeHelper(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ ldacAudioQualityHigh,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ sbcCodecSpecificParameter,
+ false);
+ }
+
+ @Test
+ public void testDisableOptionalCodecs() {
+ BluetoothCodecConfig[] codecConfigsArray =
+ new BluetoothCodecConfig[BluetoothCodecConfig.SOURCE_CODEC_TYPE_MAX];
+ codecConfigsArray[0] = new BluetoothCodecConfig(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST,
+ BluetoothCodecConfig.SAMPLE_RATE_NONE,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_NONE,
+ BluetoothCodecConfig.CHANNEL_MODE_NONE,
+ 0, 0, 0, 0); // Codec-specific fields
+
+ // shouldn't invoke to native when current codec is SBC
+ mA2dpCodecConfig.disableOptionalCodecs(
+ mTestDevice,
+ getDefaultCodecConfigByType(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT));
+ verify(mA2dpNativeInterface, times(0)).setCodecConfigPreference(mTestDevice,
+ codecConfigsArray);
+
+ // should invoke to native when current codec is an optional codec
+ int invokedCounter = 0;
+ for (int codecType : sOptionalCodecTypes) {
+ mA2dpCodecConfig.disableOptionalCodecs(
+ mTestDevice,
+ getDefaultCodecConfigByType(codecType,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT));
+ verify(mA2dpNativeInterface, times(++invokedCounter))
+ .setCodecConfigPreference(mTestDevice, codecConfigsArray);
+ }
+ }
+
+ @Test
+ public void testEnableOptionalCodecs() {
+ BluetoothCodecConfig[] codecConfigsArray =
+ new BluetoothCodecConfig[BluetoothCodecConfig.SOURCE_CODEC_TYPE_MAX];
+ codecConfigsArray[0] = new BluetoothCodecConfig(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ SBC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_NONE,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_NONE,
+ BluetoothCodecConfig.CHANNEL_MODE_NONE,
+ 0, 0, 0, 0); // Codec-specific fields
+
+ // should invoke to native when current codec is SBC
+ mA2dpCodecConfig.enableOptionalCodecs(
+ mTestDevice,
+ getDefaultCodecConfigByType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT));
+ verify(mA2dpNativeInterface, times(1))
+ .setCodecConfigPreference(mTestDevice, codecConfigsArray);
+
+ // shouldn't invoke to native when current codec is already an optional
+ for (int codecType : sOptionalCodecTypes) {
+ mA2dpCodecConfig.enableOptionalCodecs(
+ mTestDevice,
+ getDefaultCodecConfigByType(codecType,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT));
+ verify(mA2dpNativeInterface, times(1))
+ .setCodecConfigPreference(mTestDevice, codecConfigsArray);
+ }
+ }
+
+ private BluetoothCodecConfig getDefaultCodecConfigByType(int codecType, int codecPriority) {
+ for (BluetoothCodecConfig codecConfig : sDefaultCodecConfigs) {
+ if (codecConfig.getCodecType() != codecType) {
+ continue;
+ }
+ return new BluetoothCodecConfig(
+ codecConfig.getCodecType(),
+ (codecPriority != BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT
+ ? codecPriority : codecConfig.getCodecPriority()),
+ codecConfig.getSampleRate(), codecConfig.getBitsPerSample(),
+ codecConfig.getChannelMode(), codecConfig.getCodecSpecific1(),
+ codecConfig.getCodecSpecific2(), codecConfig.getCodecSpecific3(),
+ codecConfig.getCodecSpecific4());
+ }
+ Assert.fail("getDefaultCodecConfigByType: No such codecType=" + codecType
+ + " in sDefaultCodecConfigs");
+ return null;
+ }
+
+ private BluetoothCodecConfig getCodecCapabilitiesByType(int codecType) {
+ for (BluetoothCodecConfig codecCapabilities : sCodecCapabilities) {
+ if (codecCapabilities.getCodecType() != codecType) {
+ continue;
+ }
+ return new BluetoothCodecConfig(
+ codecCapabilities.getCodecType(), codecCapabilities.getCodecPriority(),
+ codecCapabilities.getSampleRate(), codecCapabilities.getBitsPerSample(),
+ codecCapabilities.getChannelMode(), codecCapabilities.getCodecSpecific1(),
+ codecCapabilities.getCodecSpecific2(), codecCapabilities.getCodecSpecific3(),
+ codecCapabilities.getCodecSpecific4());
+ }
+ Assert.fail("getCodecCapabilitiesByType: No such codecType=" + codecType
+ + " in sCodecCapabilities");
+ return null;
+ }
+
+ private void testCodecParametersChangeHelper(int newCodecType, int oldCodecType,
+ int sampleRate, int bitsPerSample, int channelMode, boolean invokeNative) {
+ BluetoothCodecConfig oldCodecConfig =
+ getDefaultCodecConfigByType(oldCodecType, PRIORITY_HIGH);
+ BluetoothCodecConfig[] newCodecConfigsArray = new BluetoothCodecConfig[] {
+ new BluetoothCodecConfig(newCodecType,
+ PRIORITY_HIGH,
+ sampleRate, bitsPerSample, channelMode,
+ 0, 0, 0, 0) // Codec-specific fields
+ };
+
+ // Test cases: 1. no mandatory; 2. mandatory + old + new; 3. all codecs
+ BluetoothCodecConfig[] minimumCodecsArray;
+ if (!oldCodecConfig.isMandatoryCodec() && !newCodecConfigsArray[0].isMandatoryCodec()) {
+ BluetoothCodecConfig[] optionalCodecsArray;
+ if (oldCodecType != newCodecType) {
+ optionalCodecsArray = new BluetoothCodecConfig[] {
+ getCodecCapabilitiesByType(oldCodecType),
+ getCodecCapabilitiesByType(newCodecType)
+ };
+ minimumCodecsArray = new BluetoothCodecConfig[] {
+ getCodecCapabilitiesByType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC),
+ getCodecCapabilitiesByType(oldCodecType),
+ getCodecCapabilitiesByType(newCodecType)
+ };
+ } else {
+ optionalCodecsArray = new BluetoothCodecConfig[]
+ {getCodecCapabilitiesByType(oldCodecType)};
+ minimumCodecsArray = new BluetoothCodecConfig[] {
+ getCodecCapabilitiesByType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC),
+ getCodecCapabilitiesByType(oldCodecType)
+ };
+ }
+ BluetoothCodecStatus codecStatus = new BluetoothCodecStatus(oldCodecConfig,
+ sCodecCapabilities,
+ optionalCodecsArray);
+ mA2dpCodecConfig.setCodecConfigPreference(mTestDevice,
+ codecStatus,
+ newCodecConfigsArray[0]);
+ // no mandatory codec in selectable, and should not apply
+ verify(mA2dpNativeInterface, times(0)).setCodecConfigPreference(mTestDevice,
+ newCodecConfigsArray);
+
+
+ } else {
+ if (oldCodecType != newCodecType) {
+ minimumCodecsArray = new BluetoothCodecConfig[] {
+ getCodecCapabilitiesByType(oldCodecType),
+ getCodecCapabilitiesByType(newCodecType)
+ };
+ } else {
+ minimumCodecsArray = new BluetoothCodecConfig[] {
+ getCodecCapabilitiesByType(oldCodecType),
+ };
+ }
+ }
+
+ // 2. mandatory + old + new codecs only
+ BluetoothCodecStatus codecStatus =
+ new BluetoothCodecStatus(oldCodecConfig, sCodecCapabilities, minimumCodecsArray);
+ mA2dpCodecConfig.setCodecConfigPreference(mTestDevice,
+ codecStatus,
+ newCodecConfigsArray[0]);
+ verify(mA2dpNativeInterface, times(invokeNative ? 1 : 0))
+ .setCodecConfigPreference(mTestDevice, newCodecConfigsArray);
+
+ // 3. all codecs were selectable
+ codecStatus = new BluetoothCodecStatus(oldCodecConfig,
+ sCodecCapabilities,
+ sCodecCapabilities);
+ mA2dpCodecConfig.setCodecConfigPreference(mTestDevice,
+ codecStatus,
+ newCodecConfigsArray[0]);
+ verify(mA2dpNativeInterface, times(invokeNative ? 2 : 0))
+ .setCodecConfigPreference(mTestDevice, newCodecConfigsArray);
+ }
+
+ private void testCodecSpecificParametersChangeHelper(int newCodecType, int newCodecSpecific,
+ int oldCodecType, int oldCodecSpecific, boolean invokeNative) {
+ BluetoothCodecConfig codecDefaultTemp =
+ getDefaultCodecConfigByType(oldCodecType, PRIORITY_HIGH);
+ BluetoothCodecConfig oldCodecConfig =
+ new BluetoothCodecConfig(codecDefaultTemp.getCodecType(),
+ codecDefaultTemp.getCodecPriority(),
+ codecDefaultTemp.getSampleRate(),
+ codecDefaultTemp.getBitsPerSample(),
+ codecDefaultTemp.getChannelMode(),
+ oldCodecSpecific, 0, 0, 0); // Codec-specific fields
+ codecDefaultTemp = getDefaultCodecConfigByType(newCodecType, PRIORITY_HIGH);
+ BluetoothCodecConfig[] newCodecConfigsArray = new BluetoothCodecConfig[] {
+ new BluetoothCodecConfig(codecDefaultTemp.getCodecType(),
+ codecDefaultTemp.getCodecPriority(),
+ codecDefaultTemp.getSampleRate(),
+ codecDefaultTemp.getBitsPerSample(),
+ codecDefaultTemp.getChannelMode(),
+ newCodecSpecific, 0, 0, 0) // Codec-specific fields
+ };
+ BluetoothCodecStatus codecStatus = new BluetoothCodecStatus(oldCodecConfig,
+ sCodecCapabilities,
+ sCodecCapabilities);
+ mA2dpCodecConfig.setCodecConfigPreference(mTestDevice,
+ codecStatus,
+ newCodecConfigsArray[0]);
+ verify(mA2dpNativeInterface, times(invokeNative ? 1 : 0))
+ .setCodecConfigPreference(mTestDevice, newCodecConfigsArray);
+ }
+
+ private void testCodecPriorityChangeHelper(int newCodecType, int newCodecPriority,
+ int oldCodecType, int oldCodecPriority, boolean shouldApplyWhenAllSelectable) {
+
+ BluetoothCodecConfig[] newCodecConfigsArray =
+ new BluetoothCodecConfig[] {
+ getDefaultCodecConfigByType(newCodecType, newCodecPriority)
+ };
+ BluetoothCodecConfig oldCodecConfig = getDefaultCodecConfigByType(oldCodecType,
+ oldCodecPriority);
+
+ // Test cases: 1. no mandatory; 2. no new codec; 3. mandatory + old + new; 4. all codecs
+ BluetoothCodecConfig[] minimumCodecsArray;
+ boolean isMinimumCodecsArraySelectable;
+ if (!oldCodecConfig.isMandatoryCodec()) {
+ if (oldCodecType == newCodecType || newCodecConfigsArray[0].isMandatoryCodec()) {
+ // selectable: {-mandatory, +oldCodec = newCodec}, or
+ // selectable: {-mandatory = newCodec, +oldCodec}. Not applied
+ BluetoothCodecConfig[] poorCodecsArray = new BluetoothCodecConfig[]
+ {getCodecCapabilitiesByType(oldCodecType)};
+ BluetoothCodecStatus codecStatus = new BluetoothCodecStatus(oldCodecConfig,
+ sCodecCapabilities,
+ poorCodecsArray);
+ mA2dpCodecConfig.setCodecConfigPreference(mTestDevice,
+ codecStatus,
+ newCodecConfigsArray[0]);
+ verify(mA2dpNativeInterface, times(0))
+ .setCodecConfigPreference(mTestDevice, newCodecConfigsArray);
+
+ // selectable: {+mandatory, +oldCodec = newCodec}, or
+ // selectable: {+mandatory = newCodec, +oldCodec}.
+ minimumCodecsArray = new BluetoothCodecConfig[] {
+ getCodecCapabilitiesByType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC),
+ getCodecCapabilitiesByType(oldCodecType)
+ };
+ } else {
+ // selectable: {-mandatory, +oldCodec, +newCodec}. Not applied
+ BluetoothCodecConfig[] poorCodecsArray = new BluetoothCodecConfig[] {
+ getCodecCapabilitiesByType(oldCodecType),
+ getCodecCapabilitiesByType(newCodecType)
+ };
+ BluetoothCodecStatus codecStatus = new BluetoothCodecStatus(oldCodecConfig,
+ sCodecCapabilities,
+ poorCodecsArray);
+ mA2dpCodecConfig.setCodecConfigPreference(
+ mTestDevice, codecStatus, newCodecConfigsArray[0]);
+ verify(mA2dpNativeInterface, times(0))
+ .setCodecConfigPreference(mTestDevice, newCodecConfigsArray);
+
+ // selectable: {+mandatory, +oldCodec, -newCodec}. Not applied
+ poorCodecsArray = new BluetoothCodecConfig[] {
+ getCodecCapabilitiesByType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC),
+ getCodecCapabilitiesByType(oldCodecType)
+ };
+ codecStatus = new BluetoothCodecStatus(oldCodecConfig,
+ sCodecCapabilities,
+ poorCodecsArray);
+ mA2dpCodecConfig.setCodecConfigPreference(mTestDevice,
+ codecStatus,
+ newCodecConfigsArray[0]);
+ verify(mA2dpNativeInterface, times(0))
+ .setCodecConfigPreference(mTestDevice, newCodecConfigsArray);
+
+ // selectable: {+mandatory, +oldCodec, +newCodec}.
+ minimumCodecsArray = new BluetoothCodecConfig[] {
+ getCodecCapabilitiesByType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC),
+ getCodecCapabilitiesByType(oldCodecType),
+ getCodecCapabilitiesByType(newCodecType)
+ };
+ }
+ // oldCodec priority should be reset to default, so compare with the default
+ if (newCodecConfigsArray[0].getCodecPriority()
+ > getDefaultCodecConfigByType(
+ oldCodecType,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT).getCodecPriority()
+ && oldCodecType != newCodecType) {
+ isMinimumCodecsArraySelectable = true;
+ } else {
+ // the old codec was still the highest priority after reset to default
+ isMinimumCodecsArraySelectable = false;
+ }
+ } else if (oldCodecType != newCodecType) {
+ // selectable: {+mandatory = oldCodec, -newCodec}. Not applied
+ BluetoothCodecConfig[] poorCodecsArray = new BluetoothCodecConfig[]
+ {getCodecCapabilitiesByType(oldCodecType)};
+ BluetoothCodecStatus codecStatus =
+ new BluetoothCodecStatus(oldCodecConfig, sCodecCapabilities, poorCodecsArray);
+ mA2dpCodecConfig.setCodecConfigPreference(mTestDevice,
+ codecStatus,
+ newCodecConfigsArray[0]);
+ verify(mA2dpNativeInterface, times(0))
+ .setCodecConfigPreference(mTestDevice, newCodecConfigsArray);
+
+ // selectable: {+mandatory = oldCodec, +newCodec}.
+ minimumCodecsArray = new BluetoothCodecConfig[] {
+ getCodecCapabilitiesByType(oldCodecType),
+ getCodecCapabilitiesByType(newCodecType)
+ };
+ isMinimumCodecsArraySelectable = true;
+ } else {
+ // selectable: {mandatory = oldCodec = newCodec}.
+ minimumCodecsArray = new BluetoothCodecConfig[]
+ {getCodecCapabilitiesByType(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC)};
+ isMinimumCodecsArraySelectable = false;
+ }
+
+ // 3. mandatory + old + new codecs only
+ int invokedCounter = (isMinimumCodecsArraySelectable ? 1 : 0);
+ BluetoothCodecStatus codecStatus =
+ new BluetoothCodecStatus(oldCodecConfig, sCodecCapabilities, minimumCodecsArray);
+ mA2dpCodecConfig.setCodecConfigPreference(mTestDevice,
+ codecStatus,
+ newCodecConfigsArray[0]);
+ verify(mA2dpNativeInterface, times(invokedCounter))
+ .setCodecConfigPreference(mTestDevice, newCodecConfigsArray);
+
+ // 4. all codecs were selectable
+ invokedCounter += (shouldApplyWhenAllSelectable ? 1 : 0);
+ codecStatus = new BluetoothCodecStatus(oldCodecConfig,
+ sCodecCapabilities,
+ sCodecCapabilities);
+ mA2dpCodecConfig.setCodecConfigPreference(mTestDevice,
+ codecStatus,
+ newCodecConfigsArray[0]);
+ verify(mA2dpNativeInterface, times(invokedCounter))
+ .setCodecConfigPreference(mTestDevice, newCodecConfigsArray);
+ }
+}
diff --git a/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceTest.java b/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceTest.java
index bef3bdb..78f4ad0 100644
--- a/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceTest.java
@@ -31,14 +31,18 @@
import android.content.IntentFilter;
import android.os.Looper;
import android.os.ParcelUuid;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.avrcp.AvrcpTargetService;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.ServiceFactory;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
import org.junit.After;
import org.junit.Assert;
@@ -73,6 +77,9 @@
@Mock private AdapterService mAdapterService;
@Mock private A2dpNativeInterface mA2dpNativeInterface;
+ @Mock private DatabaseManager mDatabaseManager;
+ @Mock private AvrcpTargetService mAvrcpTargetService;
+ @Mock private ServiceFactory mFactory;
@Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
@@ -91,11 +98,13 @@
TestUtils.setAdapterService(mAdapterService);
doReturn(MAX_CONNECTED_AUDIO_DEVICES).when(mAdapterService).getMaxConnectedAudioDevices();
doReturn(false).when(mAdapterService).isQuietModeEnabled();
+ doReturn(mAvrcpTargetService).when(mFactory).getAvrcpTargetService();
mAdapter = BluetoothAdapter.getDefaultAdapter();
startService();
mA2dpService.mA2dpNativeInterface = mA2dpNativeInterface;
+ mA2dpService.mFactory = mFactory;
// Override the timeout value to speed up the test
A2dpStateMachine.sConnectTimeoutMs = TIMEOUT_MS; // 1s
@@ -110,7 +119,6 @@
// Get a device for testing
mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
- mA2dpService.setPriority(mTestDevice, BluetoothProfile.PRIORITY_UNDEFINED);
doReturn(BluetoothDevice.BOND_BONDED).when(mAdapterService)
.getBondState(any(BluetoothDevice.class));
doReturn(new ParcelUuid[]{BluetoothUuid.AudioSink}).when(mAdapterService)
@@ -244,6 +252,8 @@
});
// Verify that setActiveDevice(null) was called during shutdown
verify(mA2dpNativeInterface).setActiveDevice(null);
+ // Verify that storeVolumeForDevice(mTestDevice) was called during shutdown
+ verify(mAvrcpTargetService).storeVolumeForDevice(mTestDevice);
// Try to restart the service. Note: must be done on the main thread.
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
public void run() {
@@ -253,26 +263,34 @@
}
/**
- * Test get/set priority for BluetoothDevice
+ * Test get priority for BluetoothDevice
*/
@Test
- public void testGetSetPriority() {
+ public void testGetPriority() {
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
Assert.assertEquals("Initial device priority",
BluetoothProfile.PRIORITY_UNDEFINED,
mA2dpService.getPriority(mTestDevice));
- Assert.assertTrue(mA2dpService.setPriority(mTestDevice, BluetoothProfile.PRIORITY_OFF));
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
Assert.assertEquals("Setting device priority to PRIORITY_OFF",
BluetoothProfile.PRIORITY_OFF,
mA2dpService.getPriority(mTestDevice));
- Assert.assertTrue(mA2dpService.setPriority(mTestDevice, BluetoothProfile.PRIORITY_ON));
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
Assert.assertEquals("Setting device priority to PRIORITY_ON",
BluetoothProfile.PRIORITY_ON,
mA2dpService.getPriority(mTestDevice));
- Assert.assertTrue(mA2dpService.setPriority(mTestDevice,
- BluetoothProfile.PRIORITY_AUTO_CONNECT));
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_AUTO_CONNECT);
Assert.assertEquals("Setting device priority to PRIORITY_AUTO_CONNECT",
BluetoothProfile.PRIORITY_AUTO_CONNECT,
mA2dpService.getPriority(mTestDevice));
@@ -325,9 +343,6 @@
badBondState, BluetoothProfile.PRIORITY_AUTO_CONNECT, false);
testOkToConnectCase(mTestDevice,
badBondState, badPriorityValue, false);
- // Restore prirority to undefined for this test device
- Assert.assertTrue(mA2dpService.setPriority(
- mTestDevice, BluetoothProfile.PRIORITY_UNDEFINED));
}
@@ -337,7 +352,9 @@
@Test
public void testOutgoingConnectMissingAudioSinkUuid() {
// Update the device priority so okToConnect() returns true
- mA2dpService.setPriority(mTestDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
doReturn(true).when(mA2dpNativeInterface).connectA2dp(any(BluetoothDevice.class));
doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(any(BluetoothDevice.class));
@@ -358,7 +375,9 @@
doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(any(BluetoothDevice.class));
// Set the device priority to PRIORITY_OFF so connect() should fail
- mA2dpService.setPriority(mTestDevice, BluetoothProfile.PRIORITY_OFF);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
// Send a connect request
Assert.assertFalse("Connect expected to fail", mA2dpService.connect(mTestDevice));
@@ -370,7 +389,9 @@
@Test
public void testOutgoingConnectTimeout() {
// Update the device priority so okToConnect() returns true
- mA2dpService.setPriority(mTestDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
doReturn(true).when(mA2dpNativeInterface).connectA2dp(any(BluetoothDevice.class));
doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(any(BluetoothDevice.class));
@@ -399,7 +420,9 @@
A2dpStackEvent connCompletedEvent;
// Update the device priority so okToConnect() returns true
- mA2dpService.setPriority(mTestDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
doReturn(true).when(mA2dpNativeInterface).connectA2dp(any(BluetoothDevice.class));
doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(any(BluetoothDevice.class));
@@ -468,7 +491,9 @@
for (int i = 0; i < MAX_CONNECTED_AUDIO_DEVICES; i++) {
BluetoothDevice testDevice = TestUtils.getTestDevice(mAdapter, i);
testDevices[i] = testDevice;
- mA2dpService.setPriority(testDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(testDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
// Send a connect request
Assert.assertTrue("Connect failed", mA2dpService.connect(testDevice));
// Verify the connection state broadcast, and that we are in Connecting state
@@ -494,7 +519,9 @@
// Prepare and connect the extra test device. The connect request should fail
extraTestDevice = TestUtils.getTestDevice(mAdapter, MAX_CONNECTED_AUDIO_DEVICES);
- mA2dpService.setPriority(extraTestDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(extraTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
// Send a connect request
Assert.assertFalse("Connect expected to fail", mA2dpService.connect(extraTestDevice));
}
@@ -508,7 +535,9 @@
A2dpStackEvent stackEvent;
// Update the device priority so okToConnect() returns true
- mA2dpService.setPriority(mTestDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
doReturn(true).when(mA2dpNativeInterface).connectA2dp(any(BluetoothDevice.class));
doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(any(BluetoothDevice.class));
@@ -586,7 +615,9 @@
codecsSelectableCapabilities);
// Update the device priority so okToConnect() returns true
- mA2dpService.setPriority(mTestDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
doReturn(true).when(mA2dpNativeInterface).connectA2dp(any(BluetoothDevice.class));
doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(any(BluetoothDevice.class));
@@ -649,7 +680,9 @@
A2dpStackEvent stackEvent;
// Update the device priority so okToConnect() returns true
- mA2dpService.setPriority(mTestDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
doReturn(true).when(mA2dpNativeInterface).connectA2dp(any(BluetoothDevice.class));
doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(any(BluetoothDevice.class));
@@ -706,7 +739,9 @@
A2dpStackEvent stackEvent;
// Update the device priority so okToConnect() returns true
- mA2dpService.setPriority(mTestDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
doReturn(true).when(mA2dpNativeInterface).connectA2dp(any(BluetoothDevice.class));
doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(any(BluetoothDevice.class));
@@ -744,15 +779,173 @@
Assert.assertFalse(mA2dpService.getDevices().contains(mTestDevice));
}
+ /**
+ * Test that whether active device been removed after enable silence mode
+ */
+ @Test
+ public void testSetSilenceMode() {
+ BluetoothDevice otherDevice = mAdapter.getRemoteDevice("05:04:03:02:01:00");
+ connectDevice(mTestDevice);
+ connectDevice(otherDevice);
+ doReturn(true).when(mA2dpNativeInterface).setActiveDevice(any(BluetoothDevice.class));
+ doReturn(true).when(mA2dpNativeInterface).setSilenceDevice(any(BluetoothDevice.class),
+ anyBoolean());
+
+ // Test whether active device been removed after enable silence mode.
+ Assert.assertTrue(mA2dpService.setActiveDevice(mTestDevice));
+ Assert.assertEquals(mTestDevice, mA2dpService.getActiveDevice());
+ Assert.assertTrue(mA2dpService.setSilenceMode(mTestDevice, true));
+ verify(mA2dpNativeInterface).setSilenceDevice(mTestDevice, true);
+ verify(mAvrcpTargetService).storeVolumeForDevice(mTestDevice);
+ Assert.assertNull(mA2dpService.getActiveDevice());
+
+ // Test whether active device been resumeed after disable silence mode.
+ Assert.assertTrue(mA2dpService.setSilenceMode(mTestDevice, false));
+ verify(mA2dpNativeInterface).setSilenceDevice(mTestDevice, false);
+ verify(mAvrcpTargetService).storeVolumeForDevice(mTestDevice);
+ Assert.assertEquals(mTestDevice, mA2dpService.getActiveDevice());
+
+ // Test that active device should not be changed when silence a non-active device
+ Assert.assertTrue(mA2dpService.setActiveDevice(mTestDevice));
+ Assert.assertEquals(mTestDevice, mA2dpService.getActiveDevice());
+ Assert.assertTrue(mA2dpService.setSilenceMode(otherDevice, true));
+ verify(mA2dpNativeInterface).setSilenceDevice(otherDevice, true);
+ verify(mAvrcpTargetService).storeVolumeForDevice(mTestDevice);
+ Assert.assertEquals(mTestDevice, mA2dpService.getActiveDevice());
+
+ // Test that active device should not be changed when another device exits silence mode
+ Assert.assertTrue(mA2dpService.setSilenceMode(otherDevice, false));
+ verify(mA2dpNativeInterface).setSilenceDevice(otherDevice, false);
+ verify(mAvrcpTargetService).storeVolumeForDevice(mTestDevice);
+ Assert.assertEquals(mTestDevice, mA2dpService.getActiveDevice());
+ }
+
+ /**
+ * Test that whether updateOptionalCodecsSupport() method is working as intended
+ * when a Bluetooth device is connected with A2DP.
+ */
+ @Test
+ public void testUpdateOptionalCodecsSupport() {
+ int verifySupportTime = 0;
+ int verifyNotSupportTime = 0;
+ int verifyEnabledTime = 0;
+ // Test for device supports optional codec
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN, true,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN,
+ ++verifySupportTime, verifyNotSupportTime, ++verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN, true,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED,
+ ++verifySupportTime, verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN, true,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED,
+ ++verifySupportTime, verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED, true,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN,
+ verifySupportTime, verifyNotSupportTime, ++verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED, true,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED,
+ verifySupportTime, verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED, true,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED,
+ verifySupportTime, verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED, true,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN,
+ ++verifySupportTime, verifyNotSupportTime, ++verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED, true,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED,
+ ++verifySupportTime, verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED, true,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED,
+ ++verifySupportTime, verifyNotSupportTime, verifyEnabledTime);
+
+ // Test for device not supports optional codec
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN, false,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN,
+ verifySupportTime, ++verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN, false,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED,
+ verifySupportTime, ++verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN, false,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED,
+ verifySupportTime, ++verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED, false,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN,
+ verifySupportTime, ++verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED, false,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED,
+ verifySupportTime, ++verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED, false,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED,
+ verifySupportTime, ++verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED, false,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN,
+ verifySupportTime, verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED, false,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED,
+ verifySupportTime, verifyNotSupportTime, verifyEnabledTime);
+ testUpdateOptionalCodecsSupportCase(
+ BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED, false,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED,
+ verifySupportTime, verifyNotSupportTime, verifyEnabledTime);
+ }
+
+ /**
+ * Test that volume level of previous active device will be stored after set active device.
+ */
+ @Test
+ public void testStoreVolumeAfterSetActiveDevice() {
+ BluetoothDevice otherDevice = mAdapter.getRemoteDevice("05:04:03:02:01:00");
+ connectDevice(otherDevice);
+ connectDevice(mTestDevice);
+ doReturn(true).when(mA2dpNativeInterface).setActiveDevice(any(BluetoothDevice.class));
+ doReturn(true).when(mA2dpNativeInterface).setActiveDevice(null);
+ Assert.assertTrue(mA2dpService.setActiveDevice(mTestDevice));
+
+ // Test volume stored for previous active device an adjust for current active device
+ Assert.assertTrue(mA2dpService.setActiveDevice(otherDevice));
+ verify(mAvrcpTargetService).storeVolumeForDevice(mTestDevice);
+ verify(mAvrcpTargetService).getRememberedVolumeForDevice(otherDevice);
+
+ // Test volume store for previous active device when set active device to null
+ Assert.assertTrue(mA2dpService.setActiveDevice(null));
+ verify(mAvrcpTargetService).storeVolumeForDevice(otherDevice);
+ }
+
private void connectDevice(BluetoothDevice device) {
+ connectDeviceWithCodecStatus(device, null);
+ }
+
+ private void connectDeviceWithCodecStatus(BluetoothDevice device,
+ BluetoothCodecStatus codecStatus) {
A2dpStackEvent connCompletedEvent;
List<BluetoothDevice> prevConnectedDevices = mA2dpService.getConnectedDevices();
// Update the device priority so okToConnect() returns true
- mA2dpService.setPriority(device, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(device, BluetoothProfile.A2DP))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
doReturn(true).when(mA2dpNativeInterface).connectA2dp(device);
doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(device);
+ doReturn(true).when(mA2dpNativeInterface).setCodecConfigPreference(
+ any(BluetoothDevice.class), any(BluetoothCodecConfig[].class));
// Send a connect request
Assert.assertTrue("Connect failed", mA2dpService.connect(device));
@@ -763,6 +956,10 @@
Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
mA2dpService.getConnectionState(device));
+ if (codecStatus != null) {
+ generateCodecMessageFromNative(device, codecStatus);
+ }
+
// Send a message to trigger connection completed
connCompletedEvent = new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
connCompletedEvent.device = device;
@@ -862,7 +1059,9 @@
private void testOkToConnectCase(BluetoothDevice device, int bondState, int priority,
boolean expected) {
doReturn(bondState).when(mAdapterService).getBondState(device);
- Assert.assertTrue(mA2dpService.setPriority(device, priority));
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(device, BluetoothProfile.A2DP))
+ .thenReturn(priority);
// Test when the AdapterService is in non-quiet mode: the result should not depend
// on whether the connection request is outgoing or incoming.
@@ -876,4 +1075,85 @@
Assert.assertEquals(expected, mA2dpService.okToConnect(device, true)); // Outgoing
Assert.assertEquals(false, mA2dpService.okToConnect(device, false)); // Incoming
}
+
+ /**
+ * Helper function to test updateOptionalCodecsSupport() method
+ *
+ * @param previousSupport previous optional codec support status
+ * @param support new optional codec support status
+ * @param previousEnabled previous optional codec enable status
+ * @param verifySupportTime verify times of optional codec set to support
+ * @param verifyNotSupportTime verify times of optional codec set to not support
+ * @param verifyEnabledTime verify times of optional codec set to enabled
+ */
+ private void testUpdateOptionalCodecsSupportCase(int previousSupport, boolean support,
+ int previousEnabled, int verifySupportTime, int verifyNotSupportTime,
+ int verifyEnabledTime) {
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ doReturn(true).when(mA2dpNativeInterface).setActiveDevice(any(BluetoothDevice.class));
+
+ BluetoothCodecConfig codecConfigSbc =
+ new BluetoothCodecConfig(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0); // Codec-specific fields
+ BluetoothCodecConfig codecConfigAac =
+ new BluetoothCodecConfig(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0); // Codec-specific fields
+
+ BluetoothCodecConfig[] codecsLocalCapabilities;
+ BluetoothCodecConfig[] codecsSelectableCapabilities;
+ if (support) {
+ codecsLocalCapabilities = new BluetoothCodecConfig[2];
+ codecsSelectableCapabilities = new BluetoothCodecConfig[2];
+ codecsLocalCapabilities[0] = codecConfigSbc;
+ codecsLocalCapabilities[1] = codecConfigAac;
+ codecsSelectableCapabilities[0] = codecConfigSbc;
+ codecsSelectableCapabilities[1] = codecConfigAac;
+ } else {
+ codecsLocalCapabilities = new BluetoothCodecConfig[1];
+ codecsSelectableCapabilities = new BluetoothCodecConfig[1];
+ codecsLocalCapabilities[0] = codecConfigSbc;
+ codecsSelectableCapabilities[0] = codecConfigSbc;
+ }
+ BluetoothCodecConfig[] badCodecsSelectableCapabilities;
+ badCodecsSelectableCapabilities = new BluetoothCodecConfig[1];
+ badCodecsSelectableCapabilities[0] = codecConfigAac;
+
+ BluetoothCodecStatus codecStatus = new BluetoothCodecStatus(codecConfigSbc,
+ codecsLocalCapabilities, codecsSelectableCapabilities);
+ BluetoothCodecStatus badCodecStatus = new BluetoothCodecStatus(codecConfigAac,
+ codecsLocalCapabilities, badCodecsSelectableCapabilities);
+
+ when(mDatabaseManager.getA2dpSupportsOptionalCodecs(mTestDevice))
+ .thenReturn(previousSupport);
+ when(mDatabaseManager.getA2dpOptionalCodecsEnabled(mTestDevice))
+ .thenReturn(previousEnabled);
+
+ // Generate connection request from native with bad codec status
+ connectDeviceWithCodecStatus(mTestDevice, badCodecStatus);
+ generateConnectionMessageFromNative(mTestDevice, BluetoothProfile.STATE_DISCONNECTED,
+ BluetoothProfile.STATE_CONNECTED);
+
+ // Generate connection request from native with good codec status
+ connectDeviceWithCodecStatus(mTestDevice, codecStatus);
+ generateConnectionMessageFromNative(mTestDevice, BluetoothProfile.STATE_DISCONNECTED,
+ BluetoothProfile.STATE_CONNECTED);
+
+ // Check optional codec status is set properly
+ verify(mDatabaseManager, times(verifyNotSupportTime)).setA2dpSupportsOptionalCodecs(
+ mTestDevice, BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED);
+ verify(mDatabaseManager, times(verifySupportTime)).setA2dpSupportsOptionalCodecs(
+ mTestDevice, BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED);
+ verify(mDatabaseManager, times(verifyEnabledTime)).setA2dpOptionalCodecsEnabled(
+ mTestDevice, BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED);
+ }
}
diff --git a/tests/unit/src/com/android/bluetooth/a2dp/A2dpStateMachineTest.java b/tests/unit/src/com/android/bluetooth/a2dp/A2dpStateMachineTest.java
index 0b825bf..cacd71a 100644
--- a/tests/unit/src/com/android/bluetooth/a2dp/A2dpStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/a2dp/A2dpStateMachineTest.java
@@ -20,14 +20,17 @@
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecStatus;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.os.HandlerThread;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
@@ -54,6 +57,9 @@
private BluetoothDevice mTestDevice;
private static final int TIMEOUT_MS = 1000; // 1s
+ private BluetoothCodecConfig mCodecConfigSbc;
+ private BluetoothCodecConfig mCodecConfigAac;
+
@Mock private AdapterService mAdapterService;
@Mock private A2dpService mA2dpService;
@Mock private A2dpNativeInterface mA2dpNativeInterface;
@@ -72,6 +78,22 @@
// Get a device for testing
mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
+ // Set up sample codec config
+ mCodecConfigSbc = new BluetoothCodecConfig(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0); // Codec-specific fields
+ mCodecConfigAac = new BluetoothCodecConfig(
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 0, 0, 0, 0); // Codec-specific fields
+
// Set up thread and looper
mHandlerThread = new HandlerThread("A2dpStateMachineTestHandlerThread");
mHandlerThread.start();
@@ -255,4 +277,87 @@
Assert.assertThat(mA2dpStateMachine.getCurrentState(),
IsInstanceOf.instanceOf(A2dpStateMachine.Disconnected.class));
}
+
+ /**
+ * Test that codec config change been reported to A2dpService properly.
+ */
+ @Test
+ public void testProcessCodecConfigEvent() {
+ testProcessCodecConfigEventCase(false);
+ }
+
+ /**
+ * Test that codec config change been reported to A2dpService properly when
+ * A2DP hardware offloading is enabled.
+ */
+ @Test
+ public void testProcessCodecConfigEvent_OffloadEnabled() {
+ testProcessCodecConfigEventCase(true);
+ }
+
+ /**
+ * Helper methold to test processCodecConfigEvent()
+ */
+ public void testProcessCodecConfigEventCase(boolean offloadEnabled) {
+ if (offloadEnabled) {
+ mA2dpStateMachine.mA2dpOffloadEnabled = true;
+ }
+
+ doNothing().when(mA2dpService).codecConfigUpdated(any(BluetoothDevice.class),
+ any(BluetoothCodecStatus.class), anyBoolean());
+ doNothing().when(mA2dpService).updateOptionalCodecsSupport(any(BluetoothDevice.class));
+ allowConnection(true);
+
+ BluetoothCodecConfig[] codecsSelectableSbc;
+ codecsSelectableSbc = new BluetoothCodecConfig[1];
+ codecsSelectableSbc[0] = mCodecConfigSbc;
+
+ BluetoothCodecConfig[] codecsSelectableSbcAac;
+ codecsSelectableSbcAac = new BluetoothCodecConfig[2];
+ codecsSelectableSbcAac[0] = mCodecConfigSbc;
+ codecsSelectableSbcAac[1] = mCodecConfigAac;
+
+ BluetoothCodecStatus codecStatusSbcAndSbc = new BluetoothCodecStatus(mCodecConfigSbc,
+ codecsSelectableSbcAac, codecsSelectableSbc);
+ BluetoothCodecStatus codecStatusSbcAndSbcAac = new BluetoothCodecStatus(mCodecConfigSbc,
+ codecsSelectableSbcAac, codecsSelectableSbcAac);
+ BluetoothCodecStatus codecStatusAacAndSbcAac = new BluetoothCodecStatus(mCodecConfigAac,
+ codecsSelectableSbcAac, codecsSelectableSbcAac);
+
+ // Set default codec status when device disconnected
+ // Selected codec = SBC, selectable codec = SBC
+ mA2dpStateMachine.processCodecConfigEvent(codecStatusSbcAndSbc);
+ verify(mA2dpService).codecConfigUpdated(mTestDevice, codecStatusSbcAndSbc, false);
+
+ // Inject an event to change state machine to connected state
+ A2dpStackEvent connStCh =
+ new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ connStCh.device = mTestDevice;
+ connStCh.valueInt = A2dpStackEvent.CONNECTION_STATE_CONNECTED;
+ mA2dpStateMachine.sendMessage(A2dpStateMachine.STACK_EVENT, connStCh);
+
+ // Verify that the expected number of broadcasts are executed:
+ // - two calls to broadcastConnectionState(): Disconnected -> Conecting -> Connected
+ // - one call to broadcastAudioState() when entering Connected state
+ ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
+ verify(mA2dpService, timeout(TIMEOUT_MS).times(2)).sendBroadcast(intentArgument2.capture(),
+ anyString());
+
+ // Verify that state machine update optional codec when enter connected state
+ verify(mA2dpService, times(1)).updateOptionalCodecsSupport(mTestDevice);
+
+ // Change codec status when device connected.
+ // Selected codec = SBC, selectable codec = SBC+AAC
+ mA2dpStateMachine.processCodecConfigEvent(codecStatusSbcAndSbcAac);
+ if (!offloadEnabled) {
+ verify(mA2dpService).codecConfigUpdated(mTestDevice, codecStatusSbcAndSbcAac, true);
+ }
+ verify(mA2dpService, times(2)).updateOptionalCodecsSupport(mTestDevice);
+
+ // Update selected codec with selectable codec unchanged.
+ // Selected codec = AAC, selectable codec = SBC+AAC
+ mA2dpStateMachine.processCodecConfigEvent(codecStatusAacAndSbcAac);
+ verify(mA2dpService).codecConfigUpdated(mTestDevice, codecStatusAacAndSbcAac, false);
+ verify(mA2dpService, times(2)).updateOptionalCodecsSupport(mTestDevice);
+ }
}
diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java
index 2735c11..d82d28b 100644
--- a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java
@@ -15,16 +15,22 @@
*/
package com.android.bluetooth.a2dpsink;
+import static org.mockito.Mockito.*;
+
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
import org.junit.After;
import org.junit.Assert;
@@ -46,6 +52,7 @@
@Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
@Mock private AdapterService mAdapterService;
+ @Mock private DatabaseManager mDatabaseManager;
@Before
public void setUp() throws Exception {
@@ -60,6 +67,7 @@
// Try getting the Bluetooth adapter
mAdapter = BluetoothAdapter.getDefaultAdapter();
Assert.assertNotNull(mAdapter);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
}
@After
@@ -73,8 +81,43 @@
TestUtils.clearAdapterService(mAdapterService);
}
+ private BluetoothDevice makeBluetoothDevice(String address) {
+ return mAdapter.getRemoteDevice(address);
+ }
+
+ /**
+ * Mock the priority of a bluetooth device
+ *
+ * @param device - The bluetooth device you wish to mock the priority of
+ * @param priority - The priority value you want the device to have
+ */
+ private void mockDevicePriority(BluetoothDevice device, int priority) {
+ when(mDatabaseManager.getProfilePriority(device, BluetoothProfile.A2DP_SINK))
+ .thenReturn(priority);
+ }
+
@Test
public void testInitialize() {
Assert.assertNotNull(A2dpSinkService.getA2dpSinkService());
}
+
+ /**
+ * Test that a PRIORITY_ON device is connected to
+ */
+ @Test
+ public void testConnect() {
+ BluetoothDevice device = makeBluetoothDevice("11:11:11:11:11:11");
+ mockDevicePriority(device, BluetoothProfile.PRIORITY_ON);
+ Assert.assertTrue(mService.connect(device));
+ }
+
+ /**
+ * Test that a PRIORITY_OFF device is not connected to
+ */
+ @Test
+ public void testConnectPriorityOffDevice() {
+ BluetoothDevice device = makeBluetoothDevice("11:11:11:11:11:11");
+ mockDevicePriority(device, BluetoothProfile.PRIORITY_OFF);
+ Assert.assertFalse(mService.connect(device));
+ }
}
diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
index eddab48..53e8419 100644
--- a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
+++ b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
@@ -24,9 +24,10 @@
import android.media.AudioManager;
import android.os.HandlerThread;
import android.os.Looper;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
@@ -47,7 +48,7 @@
@Mock private Context mMockContext;
- @Mock private A2dpSinkStateMachine mMockA2dpSink;
+ @Mock private A2dpSinkService mMockA2dpSink;
@Mock private AudioManager mMockAudioManager;
@@ -189,7 +190,8 @@
mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT));
verify(mMockAudioManager, times(0)).abandonAudioFocus(any());
- verify(mMockA2dpSink, times(1)).informAudioFocusStateNative(0);
+ verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(0);
+ verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(0);
}
@Test
diff --git a/tests/unit/src/com/android/bluetooth/avrcp/AvrcpTest.java b/tests/unit/src/com/android/bluetooth/avrcp/AvrcpTest.java
deleted file mode 100644
index 5af45e1..0000000
--- a/tests/unit/src/com/android/bluetooth/avrcp/AvrcpTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package com.android.bluetooth.avrcp;
-
-import static org.mockito.Mockito.*;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.media.AudioManager;
-import android.os.Looper;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-
-/**
- * Unit tests for {@link Avrcp}
- */
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class AvrcpTest {
- @Test
- public void testCanStart() {
- if (Looper.myLooper() == null) {
- Looper.prepare();
- }
-
- Avrcp a = Avrcp.make(InstrumentationRegistry.getTargetContext());
- }
-
- @Test
- public void testFailedBrowseStart() {
- if (Looper.myLooper() == null) {
- Looper.prepare();
- }
-
- Context mockContext = mock(Context.class);
- AudioManager mockAudioManager = mock(AudioManager.class);
- PackageManager mockPackageManager = mock(PackageManager.class);
-
- when(mockAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)).thenReturn(100);
-
- when(mockContext.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mockAudioManager);
-
- when(mockContext.getApplicationContext()).thenReturn(mockContext);
- when(mockContext.getPackageManager()).thenReturn(mockPackageManager);
-
-
- // Call to get the BrowsableMediaPlayers
- // We must return at least one to try to startService
- List<ResolveInfo> resInfos = new ArrayList<ResolveInfo>();
-
- ServiceInfo fakeService = new ServiceInfo();
- fakeService.name = ".browse.MediaBrowserService";
- fakeService.packageName = "com.test.android.fake";
-
- ResolveInfo fakePackage = new ResolveInfo();
- fakePackage.serviceInfo = fakeService;
- fakePackage.nonLocalizedLabel = "Fake Package";
- resInfos.add(fakePackage);
- when(mockPackageManager.queryIntentServices(isA(Intent.class), anyInt())).thenReturn(
- resInfos);
-
- when(mockContext.startService(isA(Intent.class))).thenThrow(new SecurityException("test"));
-
- // Make calls start() which calls buildMediaPlayersList() which should
- // try to start the service?
- try {
- Avrcp a = Avrcp.make(mockContext);
- } catch (SecurityException e) {
- Assert.fail(
- "Threw SecurityException instead of protecting against it: " + e.toString());
- }
- }
-}
diff --git a/tests/unit/src/com/android/bluetooth/avrcp/EvictingQueueTest.java b/tests/unit/src/com/android/bluetooth/avrcp/EvictingQueueTest.java
deleted file mode 100644
index ff44f06..0000000
--- a/tests/unit/src/com/android/bluetooth/avrcp/EvictingQueueTest.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.android.bluetooth.avrcp;
-
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Unit tests for {@link EvictingQueue}.
- */
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class EvictingQueueTest {
- @Test
- public void testEvictingQueue_canAddItems() {
- EvictingQueue<Integer> e = new EvictingQueue<Integer>(10);
-
- e.add(1);
-
- Assert.assertEquals((long) e.size(), (long) 1);
- }
-
- @Test
- public void testEvictingQueue_maxItems() {
- EvictingQueue<Integer> e = new EvictingQueue<Integer>(5);
-
- e.add(1);
- e.add(2);
- e.add(3);
- e.add(4);
- e.add(5);
- e.add(6);
-
- Assert.assertEquals((long) e.size(), (long) 5);
- // Items drop off the front
- Assert.assertEquals((long) e.peek(), (long) 2);
- }
-
- @Test
- public void testEvictingQueue_frontDrop() {
- EvictingQueue<Integer> e = new EvictingQueue<Integer>(5);
-
- e.add(1);
- e.add(2);
- e.add(3);
- e.add(4);
- e.add(5);
-
- Assert.assertEquals((long) e.size(), (long) 5);
-
- e.addFirst(6);
-
- Assert.assertEquals((long) e.size(), (long) 5);
- Assert.assertEquals((long) e.peek(), (long) 1);
- }
-}
diff --git a/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceTest.java b/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceTest.java
index 8bf4679..e9440f5 100644
--- a/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerServiceTest.java
@@ -17,10 +17,11 @@
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
diff --git a/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java b/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java
new file mode 100644
index 0000000..4c77e0a
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpControllerStateMachineTest.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.bluetooth.avrcpcontroller;
+
+import static org.mockito.Mockito.*;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAvrcpController;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.os.Looper;
+import android.support.v4.media.session.MediaControllerCompat;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.ProfileService;
+
+import org.hamcrest.core.IsInstanceOf;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class AvrcpControllerStateMachineTest {
+ private static final int ASYNC_CALL_TIMEOUT_MILLIS = 100;
+ private static final int CONNECT_TIMEOUT_TEST_MILLIS = 1000;
+ private static final int KEY_DOWN = 0;
+ private static final int KEY_UP = 1;
+ private BluetoothAdapter mAdapter;
+ private AvrcpControllerStateMachine mAvrcpControllerStateMachine;
+ private Context mTargetContext;
+ private BluetoothDevice mTestDevice;
+ private ArgumentCaptor<Intent> mIntentArgument = ArgumentCaptor.forClass(Intent.class);
+ private byte[] mTestAddress = new byte[]{00, 01, 02, 03, 04, 05};
+
+ @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
+
+ @Mock
+ private AdapterService mAdapterService;
+ @Mock
+ private AudioManager mAudioManager;
+ @Mock
+ private AvrcpControllerService mAvrcpControllerService;
+
+ AvrcpControllerStateMachine mAvrcpStateMachine;
+
+ @Before
+ public void setUp() throws Exception {
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ Assume.assumeTrue("Ignore test when AVRCP Controller is not enabled",
+ mTargetContext.getResources().getBoolean(
+ R.bool.profile_supported_avrcp_controller));
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ Assert.assertNotNull(Looper.myLooper());
+
+ // Setup mocks and test assets
+ MockitoAnnotations.initMocks(this);
+ TestUtils.setAdapterService(mAdapterService);
+ TestUtils.startService(mServiceRule, AvrcpControllerService.class);
+ doReturn(mTargetContext.getResources()).when(mAvrcpControllerService).getResources();
+ doReturn(15).when(mAudioManager).getStreamMaxVolume(anyInt());
+ doReturn(8).when(mAudioManager).getStreamVolume(anyInt());
+ doReturn(true).when(mAudioManager).isVolumeFixed();
+ doReturn(mAudioManager).when(mAvrcpControllerService)
+ .getSystemService(Context.AUDIO_SERVICE);
+
+ // This line must be called to make sure relevant objects are initialized properly
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ // Get a device for testing
+ mTestDevice = mAdapter.getRemoteDevice(mTestAddress);
+ mAvrcpControllerService.start();
+ mAvrcpControllerService.sBrowseTree = new BrowseTree(null);
+ mAvrcpStateMachine = new AvrcpControllerStateMachine(mTestDevice, mAvrcpControllerService);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_avrcp_controller)) {
+ return;
+ }
+ TestUtils.clearAdapterService(mAdapterService);
+ }
+
+ /**
+ * Test to confirm that the state machine is capable of cycling through the 4
+ * connection states, and that upon completion, it cleans up aftwards.
+ */
+ @Test
+ public void testDisconnect() {
+ int numBroadcastsSent = setUpConnectedState(true, true);
+ StackEvent event =
+ StackEvent.connectionStateChanged(false, false);
+
+ mAvrcpStateMachine.disconnect();
+ numBroadcastsSent += 2;
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(numBroadcastsSent)).sendBroadcast(
+ mIntentArgument.capture(), eq(ProfileService.BLUETOOTH_PERM));
+ Assert.assertEquals(mTestDevice, mIntentArgument.getValue().getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED,
+ mIntentArgument.getValue().getAction());
+ Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
+ mIntentArgument.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+ Assert.assertThat(mAvrcpStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(AvrcpControllerStateMachine.Disconnected.class));
+ Assert.assertEquals(mAvrcpStateMachine.getState(), BluetoothProfile.STATE_DISCONNECTED);
+ verify(mAvrcpControllerService).removeStateMachine(eq(mAvrcpStateMachine));
+ }
+
+ /**
+ * Test to confirm that a control only device can be established (no browsing)
+ */
+ @Test
+ public void testControlOnly() {
+ int numBroadcastsSent = setUpConnectedState(true, false);
+ StackEvent event =
+ StackEvent.connectionStateChanged(false, false);
+ mAvrcpStateMachine.disconnect();
+ numBroadcastsSent += 2;
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(numBroadcastsSent)).sendBroadcast(
+ mIntentArgument.capture(), eq(ProfileService.BLUETOOTH_PERM));
+ Assert.assertEquals(mTestDevice, mIntentArgument.getValue().getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED,
+ mIntentArgument.getValue().getAction());
+ Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
+ mIntentArgument.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+ Assert.assertThat(mAvrcpStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(AvrcpControllerStateMachine.Disconnected.class));
+ Assert.assertEquals(mAvrcpStateMachine.getState(), BluetoothProfile.STATE_DISCONNECTED);
+ verify(mAvrcpControllerService).removeStateMachine(eq(mAvrcpStateMachine));
+ }
+
+ /**
+ * Test to confirm that a browsing only device can be established (no control)
+ */
+ @Test
+ public void testBrowsingOnly() {
+ Assert.assertEquals(0, mAvrcpControllerService.sBrowseTree.mRootNode.getChildrenCount());
+ int numBroadcastsSent = setUpConnectedState(false, true);
+ Assert.assertEquals(1, mAvrcpControllerService.sBrowseTree.mRootNode.getChildrenCount());
+ StackEvent event =
+ StackEvent.connectionStateChanged(false, false);
+ mAvrcpStateMachine.disconnect();
+ numBroadcastsSent += 2;
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(numBroadcastsSent)).sendBroadcast(
+ mIntentArgument.capture(), eq(ProfileService.BLUETOOTH_PERM));
+ Assert.assertEquals(mTestDevice, mIntentArgument.getValue().getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED,
+ mIntentArgument.getValue().getAction());
+ Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
+ mIntentArgument.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+ Assert.assertThat(mAvrcpStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(AvrcpControllerStateMachine.Disconnected.class));
+ Assert.assertEquals(mAvrcpStateMachine.getState(), BluetoothProfile.STATE_DISCONNECTED);
+ verify(mAvrcpControllerService).removeStateMachine(eq(mAvrcpStateMachine));
+ }
+
+ /**
+ * Test to make sure the state machine is tracking the correct device
+ */
+ @Test
+ public void testGetDevice() {
+ Assert.assertEquals(mAvrcpStateMachine.getDevice(), mTestDevice);
+ }
+
+ /**
+ * Test that dumpsys will generate information about connected devices
+ */
+ @Test
+ public void testDump() {
+ StringBuilder sb = new StringBuilder();
+ mAvrcpStateMachine.dump(sb);
+ Assert.assertEquals(sb.toString(),
+ " mDevice: " + mTestDevice.toString()
+ + "(null) name=AvrcpControllerStateMachine state=(null)\n");
+ }
+
+ /**
+ * Test media browser play command
+ */
+ @Test
+ public void testPlay() throws Exception {
+ setUpConnectedState(true, true);
+ MediaControllerCompat.TransportControls transportControls =
+ BluetoothMediaBrowserService.getTransportControls();
+
+ //Play
+ transportControls.play();
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_PLAY), eq(KEY_DOWN));
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_PLAY), eq(KEY_UP));
+ }
+
+ /**
+ * Test media browser pause command
+ */
+ @Test
+ public void testPause() throws Exception {
+ setUpConnectedState(true, true);
+ MediaControllerCompat.TransportControls transportControls =
+ BluetoothMediaBrowserService.getTransportControls();
+
+ //Pause
+ transportControls.pause();
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE), eq(KEY_DOWN));
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_PAUSE), eq(KEY_UP));
+ }
+
+ /**
+ * Test media browser stop command
+ */
+ @Test
+ public void testStop() throws Exception {
+ setUpConnectedState(true, true);
+ MediaControllerCompat.TransportControls transportControls =
+ BluetoothMediaBrowserService.getTransportControls();
+
+ //Stop
+ transportControls.stop();
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_STOP), eq(KEY_DOWN));
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_STOP), eq(KEY_UP));
+ }
+
+ /**
+ * Test media browser next command
+ */
+ @Test
+ public void testNext() throws Exception {
+ setUpConnectedState(true, true);
+ MediaControllerCompat.TransportControls transportControls =
+ BluetoothMediaBrowserService.getTransportControls();
+
+ //Next
+ transportControls.skipToNext();
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD),
+ eq(KEY_DOWN));
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_FORWARD), eq(KEY_UP));
+ }
+
+ /**
+ * Test media browser previous command
+ */
+ @Test
+ public void testPrevious() throws Exception {
+ setUpConnectedState(true, true);
+ MediaControllerCompat.TransportControls transportControls =
+ BluetoothMediaBrowserService.getTransportControls();
+
+ //Previous
+ transportControls.skipToPrevious();
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD),
+ eq(KEY_DOWN));
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_BACKWARD), eq(KEY_UP));
+ }
+
+ /**
+ * Test media browser fast forward command
+ */
+ @Test
+ public void testFastForward() throws Exception {
+ setUpConnectedState(true, true);
+ MediaControllerCompat.TransportControls transportControls =
+ BluetoothMediaBrowserService.getTransportControls();
+
+ //FastForward
+ transportControls.fastForward();
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_FF), eq(KEY_DOWN));
+ //Finish FastForwarding
+ transportControls.play();
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_FF), eq(KEY_UP));
+ }
+
+ /**
+ * Test media browser rewind command
+ */
+ @Test
+ public void testRewind() throws Exception {
+ setUpConnectedState(true, true);
+ MediaControllerCompat.TransportControls transportControls =
+ BluetoothMediaBrowserService.getTransportControls();
+
+ //Rewind
+ transportControls.rewind();
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_REWIND), eq(KEY_DOWN));
+ //Finish Rewinding
+ transportControls.play();
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_REWIND), eq(KEY_UP));
+ }
+
+ /**
+ * Test media browser shuffle command
+ */
+ @Test
+ public void testShuffle() throws Exception {
+ byte[] shuffleSetting = new byte[]{3};
+ byte[] shuffleMode = new byte[]{2};
+
+ setUpConnectedState(true, true);
+ MediaControllerCompat.TransportControls transportControls =
+ BluetoothMediaBrowserService.getTransportControls();
+
+ //Shuffle
+ transportControls.setShuffleMode(1);
+ verify(mAvrcpControllerService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1))
+ .setPlayerApplicationSettingValuesNative(
+ eq(mTestAddress), eq((byte) 1), eq(shuffleSetting), eq(shuffleMode));
+ }
+
+ /**
+ * Test media browser repeat command
+ */
+ @Test
+ public void testRepeat() throws Exception {
+ byte[] repeatSetting = new byte[]{2};
+ byte[] repeatMode = new byte[]{3};
+
+ setUpConnectedState(true, true);
+ MediaControllerCompat.TransportControls transportControls =
+ BluetoothMediaBrowserService.getTransportControls();
+
+ //Shuffle
+ transportControls.setRepeatMode(2);
+ verify(mAvrcpControllerService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1))
+ .setPlayerApplicationSettingValuesNative(
+ eq(mTestAddress), eq((byte) 1), eq(repeatSetting), eq(repeatMode));
+ }
+
+ /**
+ * Test media browsing
+ * Verify that a browse tree is created with the proper root
+ * Verify that a player can be fetched and added to the browse tree
+ * Verify that the contents of a player are fetched upon request
+ */
+ @Test
+ public void testBrowsingCommands() {
+ setUpConnectedState(true, true);
+ final String rootName = "__ROOT__";
+ final String playerName = "Player 1";
+
+ //Get the root of the device
+ BrowseTree.BrowseNode results = mAvrcpStateMachine.findNode(rootName);
+ Assert.assertEquals(rootName + mTestDevice.toString(), results.getID());
+
+ //Request fetch the list of players
+ BrowseTree.BrowseNode playerNodes = mAvrcpStateMachine.findNode(results.getID());
+ mAvrcpStateMachine.requestContents(results);
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).getPlayerListNative(eq(mTestAddress),
+ eq(0), eq(19));
+
+ //Provide back a player object
+ byte[] playerFeatures =
+ new byte[]{0, 0, 0, 0, 0, (byte) 0xb7, 0x01, 0x0c, 0x0a, 0, 0, 0, 0, 0, 0, 0};
+ AvrcpPlayer playerOne = new AvrcpPlayer(1, playerName, playerFeatures, 1, 1);
+ List<AvrcpPlayer> testPlayers = new ArrayList<>();
+ testPlayers.add(playerOne);
+ mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_PLAYER_ITEMS,
+ testPlayers);
+
+ //Verify that the player object is available.
+ mAvrcpStateMachine.requestContents(results);
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).getPlayerListNative(eq(mTestAddress),
+ eq(1), eq(0));
+ mAvrcpStateMachine.sendMessage(
+ AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE);
+ playerNodes = mAvrcpStateMachine.findNode(results.getID());
+ Assert.assertEquals(true, results.isCached());
+ Assert.assertEquals("MediaItem{mFlags=1, mDescription=" + playerName + ", null, null}",
+ results.getChildren().get(0).getMediaItem().toString());
+
+ //Fetch contents of that player object
+ BrowseTree.BrowseNode playerOneNode = mAvrcpStateMachine.findNode(
+ results.getChildren().get(0).getID());
+ mAvrcpStateMachine.requestContents(playerOneNode);
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).setBrowsedPlayerNative(
+ eq(mTestAddress), eq(1));
+ mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_FOLDER_PATH, 5);
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).getFolderListNative(eq(mTestAddress),
+ eq(0), eq(4));
+ }
+
+ /**
+ * Test addressed media player changed
+ * Verify when the addressed media player changes browsing data updates
+ * Verify that the contents of a player are fetched upon request
+ */
+ @Test
+ public void testPlayerChanged() {
+ setUpConnectedState(true, true);
+ final String rootName = "__ROOT__";
+ final String playerName = "Player 1";
+
+ //Get the root of the device
+ BrowseTree.BrowseNode results = mAvrcpStateMachine.findNode(rootName);
+ Assert.assertEquals(rootName + mTestDevice.toString(), results.getID());
+
+ //Request fetch the list of players
+ BrowseTree.BrowseNode playerNodes = mAvrcpStateMachine.findNode(results.getID());
+ mAvrcpStateMachine.requestContents(results);
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).getPlayerListNative(eq(mTestAddress),
+ eq(0), eq(19));
+
+ //Provide back a player object
+ byte[] playerFeatures =
+ new byte[]{0, 0, 0, 0, 0, (byte) 0xb7, 0x01, 0x0c, 0x0a, 0, 0, 0, 0, 0, 0, 0};
+ AvrcpPlayer playerOne = new AvrcpPlayer(1, playerName, playerFeatures, 1, 1);
+ List<AvrcpPlayer> testPlayers = new ArrayList<>();
+ testPlayers.add(playerOne);
+ mAvrcpStateMachine.sendMessage(AvrcpControllerStateMachine.MESSAGE_PROCESS_GET_PLAYER_ITEMS,
+ testPlayers);
+
+ //Change players and verify that BT attempts to update the results
+ mAvrcpStateMachine.sendMessage(
+ AvrcpControllerStateMachine.MESSAGE_PROCESS_ADDRESSED_PLAYER_CHANGED, 4);
+ results = mAvrcpStateMachine.findNode(rootName);
+
+ mAvrcpStateMachine.requestContents(results);
+
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).getPlayerListNative(eq(mTestAddress),
+ eq(0), eq(19));
+ }
+
+ /**
+ * Test that the Now Playing playlist is updated when it changes.
+ */
+ @Test
+ public void testNowPlaying() {
+ setUpConnectedState(true, true);
+ mAvrcpStateMachine.nowPlayingContentChanged();
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).getNowPlayingListNative(
+ eq(mTestAddress), eq(0), eq(19));
+ }
+
+ /**
+ * Test that AVRCP events such as playback commands can execute while performing browsing.
+ */
+ @Test
+ public void testPlayWhileBrowsing() {
+ setUpConnectedState(true, true);
+ final String rootName = "__ROOT__";
+ final String playerName = "Player 1";
+
+ //Get the root of the device
+ BrowseTree.BrowseNode results = mAvrcpStateMachine.findNode(rootName);
+ Assert.assertEquals(rootName + mTestDevice.toString(), results.getID());
+
+ //Request fetch the list of players
+ BrowseTree.BrowseNode playerNodes = mAvrcpStateMachine.findNode(results.getID());
+ mAvrcpStateMachine.requestContents(results);
+
+ MediaControllerCompat.TransportControls transportControls =
+ BluetoothMediaBrowserService.getTransportControls();
+ transportControls.play();
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_PLAY), eq(KEY_DOWN));
+ verify(mAvrcpControllerService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendPassThroughCommandNative(
+ eq(mTestAddress), eq(AvrcpControllerService.PASS_THRU_CMD_ID_PLAY), eq(KEY_UP));
+ }
+
+ /**
+ * Test that Absolute Volume Registration is working
+ */
+ @Test
+ public void testRegisterAbsVolumeNotification() {
+ setUpConnectedState(true, true);
+ mAvrcpStateMachine.sendMessage(
+ AvrcpControllerStateMachine.MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION);
+ verify(mAvrcpControllerService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1))
+ .sendRegisterAbsVolRspNative(any(), anyByte(), eq(127), anyInt());
+ }
+
+ /**
+ * Setup Connected State
+ *
+ * @return number of times mAvrcpControllerService.sendBroadcastAsUser() has been invoked
+ */
+ private int setUpConnectedState(boolean control, boolean browsing) {
+ // Put test state machine into connected state
+ mAvrcpStateMachine.start();
+ Assert.assertThat(mAvrcpStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(AvrcpControllerStateMachine.Disconnected.class));
+
+ mAvrcpStateMachine.connect(StackEvent.connectionStateChanged(control, browsing));
+ verify(mAvrcpControllerService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast(
+ mIntentArgument.capture(), eq(ProfileService.BLUETOOTH_PERM));
+ Assert.assertThat(mAvrcpStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(AvrcpControllerStateMachine.Connected.class));
+ Assert.assertEquals(mAvrcpStateMachine.getState(), BluetoothProfile.STATE_CONNECTED);
+
+ return BluetoothProfile.STATE_CONNECTED;
+ }
+
+}
diff --git a/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java b/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
index 2d01bef..5580155 100644
--- a/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
+++ b/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
@@ -27,9 +27,10 @@
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
@@ -216,7 +217,8 @@
@Test
public void hearingAidActive_clearA2dpAndHeadsetActive() {
Assume.assumeTrue("Ignore test when HearingAidService is not enabled",
- mContext.getResources().getBoolean(R.bool.profile_supported_hearing_aid));
+ mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_hearing_aid_profile_supported));
a2dpConnected(mA2dpHeadsetDevice);
headsetConnected(mA2dpHeadsetDevice);
@@ -234,7 +236,8 @@
@Test
public void hearingAidActive_dontSetA2dpAndHeadsetActive() {
Assume.assumeTrue("Ignore test when HearingAidService is not enabled",
- mContext.getResources().getBoolean(R.bool.profile_supported_hearing_aid));
+ mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_hearing_aid_profile_supported));
hearingAidActiveDeviceChanged(mHearingAidDevice);
a2dpConnected(mA2dpHeadsetDevice);
@@ -251,7 +254,8 @@
@Test
public void hearingAidActive_setA2dpActiveExplicitly() {
Assume.assumeTrue("Ignore test when HearingAidService is not enabled",
- mContext.getResources().getBoolean(R.bool.profile_supported_hearing_aid));
+ mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_hearing_aid_profile_supported));
hearingAidActiveDeviceChanged(mHearingAidDevice);
a2dpConnected(mA2dpHeadsetDevice);
@@ -271,7 +275,8 @@
@Test
public void hearingAidActive_setHeadsetActiveExplicitly() {
Assume.assumeTrue("Ignore test when HearingAidService is not enabled",
- mContext.getResources().getBoolean(R.bool.profile_supported_hearing_aid));
+ mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_hearing_aid_profile_supported));
hearingAidActiveDeviceChanged(mHearingAidDevice);
headsetConnected(mA2dpHeadsetDevice);
diff --git a/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java b/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
index 2b19a20..e626cbc 100644
--- a/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
@@ -17,15 +17,11 @@
package com.android.bluetooth.btservice;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
import android.app.AlarmManager;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
import android.bluetooth.IBluetoothCallback;
import android.content.Context;
import android.content.pm.ApplicationInfo;
@@ -38,24 +34,42 @@
import android.os.Process;
import android.os.SystemProperties;
import android.os.UserManager;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
import android.test.mock.MockContentResolver;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.Utils;
+
+import libcore.util.HexEncoding;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
@MediumTest
@RunWith(AndroidJUnit4.class)
public class AdapterServiceTest {
+ private static final String TAG = AdapterServiceTest.class.getSimpleName();
+
private AdapterService mAdapterService;
private @Mock Context mMockContext;
@@ -73,10 +87,28 @@
private static final int CONTEXT_SWITCH_MS = 100;
private static final int ONE_SECOND_MS = 1000;
private static final int NATIVE_INIT_MS = 8000;
+ private static final int NATIVE_DISABLE_MS = 1000;
private PowerManager mPowerManager;
private PackageManager mMockPackageManager;
private MockContentResolver mMockContentResolver;
+ private HashMap<String, HashMap<String, String>> mAdapterConfig;
+
+ @BeforeClass
+ public static void setupClass() {
+ // Bring native layer up and down to make sure config files are properly loaded
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ Assert.assertNotNull(Looper.myLooper());
+ AdapterService adapterService = new AdapterService();
+ adapterService.initNative();
+ adapterService.cleanupNative();
+ HashMap<String, HashMap<String, String>> adapterConfig = TestUtils.readAdapterConfig();
+ Assert.assertNotNull(adapterConfig);
+ Assert.assertNotNull("metrics salt is null: " + adapterConfig.toString(),
+ getMetricsSalt(adapterConfig));
+ }
@Before
public void setUp() throws PackageManager.NameNotFoundException {
@@ -85,12 +117,8 @@
}
Assert.assertNotNull(Looper.myLooper());
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- mAdapterService = new AdapterService();
- }
- });
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> mAdapterService = new AdapterService());
mMockPackageManager = mock(PackageManager.class);
mMockContentResolver = new MockContentResolver(mMockContext);
MockitoAnnotations.initMocks(this);
@@ -107,6 +135,10 @@
when(mMockContext.getSystemService(Context.POWER_SERVICE)).thenReturn(mPowerManager);
when(mMockContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mMockAlarmManager);
when(mMockContext.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mAudioManager);
+ doAnswer(invocation -> {
+ Object[] args = invocation.getArguments();
+ return InstrumentationRegistry.getTargetContext().getDatabasePath((String) args[0]);
+ }).when(mMockContext).getDatabasePath(anyString());
when(mMockResources.getBoolean(R.bool.profile_supported_gatt)).thenReturn(true);
when(mMockResources.getBoolean(R.bool.profile_supported_pbap)).thenReturn(true);
@@ -128,6 +160,9 @@
mAdapterService.registerCallback(mIBluetoothCallback);
Config.init(mMockContext);
+
+ mAdapterConfig = TestUtils.readAdapterConfig();
+ Assert.assertNotNull(mAdapterConfig);
}
@After
@@ -183,6 +218,8 @@
verifyStateChange(BluetoothAdapter.STATE_TURNING_ON, BluetoothAdapter.STATE_ON,
invocationNumber + 1, CONTEXT_SWITCH_MS);
+ verify(mMockContext, timeout(CONTEXT_SWITCH_MS).times(2 * invocationNumber + 2))
+ .sendBroadcast(any(), eq(android.Manifest.permission.BLUETOOTH));
final int scanMode = mAdapterService.getScanMode();
Assert.assertTrue(scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE
|| scanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
@@ -221,7 +258,7 @@
mAdapterService.onProfileServiceStateChanged(mMockGattService, BluetoothAdapter.STATE_OFF);
verifyStateChange(BluetoothAdapter.STATE_BLE_TURNING_OFF, BluetoothAdapter.STATE_OFF,
- invocationNumber + 1, CONTEXT_SWITCH_MS);
+ invocationNumber + 1, NATIVE_DISABLE_MS);
Assert.assertFalse(mAdapterService.isEnabled());
}
@@ -297,7 +334,7 @@
.times(2)).startService(any());
verifyStateChange(BluetoothAdapter.STATE_BLE_TURNING_OFF, BluetoothAdapter.STATE_OFF, 1,
- CONTEXT_SWITCH_MS);
+ NATIVE_DISABLE_MS);
Assert.assertFalse(mAdapterService.isEnabled());
}
@@ -333,7 +370,7 @@
verify(mMockContext, timeout(ONE_SECOND_MS).times(6)).startService(any());
verifyStateChange(BluetoothAdapter.STATE_BLE_TURNING_OFF, BluetoothAdapter.STATE_OFF, 1,
- AdapterState.BLE_STOP_TIMEOUT_DELAY + CONTEXT_SWITCH_MS);
+ AdapterState.BLE_STOP_TIMEOUT_DELAY + NATIVE_DISABLE_MS);
Assert.assertFalse(mAdapterService.isEnabled());
}
@@ -409,7 +446,7 @@
mAdapterService.onProfileServiceStateChanged(mMockGattService, BluetoothAdapter.STATE_OFF);
verifyStateChange(BluetoothAdapter.STATE_BLE_TURNING_OFF, BluetoothAdapter.STATE_OFF, 1,
- AdapterState.BLE_STOP_TIMEOUT_DELAY + CONTEXT_SWITCH_MS);
+ AdapterState.BLE_STOP_TIMEOUT_DELAY + NATIVE_DISABLE_MS);
Assert.assertFalse(mAdapterService.isEnabled());
}
@@ -456,11 +493,225 @@
mAdapterService.onProfileServiceStateChanged(mMockGattService, BluetoothAdapter.STATE_OFF);
verifyStateChange(BluetoothAdapter.STATE_BLE_TURNING_OFF, BluetoothAdapter.STATE_OFF, 1,
- CONTEXT_SWITCH_MS);
+ NATIVE_DISABLE_MS);
Assert.assertFalse(mAdapterService.isEnabled());
// Restore earlier setting
SystemProperties.set(AdapterService.BLUETOOTH_BTSNOOP_ENABLE_PROPERTY, snoopSetting);
}
+
+
+ /**
+ * Test: Obfuscate a null Bluetooth
+ * Check if returned value from {@link AdapterService#obfuscateAddress(BluetoothDevice)} is
+ * an empty array when device address is null
+ */
+ @Test
+ public void testObfuscateBluetoothAddress_NullAddress() {
+ Assert.assertArrayEquals(mAdapterService.obfuscateAddress(null), new byte[0]);
+ }
+
+ /**
+ * Test: Obfuscate Bluetooth address when Bluetooth is disabled
+ * Check whether the returned value meets expectation
+ */
+ @Test
+ public void testObfuscateBluetoothAddress_BluetoothDisabled() {
+ Assert.assertFalse(mAdapterService.isEnabled());
+ byte[] metricsSalt = getMetricsSalt(mAdapterConfig);
+ Assert.assertNotNull(metricsSalt);
+ BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0);
+ byte[] obfuscatedAddress = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress));
+ Assert.assertArrayEquals(obfuscateInJava(metricsSalt, device), obfuscatedAddress);
+ }
+
+ /**
+ * Test: Obfuscate Bluetooth address when Bluetooth is enabled
+ * Check whether the returned value meets expectation
+ */
+ @Test
+ public void testObfuscateBluetoothAddress_BluetoothEnabled() {
+ Assert.assertFalse(mAdapterService.isEnabled());
+ doEnable(0, false);
+ Assert.assertTrue(mAdapterService.isEnabled());
+ byte[] metricsSalt = getMetricsSalt(mAdapterConfig);
+ Assert.assertNotNull(metricsSalt);
+ BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0);
+ byte[] obfuscatedAddress = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress));
+ Assert.assertArrayEquals(obfuscateInJava(metricsSalt, device), obfuscatedAddress);
+ }
+
+ /**
+ * Test: Check if obfuscated Bluetooth address stays the same after toggling Bluetooth
+ */
+ @Test
+ public void testObfuscateBluetoothAddress_PersistentBetweenToggle() {
+ Assert.assertFalse(mAdapterService.isEnabled());
+ byte[] metricsSalt = getMetricsSalt(mAdapterConfig);
+ Assert.assertNotNull(metricsSalt);
+ BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0);
+ byte[] obfuscatedAddress1 = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress1.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress1));
+ Assert.assertArrayEquals(obfuscateInJava(metricsSalt, device),
+ obfuscatedAddress1);
+ // Enable
+ doEnable(0, false);
+ Assert.assertTrue(mAdapterService.isEnabled());
+ byte[] obfuscatedAddress3 = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress3.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress3));
+ Assert.assertArrayEquals(obfuscatedAddress3,
+ obfuscatedAddress1);
+ // Disable
+ doDisable(0, false);
+ Assert.assertFalse(mAdapterService.isEnabled());
+ byte[] obfuscatedAddress4 = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress4.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress4));
+ Assert.assertArrayEquals(obfuscatedAddress4,
+ obfuscatedAddress1);
+ }
+
+ /**
+ * Test: Check if obfuscated Bluetooth address stays the same after re-initializing
+ * {@link AdapterService}
+ */
+ @Test
+ public void testObfuscateBluetoothAddress_PersistentBetweenAdapterServiceInitialization() throws
+ PackageManager.NameNotFoundException {
+ byte[] metricsSalt = getMetricsSalt(mAdapterConfig);
+ Assert.assertNotNull(metricsSalt);
+ Assert.assertFalse(mAdapterService.isEnabled());
+ BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0);
+ byte[] obfuscatedAddress1 = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress1.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress1));
+ Assert.assertArrayEquals(obfuscateInJava(metricsSalt, device),
+ obfuscatedAddress1);
+ tearDown();
+ setUp();
+ Assert.assertFalse(mAdapterService.isEnabled());
+ byte[] obfuscatedAddress2 = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress2.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress2));
+ Assert.assertArrayEquals(obfuscatedAddress2,
+ obfuscatedAddress1);
+ }
+
+ /**
+ * Test: Verify that obfuscated Bluetooth address changes after factory reset
+ *
+ * There are 4 types of factory reset that we are talking about:
+ * 1. Factory reset all user data from Settings -> Will restart phone
+ * 2. Factory reset WiFi and Bluetooth from Settings -> Will only restart WiFi and BT
+ * 3. Call BluetoothAdapter.factoryReset() -> Will disable Bluetooth and reset config in
+ * memory and disk
+ * 4. Call AdapterService.factoryReset() -> Will only reset config in memory
+ *
+ * We can only use No. 4 here
+ */
+ @Ignore("AdapterService.factoryReset() does not reload config into memory and hence old salt"
+ + " is still used until next time Bluetooth library is initialized. However Bluetooth"
+ + " cannot be used until Bluetooth process restart any way. Thus it is almost"
+ + " guaranteed that user has to re-enable Bluetooth and hence re-generate new salt"
+ + " after factory reset")
+ @Test
+ public void testObfuscateBluetoothAddress_FactoryReset() {
+ Assert.assertFalse(mAdapterService.isEnabled());
+ BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0);
+ byte[] obfuscatedAddress1 = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress1.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress1));
+ mAdapterService.factoryReset();
+ byte[] obfuscatedAddress2 = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress2.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress2));
+ Assert.assertFalse(Arrays.equals(obfuscatedAddress2,
+ obfuscatedAddress1));
+ doEnable(0, false);
+ byte[] obfuscatedAddress3 = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress3.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress3));
+ Assert.assertArrayEquals(obfuscatedAddress3,
+ obfuscatedAddress2);
+ mAdapterService.factoryReset();
+ byte[] obfuscatedAddress4 = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress4.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress4));
+ Assert.assertFalse(Arrays.equals(obfuscatedAddress4,
+ obfuscatedAddress3));
+ }
+
+ /**
+ * Test: Verify that obfuscated Bluetooth address changes after factory reset and reloading
+ * native layer
+ */
+ @Test
+ public void testObfuscateBluetoothAddress_FactoryResetAndReloadNativeLayer() throws
+ PackageManager.NameNotFoundException {
+ byte[] metricsSalt1 = getMetricsSalt(mAdapterConfig);
+ Assert.assertNotNull(metricsSalt1);
+ Assert.assertFalse(mAdapterService.isEnabled());
+ BluetoothDevice device = TestUtils.getTestDevice(BluetoothAdapter.getDefaultAdapter(), 0);
+ byte[] obfuscatedAddress1 = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress1.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress1));
+ Assert.assertArrayEquals(obfuscateInJava(metricsSalt1, device),
+ obfuscatedAddress1);
+ mAdapterService.factoryReset();
+ tearDown();
+ setUp();
+ // Cannot verify metrics salt since it is not written to disk until native cleanup
+ byte[] obfuscatedAddress2 = mAdapterService.obfuscateAddress(device);
+ Assert.assertTrue(obfuscatedAddress2.length > 0);
+ Assert.assertFalse(isByteArrayAllZero(obfuscatedAddress2));
+ Assert.assertFalse(Arrays.equals(obfuscatedAddress2,
+ obfuscatedAddress1));
+ }
+
+ private static byte[] getMetricsSalt(HashMap<String, HashMap<String, String>> adapterConfig) {
+ HashMap<String, String> metricsSection = adapterConfig.get("Metrics");
+ if (metricsSection == null) {
+ Log.e(TAG, "Metrics section is null: " + adapterConfig.toString());
+ return null;
+ }
+ String saltString = metricsSection.get("Salt256Bit");
+ if (saltString == null) {
+ Log.e(TAG, "Salt256Bit is null: " + metricsSection.toString());
+ return null;
+ }
+ byte[] metricsSalt = HexEncoding.decode(saltString, false /* allowSingleChar */);
+ if (metricsSalt.length != 32) {
+ Log.e(TAG, "Salt length is not 32 bit, but is " + metricsSalt.length);
+ return null;
+ }
+ return metricsSalt;
+ }
+
+ private static byte[] obfuscateInJava(byte[] key, BluetoothDevice device) {
+ String algorithm = "HmacSHA256";
+ try {
+ Mac hmac256 = Mac.getInstance(algorithm);
+ hmac256.init(new SecretKeySpec(key, algorithm));
+ return hmac256.doFinal(Utils.getByteAddress(device));
+ } catch (NoSuchAlgorithmException | IllegalStateException | InvalidKeyException exp) {
+ exp.printStackTrace();
+ return null;
+ }
+ }
+
+ private static boolean isByteArrayAllZero(byte[] byteArray) {
+ for (byte i : byteArray) {
+ if (i != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
}
diff --git a/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java b/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java
index 29585ff..b9c8953 100644
--- a/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java
@@ -23,9 +23,10 @@
import android.os.HandlerThread;
import android.os.ParcelUuid;
import android.os.UserHandle;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.TestUtils;
diff --git a/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java b/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java
index b15c7b4..cc4e8b7 100644
--- a/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java
+++ b/tests/unit/src/com/android/bluetooth/btservice/MetricsLoggerTest.java
@@ -15,8 +15,8 @@
*/
package com.android.bluetooth.btservice;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.BluetoothMetricsProto.BluetoothLog;
import com.android.bluetooth.BluetoothMetricsProto.ProfileConnectionStats;
diff --git a/tests/unit/src/com/android/bluetooth/btservice/PhonePolicyTest.java b/tests/unit/src/com/android/bluetooth/btservice/PhonePolicyTest.java
index 510b9af..dafdc5a 100644
--- a/tests/unit/src/com/android/bluetooth/btservice/PhonePolicyTest.java
+++ b/tests/unit/src/com/android/bluetooth/btservice/PhonePolicyTest.java
@@ -24,12 +24,12 @@
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
-import android.content.BroadcastReceiver;
import android.content.Intent;
import android.os.HandlerThread;
import android.os.ParcelUuid;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.a2dp.A2dpService;
@@ -234,21 +234,20 @@
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
mPhonePolicy.getBroadcastReceiver().onReceive(null /* context */, intent);
- // This should not have any effect
+ // Verify that the priority of previous active device won't be changed while active device
+ // set to null
verify(mHeadsetService, after(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).setPriority(
bondedDevices[1], BluetoothProfile.PRIORITY_ON);
+ verify(mHeadsetService).setPriority(bondedDevices[1],
+ BluetoothProfile.PRIORITY_AUTO_CONNECT);
+ verify(mHeadsetService, never()).setPriority(bondedDevices[1],
+ BluetoothProfile.PRIORITY_OFF);
// Make the current active device fail to connect
- when(mHeadsetService.getConnectionState(bondedDevices[1])).thenReturn(
- BluetoothProfile.STATE_DISCONNECTED);
when(mA2dpService.getConnectionState(bondedDevices[1])).thenReturn(
BluetoothProfile.STATE_DISCONNECTED);
- intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bondedDevices[1]);
- intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTING);
- intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED);
- intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
- mPhonePolicy.getBroadcastReceiver().onReceive(null /* context */, intent);
+ updateProfileConnectionStateHelper(bondedDevices[1], BluetoothProfile.HEADSET,
+ BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING);
// This device should be set to ON
verify(mHeadsetService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).setPriority(
@@ -259,8 +258,8 @@
}
/**
- * Test that we will try to re-connect to a profile on a device if an attempt failed previously.
- * This is to add robustness to the connection mechanism
+ * Test that we will try to re-connect to a profile on a device if other profile(s) are
+ * connected. This is to add robustness to the connection mechanism
*/
@Test
public void testReconnectOnPartialConnect() {
@@ -290,12 +289,8 @@
// We send a connection successful for one profile since the re-connect *only* works if we
// have already connected successfully over one of the profiles
- Intent intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bondedDevices[0]);
- intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_DISCONNECTED);
- intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED);
- intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
- mPhonePolicy.getBroadcastReceiver().onReceive(null /* context */, intent);
+ updateProfileConnectionStateHelper(bondedDevices[0], BluetoothProfile.HEADSET,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
// Check that we get a call to A2DP connect
verify(mA2dpService, timeout(CONNECT_OTHER_PROFILES_TIMEOUT_WAIT_MILLIS)).connect(
@@ -303,6 +298,81 @@
}
/**
+ * Test that we will try to re-connect to a profile on a device next time if a previous attempt
+ * failed partially. This will make sure the connection mechanism still works at next try while
+ * the previous attempt is some profiles connected on a device but some not.
+ */
+ @Test
+ public void testReconnectOnPartialConnect_PreviousPartialFail() {
+ // Return a list of bonded devices (just one)
+ BluetoothDevice[] bondedDevices = new BluetoothDevice[1];
+ bondedDevices[0] = TestUtils.getTestDevice(mAdapter, 0);
+ when(mAdapterService.getBondedDevices()).thenReturn(bondedDevices);
+
+ // Return PRIORITY_AUTO_CONNECT over HFP and A2DP. This would imply that the profiles are
+ // auto-connectable.
+ when(mHeadsetService.getPriority(bondedDevices[0])).thenReturn(
+ BluetoothProfile.PRIORITY_AUTO_CONNECT);
+ when(mA2dpService.getPriority(bondedDevices[0])).thenReturn(
+ BluetoothProfile.PRIORITY_AUTO_CONNECT);
+
+ when(mAdapterService.getState()).thenReturn(BluetoothAdapter.STATE_ON);
+
+ // We want to trigger (in CONNECT_OTHER_PROFILES_TIMEOUT) a call to connect A2DP
+ // To enable that we need to make sure that HeadsetService returns the device among a list
+ // of connected devices
+ ArrayList<BluetoothDevice> hsConnectedDevices = new ArrayList<>();
+ hsConnectedDevices.add(bondedDevices[0]);
+ when(mHeadsetService.getConnectedDevices()).thenReturn(hsConnectedDevices);
+ // Also the A2DP should say that its not connected for same device
+ when(mA2dpService.getConnectionState(bondedDevices[0])).thenReturn(
+ BluetoothProfile.STATE_DISCONNECTED);
+
+ // We send a connection success event for one profile since the re-connect *only* works if
+ // we have already connected successfully over one of the profiles
+ updateProfileConnectionStateHelper(bondedDevices[0], BluetoothProfile.HEADSET,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
+
+ // Check that we get a call to A2DP reconnect
+ verify(mA2dpService, timeout(CONNECT_OTHER_PROFILES_TIMEOUT_WAIT_MILLIS)).connect(
+ bondedDevices[0]);
+
+ // We send a connection failure event for the attempted profile, and keep the connected
+ // profile connected.
+ updateProfileConnectionStateHelper(bondedDevices[0], BluetoothProfile.A2DP,
+ BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING);
+
+ TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+
+ // Verify no one changes the priority of the failed profile
+ verify(mA2dpService, never()).setPriority(eq(bondedDevices[0]), anyInt());
+
+ // Send a connection success event for one profile again without disconnecting all profiles
+ updateProfileConnectionStateHelper(bondedDevices[0], BluetoothProfile.HEADSET,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
+
+ // Check that we won't get a call to A2DP reconnect again before all profiles disconnected
+ verify(mA2dpService, timeout(CONNECT_OTHER_PROFILES_TIMEOUT_WAIT_MILLIS)).connect(
+ bondedDevices[0]);
+
+ // Send a disconnection event for all connected profiles
+ hsConnectedDevices.remove(bondedDevices[0]);
+ updateProfileConnectionStateHelper(bondedDevices[0], BluetoothProfile.HEADSET,
+ BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTED);
+
+ TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+
+ // Send a connection success event for one profile again to trigger re-connect
+ hsConnectedDevices.add(bondedDevices[0]);
+ updateProfileConnectionStateHelper(bondedDevices[0], BluetoothProfile.HEADSET,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
+
+ // Check that we get a call to A2DP connect again
+ verify(mA2dpService, timeout(CONNECT_OTHER_PROFILES_TIMEOUT_WAIT_MILLIS).times(2)).connect(
+ bondedDevices[0]);
+ }
+
+ /**
* Test that a second device will auto-connect if there is already one connected device.
*
* Even though we currently only set one device to be auto connect. The consumer of the auto
@@ -352,21 +422,13 @@
// We send a connection successful for one profile since the re-connect *only* works if we
// have already connected successfully over one of the profiles
- Intent intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, a2dpNotConnectedDevice1);
- intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_DISCONNECTED);
- intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED);
- intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
- mPhonePolicy.getBroadcastReceiver().onReceive(null /* context */, intent);
+ updateProfileConnectionStateHelper(a2dpNotConnectedDevice1, BluetoothProfile.HEADSET,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
// We send a connection successful for one profile since the re-connect *only* works if we
// have already connected successfully over one of the profiles
- intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, a2dpNotConnectedDevice2);
- intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_DISCONNECTED);
- intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED);
- intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
- mPhonePolicy.getBroadcastReceiver().onReceive(null /* context */, intent);
+ updateProfileConnectionStateHelper(a2dpNotConnectedDevice2, BluetoothProfile.HEADSET,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
// Check that we get a call to A2DP connect
verify(mA2dpService, timeout(CONNECT_OTHER_PROFILES_TIMEOUT_WAIT_MILLIS)).connect(
@@ -453,17 +515,12 @@
when(mA2dpService.getConnectionState(testDevices[3])).thenReturn(
BluetoothProfile.STATE_DISCONNECTED);
- // Get the broadcast receiver to inject events
- BroadcastReceiver injector = mPhonePolicy.getBroadcastReceiver();
// Generate connection state changed for HFP for testDevices[1] and trigger
// auto-connect for A2DP.
- Intent intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, testDevices[1]);
- intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_DISCONNECTED);
- intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED);
- intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
- injector.onReceive(null /* context */, intent);
+ updateProfileConnectionStateHelper(testDevices[1], BluetoothProfile.HEADSET,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
+
// Check that we get a call to A2DP connect
verify(mA2dpService, timeout(CONNECT_OTHER_PROFILES_TIMEOUT_WAIT_MILLIS)).connect(
eq(testDevices[1]));
@@ -494,12 +551,9 @@
// Generate connection state changed for A2DP for testDevices[2] and trigger
// auto-connect for HFP.
- intent = new Intent(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, testDevices[2]);
- intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_DISCONNECTED);
- intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED);
- intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
- injector.onReceive(null /* context */, intent);
+ updateProfileConnectionStateHelper(testDevices[2], BluetoothProfile.A2DP,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
+
// Check that we get a call to HFP connect
verify(mHeadsetService, timeout(CONNECT_OTHER_PROFILES_TIMEOUT_WAIT_MILLIS)).connect(
eq(testDevices[2]));
@@ -665,17 +719,11 @@
BluetoothProfile.STATE_DISCONNECTED);
when(mA2dpService.getConnectionState(bondedDevices[1])).thenReturn(
BluetoothProfile.STATE_DISCONNECTED);
- when(mHeadsetService.getConnectionState(bondedDevices[1])).thenReturn(
- BluetoothProfile.STATE_CONNECTED);
// We send a connection successful for one profile since the re-connect *only* works if we
// have already connected successfully over one of the profiles
- Intent intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bondedDevices[1]);
- intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_DISCONNECTED);
- intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED);
- intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
- mPhonePolicy.getBroadcastReceiver().onReceive(null /* context */, intent);
+ updateProfileConnectionStateHelper(bondedDevices[1], BluetoothProfile.HEADSET,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
// Check that we don't get any calls to reconnect
verify(mA2dpService, timeout(CONNECT_OTHER_PROFILES_TIMEOUT_WAIT_MILLIS)).connect(
@@ -708,4 +756,27 @@
eq(BluetoothProfile.PRIORITY_ON));
verify(mA2dpService, never()).setPriority(eq(device), eq(BluetoothProfile.PRIORITY_ON));
}
+
+ private void updateProfileConnectionStateHelper(BluetoothDevice device, int profileId,
+ int nextState, int prevState) {
+ Intent intent;
+ switch (profileId) {
+ case BluetoothProfile.A2DP:
+ when(mA2dpService.getConnectionState(device)).thenReturn(nextState);
+ intent = new Intent(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+ break;
+ case BluetoothProfile.HEADSET:
+ when(mHeadsetService.getConnectionState(device)).thenReturn(nextState);
+ intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+ break;
+ default:
+ intent = new Intent(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
+ break;
+ }
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
+ intent.putExtra(BluetoothProfile.EXTRA_STATE, nextState);
+ intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+ mPhonePolicy.getBroadcastReceiver().onReceive(null /* context */, intent);
+ }
}
diff --git a/tests/unit/src/com/android/bluetooth/btservice/ProfileServiceTest.java b/tests/unit/src/com/android/bluetooth/btservice/ProfileServiceTest.java
index ca20fc3..999ccd1 100644
--- a/tests/unit/src/com/android/bluetooth/btservice/ProfileServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/btservice/ProfileServiceTest.java
@@ -22,10 +22,11 @@
import android.bluetooth.BluetoothAdapter;
import android.content.Intent;
import android.os.Looper;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.TestUtils;
diff --git a/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java b/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java
index 35215ef..e22f485 100644
--- a/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java
+++ b/tests/unit/src/com/android/bluetooth/btservice/RemoteDevicesTest.java
@@ -11,9 +11,10 @@
import android.os.HandlerThread;
import android.os.Message;
import android.os.TestLooperManager;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.Utils;
import com.android.bluetooth.hfp.HeadsetHalConstants;
@@ -274,6 +275,7 @@
// Verify ACTION_ACL_DISCONNECTED and BATTERY_LEVEL_CHANGED intent are sent
verify(mAdapterService, times(3)).sendBroadcast(mIntentArgument.capture(),
mStringArgument.capture());
+ verify(mAdapterService).obfuscateAddress(mDevice1);
verifyBatteryLevelChangedIntent(mDevice1, BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
mIntentArgument.getAllValues().get(mIntentArgument.getAllValues().size() - 2));
Assert.assertEquals(AdapterService.BLUETOOTH_PERM,
@@ -336,7 +338,7 @@
BluetoothAssignedNumbers.PLANTRONICS, BluetoothHeadset.AT_CMD_TYPE_SET,
getXEventArray(3, 8), mDevice1));
verify(mAdapterService).sendBroadcast(mIntentArgument.capture(), mStringArgument.capture());
- verifyBatteryLevelChangedIntent(mDevice1, 37, mIntentArgument);
+ verifyBatteryLevelChangedIntent(mDevice1, 42, mIntentArgument);
Assert.assertEquals(AdapterService.BLUETOOTH_PERM, mStringArgument.getValue());
}
@@ -364,8 +366,11 @@
@Test
public void testGetBatteryLevelFromXEventVsc() {
- Assert.assertEquals(37, RemoteDevices.getBatteryLevelFromXEventVsc(getXEventArray(3, 8)));
- Assert.assertEquals(100, RemoteDevices.getBatteryLevelFromXEventVsc(getXEventArray(1, 1)));
+ Assert.assertEquals(42, RemoteDevices.getBatteryLevelFromXEventVsc(getXEventArray(3, 8)));
+ Assert.assertEquals(100,
+ RemoteDevices.getBatteryLevelFromXEventVsc(getXEventArray(10, 11)));
+ Assert.assertEquals(BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
+ RemoteDevices.getBatteryLevelFromXEventVsc(getXEventArray(1, 1)));
Assert.assertEquals(BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
RemoteDevices.getBatteryLevelFromXEventVsc(getXEventArray(3, 1)));
Assert.assertEquals(BluetoothDevice.BATTERY_LEVEL_UNKNOWN,
@@ -436,7 +441,8 @@
Assert.assertEquals(device, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
Assert.assertEquals(batteryLevel,
intent.getIntExtra(BluetoothDevice.EXTRA_BATTERY_LEVEL, -15));
- Assert.assertEquals(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT, intent.getFlags());
+ Assert.assertEquals(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+ | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, intent.getFlags());
}
private static Intent getHeadsetConnectionStateChangedIntent(BluetoothDevice device,
diff --git a/tests/unit/src/com/android/bluetooth/btservice/SilenceDeviceManagerTest.java b/tests/unit/src/com/android/bluetooth/btservice/SilenceDeviceManagerTest.java
new file mode 100644
index 0000000..0f7864a
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/btservice/SilenceDeviceManagerTest.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.btservice;
+
+import static org.mockito.Mockito.*;
+
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.Intent;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.UserHandle;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.a2dp.A2dpService;
+import com.android.bluetooth.hfp.HeadsetService;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SilenceDeviceManagerTest {
+ private BluetoothAdapter mAdapter;
+ private Context mContext;
+ private BluetoothDevice mTestDevice;
+ private SilenceDeviceManager mSilenceDeviceManager;
+ private HandlerThread mHandlerThread;
+ private Looper mLooper;
+ private static final String TEST_BT_ADDR = "11:22:33:44:55:66";
+ private int mVerifyCount = 0;
+
+ @Mock private AdapterService mAdapterService;
+ @Mock private ServiceFactory mServiceFactory;
+ @Mock private A2dpService mA2dpService;
+ @Mock private HeadsetService mHeadsetService;
+
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+
+ // Set up mocks and test assets
+ MockitoAnnotations.initMocks(this);
+ TestUtils.setAdapterService(mAdapterService);
+ when(mServiceFactory.getA2dpService()).thenReturn(mA2dpService);
+ when(mServiceFactory.getHeadsetService()).thenReturn(mHeadsetService);
+
+ // Get devices for testing
+ mTestDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(TEST_BT_ADDR);
+
+ mHandlerThread = new HandlerThread("SilenceManagerTestHandlerThread");
+ mHandlerThread.start();
+ mLooper = mHandlerThread.getLooper();
+ mSilenceDeviceManager = new SilenceDeviceManager(mAdapterService, mServiceFactory,
+ mLooper);
+ mSilenceDeviceManager.start();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mSilenceDeviceManager.cleanup();
+ mHandlerThread.quit();
+ TestUtils.clearAdapterService(mAdapterService);
+ }
+
+ @Test
+ public void testSetGetDeviceSilence() {
+ testSetGetDeviceSilenceConnectedCase(false, true);
+ testSetGetDeviceSilenceConnectedCase(false, false);
+ testSetGetDeviceSilenceConnectedCase(true, true);
+ testSetGetDeviceSilenceConnectedCase(true, false);
+
+ testSetGetDeviceSilenceDisconnectedCase(false);
+ testSetGetDeviceSilenceDisconnectedCase(true);
+ }
+
+ void testSetGetDeviceSilenceConnectedCase(boolean wasSilenced, boolean enableSilence) {
+ ArgumentCaptor<Intent> intentArgument = ArgumentCaptor.forClass(Intent.class);
+ doReturn(true).when(mA2dpService).setSilenceMode(mTestDevice, enableSilence);
+ doReturn(true).when(mHeadsetService).setSilenceMode(mTestDevice, enableSilence);
+
+ // Send A2DP/HFP connected intent
+ a2dpConnected(mTestDevice);
+ headsetConnected(mTestDevice);
+
+ // Set pre-state for mSilenceDeviceManager
+ if (wasSilenced) {
+ Assert.assertTrue(mSilenceDeviceManager.setSilenceMode(mTestDevice, true));
+ TestUtils.waitForLooperToFinishScheduledTask(mLooper);
+ verify(mAdapterService, times(++mVerifyCount)).sendBroadcastAsUser(
+ intentArgument.capture(), eq(UserHandle.ALL),
+ eq(AdapterService.BLUETOOTH_PERM));
+ }
+
+ // Set silence state and check whether state changed successfully
+ Assert.assertTrue(mSilenceDeviceManager.setSilenceMode(mTestDevice, enableSilence));
+ TestUtils.waitForLooperToFinishScheduledTask(mLooper);
+ Assert.assertEquals(enableSilence, mSilenceDeviceManager.getSilenceMode(mTestDevice));
+
+ // Check for silence state changed intent
+ if (wasSilenced != enableSilence) {
+ verify(mAdapterService, times(++mVerifyCount)).sendBroadcastAsUser(
+ intentArgument.capture(), eq(UserHandle.ALL),
+ eq(AdapterService.BLUETOOTH_PERM));
+ verifySilenceStateIntent(intentArgument.getValue());
+ }
+
+ // Remove test devices
+ a2dpDisconnected(mTestDevice);
+ headsetDisconnected(mTestDevice);
+
+ Assert.assertFalse(mSilenceDeviceManager.getSilenceMode(mTestDevice));
+ if (enableSilence) {
+ // If the silence mode is enabled, it should be automatically disabled
+ // after device is disconnected.
+ verify(mAdapterService, times(++mVerifyCount)).sendBroadcastAsUser(
+ intentArgument.capture(), eq(UserHandle.ALL),
+ eq(AdapterService.BLUETOOTH_PERM));
+ }
+ }
+
+ void testSetGetDeviceSilenceDisconnectedCase(boolean enableSilence) {
+ ArgumentCaptor<Intent> intentArgument = ArgumentCaptor.forClass(Intent.class);
+ // Set silence mode and it should stay disabled
+ Assert.assertTrue(mSilenceDeviceManager.setSilenceMode(mTestDevice, enableSilence));
+ TestUtils.waitForLooperToFinishScheduledTask(mLooper);
+ Assert.assertFalse(mSilenceDeviceManager.getSilenceMode(mTestDevice));
+
+ // Should be no intent been broadcasted
+ verify(mAdapterService, times(mVerifyCount)).sendBroadcastAsUser(
+ intentArgument.capture(), eq(UserHandle.ALL),
+ eq(AdapterService.BLUETOOTH_PERM));
+ }
+
+ void verifySilenceStateIntent(Intent intent) {
+ Assert.assertEquals(BluetoothDevice.ACTION_SILENCE_MODE_CHANGED, intent.getAction());
+ Assert.assertEquals(mTestDevice, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ }
+
+ /**
+ * Helper to indicate A2dp connected for a device.
+ */
+ private void a2dpConnected(BluetoothDevice device) {
+ Intent intent = new Intent(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_DISCONNECTED);
+ intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED);
+ mSilenceDeviceManager.getBroadcastReceiver().onReceive(mContext, intent);
+ TestUtils.waitForLooperToFinishScheduledTask(mLooper);
+ }
+
+ /**
+ * Helper to indicate A2dp disconnected for a device.
+ */
+ private void a2dpDisconnected(BluetoothDevice device) {
+ Intent intent = new Intent(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTED);
+ intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED);
+ mSilenceDeviceManager.getBroadcastReceiver().onReceive(mContext, intent);
+ TestUtils.waitForLooperToFinishScheduledTask(mLooper);
+ }
+
+ /**
+ * Helper to indicate Headset connected for a device.
+ */
+ private void headsetConnected(BluetoothDevice device) {
+ Intent intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_DISCONNECTED);
+ intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED);
+ mSilenceDeviceManager.getBroadcastReceiver().onReceive(mContext, intent);
+ TestUtils.waitForLooperToFinishScheduledTask(mLooper);
+ }
+
+ /**
+ * Helper to indicate Headset disconnected for a device.
+ */
+ private void headsetDisconnected(BluetoothDevice device) {
+ Intent intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTED);
+ intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED);
+ mSilenceDeviceManager.getBroadcastReceiver().onReceive(mContext, intent);
+ TestUtils.waitForLooperToFinishScheduledTask(mLooper);
+ }
+}
+
diff --git a/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java b/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
new file mode 100644
index 0000000..2cb3630
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
@@ -0,0 +1,698 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.btservice.storage;
+
+import static org.mockito.Mockito.*;
+
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import androidx.room.Room;
+import androidx.room.testing.MigrationTestHelper;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.btservice.AdapterService;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.util.List;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public final class DatabaseManagerTest {
+
+ @Mock private AdapterService mAdapterService;
+
+ private MetadataDatabase mDatabase;
+ private DatabaseManager mDatabaseManager;
+ private BluetoothDevice mTestDevice;
+ private BluetoothDevice mTestDevice2;
+
+ private static final String LOCAL_STORAGE = "LocalStorage";
+ private static final String TEST_BT_ADDR = "11:22:33:44:55:66";
+ private static final String OTHER_BT_ADDR1 = "11:11:11:11:11:11";
+ private static final String OTHER_BT_ADDR2 = "22:22:22:22:22:22";
+ private static final String DB_NAME = "test_db";
+ private static final int A2DP_SUPPORT_OP_CODEC_TEST = 0;
+ private static final int A2DP_ENALBED_OP_CODEC_TEST = 1;
+ private static final int MAX_META_ID = 16;
+ private static final byte[] TEST_BYTE_ARRAY = "TEST_VALUE".getBytes();
+
+ @Rule
+ public MigrationTestHelper testHelper = new MigrationTestHelper(
+ InstrumentationRegistry.getInstrumentation(),
+ MetadataDatabase.class.getCanonicalName(),
+ new FrameworkSQLiteOpenHelperFactory());
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ TestUtils.setAdapterService(mAdapterService);
+
+ mTestDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(TEST_BT_ADDR);
+
+ // Create a memory database for DatabaseManager instead of use a real database.
+ mDatabase = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
+ MetadataDatabase.class).build();
+
+ when(mAdapterService.getPackageManager()).thenReturn(
+ InstrumentationRegistry.getTargetContext().getPackageManager());
+ mDatabaseManager = new DatabaseManager(mAdapterService);
+
+ BluetoothDevice[] bondedDevices = {mTestDevice};
+ doReturn(bondedDevices).when(mAdapterService).getBondedDevices();
+ doNothing().when(mAdapterService).metadataChanged(
+ anyString(), anyInt(), any(byte[].class));
+
+ restartDatabaseManagerHelper();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ TestUtils.clearAdapterService(mAdapterService);
+ mDatabase.deleteAll();
+ mDatabaseManager.cleanup();
+ }
+
+ @Test
+ public void testMetadataDefault() {
+ Metadata data = new Metadata(TEST_BT_ADDR);
+ mDatabase.insert(data);
+ restartDatabaseManagerHelper();
+
+ for (int id = 0; id < BluetoothProfile.MAX_PROFILE_ID; id++) {
+ Assert.assertEquals(BluetoothProfile.PRIORITY_UNDEFINED,
+ mDatabaseManager.getProfilePriority(mTestDevice, id));
+ }
+
+ Assert.assertEquals(BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN,
+ mDatabaseManager.getA2dpSupportsOptionalCodecs(mTestDevice));
+
+ Assert.assertEquals(BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN,
+ mDatabaseManager.getA2dpOptionalCodecsEnabled(mTestDevice));
+
+ for (int id = 0; id < MAX_META_ID; id++) {
+ Assert.assertNull(mDatabaseManager.getCustomMeta(mTestDevice, id));
+ }
+ }
+
+ @Test
+ public void testSetGetProfilePriority() {
+ int badPriority = -100;
+
+ // Cases of device not in database
+ testSetGetProfilePriorityCase(false, BluetoothProfile.PRIORITY_UNDEFINED,
+ BluetoothProfile.PRIORITY_UNDEFINED, true);
+ testSetGetProfilePriorityCase(false, BluetoothProfile.PRIORITY_OFF,
+ BluetoothProfile.PRIORITY_OFF, true);
+ testSetGetProfilePriorityCase(false, BluetoothProfile.PRIORITY_ON,
+ BluetoothProfile.PRIORITY_ON, true);
+ testSetGetProfilePriorityCase(false, BluetoothProfile.PRIORITY_AUTO_CONNECT,
+ BluetoothProfile.PRIORITY_AUTO_CONNECT, true);
+ testSetGetProfilePriorityCase(false, badPriority,
+ BluetoothProfile.PRIORITY_UNDEFINED, false);
+
+ // Cases of device already in database
+ testSetGetProfilePriorityCase(true, BluetoothProfile.PRIORITY_UNDEFINED,
+ BluetoothProfile.PRIORITY_UNDEFINED, true);
+ testSetGetProfilePriorityCase(true, BluetoothProfile.PRIORITY_OFF,
+ BluetoothProfile.PRIORITY_OFF, true);
+ testSetGetProfilePriorityCase(true, BluetoothProfile.PRIORITY_ON,
+ BluetoothProfile.PRIORITY_ON, true);
+ testSetGetProfilePriorityCase(true, BluetoothProfile.PRIORITY_AUTO_CONNECT,
+ BluetoothProfile.PRIORITY_AUTO_CONNECT, true);
+ testSetGetProfilePriorityCase(true, badPriority,
+ BluetoothProfile.PRIORITY_UNDEFINED, false);
+ }
+
+ @Test
+ public void testSetGetA2dpSupportsOptionalCodecs() {
+ int badValue = -100;
+
+ // Cases of device not in database
+ testSetGetA2dpOptionalCodecsCase(A2DP_SUPPORT_OP_CODEC_TEST, false,
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN,
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN);
+ testSetGetA2dpOptionalCodecsCase(A2DP_SUPPORT_OP_CODEC_TEST, false,
+ BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED,
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN);
+ testSetGetA2dpOptionalCodecsCase(A2DP_SUPPORT_OP_CODEC_TEST, false,
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED,
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN);
+ testSetGetA2dpOptionalCodecsCase(A2DP_SUPPORT_OP_CODEC_TEST, false,
+ badValue, BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN);
+
+ // Cases of device already in database
+ testSetGetA2dpOptionalCodecsCase(A2DP_SUPPORT_OP_CODEC_TEST, true,
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN,
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN);
+ testSetGetA2dpOptionalCodecsCase(A2DP_SUPPORT_OP_CODEC_TEST, true,
+ BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED,
+ BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED);
+ testSetGetA2dpOptionalCodecsCase(A2DP_SUPPORT_OP_CODEC_TEST, true,
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED,
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED);
+ testSetGetA2dpOptionalCodecsCase(A2DP_SUPPORT_OP_CODEC_TEST, true,
+ badValue, BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN);
+ }
+
+ @Test
+ public void testSetGetA2dpOptionalCodecsEnabled() {
+ int badValue = -100;
+
+ // Cases of device not in database
+ testSetGetA2dpOptionalCodecsCase(A2DP_ENALBED_OP_CODEC_TEST, false,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN);
+ testSetGetA2dpOptionalCodecsCase(A2DP_ENALBED_OP_CODEC_TEST, false,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN);
+ testSetGetA2dpOptionalCodecsCase(A2DP_ENALBED_OP_CODEC_TEST, false,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN);
+ testSetGetA2dpOptionalCodecsCase(A2DP_ENALBED_OP_CODEC_TEST, false,
+ badValue, BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN);
+
+ // Cases of device already in database
+ testSetGetA2dpOptionalCodecsCase(A2DP_ENALBED_OP_CODEC_TEST, true,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN);
+ testSetGetA2dpOptionalCodecsCase(A2DP_ENALBED_OP_CODEC_TEST, true,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED);
+ testSetGetA2dpOptionalCodecsCase(A2DP_ENALBED_OP_CODEC_TEST, true,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED);
+ testSetGetA2dpOptionalCodecsCase(A2DP_ENALBED_OP_CODEC_TEST, true,
+ badValue, BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN);
+ }
+
+ @Test
+ public void testRemoveUnusedMetadata_WithSingleBondedDevice() {
+ // Insert two devices to database and cache, only mTestDevice is
+ // in the bonded list
+ Metadata otherData = new Metadata(OTHER_BT_ADDR1);
+ // Add metadata for otherDevice
+ otherData.setCustomizedMeta(0, TEST_BYTE_ARRAY);
+ mDatabaseManager.mMetadataCache.put(OTHER_BT_ADDR1, otherData);
+ mDatabase.insert(otherData);
+
+ Metadata data = new Metadata(TEST_BT_ADDR);
+ mDatabaseManager.mMetadataCache.put(TEST_BT_ADDR, data);
+ mDatabase.insert(data);
+
+ mDatabaseManager.removeUnusedMetadata();
+ // Wait for database update
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+
+ // Check removed device report metadata changed to null
+ verify(mAdapterService).metadataChanged(OTHER_BT_ADDR1, 0, null);
+
+ List<Metadata> list = mDatabase.load();
+
+ // Check number of metadata in the database
+ Assert.assertEquals(1, list.size());
+
+ // Check whether the device is in database
+ Metadata checkData = list.get(0);
+ Assert.assertEquals(TEST_BT_ADDR, checkData.getAddress());
+
+ mDatabase.deleteAll();
+ // Wait for clear database
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+ mDatabaseManager.mMetadataCache.clear();
+ }
+
+ @Test
+ public void testRemoveUnusedMetadata_WithMultiBondedDevices() {
+ // Insert three devices to database and cache, otherDevice1 and otherDevice2
+ // are in the bonded list
+
+ // Add metadata for TEST_BT_ADDR
+ Metadata testData = new Metadata(TEST_BT_ADDR);
+ testData.setCustomizedMeta(0, TEST_BYTE_ARRAY);
+ mDatabaseManager.mMetadataCache.put(TEST_BT_ADDR, testData);
+ mDatabase.insert(testData);
+
+ // Add metadata for OTHER_BT_ADDR1
+ Metadata otherData1 = new Metadata(OTHER_BT_ADDR1);
+ otherData1.setCustomizedMeta(0, TEST_BYTE_ARRAY);
+ mDatabaseManager.mMetadataCache.put(OTHER_BT_ADDR1, otherData1);
+ mDatabase.insert(otherData1);
+
+ // Add metadata for OTHER_BT_ADDR2
+ Metadata otherData2 = new Metadata(OTHER_BT_ADDR2);
+ otherData2.setCustomizedMeta(0, TEST_BYTE_ARRAY);
+ mDatabaseManager.mMetadataCache.put(OTHER_BT_ADDR2, otherData2);
+ mDatabase.insert(otherData2);
+
+ // Add OTHER_BT_ADDR1 OTHER_BT_ADDR2 to bonded devices
+ BluetoothDevice otherDevice1 = BluetoothAdapter.getDefaultAdapter()
+ .getRemoteDevice(OTHER_BT_ADDR1);
+ BluetoothDevice otherDevice2 = BluetoothAdapter.getDefaultAdapter()
+ .getRemoteDevice(OTHER_BT_ADDR2);
+ BluetoothDevice[] bondedDevices = {otherDevice1, otherDevice2};
+ doReturn(bondedDevices).when(mAdapterService).getBondedDevices();
+
+ mDatabaseManager.removeUnusedMetadata();
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+
+ // Check TEST_BT_ADDR report metadata changed to null
+ verify(mAdapterService).metadataChanged(TEST_BT_ADDR, 0, null);
+
+ // Check number of metadata in the database
+ List<Metadata> list = mDatabase.load();
+ // OTHER_BT_ADDR1 and OTHER_BT_ADDR2 should still in database
+ Assert.assertEquals(2, list.size());
+
+ // Check whether the devices are in the database
+ Metadata checkData1 = list.get(0);
+ Assert.assertEquals(OTHER_BT_ADDR1, checkData1.getAddress());
+ Metadata checkData2 = list.get(1);
+ Assert.assertEquals(OTHER_BT_ADDR2, checkData2.getAddress());
+
+ mDatabase.deleteAll();
+ // Wait for clear database
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+ mDatabaseManager.mMetadataCache.clear();
+
+ }
+
+ @Test
+ public void testSetGetCustomMeta() {
+ int badKey = 100;
+ byte[] value = "input value".getBytes();
+
+ // Device is not in database
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_MANUFACTURER_NAME,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_MODEL_NAME,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_SOFTWARE_VERSION,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_HARDWARE_VERSION,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_COMPANION_APP,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_MAIN_ICON,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_UNTETHERED_CASE_ICON,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING,
+ value, true);
+ testSetGetCustomMetaCase(false, BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI,
+ value, true);
+ testSetGetCustomMetaCase(false, badKey, value, false);
+
+ // Device is in database
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_MANUFACTURER_NAME,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_MODEL_NAME,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_SOFTWARE_VERSION,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_HARDWARE_VERSION,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_COMPANION_APP,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_MAIN_ICON,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_UNTETHERED_CASE_ICON,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING,
+ value, true);
+ testSetGetCustomMetaCase(true, BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI,
+ value, true);
+ }
+
+ @Test
+ public void testDatabaseMigration_100_101() throws IOException {
+ // Create a database with version 100
+ SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 100);
+ Cursor cursor = db.query("SELECT * FROM metadata");
+
+ // pbap_client_priority should not in version 100
+ assertHasColumn(cursor, "pbap_client_priority", false);
+
+ // Migrate database from 100 to 101
+ db.close();
+ db = testHelper.runMigrationsAndValidate(DB_NAME, 101, true,
+ MetadataDatabase.MIGRATION_100_101);
+ cursor = db.query("SELECT * FROM metadata");
+
+ // Check whether pbap_client_priority exists in version 101
+ assertHasColumn(cursor, "pbap_client_priority", true);
+ }
+
+ @Test
+ public void testDatabaseMigration_101_102() throws IOException {
+ String testString = "TEST STRING";
+
+ // Create a database with version 101
+ SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 101);
+ Cursor cursor = db.query("SELECT * FROM metadata");
+
+ // insert a device to the database
+ ContentValues device = new ContentValues();
+ device.put("address", TEST_BT_ADDR);
+ device.put("migrated", false);
+ device.put("a2dpSupportsOptionalCodecs", -1);
+ device.put("a2dpOptionalCodecsEnabled", -1);
+ device.put("a2dp_priority", -1);
+ device.put("a2dp_sink_priority", -1);
+ device.put("hfp_priority", -1);
+ device.put("hfp_client_priority", -1);
+ device.put("hid_host_priority", -1);
+ device.put("pan_priority", -1);
+ device.put("pbap_priority", -1);
+ device.put("pbap_client_priority", -1);
+ device.put("map_priority", -1);
+ device.put("sap_priority", -1);
+ device.put("hearing_aid_priority", -1);
+ device.put("map_client_priority", -1);
+ device.put("manufacturer_name", testString);
+ device.put("model_name", testString);
+ device.put("software_version", testString);
+ device.put("hardware_version", testString);
+ device.put("companion_app", testString);
+ device.put("main_icon", testString);
+ device.put("is_unthethered_headset", testString);
+ device.put("unthethered_left_icon", testString);
+ device.put("unthethered_right_icon", testString);
+ device.put("unthethered_case_icon", testString);
+ device.put("unthethered_left_battery", testString);
+ device.put("unthethered_right_battery", testString);
+ device.put("unthethered_case_battery", testString);
+ device.put("unthethered_left_charging", testString);
+ device.put("unthethered_right_charging", testString);
+ device.put("unthethered_case_charging", testString);
+ Assert.assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device),
+ CoreMatchers.not(-1));
+
+ // Check the metadata names on version 101
+ assertHasColumn(cursor, "is_unthethered_headset", true);
+ assertHasColumn(cursor, "unthethered_left_icon", true);
+ assertHasColumn(cursor, "unthethered_right_icon", true);
+ assertHasColumn(cursor, "unthethered_case_icon", true);
+ assertHasColumn(cursor, "unthethered_left_battery", true);
+ assertHasColumn(cursor, "unthethered_right_battery", true);
+ assertHasColumn(cursor, "unthethered_case_battery", true);
+ assertHasColumn(cursor, "unthethered_left_charging", true);
+ assertHasColumn(cursor, "unthethered_right_charging", true);
+ assertHasColumn(cursor, "unthethered_case_charging", true);
+
+ // Migrate database from 101 to 102
+ db.close();
+ db = testHelper.runMigrationsAndValidate(DB_NAME, 102, true,
+ MetadataDatabase.MIGRATION_101_102);
+ cursor = db.query("SELECT * FROM metadata");
+
+ // metadata names should be changed on version 102
+ assertHasColumn(cursor, "is_unthethered_headset", false);
+ assertHasColumn(cursor, "unthethered_left_icon", false);
+ assertHasColumn(cursor, "unthethered_right_icon", false);
+ assertHasColumn(cursor, "unthethered_case_icon", false);
+ assertHasColumn(cursor, "unthethered_left_battery", false);
+ assertHasColumn(cursor, "unthethered_right_battery", false);
+ assertHasColumn(cursor, "unthethered_case_battery", false);
+ assertHasColumn(cursor, "unthethered_left_charging", false);
+ assertHasColumn(cursor, "unthethered_right_charging", false);
+ assertHasColumn(cursor, "unthethered_case_charging", false);
+
+ assertHasColumn(cursor, "is_untethered_headset", true);
+ assertHasColumn(cursor, "untethered_left_icon", true);
+ assertHasColumn(cursor, "untethered_right_icon", true);
+ assertHasColumn(cursor, "untethered_case_icon", true);
+ assertHasColumn(cursor, "untethered_left_battery", true);
+ assertHasColumn(cursor, "untethered_right_battery", true);
+ assertHasColumn(cursor, "untethered_case_battery", true);
+ assertHasColumn(cursor, "untethered_left_charging", true);
+ assertHasColumn(cursor, "untethered_right_charging", true);
+ assertHasColumn(cursor, "untethered_case_charging", true);
+
+ while (cursor.moveToNext()) {
+ // Check whether metadata data type are blob
+ assertColumnBlob(cursor, "manufacturer_name");
+ assertColumnBlob(cursor, "model_name");
+ assertColumnBlob(cursor, "software_version");
+ assertColumnBlob(cursor, "hardware_version");
+ assertColumnBlob(cursor, "companion_app");
+ assertColumnBlob(cursor, "main_icon");
+ assertColumnBlob(cursor, "is_untethered_headset");
+ assertColumnBlob(cursor, "untethered_left_icon");
+ assertColumnBlob(cursor, "untethered_right_icon");
+ assertColumnBlob(cursor, "untethered_case_icon");
+ assertColumnBlob(cursor, "untethered_left_battery");
+ assertColumnBlob(cursor, "untethered_right_battery");
+ assertColumnBlob(cursor, "untethered_case_battery");
+ assertColumnBlob(cursor, "untethered_left_charging");
+ assertColumnBlob(cursor, "untethered_right_charging");
+ assertColumnBlob(cursor, "untethered_case_charging");
+
+ // Check whether metadata values are migrated to version 102 successfully
+ assertColumnBlobData(cursor, "manufacturer_name", testString.getBytes());
+ assertColumnBlobData(cursor, "model_name", testString.getBytes());
+ assertColumnBlobData(cursor, "software_version", testString.getBytes());
+ assertColumnBlobData(cursor, "hardware_version", testString.getBytes());
+ assertColumnBlobData(cursor, "companion_app", testString.getBytes());
+ assertColumnBlobData(cursor, "main_icon", testString.getBytes());
+ assertColumnBlobData(cursor, "is_untethered_headset", testString.getBytes());
+ assertColumnBlobData(cursor, "untethered_left_icon", testString.getBytes());
+ assertColumnBlobData(cursor, "untethered_right_icon", testString.getBytes());
+ assertColumnBlobData(cursor, "untethered_case_icon", testString.getBytes());
+ assertColumnBlobData(cursor, "untethered_left_battery", testString.getBytes());
+ assertColumnBlobData(cursor, "untethered_right_battery", testString.getBytes());
+ assertColumnBlobData(cursor, "untethered_case_battery", testString.getBytes());
+ assertColumnBlobData(cursor, "untethered_left_charging", testString.getBytes());
+ assertColumnBlobData(cursor, "untethered_right_charging", testString.getBytes());
+ assertColumnBlobData(cursor, "untethered_case_charging", testString.getBytes());
+ }
+ }
+
+ /**
+ * Helper function to check whether the database has the expected column
+ */
+ void assertHasColumn(Cursor cursor, String columnName, boolean hasColumn) {
+ if (hasColumn) {
+ Assert.assertThat(cursor.getColumnIndex(columnName), CoreMatchers.not(-1));
+ } else {
+ Assert.assertThat(cursor.getColumnIndex(columnName), CoreMatchers.is(-1));
+ }
+ }
+
+ /**
+ * Helper function to check whether the column data type is BLOB
+ */
+ void assertColumnBlob(Cursor cursor, String columnName) {
+ Assert.assertThat(cursor.getType(cursor.getColumnIndex(columnName)),
+ CoreMatchers.is(Cursor.FIELD_TYPE_BLOB));
+ }
+
+ /**
+ * Helper function to check the BLOB data in a column is expected
+ */
+ void assertColumnBlobData(Cursor cursor, String columnName, byte[] data) {
+ Assert.assertThat(cursor.getBlob(cursor.getColumnIndex(columnName)),
+ CoreMatchers.is(data));
+ }
+
+ void restartDatabaseManagerHelper() {
+ Metadata data = new Metadata(LOCAL_STORAGE);
+ data.migrated = true;
+ mDatabase.insert(data);
+
+ mDatabaseManager.cleanup();
+ mDatabaseManager.start(mDatabase);
+ // Wait for handler thread finish its task.
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+
+ // Remove local storage
+ mDatabaseManager.mMetadataCache.remove(LOCAL_STORAGE);
+ mDatabase.delete(LOCAL_STORAGE);
+ }
+
+ void testSetGetProfilePriorityCase(boolean stored, int priority, int expectedPriority,
+ boolean expectedSetResult) {
+ if (stored) {
+ Metadata data = new Metadata(TEST_BT_ADDR);
+ mDatabaseManager.mMetadataCache.put(TEST_BT_ADDR, data);
+ mDatabase.insert(data);
+ }
+ Assert.assertEquals(expectedSetResult,
+ mDatabaseManager.setProfilePriority(mTestDevice,
+ BluetoothProfile.HEADSET, priority));
+ Assert.assertEquals(expectedPriority,
+ mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.HEADSET));
+ // Wait for database update
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+
+ List<Metadata> list = mDatabase.load();
+
+ // Check number of metadata in the database
+ if (!stored) {
+ if (priority != BluetoothProfile.PRIORITY_OFF
+ && priority != BluetoothProfile.PRIORITY_ON
+ && priority != BluetoothProfile.PRIORITY_AUTO_CONNECT) {
+ // Database won't be updated
+ Assert.assertEquals(0, list.size());
+ return;
+ }
+ }
+ Assert.assertEquals(1, list.size());
+
+ // Check whether the device is in database
+ restartDatabaseManagerHelper();
+ Assert.assertEquals(expectedPriority,
+ mDatabaseManager.getProfilePriority(mTestDevice, BluetoothProfile.HEADSET));
+
+ mDatabase.deleteAll();
+ // Wait for clear database
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+ mDatabaseManager.mMetadataCache.clear();
+ }
+
+ void testSetGetA2dpOptionalCodecsCase(int test, boolean stored, int value, int expectedValue) {
+ if (stored) {
+ Metadata data = new Metadata(TEST_BT_ADDR);
+ mDatabaseManager.mMetadataCache.put(TEST_BT_ADDR, data);
+ mDatabase.insert(data);
+ }
+ if (test == A2DP_SUPPORT_OP_CODEC_TEST) {
+ mDatabaseManager.setA2dpSupportsOptionalCodecs(mTestDevice, value);
+ Assert.assertEquals(expectedValue,
+ mDatabaseManager.getA2dpSupportsOptionalCodecs(mTestDevice));
+ } else {
+ mDatabaseManager.setA2dpOptionalCodecsEnabled(mTestDevice, value);
+ Assert.assertEquals(expectedValue,
+ mDatabaseManager.getA2dpOptionalCodecsEnabled(mTestDevice));
+ }
+ // Wait for database update
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+
+ List<Metadata> list = mDatabase.load();
+
+ // Check number of metadata in the database
+ if (!stored) {
+ // Database won't be updated
+ Assert.assertEquals(0, list.size());
+ return;
+ }
+ Assert.assertEquals(1, list.size());
+
+ // Check whether the device is in database
+ restartDatabaseManagerHelper();
+ if (test == A2DP_SUPPORT_OP_CODEC_TEST) {
+ Assert.assertEquals(expectedValue,
+ mDatabaseManager.getA2dpSupportsOptionalCodecs(mTestDevice));
+ } else {
+ Assert.assertEquals(expectedValue,
+ mDatabaseManager.getA2dpOptionalCodecsEnabled(mTestDevice));
+ }
+
+ mDatabase.deleteAll();
+ // Wait for clear database
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+ mDatabaseManager.mMetadataCache.clear();
+ }
+
+ void testSetGetCustomMetaCase(boolean stored, int key, byte[] value, boolean expectedResult) {
+ byte[] testValue = "test value".getBytes();
+ int verifyTime = 1;
+ if (stored) {
+ Metadata data = new Metadata(TEST_BT_ADDR);
+ mDatabaseManager.mMetadataCache.put(TEST_BT_ADDR, data);
+ mDatabase.insert(data);
+ Assert.assertEquals(expectedResult,
+ mDatabaseManager.setCustomMeta(mTestDevice, key, testValue));
+ verify(mAdapterService).metadataChanged(TEST_BT_ADDR, key, testValue);
+ verifyTime++;
+ }
+ Assert.assertEquals(expectedResult,
+ mDatabaseManager.setCustomMeta(mTestDevice, key, value));
+ if (expectedResult) {
+ // Check for callback and get value
+ verify(mAdapterService, times(verifyTime)).metadataChanged(TEST_BT_ADDR, key, value);
+ Assert.assertEquals(value,
+ mDatabaseManager.getCustomMeta(mTestDevice, key));
+ } else {
+ Assert.assertNull(mDatabaseManager.getCustomMeta(mTestDevice, key));
+ return;
+ }
+ // Wait for database update
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+
+ // Check whether the value is saved in database
+ restartDatabaseManagerHelper();
+ Assert.assertArrayEquals(value,
+ mDatabaseManager.getCustomMeta(mTestDevice, key));
+
+ mDatabase.deleteAll();
+ // Wait for clear database
+ TestUtils.waitForLooperToFinishScheduledTask(mDatabaseManager.getHandlerLooper());
+ mDatabaseManager.mMetadataCache.clear();
+ }
+}
diff --git a/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/100.json b/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/100.json
new file mode 100644
index 0000000..c06fcc1
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/100.json
@@ -0,0 +1,219 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 100,
+ "identityHash": "1f6d4034a042816719ecd2f92b5bbc5c",
+ "entities": [
+ {
+ "tableName": "metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `a2dp_priority` INTEGER, `a2dp_sink_priority` INTEGER, `hfp_priority` INTEGER, `hfp_client_priority` INTEGER, `hid_host_priority` INTEGER, `pan_priority` INTEGER, `pbap_priority` INTEGER, `map_priority` INTEGER, `sap_priority` INTEGER, `hearing_aid_priority` INTEGER, `map_client_priority` INTEGER, `manufacturer_name` TEXT, `model_name` TEXT, `software_version` TEXT, `hardware_version` TEXT, `companion_app` TEXT, `main_icon` TEXT, `is_unthethered_headset` TEXT, `unthethered_left_icon` TEXT, `unthethered_right_icon` TEXT, `unthethered_case_icon` TEXT, `unthethered_left_battery` TEXT, `unthethered_right_battery` TEXT, `unthethered_case_battery` TEXT, `unthethered_left_charging` TEXT, `unthethered_right_charging` TEXT, `unthethered_case_charging` TEXT, `enhanced_settings_ui_uri` TEXT, PRIMARY KEY(`address`))",
+ "fields": [
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "migrated",
+ "columnName": "migrated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpSupportsOptionalCodecs",
+ "columnName": "a2dpSupportsOptionalCodecs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpOptionalCodecsEnabled",
+ "columnName": "a2dpOptionalCodecsEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profilePriorites.a2dp_priority",
+ "columnName": "a2dp_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorites.a2dp_sink_priority",
+ "columnName": "a2dp_sink_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorites.hfp_priority",
+ "columnName": "hfp_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorites.hfp_client_priority",
+ "columnName": "hfp_client_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorites.hid_host_priority",
+ "columnName": "hid_host_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorites.pan_priority",
+ "columnName": "pan_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorites.pbap_priority",
+ "columnName": "pbap_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorites.map_priority",
+ "columnName": "map_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorites.sap_priority",
+ "columnName": "sap_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorites.hearing_aid_priority",
+ "columnName": "hearing_aid_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorites.map_client_priority",
+ "columnName": "map_client_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.manufacturer_name",
+ "columnName": "manufacturer_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.model_name",
+ "columnName": "model_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.software_version",
+ "columnName": "software_version",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.hardware_version",
+ "columnName": "hardware_version",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.companion_app",
+ "columnName": "companion_app",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_icon",
+ "columnName": "main_icon",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.is_unthethered_headset",
+ "columnName": "is_unthethered_headset",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_left_icon",
+ "columnName": "unthethered_left_icon",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_right_icon",
+ "columnName": "unthethered_right_icon",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_case_icon",
+ "columnName": "unthethered_case_icon",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_left_battery",
+ "columnName": "unthethered_left_battery",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_right_battery",
+ "columnName": "unthethered_right_battery",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_case_battery",
+ "columnName": "unthethered_case_battery",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_left_charging",
+ "columnName": "unthethered_left_charging",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_right_charging",
+ "columnName": "unthethered_right_charging",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_case_charging",
+ "columnName": "unthethered_case_charging",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.enhanced_settings_ui_uri",
+ "columnName": "enhanced_settings_ui_uri",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "address"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"1f6d4034a042816719ecd2f92b5bbc5c\")"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/101.json b/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/101.json
new file mode 100644
index 0000000..99c0614
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/101.json
@@ -0,0 +1,225 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 101,
+ "identityHash": "628b9033f73db692c575e18a6aea63c3",
+ "entities": [
+ {
+ "tableName": "metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `a2dp_priority` INTEGER, `a2dp_sink_priority` INTEGER, `hfp_priority` INTEGER, `hfp_client_priority` INTEGER, `hid_host_priority` INTEGER, `pan_priority` INTEGER, `pbap_priority` INTEGER, `pbap_client_priority` INTEGER, `map_priority` INTEGER, `sap_priority` INTEGER, `hearing_aid_priority` INTEGER, `map_client_priority` INTEGER, `manufacturer_name` TEXT, `model_name` TEXT, `software_version` TEXT, `hardware_version` TEXT, `companion_app` TEXT, `main_icon` TEXT, `is_unthethered_headset` TEXT, `unthethered_left_icon` TEXT, `unthethered_right_icon` TEXT, `unthethered_case_icon` TEXT, `unthethered_left_battery` TEXT, `unthethered_right_battery` TEXT, `unthethered_case_battery` TEXT, `unthethered_left_charging` TEXT, `unthethered_right_charging` TEXT, `unthethered_case_charging` TEXT, `enhanced_settings_ui_uri` TEXT, PRIMARY KEY(`address`))",
+ "fields": [
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "migrated",
+ "columnName": "migrated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpSupportsOptionalCodecs",
+ "columnName": "a2dpSupportsOptionalCodecs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpOptionalCodecsEnabled",
+ "columnName": "a2dpOptionalCodecsEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profilePriorities.a2dp_priority",
+ "columnName": "a2dp_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.a2dp_sink_priority",
+ "columnName": "a2dp_sink_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.hfp_priority",
+ "columnName": "hfp_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.hfp_client_priority",
+ "columnName": "hfp_client_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.hid_host_priority",
+ "columnName": "hid_host_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.pan_priority",
+ "columnName": "pan_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.pbap_priority",
+ "columnName": "pbap_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.pbap_client_priority",
+ "columnName": "pbap_client_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.map_priority",
+ "columnName": "map_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.sap_priority",
+ "columnName": "sap_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.hearing_aid_priority",
+ "columnName": "hearing_aid_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.map_client_priority",
+ "columnName": "map_client_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.manufacturer_name",
+ "columnName": "manufacturer_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.model_name",
+ "columnName": "model_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.software_version",
+ "columnName": "software_version",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.hardware_version",
+ "columnName": "hardware_version",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.companion_app",
+ "columnName": "companion_app",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_icon",
+ "columnName": "main_icon",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.is_unthethered_headset",
+ "columnName": "is_unthethered_headset",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_left_icon",
+ "columnName": "unthethered_left_icon",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_right_icon",
+ "columnName": "unthethered_right_icon",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_case_icon",
+ "columnName": "unthethered_case_icon",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_left_battery",
+ "columnName": "unthethered_left_battery",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_right_battery",
+ "columnName": "unthethered_right_battery",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_case_battery",
+ "columnName": "unthethered_case_battery",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_left_charging",
+ "columnName": "unthethered_left_charging",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_right_charging",
+ "columnName": "unthethered_right_charging",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.unthethered_case_charging",
+ "columnName": "unthethered_case_charging",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.enhanced_settings_ui_uri",
+ "columnName": "enhanced_settings_ui_uri",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "address"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"628b9033f73db692c575e18a6aea63c3\")"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/102.json b/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/102.json
new file mode 100644
index 0000000..11d80c7
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/102.json
@@ -0,0 +1,225 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 102,
+ "identityHash": "fd763bdc49c947fc2b87c89531559d53",
+ "entities": [
+ {
+ "tableName": "metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `a2dp_priority` INTEGER, `a2dp_sink_priority` INTEGER, `hfp_priority` INTEGER, `hfp_client_priority` INTEGER, `hid_host_priority` INTEGER, `pan_priority` INTEGER, `pbap_priority` INTEGER, `pbap_client_priority` INTEGER, `map_priority` INTEGER, `sap_priority` INTEGER, `hearing_aid_priority` INTEGER, `map_client_priority` INTEGER, `manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, `hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, `is_untethered_headset` BLOB, `untethered_left_icon` BLOB, `untethered_right_icon` BLOB, `untethered_case_icon` BLOB, `untethered_left_battery` BLOB, `untethered_right_battery` BLOB, `untethered_case_battery` BLOB, `untethered_left_charging` BLOB, `untethered_right_charging` BLOB, `untethered_case_charging` BLOB, `enhanced_settings_ui_uri` BLOB, PRIMARY KEY(`address`))",
+ "fields": [
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "migrated",
+ "columnName": "migrated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpSupportsOptionalCodecs",
+ "columnName": "a2dpSupportsOptionalCodecs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpOptionalCodecsEnabled",
+ "columnName": "a2dpOptionalCodecsEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profilePriorities.a2dp_priority",
+ "columnName": "a2dp_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.a2dp_sink_priority",
+ "columnName": "a2dp_sink_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.hfp_priority",
+ "columnName": "hfp_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.hfp_client_priority",
+ "columnName": "hfp_client_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.hid_host_priority",
+ "columnName": "hid_host_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.pan_priority",
+ "columnName": "pan_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.pbap_priority",
+ "columnName": "pbap_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.pbap_client_priority",
+ "columnName": "pbap_client_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.map_priority",
+ "columnName": "map_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.sap_priority",
+ "columnName": "sap_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.hearing_aid_priority",
+ "columnName": "hearing_aid_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profilePriorities.map_client_priority",
+ "columnName": "map_client_priority",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.manufacturer_name",
+ "columnName": "manufacturer_name",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.model_name",
+ "columnName": "model_name",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.software_version",
+ "columnName": "software_version",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.hardware_version",
+ "columnName": "hardware_version",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.companion_app",
+ "columnName": "companion_app",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_icon",
+ "columnName": "main_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.is_untethered_headset",
+ "columnName": "is_untethered_headset",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_icon",
+ "columnName": "untethered_left_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_icon",
+ "columnName": "untethered_right_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_icon",
+ "columnName": "untethered_case_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_battery",
+ "columnName": "untethered_left_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_battery",
+ "columnName": "untethered_right_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_battery",
+ "columnName": "untethered_case_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_charging",
+ "columnName": "untethered_left_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_charging",
+ "columnName": "untethered_right_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_charging",
+ "columnName": "untethered_case_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.enhanced_settings_ui_uri",
+ "columnName": "enhanced_settings_ui_uri",
+ "affinity": "BLOB",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "address"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"fd763bdc49c947fc2b87c89531559d53\")"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java b/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
index 63b6d4d..6882b50 100644
--- a/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
@@ -3,10 +3,11 @@
import static org.mockito.Mockito.*;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
diff --git a/tests/unit/src/com/android/bluetooth/hdp/HealthServiceTest.java b/tests/unit/src/com/android/bluetooth/hdp/HealthServiceTest.java
deleted file mode 100644
index 04023fc..0000000
--- a/tests/unit/src/com/android/bluetooth/hdp/HealthServiceTest.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.bluetooth.hdp;
-
-import android.bluetooth.BluetoothAdapter;
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
-
-import com.android.bluetooth.R;
-import com.android.bluetooth.TestUtils;
-import com.android.bluetooth.btservice.AdapterService;
-
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Assume;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class HealthServiceTest {
- private HealthService mService = null;
- private BluetoothAdapter mAdapter = null;
- private Context mTargetContext;
-
- @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
-
- @Mock private AdapterService mAdapterService;
-
- @Before
- public void setUp() throws Exception {
- mTargetContext = InstrumentationRegistry.getTargetContext();
- Assume.assumeTrue("Ignore test when HealthService is not enabled",
- mTargetContext.getResources().getBoolean(R.bool.profile_supported_hdp));
- MockitoAnnotations.initMocks(this);
- TestUtils.setAdapterService(mAdapterService);
- TestUtils.startService(mServiceRule, HealthService.class);
- mService = HealthService.getHealthService();
- Assert.assertNotNull(mService);
- // Try getting the Bluetooth adapter
- mAdapter = BluetoothAdapter.getDefaultAdapter();
- Assert.assertNotNull(mAdapter);
- }
-
- @After
- public void tearDown() throws Exception {
- if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_hdp)) {
- return;
- }
- TestUtils.stopService(mServiceRule, HealthService.class);
- mService = HealthService.getHealthService();
- Assert.assertNull(mService);
- TestUtils.clearAdapterService(mAdapterService);
- }
-
- @Test
- public void testInitialize() {
- Assert.assertNotNull(HealthService.getHealthService());
- }
-
- @Test
- public void testRegisterAppConfiguration() {
- // Test registering a null config
- Assert.assertEquals(false, mService.registerAppConfiguration(null, null));
- }
-}
diff --git a/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java b/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java
index d4a978f..f5f9e76 100644
--- a/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java
@@ -30,14 +30,16 @@
import android.media.AudioManager;
import android.os.Looper;
import android.os.ParcelUuid;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
-import com.android.bluetooth.R;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
+import com.android.internal.R;
import org.junit.After;
import org.junit.Assert;
@@ -69,6 +71,7 @@
private BroadcastReceiver mHearingAidIntentReceiver;
@Mock private AdapterService mAdapterService;
+ @Mock private DatabaseManager mDatabaseManager;
@Mock private HearingAidNativeInterface mNativeInterface;
@Mock private AudioManager mAudioManager;
@@ -78,7 +81,8 @@
public void setUp() throws Exception {
mTargetContext = InstrumentationRegistry.getTargetContext();
Assume.assumeTrue("Ignore test when HearingAidService is not enabled",
- mTargetContext.getResources().getBoolean(R.bool.profile_supported_hearing_aid));
+ mTargetContext.getResources().getBoolean(
+ R.bool.config_hearing_aid_profile_supported));
// Set up mocks and test assets
MockitoAnnotations.initMocks(this);
@@ -111,20 +115,16 @@
mDeviceQueueMap.put(mLeftDevice, new LinkedBlockingQueue<>());
mDeviceQueueMap.put(mRightDevice, new LinkedBlockingQueue<>());
mDeviceQueueMap.put(mSingleDevice, new LinkedBlockingQueue<>());
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_UNDEFINED);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_UNDEFINED);
- mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_UNDEFINED);
doReturn(BluetoothDevice.BOND_BONDED).when(mAdapterService)
.getBondState(any(BluetoothDevice.class));
doReturn(new ParcelUuid[]{BluetoothUuid.HearingAid}).when(mAdapterService)
.getRemoteUuids(any(BluetoothDevice.class));
- HearingAidService.sConnectTimeoutForEachSideMs = 1000;
- HearingAidService.sCheckWhitelistTimeoutMs = 2000;
}
@After
public void tearDown() throws Exception {
- if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_hearing_aid)) {
+ if (!mTargetContext.getResources().getBoolean(
+ R.bool.config_hearing_aid_profile_supported)) {
return;
}
stopService();
@@ -216,22 +216,30 @@
*/
@Test
public void testGetSetPriority() {
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
Assert.assertEquals("Initial device priority",
BluetoothProfile.PRIORITY_UNDEFINED,
mService.getPriority(mLeftDevice));
- Assert.assertTrue(mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_OFF));
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
Assert.assertEquals("Setting device priority to PRIORITY_OFF",
BluetoothProfile.PRIORITY_OFF,
mService.getPriority(mLeftDevice));
- Assert.assertTrue(mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON));
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
Assert.assertEquals("Setting device priority to PRIORITY_ON",
BluetoothProfile.PRIORITY_ON,
mService.getPriority(mLeftDevice));
- Assert.assertTrue(mService.setPriority(mLeftDevice,
- BluetoothProfile.PRIORITY_AUTO_CONNECT));
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_AUTO_CONNECT);
Assert.assertEquals("Setting device priority to PRIORITY_AUTO_CONNECT",
BluetoothProfile.PRIORITY_AUTO_CONNECT,
mService.getPriority(mLeftDevice));
@@ -284,8 +292,6 @@
badBondState, BluetoothProfile.PRIORITY_AUTO_CONNECT, false);
testOkToConnectCase(mSingleDevice,
badBondState, badPriorityValue, false);
- // Restore prirority to undefined for this test device
- Assert.assertTrue(mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_UNDEFINED));
}
/**
@@ -294,9 +300,13 @@
@Test
public void testOutgoingConnectMissingHearingAidUuid() {
// Update the device priority so okToConnect() returns true
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_OFF);
- mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_OFF);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mRightDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
+ when(mDatabaseManager.getProfilePriority(mSingleDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
doReturn(true).when(mNativeInterface).connectHearingAid(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).disconnectHearingAid(any(BluetoothDevice.class));
@@ -317,7 +327,9 @@
doReturn(true).when(mNativeInterface).disconnectHearingAid(any(BluetoothDevice.class));
// Set the device priority to PRIORITY_OFF so connect() should fail
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_OFF);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
// Send a connect request
Assert.assertFalse("Connect expected to fail", mService.connect(mLeftDevice));
@@ -329,9 +341,13 @@
@Test
public void testOutgoingConnectTimeout() {
// Update the device priority so okToConnect() returns true
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_OFF);
- mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_OFF);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mRightDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
+ when(mDatabaseManager.getProfilePriority(mSingleDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
doReturn(true).when(mNativeInterface).connectHearingAid(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).disconnectHearingAid(any(BluetoothDevice.class));
@@ -345,8 +361,7 @@
mService.getConnectionState(mLeftDevice));
// Verify the connection state broadcast, and that we are in Disconnected state
- verifyConnectionStateIntent(HearingAidService.sConnectTimeoutForEachSideMs * 3,
- mLeftDevice,
+ verifyConnectionStateIntent(HearingAidStateMachine.sConnectTimeoutMs * 2, mLeftDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTING);
Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
@@ -361,9 +376,13 @@
// Update hiSyncId map
getHiSyncIdFromNative();
// Update the device priority so okToConnect() returns true
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_OFF);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mRightDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mSingleDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
doReturn(true).when(mNativeInterface).connectHearingAid(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).disconnectHearingAid(any(BluetoothDevice.class));
@@ -389,9 +408,13 @@
// Update hiSyncId map
getHiSyncIdFromNative();
// Update the device priority so okToConnect() returns true
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mRightDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mSingleDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
doReturn(true).when(mNativeInterface).connectHearingAid(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).disconnectHearingAid(any(BluetoothDevice.class));
@@ -449,9 +472,13 @@
// Update hiSyncId map
getHiSyncIdFromNative();
// Update the device priority so okToConnect() returns true
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_OFF);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mRightDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mSingleDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
doReturn(true).when(mNativeInterface).connectHearingAid(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).disconnectHearingAid(any(BluetoothDevice.class));
@@ -562,9 +589,13 @@
@Test
public void testCreateStateMachineStackEvents() {
// Update the device priority so okToConnect() returns true
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_OFF);
- mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_OFF);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mRightDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
+ when(mDatabaseManager.getProfilePriority(mSingleDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
doReturn(true).when(mNativeInterface).connectHearingAid(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).disconnectHearingAid(any(BluetoothDevice.class));
@@ -623,9 +654,13 @@
@Test
public void testDeleteStateMachineUnbondEvents() {
// Update the device priority so okToConnect() returns true
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_OFF);
- mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_OFF);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mRightDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
+ when(mDatabaseManager.getProfilePriority(mSingleDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
doReturn(true).when(mNativeInterface).connectHearingAid(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).disconnectHearingAid(any(BluetoothDevice.class));
@@ -680,9 +715,13 @@
@Test
public void testDeleteStateMachineDisconnectEvents() {
// Update the device priority so okToConnect() returns true
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_OFF);
- mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_OFF);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mRightDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
+ when(mDatabaseManager.getProfilePriority(mSingleDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
doReturn(true).when(mNativeInterface).connectHearingAid(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).disconnectHearingAid(any(BluetoothDevice.class));
@@ -725,9 +764,13 @@
// Update hiSyncId map
getHiSyncIdFromNative();
// Update the device priority so okToConnect() returns true
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_OFF);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mRightDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mSingleDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_OFF);
generateConnectionMessageFromNative(mRightDevice, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
@@ -755,9 +798,13 @@
// Update hiSyncId map
getHiSyncIdFromNative();
// Update the device priority so okToConnect() returns true
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mRightDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mSingleDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
generateConnectionMessageFromNative(mRightDevice, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
@@ -784,8 +831,11 @@
@Test
public void firstTimeConnection_shouldConnectToBothDevices() {
// Update the device priority so okToConnect() returns true
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mRightDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
doReturn(true).when(mNativeInterface).connectHearingAid(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).disconnectHearingAid(any(BluetoothDevice.class));
// Send a connect request for left device
@@ -864,9 +914,13 @@
@Test
public void getHiSyncId_afterFirstDeviceConnected() {
// Update the device priority so okToConnect() returns true
- mService.setPriority(mLeftDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mRightDevice, BluetoothProfile.PRIORITY_ON);
- mService.setPriority(mSingleDevice, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(mLeftDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mRightDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
+ when(mDatabaseManager.getProfilePriority(mSingleDevice, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
doReturn(true).when(mNativeInterface).connectHearingAid(any(BluetoothDevice.class));
doReturn(true).when(mNativeInterface).disconnectHearingAid(any(BluetoothDevice.class));
// Send a connect request
@@ -952,7 +1006,9 @@
List<BluetoothDevice> prevConnectedDevices = mService.getConnectedDevices();
// Update the device priority so okToConnect() returns true
- mService.setPriority(device, BluetoothProfile.PRIORITY_ON);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(device, BluetoothProfile.HEARING_AID))
+ .thenReturn(BluetoothProfile.PRIORITY_ON);
doReturn(true).when(mNativeInterface).connectHearingAid(device);
doReturn(true).when(mNativeInterface).disconnectHearingAid(device);
@@ -1019,7 +1075,9 @@
private void testOkToConnectCase(BluetoothDevice device, int bondState, int priority,
boolean expected) {
doReturn(bondState).when(mAdapterService).getBondState(device);
- Assert.assertTrue(mService.setPriority(device, priority));
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(device, BluetoothProfile.HEARING_AID))
+ .thenReturn(priority);
Assert.assertEquals(expected, mService.okToConnect(device));
}
diff --git a/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidStateMachineTest.java
index 474861e..2512c5e 100644
--- a/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidStateMachineTest.java
@@ -24,13 +24,14 @@
import android.content.Context;
import android.content.Intent;
import android.os.HandlerThread;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
-import com.android.bluetooth.R;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.internal.R;
import org.hamcrest.core.IsInstanceOf;
import org.junit.After;
@@ -61,7 +62,8 @@
public void setUp() throws Exception {
mTargetContext = InstrumentationRegistry.getTargetContext();
Assume.assumeTrue("Ignore test when HearingAidService is not enabled",
- mTargetContext.getResources().getBoolean(R.bool.profile_supported_hearing_aid));
+ mTargetContext.getResources().getBoolean(
+ R.bool.config_hearing_aid_profile_supported));
// Set up mocks and test assets
MockitoAnnotations.initMocks(this);
TestUtils.setAdapterService(mAdapterService);
@@ -77,16 +79,14 @@
mHearingAidStateMachine = new HearingAidStateMachine(mTestDevice, mHearingAidService,
mHearingAidNativeInterface, mHandlerThread.getLooper());
// Override the timeout value to speed up the test
- mHearingAidStateMachine.sConnectTimeoutMs = 1000;
- mHearingAidStateMachine.sDisconnectTimeoutMs = 1000;
- HearingAidService.sConnectTimeoutForEachSideMs = 1000;
- HearingAidService.sCheckWhitelistTimeoutMs = 2000;
+ mHearingAidStateMachine.sConnectTimeoutMs = 1000; // 1s
mHearingAidStateMachine.start();
}
@After
public void tearDown() throws Exception {
- if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_hearing_aid)) {
+ if (!mTargetContext.getResources().getBoolean(
+ R.bool.config_hearing_aid_profile_supported)) {
return;
}
mHearingAidStateMachine.doQuit();
@@ -186,6 +186,7 @@
BluetoothDevice.class));
doReturn(true).when(mHearingAidNativeInterface).disconnectHearingAid(any(
BluetoothDevice.class));
+ when(mHearingAidService.isConnectedPeerDevices(mTestDevice)).thenReturn(true);
// Send a connect request
mHearingAidStateMachine.sendMessage(HearingAidStateMachine.CONNECT, mTestDevice);
@@ -225,6 +226,7 @@
BluetoothDevice.class));
doReturn(true).when(mHearingAidNativeInterface).disconnectHearingAid(any(
BluetoothDevice.class));
+ when(mHearingAidService.isConnectedPeerDevices(mTestDevice)).thenReturn(true);
// Inject an event for when incoming connection is requested
HearingAidStackEvent connStCh =
@@ -255,5 +257,6 @@
// Check that we are in Disconnected state
Assert.assertThat(mHearingAidStateMachine.getCurrentState(),
IsInstanceOf.instanceOf(HearingAidStateMachine.Disconnected.class));
+ verify(mHearingAidNativeInterface).addToWhiteList(eq(mTestDevice));
}
}
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetPhoneStateTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetPhoneStateTest.java
index 865a215..f32f96f 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetPhoneStateTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetPhoneStateTest.java
@@ -24,12 +24,13 @@
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.ServiceManager;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
import android.telephony.PhoneStateListener;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
import com.android.bluetooth.TestUtils;
import com.android.internal.telephony.ISub;
@@ -72,6 +73,7 @@
// Stub other methods
when(mHeadsetService.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(
mTelephonyManager);
+ when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager);
when(mHeadsetService.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE)).thenReturn(
mSubscriptionManager);
mHandlerThread = new HandlerThread("HeadsetStateMachineTestHandlerThread");
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
index 39249a9..ab8bdd6 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
@@ -40,17 +40,19 @@
import android.os.ParcelUuid;
import android.os.PowerManager;
import android.os.RemoteException;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.espresso.intent.Intents;
-import android.support.test.espresso.intent.matcher.IntentMatchers;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
import android.telecom.PhoneAccount;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.espresso.intent.Intents;
+import androidx.test.espresso.intent.matcher.IntentMatchers;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
import org.hamcrest.Matchers;
import org.junit.After;
@@ -85,6 +87,7 @@
private static final int MAX_HEADSET_CONNECTIONS = 5;
private static final ParcelUuid[] FAKE_HEADSET_UUID = {BluetoothUuid.Handsfree};
private static final String TEST_PHONE_NUMBER = "1234567890";
+ private static final String TEST_CALLER_ID = "Test Name";
@Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
@@ -145,6 +148,7 @@
@Spy private HeadsetObjectsFactory mObjectsFactory = HeadsetObjectsFactory.getInstance();
@Mock private AdapterService mAdapterService;
+ @Mock private DatabaseManager mDatabaseManager;
@Mock private HeadsetSystemInterface mSystemInterface;
@Mock private AudioManager mAudioManager;
@Mock private HeadsetPhoneState mPhoneState;
@@ -275,6 +279,9 @@
@Test
public void testConnectFromApi() {
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(device, BluetoothProfile.HEADSET))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
mBondedDevices.add(device);
Assert.assertTrue(mHeadsetService.connect(device));
verify(mObjectsFactory).makeStateMachine(device,
@@ -316,6 +323,9 @@
@Test
public void testUnbondDevice_disconnectBeforeUnbond() {
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(device, BluetoothProfile.HEADSET))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
mBondedDevices.add(device);
Assert.assertTrue(mHeadsetService.connect(device));
verify(mObjectsFactory).makeStateMachine(device,
@@ -357,6 +367,9 @@
@Test
public void testUnbondDevice_disconnectAfterUnbond() {
BluetoothDevice device = TestUtils.getTestDevice(mAdapter, 0);
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(device, BluetoothProfile.HEADSET))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
mBondedDevices.add(device);
Assert.assertTrue(mHeadsetService.connect(device));
verify(mObjectsFactory).makeStateMachine(device,
@@ -473,12 +486,12 @@
verifyVirtualCallStartSequenceInvocations(connectedDevices);
// Virtual call should be preempted by telecom call
mHeadsetServiceBinder.phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_INCOMING,
- TEST_PHONE_NUMBER, 128);
+ TEST_PHONE_NUMBER, 128, "");
Assert.assertFalse(mHeadsetService.isVirtualCallStarted());
verifyVirtualCallStopSequenceInvocations(connectedDevices);
verifyCallStateToNativeInvocation(
new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_INCOMING,
- TEST_PHONE_NUMBER, 128), connectedDevices);
+ TEST_PHONE_NUMBER, 128, ""), connectedDevices);
}
/**
@@ -575,19 +588,19 @@
IntentMatchers.hasData(dialOutUri)), Intents.times(1));
// Verify that phone state update confirms the dial out event
mHeadsetServiceBinder.phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_DIALING,
- TEST_PHONE_NUMBER, 128);
+ TEST_PHONE_NUMBER, 128, "");
HeadsetCallState dialingCallState =
new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_DIALING,
- TEST_PHONE_NUMBER, 128);
+ TEST_PHONE_NUMBER, 128, "");
verifyCallStateToNativeInvocation(dialingCallState, connectedDevices);
verify(mNativeInterface).atResponseCode(dialingOutDevice,
HeadsetHalConstants.AT_RESPONSE_OK, 0);
// Verify that IDLE phone state clears the dialing out flag
mHeadsetServiceBinder.phoneStateChanged(1, 0, HeadsetHalConstants.CALL_STATE_IDLE,
- TEST_PHONE_NUMBER, 128);
+ TEST_PHONE_NUMBER, 128, "");
HeadsetCallState activeCallState =
new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_DIALING,
- TEST_PHONE_NUMBER, 128);
+ TEST_PHONE_NUMBER, 128, "");
verifyCallStateToNativeInvocation(activeCallState, connectedDevices);
Assert.assertFalse(mHeadsetService.hasDeviceInitiatedDialingOut());
}
@@ -1072,6 +1085,32 @@
verifyNoMoreInteractions(mNativeInterface);
}
+ /**
+ * Test to verify the call state and caller information are correctly delivered
+ * {@link BluetoothHeadset#phoneStateChanged(int, int, int, String, int, String, boolean)}
+ */
+ @Test
+ public void testPhoneStateChangedWithIncomingCallState() throws RemoteException {
+ // Connect HF
+ for (int i = 0; i < MAX_HEADSET_CONNECTIONS; ++i) {
+ BluetoothDevice device = TestUtils.getTestDevice(mAdapter, i);
+ connectTestDevice(device);
+ Assert.assertThat(mHeadsetServiceBinder.getConnectedDevices(),
+ Matchers.containsInAnyOrder(mBondedDevices.toArray()));
+ Assert.assertThat(mHeadsetServiceBinder.getDevicesMatchingConnectionStates(
+ new int[]{BluetoothProfile.STATE_CONNECTED}),
+ Matchers.containsInAnyOrder(mBondedDevices.toArray()));
+ }
+ List<BluetoothDevice> connectedDevices = mHeadsetServiceBinder.getConnectedDevices();
+ Assert.assertThat(connectedDevices, Matchers.containsInAnyOrder(mBondedDevices.toArray()));
+ // Incoming call update by telecom
+ mHeadsetServiceBinder.phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_INCOMING,
+ TEST_PHONE_NUMBER, 128, TEST_CALLER_ID);
+ HeadsetCallState incomingCallState = new HeadsetCallState(0, 0,
+ HeadsetHalConstants.CALL_STATE_INCOMING, TEST_PHONE_NUMBER, 128, TEST_CALLER_ID);
+ verifyCallStateToNativeInvocation(incomingCallState, connectedDevices);
+ }
+
private void startVoiceRecognitionFromHf(BluetoothDevice device) {
// Start voice recognition
HeadsetStackEvent startVrEvent =
@@ -1114,6 +1153,9 @@
}
private void connectTestDevice(BluetoothDevice device) {
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(device, BluetoothProfile.HEADSET))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
// Make device bonded
mBondedDevices.add(device);
// Use connecting event to indicate that device is connecting
@@ -1178,19 +1220,19 @@
private void verifyVirtualCallStartSequenceInvocations(List<BluetoothDevice> connectedDevices) {
// Do not verify HeadsetPhoneState changes as it is verified in HeadsetServiceTest
verifyCallStateToNativeInvocation(
- new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_DIALING, "", 0),
+ new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_DIALING, "", 0, ""),
connectedDevices);
verifyCallStateToNativeInvocation(
- new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_ALERTING, "", 0),
+ new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_ALERTING, "", 0, ""),
connectedDevices);
verifyCallStateToNativeInvocation(
- new HeadsetCallState(1, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0),
+ new HeadsetCallState(1, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0, ""),
connectedDevices);
}
private void verifyVirtualCallStopSequenceInvocations(List<BluetoothDevice> connectedDevices) {
verifyCallStateToNativeInvocation(
- new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0),
+ new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0, ""),
connectedDevices);
}
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
index e5395f2..57a9a2f 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
@@ -29,14 +29,16 @@
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
import org.hamcrest.Matchers;
import org.junit.After;
@@ -79,6 +81,7 @@
@Spy private HeadsetObjectsFactory mObjectsFactory = HeadsetObjectsFactory.getInstance();
@Mock private AdapterService mAdapterService;
+ @Mock private DatabaseManager mDatabaseManager;
@Mock private HeadsetSystemInterface mSystemInterface;
@Mock private AudioManager mAudioManager;
@Mock private HeadsetPhoneState mPhoneState;
@@ -147,6 +150,8 @@
mHeadsetServiceBinder = (IBluetoothHeadset.Stub) mHeadsetService.initBinder();
Assert.assertNotNull(mHeadsetServiceBinder);
mHeadsetServiceBinder.setForceScoAudio(true);
+ // Mock database for getPriority()
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
}
@After
@@ -227,9 +232,6 @@
testOkToAcceptConnectionCase(mCurrentDevice, badBondState,
BluetoothProfile.PRIORITY_AUTO_CONNECT, false);
testOkToAcceptConnectionCase(mCurrentDevice, badBondState, badPriorityValue, false);
- // Restore prirority to undefined for this test device
- Assert.assertTrue(
- mHeadsetService.setPriority(mCurrentDevice, BluetoothProfile.PRIORITY_UNDEFINED));
}
/**
@@ -239,6 +241,9 @@
*/
@Test
public void testConnectDevice_connectDeviceBelowLimit() {
+ when(mDatabaseManager.getProfilePriority(any(BluetoothDevice.class),
+ eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
Assert.assertTrue(mHeadsetService.connect(mCurrentDevice));
verify(mObjectsFactory).makeStateMachine(mCurrentDevice,
@@ -348,6 +353,9 @@
@Test
public void testConnectDevice_connectDeviceAboveLimit() {
ArrayList<BluetoothDevice> connectedDevices = new ArrayList<>();
+ when(mDatabaseManager.getProfilePriority(any(BluetoothDevice.class),
+ eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
for (int i = 0; i < MAX_HEADSET_CONNECTIONS; ++i) {
mCurrentDevice = TestUtils.getTestDevice(mAdapter, i);
Assert.assertTrue(mHeadsetService.connect(mCurrentDevice));
@@ -397,6 +405,9 @@
*/
@Test
public void testConnectAudio_withOneDevice() {
+ when(mDatabaseManager.getProfilePriority(any(BluetoothDevice.class),
+ eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
Assert.assertTrue(mHeadsetService.connect(mCurrentDevice));
verify(mObjectsFactory).makeStateMachine(mCurrentDevice,
@@ -446,6 +457,9 @@
@Test
public void testConnectAudio_withMultipleDevices() {
ArrayList<BluetoothDevice> connectedDevices = new ArrayList<>();
+ when(mDatabaseManager.getProfilePriority(any(BluetoothDevice.class),
+ eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
for (int i = 0; i < MAX_HEADSET_CONNECTIONS; ++i) {
mCurrentDevice = TestUtils.getTestDevice(mAdapter, i);
Assert.assertTrue(mHeadsetService.connect(mCurrentDevice));
@@ -519,6 +533,9 @@
@Test
public void testConnectAudio_connectTwoAudioChannelsShouldFail() {
ArrayList<BluetoothDevice> connectedDevices = new ArrayList<>();
+ when(mDatabaseManager.getProfilePriority(any(BluetoothDevice.class),
+ eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
for (int i = 0; i < MAX_HEADSET_CONNECTIONS; ++i) {
mCurrentDevice = TestUtils.getTestDevice(mAdapter, i);
Assert.assertTrue(mHeadsetService.connect(mCurrentDevice));
@@ -588,6 +605,9 @@
@Test
public void testConnectAudio_firstConnectedAudioDevice() {
ArrayList<BluetoothDevice> connectedDevices = new ArrayList<>();
+ when(mDatabaseManager.getProfilePriority(any(BluetoothDevice.class),
+ eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
doAnswer(invocation -> {
BluetoothDevice[] devicesArray = new BluetoothDevice[connectedDevices.size()];
return connectedDevices.toArray(devicesArray);
@@ -652,10 +672,13 @@
*/
@Test
public void testConnectAudio_deviceDisconnected() {
+ when(mDatabaseManager.getProfilePriority(any(BluetoothDevice.class),
+ eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
HeadsetCallState headsetCallState =
new HeadsetCallState(1, 0, HeadsetHalConstants.CALL_STATE_ALERTING,
- TEST_PHONE_NUMBER, 128);
+ TEST_PHONE_NUMBER, 128, "");
Assert.assertTrue(mHeadsetService.connect(mCurrentDevice));
verify(mObjectsFactory).makeStateMachine(mCurrentDevice,
mHeadsetService.getStateMachinesThreadLooper(), mHeadsetService, mAdapterService,
@@ -687,10 +710,10 @@
public void testPhoneStateChange_noDeviceSaveState() throws RemoteException {
HeadsetCallState headsetCallState =
new HeadsetCallState(1, 0, HeadsetHalConstants.CALL_STATE_ALERTING,
- TEST_PHONE_NUMBER, 128);
+ TEST_PHONE_NUMBER, 128, "");
mHeadsetServiceBinder.phoneStateChanged(headsetCallState.mNumActive,
headsetCallState.mNumHeld, headsetCallState.mCallState, headsetCallState.mNumber,
- headsetCallState.mType);
+ headsetCallState.mType, headsetCallState.mName);
HeadsetTestUtils.verifyPhoneStateChangeSetters(mPhoneState, headsetCallState,
ASYNC_CALL_TIMEOUT_MILLIS);
}
@@ -705,7 +728,10 @@
public void testPhoneStateChange_oneDeviceSaveState() throws RemoteException {
HeadsetCallState headsetCallState =
new HeadsetCallState(1, 0, HeadsetHalConstants.CALL_STATE_ALERTING,
- TEST_PHONE_NUMBER, 128);
+ TEST_PHONE_NUMBER, 128, "");
+ when(mDatabaseManager.getProfilePriority(any(BluetoothDevice.class),
+ eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
final ArrayList<BluetoothDevice> connectedDevices = new ArrayList<>();
// Connect one device
@@ -741,7 +767,7 @@
// Change phone state
mHeadsetServiceBinder.phoneStateChanged(headsetCallState.mNumActive,
headsetCallState.mNumHeld, headsetCallState.mCallState, headsetCallState.mNumber,
- headsetCallState.mType);
+ headsetCallState.mType, headsetCallState.mName);
// Make sure we notify device about this change
verify(mStateMachines.get(mCurrentDevice)).sendMessage(
HeadsetStateMachine.CALL_STATE_CHANGED, headsetCallState);
@@ -760,8 +786,11 @@
public void testPhoneStateChange_multipleDevicesSaveState() throws RemoteException {
HeadsetCallState headsetCallState =
new HeadsetCallState(1, 0, HeadsetHalConstants.CALL_STATE_ALERTING,
- TEST_PHONE_NUMBER, 128);
+ TEST_PHONE_NUMBER, 128, "");
final ArrayList<BluetoothDevice> connectedDevices = new ArrayList<>();
+ when(mDatabaseManager.getProfilePriority(any(BluetoothDevice.class),
+ eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
for (int i = 0; i < MAX_HEADSET_CONNECTIONS; ++i) {
mCurrentDevice = TestUtils.getTestDevice(mAdapter, i);
Assert.assertTrue(mHeadsetService.connect(mCurrentDevice));
@@ -801,7 +830,7 @@
// Change phone state
mHeadsetServiceBinder.phoneStateChanged(headsetCallState.mNumActive,
headsetCallState.mNumHeld, headsetCallState.mCallState, headsetCallState.mNumber,
- headsetCallState.mType);
+ headsetCallState.mType, headsetCallState.mName);
// Make sure we notify devices about this change
for (BluetoothDevice device : connectedDevices) {
verify(mStateMachines.get(device)).sendMessage(HeadsetStateMachine.CALL_STATE_CHANGED,
@@ -812,6 +841,47 @@
ASYNC_CALL_TIMEOUT_MILLIS);
}
+ /**
+ * Test that whether active device been removed after enable silence mode
+ */
+ @Test
+ public void testSetSilenceMode() {
+ when(mDatabaseManager.getProfilePriority(any(BluetoothDevice.class),
+ eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.PRIORITY_UNDEFINED);
+ for (int i = 0; i < 2; i++) {
+ mCurrentDevice = TestUtils.getTestDevice(mAdapter, i);
+ Assert.assertTrue(mHeadsetService.connect(mCurrentDevice));
+ when(mStateMachines.get(mCurrentDevice).getDevice()).thenReturn(mCurrentDevice);
+ when(mStateMachines.get(mCurrentDevice).getConnectionState()).thenReturn(
+ BluetoothProfile.STATE_CONNECTED);
+ when(mStateMachines.get(mCurrentDevice).setSilenceDevice(
+ anyBoolean())).thenReturn(true);
+ }
+ mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
+ BluetoothDevice otherDevice = TestUtils.getTestDevice(mAdapter, 1);
+
+ // Test whether active device been removed after enable silence mode.
+ Assert.assertTrue(mHeadsetService.setActiveDevice(mCurrentDevice));
+ Assert.assertEquals(mCurrentDevice, mHeadsetService.getActiveDevice());
+ Assert.assertTrue(mHeadsetService.setSilenceMode(mCurrentDevice, true));
+ Assert.assertNull(mHeadsetService.getActiveDevice());
+
+ // Test whether active device been resumed after disable silence mode.
+ Assert.assertTrue(mHeadsetService.setSilenceMode(mCurrentDevice, false));
+ Assert.assertEquals(mCurrentDevice, mHeadsetService.getActiveDevice());
+
+ // Test that active device should not be changed when silence a non-active device
+ Assert.assertTrue(mHeadsetService.setActiveDevice(mCurrentDevice));
+ Assert.assertEquals(mCurrentDevice, mHeadsetService.getActiveDevice());
+ Assert.assertTrue(mHeadsetService.setSilenceMode(otherDevice, true));
+ Assert.assertEquals(mCurrentDevice, mHeadsetService.getActiveDevice());
+
+ // Test that active device should not be changed when another device exits silence mode
+ Assert.assertTrue(mHeadsetService.setSilenceMode(otherDevice, false));
+ Assert.assertEquals(mCurrentDevice, mHeadsetService.getActiveDevice());
+ }
+
/*
* Helper function to test okToAcceptConnection() method
*
@@ -823,7 +893,8 @@
private void testOkToAcceptConnectionCase(BluetoothDevice device, int bondState, int priority,
boolean expected) {
doReturn(bondState).when(mAdapterService).getBondState(device);
- Assert.assertTrue(mHeadsetService.setPriority(device, priority));
+ when(mDatabaseManager.getProfilePriority(device, BluetoothProfile.HEADSET))
+ .thenReturn(priority);
Assert.assertEquals(expected, mHeadsetService.okToAcceptConnection(device));
}
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
index 80192a6..576d169 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
@@ -31,13 +31,14 @@
import android.os.HandlerThread;
import android.os.UserHandle;
import android.provider.CallLog;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
import android.telephony.PhoneStateListener;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
@@ -941,6 +942,87 @@
}
/**
+ * A test to verfiy that we correctly handles AT+BIND event with driver safety case from HF
+ */
+ @Test
+ public void testAtBindWithDriverSafetyEventWhenConnecting() {
+ setUpConnectingState();
+
+ String atString = "1";
+ mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT,
+ new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_BIND, atString, mTestDevice));
+ ArgumentCaptor<Intent> intentArgument = ArgumentCaptor.forClass(Intent.class);
+ verify(mHeadsetService, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).sendBroadcast(
+ intentArgument.capture(), eq(HeadsetService.BLUETOOTH_PERM));
+ verify(mHeadsetService, times(1)).sendBroadcast(any(), any());
+ Assert.assertEquals(mTestDevice, intentArgument.getValue().getExtra(
+ BluetoothDevice.EXTRA_DEVICE, null));
+ Assert.assertEquals(HeadsetHalConstants.HF_INDICATOR_ENHANCED_DRIVER_SAFETY,
+ intentArgument.getValue().getIntExtra(
+ BluetoothHeadset.EXTRA_HF_INDICATORS_IND_ID, -1));
+ Assert.assertEquals(-1, intentArgument.getValue().getIntExtra(
+ BluetoothHeadset.EXTRA_HF_INDICATORS_IND_VALUE, -2));
+ }
+
+ /**
+ * A test to verfiy that we correctly handles AT+BIND event with battery level case from HF
+ */
+ @Test
+ public void testAtBindEventWithBatteryLevelEventWhenConnecting() {
+ setUpConnectingState();
+
+ String atString = "2";
+ mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT,
+ new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_BIND, atString, mTestDevice));
+ ArgumentCaptor<Intent> intentArgument = ArgumentCaptor.forClass(Intent.class);
+ verify(mHeadsetService, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).sendBroadcast(
+ intentArgument.capture(), eq(HeadsetService.BLUETOOTH_PERM));
+ verify(mHeadsetService, times(1)).sendBroadcast(any(), any());
+ Assert.assertEquals(mTestDevice, intentArgument.getValue().getExtra(
+ BluetoothDevice.EXTRA_DEVICE, null));
+ Assert.assertEquals(HeadsetHalConstants.HF_INDICATOR_BATTERY_LEVEL_STATUS,
+ intentArgument.getValue().getIntExtra(
+ BluetoothHeadset.EXTRA_HF_INDICATORS_IND_ID, -1));
+ Assert.assertEquals(-1, intentArgument.getValue().getIntExtra(
+ BluetoothHeadset.EXTRA_HF_INDICATORS_IND_VALUE, -2));
+ }
+
+ /**
+ * A test to verfiy that we correctly handles AT+BIND event with error case from HF
+ */
+ @Test
+ public void testAtBindEventWithErrorEventWhenConnecting() {
+ setUpConnectingState();
+
+ String atString = "err,A,123,,1";
+ mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT,
+ new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_BIND, atString, mTestDevice));
+ ArgumentCaptor<Intent> intentArgument = ArgumentCaptor.forClass(Intent.class);
+ verify(mHeadsetService, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).sendBroadcast(
+ intentArgument.capture(), eq(HeadsetService.BLUETOOTH_PERM));
+ verify(mHeadsetService, times(1)).sendBroadcast(any(), any());
+ Assert.assertEquals(mTestDevice, intentArgument.getValue().getExtra(
+ BluetoothDevice.EXTRA_DEVICE, null));
+ Assert.assertEquals(HeadsetHalConstants.HF_INDICATOR_ENHANCED_DRIVER_SAFETY,
+ intentArgument.getValue().getIntExtra(
+ BluetoothHeadset.EXTRA_HF_INDICATORS_IND_ID, -1));
+ Assert.assertEquals(-1, intentArgument.getValue().getIntExtra(
+ BluetoothHeadset.EXTRA_HF_INDICATORS_IND_VALUE, -2));
+ }
+
+ /**
+ * A test to verify that we correctly set AG indicator mask when enter/exit silence mode
+ */
+ @Test
+ public void testSetSilenceDevice() {
+ doNothing().when(mPhoneState).listenForPhoneState(any(BluetoothDevice.class), anyInt());
+ mHeadsetStateMachine.setSilenceDevice(true);
+ mHeadsetStateMachine.setSilenceDevice(false);
+ verify(mPhoneState, times(2)).listenForPhoneState(mTestDevice,
+ PhoneStateListener.LISTEN_NONE);
+ }
+
+ /**
* Setup Connecting State
* @return number of times mHeadsetService.sendBroadcastAsUser() has been invoked
*/
diff --git a/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceTest.java b/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceTest.java
index 5826415..17afcc3 100644
--- a/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientServiceTest.java
@@ -18,10 +18,11 @@
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
diff --git a/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
index 334597b..b31c4e2 100644
--- a/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
@@ -5,6 +5,7 @@
import static org.mockito.Mockito.*;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAssignedNumbers;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHeadsetClientCall;
@@ -14,14 +15,17 @@
import android.content.res.Resources;
import android.media.AudioManager;
import android.os.HandlerThread;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.espresso.intent.matcher.IntentMatchers;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import android.os.Message;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.espresso.intent.matcher.IntentMatchers;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
+import com.android.bluetooth.Utils;
import org.hamcrest.core.AllOf;
import org.hamcrest.core.IsInstanceOf;
@@ -52,6 +56,8 @@
@Mock
private AudioManager mAudioManager;
+ private NativeInterface mNativeInterface;
+
private static final int STANDARD_WAIT_MILLIS = 1000;
private static final int QUERY_CURRENT_CALLS_WAIT_MILLIS = 2000;
private static final int QUERY_CURRENT_CALLS_TEST_WAIT_MILLIS = QUERY_CURRENT_CALLS_WAIT_MILLIS
@@ -68,10 +74,11 @@
when(mAudioManager.getStreamVolume(anyInt())).thenReturn(2);
when(mAudioManager.getStreamMaxVolume(anyInt())).thenReturn(10);
when(mAudioManager.getStreamMinVolume(anyInt())).thenReturn(1);
- when(mHeadsetClientService.getSystemService(Context.AUDIO_SERVICE)).thenReturn(
+ when(mHeadsetClientService.getAudioManager()).thenReturn(
mAudioManager);
when(mHeadsetClientService.getResources()).thenReturn(mMockHfpResources);
when(mMockHfpResources.getBoolean(R.bool.hfp_clcc_poll_during_call)).thenReturn(true);
+ mNativeInterface = spy(NativeInterface.getInstance());
// This line must be called to make sure relevant objects are initialized properly
mAdapter = BluetoothAdapter.getDefaultAdapter();
@@ -83,7 +90,8 @@
mHandlerThread.start();
// Manage looper execution in main test thread explicitly to guarantee timing consistency
mHeadsetClientStateMachine =
- new HeadsetClientStateMachine(mHeadsetClientService, mHandlerThread.getLooper());
+ new HeadsetClientStateMachine(mHeadsetClientService, mHandlerThread.getLooper(),
+ mNativeInterface);
mHeadsetClientStateMachine.start();
}
@@ -329,4 +337,227 @@
Assert.assertEquals(false, mHeadsetClientStateMachine.getInBandRing());
}
+
+ /* Utility function to simulate HfpClient is connected. */
+ private int setUpHfpClientConnection(int startBroadcastIndex) {
+ // Trigger an incoming connection is requested
+ StackEvent connStCh = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ connStCh.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED;
+ connStCh.device = mTestDevice;
+ mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, connStCh);
+ ArgumentCaptor<Intent> intentArgument = ArgumentCaptor.forClass(Intent.class);
+ verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS).times(startBroadcastIndex))
+ .sendBroadcast(intentArgument.capture(), anyString());
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
+ intentArgument.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+ startBroadcastIndex++;
+ return startBroadcastIndex;
+ }
+
+ /* Utility function to simulate SLC connection. */
+ private int setUpServiceLevelConnection(int startBroadcastIndex) {
+ // Trigger SLC connection
+ StackEvent slcEvent = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ slcEvent.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_SLC_CONNECTED;
+ slcEvent.valueInt2 = HeadsetClientHalConstants.PEER_FEAT_ECS;
+ slcEvent.device = mTestDevice;
+ mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, slcEvent);
+ ArgumentCaptor<Intent> intentArgument = ArgumentCaptor.forClass(Intent.class);
+ verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS).times(startBroadcastIndex))
+ .sendBroadcast(intentArgument.capture(), anyString());
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
+ intentArgument.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+ startBroadcastIndex++;
+ return startBroadcastIndex;
+ }
+
+ /* Utility function: supported AT command should lead to native call */
+ private void runSupportedVendorAtCommand(String atCommand, int vendorId) {
+ // Return true for priority.
+ when(mHeadsetClientService.getPriority(any(BluetoothDevice.class))).thenReturn(
+ BluetoothProfile.PRIORITY_ON);
+
+ int expectedBroadcastIndex = 1;
+
+ expectedBroadcastIndex = setUpHfpClientConnection(expectedBroadcastIndex);
+ expectedBroadcastIndex = setUpServiceLevelConnection(expectedBroadcastIndex);
+
+ Message msg = mHeadsetClientStateMachine.obtainMessage(
+ HeadsetClientStateMachine.SEND_VENDOR_AT_COMMAND, vendorId, 0, atCommand);
+ mHeadsetClientStateMachine.sendMessage(msg);
+
+ verify(mNativeInterface, timeout(STANDARD_WAIT_MILLIS).times(1)).sendATCmd(
+ Utils.getBytesFromAddress(mTestDevice.getAddress()),
+ HeadsetClientHalConstants.HANDSFREECLIENT_AT_CMD_VENDOR_SPECIFIC_CMD,
+ 0, 0, atCommand);
+ }
+
+ /**
+ * Test: supported vendor specific command: set operation
+ */
+ @LargeTest
+ @Test
+ public void testSupportedVendorAtCommandSet() {
+ int vendorId = BluetoothAssignedNumbers.APPLE;
+ String atCommand = "+XAPL=ABCD-1234-0100,100";
+ runSupportedVendorAtCommand(atCommand, vendorId);
+ }
+
+ /**
+ * Test: supported vendor specific command: read operation
+ */
+ @LargeTest
+ @Test
+ public void testSupportedVendorAtCommandRead() {
+ int vendorId = BluetoothAssignedNumbers.APPLE;
+ String atCommand = "+APLSIRI?";
+ runSupportedVendorAtCommand(atCommand, vendorId);
+ }
+
+ /* utility function: unsupported vendor specific command shall be filtered. */
+ public void runUnsupportedVendorAtCommand(String atCommand, int vendorId) {
+ // Return true for priority.
+ when(mHeadsetClientService.getPriority(any(BluetoothDevice.class))).thenReturn(
+ BluetoothProfile.PRIORITY_ON);
+
+ int expectedBroadcastIndex = 1;
+
+ expectedBroadcastIndex = setUpHfpClientConnection(expectedBroadcastIndex);
+ expectedBroadcastIndex = setUpServiceLevelConnection(expectedBroadcastIndex);
+
+ Message msg = mHeadsetClientStateMachine.obtainMessage(
+ HeadsetClientStateMachine.SEND_VENDOR_AT_COMMAND, vendorId, 0, atCommand);
+ mHeadsetClientStateMachine.sendMessage(msg);
+
+ verify(mNativeInterface, timeout(STANDARD_WAIT_MILLIS).times(0))
+ .sendATCmd(any(), anyInt(), anyInt(), anyInt(), any());
+ }
+
+ /**
+ * Test: unsupported vendor specific command shall be filtered: bad command code
+ */
+ @LargeTest
+ @Test
+ public void testUnsupportedVendorAtCommandBadCode() {
+ String atCommand = "+XAAPL=ABCD-1234-0100,100";
+ int vendorId = BluetoothAssignedNumbers.APPLE;
+ runUnsupportedVendorAtCommand(atCommand, vendorId);
+ }
+
+ /**
+ * Test: unsupported vendor specific command shall be filtered:
+ * no back to back command
+ */
+ @LargeTest
+ @Test
+ public void testUnsupportedVendorAtCommandBackToBack() {
+ String atCommand = "+XAPL=ABCD-1234-0100,100; +XAPL=ab";
+ int vendorId = BluetoothAssignedNumbers.APPLE;
+ runUnsupportedVendorAtCommand(atCommand, vendorId);
+ }
+
+ /* Utility test function: supported vendor specific event
+ * shall lead to broadcast intent
+ */
+ private void runSupportedVendorEvent(int vendorId, String vendorEventCode,
+ String vendorEventArgument) {
+ // Setup connection state machine to be in connected state
+ when(mHeadsetClientService.getPriority(any(BluetoothDevice.class))).thenReturn(
+ BluetoothProfile.PRIORITY_ON);
+ int expectedBroadcastIndex = 1;
+ expectedBroadcastIndex = setUpHfpClientConnection(expectedBroadcastIndex);
+ expectedBroadcastIndex = setUpServiceLevelConnection(expectedBroadcastIndex);
+
+ // Simulate a known event arrive
+ String vendorEvent = vendorEventCode + vendorEventArgument;
+ StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_UNKNOWN_EVENT);
+ event.device = mTestDevice;
+ event.valueString = vendorEvent;
+ mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, event);
+
+ // Validate broadcast intent
+ ArgumentCaptor<Intent> intentArgument = ArgumentCaptor.forClass(Intent.class);
+ verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS).times(expectedBroadcastIndex))
+ .sendBroadcast(intentArgument.capture(), anyString());
+ Assert.assertEquals(BluetoothHeadsetClient.ACTION_VENDOR_SPECIFIC_HEADSETCLIENT_EVENT,
+ intentArgument.getValue().getAction());
+ Assert.assertEquals(vendorId,
+ intentArgument.getValue().getIntExtra(BluetoothHeadsetClient.EXTRA_VENDOR_ID, -1));
+ Assert.assertEquals(vendorEventCode,
+ intentArgument.getValue().getStringExtra(
+ BluetoothHeadsetClient.EXTRA_VENDOR_EVENT_CODE));
+ Assert.assertEquals(vendorEvent,
+ intentArgument.getValue().getStringExtra(
+ BluetoothHeadsetClient.EXTRA_VENDOR_EVENT_FULL_ARGS));
+ }
+
+ /**
+ * Test: supported vendor specific response: response to read command
+ */
+ @LargeTest
+ @Test
+ public void testSupportedVendorEventReadResponse() {
+ final int vendorId = BluetoothAssignedNumbers.APPLE;
+ final String vendorResponseCode = "+XAPL=";
+ final String vendorResponseArgument = "iPhone,2";
+ runSupportedVendorEvent(vendorId, vendorResponseCode, vendorResponseArgument);
+ }
+
+ /**
+ * Test: supported vendor specific response: response to test command
+ */
+ @LargeTest
+ @Test
+ public void testSupportedVendorEventTestResponse() {
+ final int vendorId = BluetoothAssignedNumbers.APPLE;
+ final String vendorResponseCode = "+APLSIRI:";
+ final String vendorResponseArgumentWithSpace = " 2";
+ runSupportedVendorEvent(vendorId, vendorResponseCode, vendorResponseArgumentWithSpace);
+ }
+
+ /* Utility test function: unsupported vendor specific response shall be filtered out*/
+ public void runUnsupportedVendorEvent(int vendorId, String vendorEventCode,
+ String vendorEventArgument) {
+ // Setup connection state machine to be in connected state
+ when(mHeadsetClientService.getPriority(any(BluetoothDevice.class))).thenReturn(
+ BluetoothProfile.PRIORITY_ON);
+ int expectedBroadcastIndex = 1;
+ expectedBroadcastIndex = setUpHfpClientConnection(expectedBroadcastIndex);
+ expectedBroadcastIndex = setUpServiceLevelConnection(expectedBroadcastIndex);
+
+ // Simulate an unknown event arrive
+ String vendorEvent = vendorEventCode + vendorEventArgument;
+ StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_UNKNOWN_EVENT);
+ event.device = mTestDevice;
+ event.valueString = vendorEvent;
+ mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, event);
+
+ // Validate no broadcast intent
+ verify(mHeadsetClientService, atMost(expectedBroadcastIndex - 1))
+ .sendBroadcast(any(), anyString());
+ }
+
+ /**
+ * Test unsupported vendor response: bad read response
+ */
+ @LargeTest
+ @Test
+ public void testUnsupportedVendorEventBadReadResponse() {
+ final int vendorId = BluetoothAssignedNumbers.APPLE;
+ final String vendorResponseCode = "+XAAPL=";
+ final String vendorResponseArgument = "iPhone,2";
+ runUnsupportedVendorEvent(vendorId, vendorResponseCode, vendorResponseArgument);
+ }
+
+ /**
+ * Test unsupported vendor response: bad test response
+ */
+ @LargeTest
+ @Test
+ public void testUnsupportedVendorEventBadTestResponse() {
+ final int vendorId = BluetoothAssignedNumbers.APPLE;
+ final String vendorResponseCode = "+AAPLSIRI:";
+ final String vendorResponseArgument = "2";
+ runUnsupportedVendorEvent(vendorId, vendorResponseCode, vendorResponseArgument);
+ }
}
diff --git a/tests/unit/src/com/android/bluetooth/hid/HidDeviceTest.java b/tests/unit/src/com/android/bluetooth/hid/HidDeviceTest.java
index 618d2b4..eae1c1c 100644
--- a/tests/unit/src/com/android/bluetooth/hid/HidDeviceTest.java
+++ b/tests/unit/src/com/android/bluetooth/hid/HidDeviceTest.java
@@ -29,10 +29,11 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Looper;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
diff --git a/tests/unit/src/com/android/bluetooth/hid/HidHostServiceTest.java b/tests/unit/src/com/android/bluetooth/hid/HidHostServiceTest.java
index 939e068..a3bedf8 100644
--- a/tests/unit/src/com/android/bluetooth/hid/HidHostServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/hid/HidHostServiceTest.java
@@ -21,14 +21,16 @@
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
import org.junit.After;
import org.junit.Assert;
@@ -51,6 +53,7 @@
@Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
@Mock private AdapterService mAdapterService;
+ @Mock private DatabaseManager mDatabaseManager;
@Before
public void setUp() throws Exception {
@@ -149,7 +152,9 @@
private void testOkToConnectCase(BluetoothDevice device, int bondState, int priority,
boolean expected) {
doReturn(bondState).when(mAdapterService).getBondState(device);
- Assert.assertTrue(mService.setPriority(device, priority));
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+ when(mDatabaseManager.getProfilePriority(device, BluetoothProfile.HID_HOST))
+ .thenReturn(priority);
// Test when the AdapterService is in non-quiet mode.
doReturn(false).when(mAdapterService).isQuietModeEnabled();
diff --git a/tests/unit/src/com/android/bluetooth/map/BluetoothMapContentObserverTest.java b/tests/unit/src/com/android/bluetooth/map/BluetoothMapContentObserverTest.java
index 47b72d6..cca340e 100644
--- a/tests/unit/src/com/android/bluetooth/map/BluetoothMapContentObserverTest.java
+++ b/tests/unit/src/com/android/bluetooth/map/BluetoothMapContentObserverTest.java
@@ -25,13 +25,14 @@
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserManager;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
import android.telephony.TelephonyManager;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
import com.android.bluetooth.R;
import org.junit.Assert;
diff --git a/tests/unit/src/com/android/bluetooth/map/BluetoothMapServiceTest.java b/tests/unit/src/com/android/bluetooth/map/BluetoothMapServiceTest.java
index 34c64c5..0a000a2 100644
--- a/tests/unit/src/com/android/bluetooth/map/BluetoothMapServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/map/BluetoothMapServiceTest.java
@@ -17,10 +17,11 @@
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
diff --git a/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java b/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java
index b83cf3d..70854d8 100644
--- a/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java
@@ -27,13 +27,14 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.Suppress;
-import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
import com.android.bluetooth.R;
+import com.android.bluetooth.btservice.ProfileService;
import org.junit.After;
import org.junit.Assert;
@@ -41,33 +42,32 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@Suppress // TODO: enable when b/74609188 is debugged
@MediumTest
@RunWith(AndroidJUnit4.class)
public class MapClientStateMachineTest {
private static final String TAG = "MapStateMachineTest";
- private static final Integer TIMEOUT = 3000;
+
+ private static final int ASYNC_CALL_TIMEOUT_MILLIS = 100;
private BluetoothAdapter mAdapter;
private MceStateMachine mMceStateMachine = null;
private BluetoothDevice mTestDevice;
private Context mTargetContext;
- private FakeMapClientService mFakeMapClientService;
- private CountDownLatch mConnectedLatch = null;
- private CountDownLatch mDisconnectedLatch = null;
private Handler mHandler;
+ private ArgumentCaptor<Intent> mIntentArgument = ArgumentCaptor.forClass(Intent.class);
+
+ @Mock
+ private MapClientService mMockMapClientService;
+
@Mock
private MasClient mMockMasClient;
-
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -75,16 +75,15 @@
Assume.assumeTrue("Ignore test when MapClientService is not enabled",
mTargetContext.getResources().getBoolean(R.bool.profile_supported_mapmce));
+ doReturn(mTargetContext.getResources()).when(mMockMapClientService).getResources();
+
// This line must be called to make sure relevant objects are initialized properly
mAdapter = BluetoothAdapter.getDefaultAdapter();
// Get a device for testing
mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
- mConnectedLatch = new CountDownLatch(1);
- mDisconnectedLatch = new CountDownLatch(1);
- mFakeMapClientService = new FakeMapClientService();
when(mMockMasClient.makeRequest(any(Request.class))).thenReturn(true);
- mMceStateMachine = new MceStateMachine(mFakeMapClientService, mTestDevice, mMockMasClient);
+ mMceStateMachine = new MceStateMachine(mMockMapClientService, mTestDevice, mMockMasClient);
Assert.assertNotNull(mMceStateMachine);
if (Looper.myLooper() == null) {
Looper.prepare();
@@ -120,22 +119,15 @@
Log.i(TAG, "in testStateTransitionFromConnectingToDisconnected");
setupSdpRecordReceipt();
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_DISCONNECTED);
- mMceStateMachine.getCurrentState().processMessage(msg);
+ mMceStateMachine.sendMessage(msg);
// Wait until the message is processed and a broadcast request is sent to
// to MapClientService to change
// state from STATE_CONNECTING to STATE_DISCONNECTED
- boolean result = false;
- try {
- result = mDisconnectedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // Test that the latch reached zero; i.e., that a broadcast of state-change was received.
- Assert.assertTrue(result);
- // When the state reaches STATE_DISCONNECTED, MceStateMachine object is in the process of
- // being dismantled; i.e., can't rely on getting its current state. That means can't
- // test its current state = STATE_DISCONNECTED.
+ verify(mMockMapClientService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast(
+ mIntentArgument.capture(), eq(ProfileService.BLUETOOTH_PERM));
+ Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, mMceStateMachine.getState());
}
/**
@@ -147,45 +139,51 @@
setupSdpRecordReceipt();
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_CONNECTED);
- mMceStateMachine.getCurrentState().processMessage(msg);
+ mMceStateMachine.sendMessage(msg);
// Wait until the message is processed and a broadcast request is sent to
// to MapClientService to change
// state from STATE_CONNECTING to STATE_CONNECTED
- boolean result = false;
- try {
- result = mConnectedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // Test that the latch reached zero; i.e., that a broadcast of state-change was received.
- Assert.assertTrue(result);
+ verify(mMockMapClientService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast(
+ mIntentArgument.capture(), eq(ProfileService.BLUETOOTH_PERM));
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
+ }
+
+ /**
+ * Test receiving an empty event report
+ */
+ @Test
+ public void testReceiveEmptyEvent() {
+ setupSdpRecordReceipt();
+ Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_CONNECTED);
+ mMceStateMachine.sendMessage(msg);
+
+ // Wait until the message is processed and a broadcast request is sent to
+ // to MapClientService to change
+ // state from STATE_CONNECTING to STATE_CONNECTED
+ verify(mMockMapClientService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast(
+ mIntentArgument.capture(), eq(ProfileService.BLUETOOTH_PERM));
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
+
+ // Send an empty notification event, verify the mMceStateMachine is still connected
+ Message notification = Message.obtain(mHandler, MceStateMachine.MSG_NOTIFICATION);
+ mMceStateMachine.getCurrentState().processMessage(msg);
Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
}
private void setupSdpRecordReceipt() {
+ // Perform first part of MAP connection logic.
+ verify(mMockMapClientService,
+ timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)).sendBroadcast(
+ mIntentArgument.capture(), eq(ProfileService.BLUETOOTH_PERM));
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, mMceStateMachine.getState());
+
// Setup receipt of SDP record
- SdpMasRecord record = new SdpMasRecord(1, 1, 1, 1, 1, 1, "blah");
+ SdpMasRecord record = new SdpMasRecord(1, 1, 1, 1, 1, 1, "MasRecord");
Message msg = Message.obtain(mHandler, MceStateMachine.MSG_MAS_SDP_DONE, record);
- mMceStateMachine.getCurrentState().processMessage(msg);
+ mMceStateMachine.sendMessage(msg);
}
- private class FakeMapClientService extends MapClientService {
- @Override
- void cleanupDevice(BluetoothDevice device) {}
- @Override
- public void sendBroadcast(Intent intent, String receiverPermission) {
- int prevState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
- int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
- Log.i(TAG, "received broadcast: prevState = " + prevState
- + ", state = " + state);
- if (prevState == BluetoothProfile.STATE_CONNECTING
- && state == BluetoothProfile.STATE_CONNECTED) {
- mConnectedLatch.countDown();
- } else if (prevState == BluetoothProfile.STATE_CONNECTING
- && state == BluetoothProfile.STATE_DISCONNECTED) {
- mDisconnectedLatch.countDown();
- }
- }
- }
}
diff --git a/tests/unit/src/com/android/bluetooth/mapclient/MapClientTest.java b/tests/unit/src/com/android/bluetooth/mapclient/MapClientTest.java
index a8ac084..bc25a11 100644
--- a/tests/unit/src/com/android/bluetooth/mapclient/MapClientTest.java
+++ b/tests/unit/src/com/android/bluetooth/mapclient/MapClientTest.java
@@ -16,17 +16,22 @@
package com.android.bluetooth.mapclient;
+import static org.mockito.Mockito.*;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
import org.junit.After;
import org.junit.Assert;
@@ -52,6 +57,7 @@
@Mock private AdapterService mAdapterService;
@Mock private MnsService mMockMnsService;
+ @Mock private DatabaseManager mDatabaseManager;
@Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
@@ -68,6 +74,7 @@
Assert.assertNotNull(mService);
cleanUpInstanceMap();
mAdapter = BluetoothAdapter.getDefaultAdapter();
+ when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
}
@After
@@ -91,6 +98,17 @@
Assert.assertTrue(mService.getInstanceMap().isEmpty());
}
+ /**
+ * Mock the priority of a bluetooth device
+ *
+ * @param device - The bluetooth device you wish to mock the priority of
+ * @param priority - The priority value you want the device to have
+ */
+ private void mockDevicePriority(BluetoothDevice device, int priority) {
+ when(mDatabaseManager.getProfilePriority(device, BluetoothProfile.MAP_CLIENT))
+ .thenReturn(priority);
+ }
+
@Test
public void testInitialize() {
Assert.assertNotNull(MapClientService.getMapClientService());
@@ -106,6 +124,7 @@
Assert.assertNull(mService.getInstanceMap().get(device));
// connect a bluetooth device
+ mockDevicePriority(device, BluetoothProfile.PRIORITY_ON);
Assert.assertTrue(mService.connect(device));
// is the statemachine created
@@ -115,6 +134,25 @@
}
/**
+ * Test that a PRIORITY_OFF device is not connected to
+ */
+ @Test
+ public void testConnectPriorityOffDevice() {
+ // make sure there is no statemachine already defined for this device
+ BluetoothDevice device = makeBluetoothDevice("11:11:11:11:11:11");
+ Assert.assertNull(mService.getInstanceMap().get(device));
+
+ // connect a bluetooth device
+ mockDevicePriority(device, BluetoothProfile.PRIORITY_OFF);
+ Assert.assertFalse(mService.connect(device));
+
+ // is the statemachine created
+ Map<BluetoothDevice, MceStateMachine> map = mService.getInstanceMap();
+ Assert.assertEquals(0, map.size());
+ Assert.assertNull(map.get(device));
+ }
+
+ /**
* Test connecting MAXIMUM_CONNECTED_DEVICES devices.
*/
@Test
@@ -131,8 +169,9 @@
Assert.assertNull(mService.getInstanceMap().get(d));
}
- // run the test - connect all devices
+ // run the test - connect all devices, set their priorities to on
for (BluetoothDevice d : list) {
+ mockDevicePriority(d, BluetoothProfile.PRIORITY_ON);
Assert.assertTrue(mService.connect(d));
}
diff --git a/tests/unit/src/com/android/bluetooth/mapclient/ObexTimeTest.java b/tests/unit/src/com/android/bluetooth/mapclient/ObexTimeTest.java
new file mode 100644
index 0000000..5ef2d45
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/mapclient/ObexTimeTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.mapclient;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+@RunWith(AndroidJUnit4.class)
+public class ObexTimeTest {
+ private static final String TAG = ObexTimeTest.class.getSimpleName();
+
+ private static final String VALID_TIME_STRING = "20190101T121314";
+ private static final String VALID_TIME_STRING_WITH_OFFSET_POS = "20190101T121314+0130";
+ private static final String VALID_TIME_STRING_WITH_OFFSET_NEG = "20190101T121314-0130";
+
+ private static final String INVALID_TIME_STRING_OFFSET_EXTRA_DIGITS = "20190101T121314-99999";
+ private static final String INVALID_TIME_STRING_BAD_DELIMITER = "20190101Q121314";
+
+ // MAP message listing times, per spec, use "local time basis" if UTC offset isn't given. The
+ // ObexTime class parses using the current default timezone (assumed to be the "local timezone")
+ // in the case that UTC isn't provided. However, the Date class assumes UTC ALWAYS when
+ // initializing off of a long value. We have to take that into account when determining our
+ // expected results for time strings that don't have an offset.
+ private static final long LOCAL_TIMEZONE_OFFSET = TimeZone.getDefault().getRawOffset();
+
+ // If you are a positive offset from GMT then GMT is in the "past" and you need to subtract that
+ // offset from the time. If you are negative then GMT is in the future and you need to add that
+ // offset to the time.
+ private static final long VALID_TS = 1546344794000L; // Jan 01, 2019 at 12:13:14 GMT
+ private static final long TS_OFFSET = 5400000L; // 1 Hour, 30 minutes -> milliseconds
+ private static final long VALID_TS_LOCAL_TZ = VALID_TS - LOCAL_TIMEZONE_OFFSET;
+ private static final long VALID_TS_OFFSET_POS = VALID_TS - TS_OFFSET;
+ private static final long VALID_TS_OFFSET_NEG = VALID_TS + TS_OFFSET;
+
+ private static final Date VALID_DATE_LOCAL_TZ = new Date(VALID_TS_LOCAL_TZ);
+ private static final Date VALID_DATE_WITH_OFFSET_POS = new Date(VALID_TS_OFFSET_POS);
+ private static final Date VALID_DATE_WITH_OFFSET_NEG = new Date(VALID_TS_OFFSET_NEG);
+
+ @Test
+ public void createWithValidDateTimeString_TimestampCorrect() {
+ ObexTime timestamp = new ObexTime(VALID_TIME_STRING);
+ Assert.assertEquals("Parsed timestamp must match expected", VALID_DATE_LOCAL_TZ,
+ timestamp.getTime());
+ }
+
+ @Test
+ public void createWithValidDateTimeStringWithPosOffset_TimestampCorrect() {
+ ObexTime timestamp = new ObexTime(VALID_TIME_STRING_WITH_OFFSET_POS);
+ Assert.assertEquals("Parsed timestamp must match expected", VALID_DATE_WITH_OFFSET_POS,
+ timestamp.getTime());
+ }
+
+ @Test
+ public void createWithValidDateTimeStringWithNegOffset_TimestampCorrect() {
+ ObexTime timestamp = new ObexTime(VALID_TIME_STRING_WITH_OFFSET_NEG);
+ Assert.assertEquals("Parsed timestamp must match expected", VALID_DATE_WITH_OFFSET_NEG,
+ timestamp.getTime());
+ }
+
+ @Test
+ public void createWithValidDate_TimestampCorrect() {
+ ObexTime timestamp = new ObexTime(VALID_DATE_LOCAL_TZ);
+ Assert.assertEquals("ObexTime created with a date must return the same date",
+ VALID_DATE_LOCAL_TZ, timestamp.getTime());
+ }
+
+ @Test
+ public void printValidTime_TimestampMatchesInput() {
+ ObexTime timestamp = new ObexTime(VALID_TIME_STRING);
+ Assert.assertEquals("Timestamp as a string must match the input string", VALID_TIME_STRING,
+ timestamp.toString());
+ }
+
+ @Test
+ public void createWithInvalidDelimiterString_TimestampIsNull() {
+ ObexTime timestamp = new ObexTime(INVALID_TIME_STRING_BAD_DELIMITER);
+ Assert.assertEquals("Parsed timestamp was invalid and must result in a null object", null,
+ timestamp.getTime());
+ }
+
+ @Test
+ public void createWithInvalidOffsetString_TimestampIsNull() {
+ ObexTime timestamp = new ObexTime(INVALID_TIME_STRING_OFFSET_EXTRA_DIGITS);
+ Assert.assertEquals("Parsed timestamp was invalid and must result in a null object", null,
+ timestamp.getTime());
+ }
+
+ @Test
+ public void printInvalidTime_ReturnsNull() {
+ ObexTime timestamp = new ObexTime(INVALID_TIME_STRING_BAD_DELIMITER);
+ Assert.assertEquals("Invalid timestamps must return null for toString()", null,
+ timestamp.toString());
+ }
+}
diff --git a/tests/unit/src/com/android/bluetooth/newavrcp/BrowserPlayerWrapperTest.java b/tests/unit/src/com/android/bluetooth/newavrcp/BrowserPlayerWrapperTest.java
index 66eac21..38d9c58 100644
--- a/tests/unit/src/com/android/bluetooth/newavrcp/BrowserPlayerWrapperTest.java
+++ b/tests/unit/src/com/android/bluetooth/newavrcp/BrowserPlayerWrapperTest.java
@@ -20,8 +20,9 @@
import android.media.MediaDescription;
import android.media.browse.MediaBrowser.MediaItem;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Before;
diff --git a/tests/unit/src/com/android/bluetooth/newavrcp/MediaPlayerWrapperTest.java b/tests/unit/src/com/android/bluetooth/newavrcp/MediaPlayerWrapperTest.java
index 85ad147..ee41320 100644
--- a/tests/unit/src/com/android/bluetooth/newavrcp/MediaPlayerWrapperTest.java
+++ b/tests/unit/src/com/android/bluetooth/newavrcp/MediaPlayerWrapperTest.java
@@ -24,11 +24,12 @@
import android.media.session.PlaybackState;
import android.os.HandlerThread;
import android.os.TestLooperManager;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -151,23 +152,24 @@
@Test
public void testIsReady() {
MediaPlayerWrapper wrapper = MediaPlayerWrapper.wrap(mMockController, mThread.getLooper());
- Assert.assertTrue(wrapper.isReady());
+ Assert.assertTrue(wrapper.isPlaybackStateReady());
+ Assert.assertTrue(wrapper.isMetadataReady());
- // Test isReady() is false when the playback state is null
+ // Test isPlaybackStateReady() is false when the playback state is null
doReturn(null).when(mMockController).getPlaybackState();
- Assert.assertFalse(wrapper.isReady());
+ Assert.assertFalse(wrapper.isPlaybackStateReady());
// Restore the old playback state
doReturn(mTestState.build()).when(mMockController).getPlaybackState();
- Assert.assertTrue(wrapper.isReady());
+ Assert.assertTrue(wrapper.isPlaybackStateReady());
- // Test isReady() is false when the metadata is null
+ // Test isMetadataReady() is false when the metadata is null
doReturn(null).when(mMockController).getMetadata();
- Assert.assertFalse(wrapper.isReady());
+ Assert.assertFalse(wrapper.isMetadataReady());
// Restore the old metadata
doReturn(mTestMetadata.build()).when(mMockController).getMetadata();
- Assert.assertTrue(wrapper.isReady());
+ Assert.assertTrue(wrapper.isMetadataReady());
}
/*
@@ -178,7 +180,8 @@
public void testControllerUpdate() {
// Create the wrapper object and register the looper with the timeout handler
MediaPlayerWrapper wrapper = MediaPlayerWrapper.wrap(mMockController, mThread.getLooper());
- Assert.assertTrue(wrapper.isReady());
+ Assert.assertTrue(wrapper.isPlaybackStateReady());
+ Assert.assertTrue(wrapper.isMetadataReady());
wrapper.registerCallback(mTestCbs);
// Create a new MediaController that has different metadata than the previous controller
diff --git a/tests/unit/src/com/android/bluetooth/opp/BluetoothOppServiceTest.java b/tests/unit/src/com/android/bluetooth/opp/BluetoothOppServiceTest.java
index 8b40c62..7f479d4 100644
--- a/tests/unit/src/com/android/bluetooth/opp/BluetoothOppServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/opp/BluetoothOppServiceTest.java
@@ -17,10 +17,11 @@
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
diff --git a/tests/unit/src/com/android/bluetooth/pan/PanServiceTest.java b/tests/unit/src/com/android/bluetooth/pan/PanServiceTest.java
index ca96285..6d4574f 100644
--- a/tests/unit/src/com/android/bluetooth/pan/PanServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/pan/PanServiceTest.java
@@ -17,10 +17,11 @@
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
diff --git a/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapServiceTest.java b/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapServiceTest.java
index e3dcf8c..8990eb7 100644
--- a/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/pbap/BluetoothPbapServiceTest.java
@@ -17,10 +17,11 @@
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
diff --git a/tests/unit/src/com/android/bluetooth/pbap/PbapStateMachineTest.java b/tests/unit/src/com/android/bluetooth/pbap/PbapStateMachineTest.java
index 0f085e8..516235f 100644
--- a/tests/unit/src/com/android/bluetooth/pbap/PbapStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/pbap/PbapStateMachineTest.java
@@ -25,9 +25,10 @@
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
diff --git a/tests/unit/src/com/android/bluetooth/pbapclient/PbapClientServiceTest.java b/tests/unit/src/com/android/bluetooth/pbapclient/PbapClientServiceTest.java
index dd33d79..5e9b5ae 100644
--- a/tests/unit/src/com/android/bluetooth/pbapclient/PbapClientServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/pbapclient/PbapClientServiceTest.java
@@ -17,10 +17,11 @@
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
diff --git a/tests/unit/src/com/android/bluetooth/pbapclient/PbapParserTest.java b/tests/unit/src/com/android/bluetooth/pbapclient/PbapParserTest.java
index f8ad6bf..667e2b7 100644
--- a/tests/unit/src/com/android/bluetooth/pbapclient/PbapParserTest.java
+++ b/tests/unit/src/com/android/bluetooth/pbapclient/PbapParserTest.java
@@ -24,9 +24,10 @@
import android.net.Uri;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
diff --git a/tests/unit/src/com/android/bluetooth/sap/SapServiceTest.java b/tests/unit/src/com/android/bluetooth/sap/SapServiceTest.java
index 8505673..bc5bffe 100644
--- a/tests/unit/src/com/android/bluetooth/sap/SapServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/sap/SapServiceTest.java
@@ -17,10 +17,11 @@
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.MediumTest;
-import android.support.test.rule.ServiceTestRule;
-import android.support.test.runner.AndroidJUnit4;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;