Snap for 9267292 from 45f55e3b579e4684dcffed31b36d01581f6ec47e to mainline-networking-release
Change-Id: I0c8b317b8d4a565c7a9d2552991d479b510b6c66
diff --git a/android/BluetoothLegacyMigration/Android.bp b/android/BluetoothLegacyMigration/Android.bp
new file mode 100644
index 0000000..b755fa0
--- /dev/null
+++ b/android/BluetoothLegacyMigration/Android.bp
@@ -0,0 +1,14 @@
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "BluetoothLegacyMigration",
+
+ srcs: [ "BluetoothLegacyMigration.kt" ],
+
+ // Must match Bluetooth.apk certificate because of sharedUserId
+ certificate: ":com.android.bluetooth.certificate",
+ platform_apis: true,
+}
diff --git a/android/BluetoothLegacyMigration/AndroidManifest.xml b/android/BluetoothLegacyMigration/AndroidManifest.xml
new file mode 100644
index 0000000..86989f9
--- /dev/null
+++ b/android/BluetoothLegacyMigration/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.bluetooth"
+ android:sharedUserId="android.uid.bluetooth">
+
+ <!-- This "legacy" instance is retained on the device to preserve the
+ database contents before Bluetooth was migrated into a Mainline module.
+ This ensures that we can migrate information to new folder app -->
+<application
+ android:icon="@mipmap/bt_share"
+ android:allowBackup="false"
+ android:label="Bluetooth Legacy">
+ <provider
+ android:name="com.google.android.bluetooth.BluetoothLegacyMigration"
+ android:authorities="bluetooth_legacy.provider"
+ android:directBootAware="true"
+ android:exported="false"
+ android:permission="android.permission.BLUETOOTH_PRIVILEGED"
+ />
+ </application>
+</manifest>
diff --git a/android/BluetoothLegacyMigration/BluetoothLegacyMigration.kt b/android/BluetoothLegacyMigration/BluetoothLegacyMigration.kt
new file mode 100644
index 0000000..9c88a06
--- /dev/null
+++ b/android/BluetoothLegacyMigration/BluetoothLegacyMigration.kt
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2022 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.google.android.bluetooth
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.content.UriMatcher
+import android.database.Cursor
+import android.database.sqlite.SQLiteDatabase
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+
+/**
+ * Define an implementation of ContentProvider for the Bluetooth migration
+ */
+class BluetoothLegacyMigration: ContentProvider() {
+ companion object {
+ private const val TAG = "BluetoothLegacyMigration"
+
+ private const val AUTHORITY = "bluetooth_legacy.provider"
+
+ private const val START_LEGACY_MIGRATION_CALL = "start_legacy_migration"
+ private const val FINISH_LEGACY_MIGRATION_CALL = "finish_legacy_migration"
+
+ private const val PHONEBOOK_ACCESS_PERMISSION = "phonebook_access_permission"
+ private const val MESSAGE_ACCESS_PERMISSION = "message_access_permission"
+ private const val SIM_ACCESS_PERMISSION = "sim_access_permission"
+
+ private const val VOLUME_MAP = "bluetooth_volume_map"
+
+ private const val OPP = "OPPMGR"
+ private const val BLUETOOTH_OPP_CHANNEL = "btopp_channels"
+ private const val BLUETOOTH_OPP_NAME = "btopp_names"
+
+ private const val BLUETOOTH_SIGNED_DEFAULT = "com.google.android.bluetooth_preferences"
+
+ private const val KEY_LIST = "key_list"
+
+ private enum class UriId(
+ val fileName: String,
+ val handler: (ctx: Context) -> DatabaseHandler
+ ) {
+ BLUETOOTH(BluetoothDatabase.DATABASE_NAME, ::BluetoothDatabase),
+ OPP(OppDatabase.DATABASE_NAME, ::OppDatabase),
+ }
+
+ private val URI_MATCHER = UriMatcher(UriMatcher.NO_MATCH).apply {
+ UriId.values().map { addURI(AUTHORITY, it.fileName, it.ordinal) }
+ }
+
+ private fun putObjectInBundle(bundle: Bundle, key: String, obj: Any?) {
+ when (obj) {
+ is Boolean -> bundle.putBoolean(key, obj)
+ is Int -> bundle.putInt(key, obj)
+ is Long -> bundle.putLong(key, obj)
+ is String -> bundle.putString(key, obj)
+ null -> throw UnsupportedOperationException("null type is not handled")
+ else -> throw UnsupportedOperationException("${obj.javaClass.simpleName}: type is not handled")
+ }
+ }
+ }
+
+ private lateinit var mContext: Context
+
+ /**
+ * Always return true, indicating that the
+ * provider loaded correctly.
+ */
+ override fun onCreate(): Boolean {
+ mContext = context!!.createDeviceProtectedStorageContext()
+ return true
+ }
+
+ /**
+ * Use a content URI to get database name associated
+ *
+ * @param uri Content uri
+ * @return A {@link Cursor} containing the results of the query.
+ */
+ override fun getType(uri: Uri): String {
+ val database = UriId.values().firstOrNull { it.ordinal == URI_MATCHER.match(uri) }
+ ?: throw UnsupportedOperationException("This Uri is not supported: $uri")
+ return database.fileName
+ }
+
+ /**
+ * Use a content URI to get information about a database
+ *
+ * @param uri Content uri
+ * @param projection unused
+ * @param selection unused
+ * @param selectionArgs unused
+ * @param sortOrder unused
+ * @return A {@link Cursor} containing the results of the query.
+ *
+ */
+ @Override
+ override fun query(
+ uri: Uri,
+ projection: Array<String>?,
+ selection: String?,
+ selectionArgs: Array<String>?,
+ sortOrder: String?
+ ): Cursor? {
+ val database = UriId.values().firstOrNull { it.ordinal == URI_MATCHER.match(uri) }
+ ?: throw UnsupportedOperationException("This Uri is not supported: $uri")
+ return database.handler(mContext).toCursor()
+ }
+
+ /**
+ * insert() is not supported
+ */
+ override fun insert(uri: Uri, values: ContentValues?): Uri? {
+ throw UnsupportedOperationException()
+ }
+
+ /**
+ * delete() is not supported
+ */
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
+ throw UnsupportedOperationException()
+ }
+
+ /**
+ * update() is not supported
+ */
+ override fun update(
+ uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?
+ ): Int {
+ throw UnsupportedOperationException()
+ }
+
+ abstract class MigrationHandler {
+ abstract fun toBundle(): Bundle?
+ abstract fun delete()
+ }
+
+ private class SharedPreferencesHandler(private val ctx: Context, private val key: String) :
+ MigrationHandler() {
+
+ override fun toBundle(): Bundle? {
+ val pref = ctx.getSharedPreferences(key, Context.MODE_PRIVATE)
+ if (pref.all.isEmpty()) {
+ Log.d(TAG, "No migration needed for shared preference: $key")
+ return null
+ }
+ val bundle = Bundle()
+ val keys = arrayListOf<String>()
+ for (e in pref.all) {
+ keys += e.key
+ putObjectInBundle(bundle, e.key, e.value)
+ }
+ bundle.putStringArrayList(KEY_LIST, keys)
+ Log.d(TAG, "SharedPreferences migrating ${keys.size} key(s) from $key")
+ return bundle
+ }
+
+ override fun delete() {
+ ctx.deleteSharedPreferences(key)
+ Log.d(TAG, "$key: SharedPreferences deleted")
+ }
+ }
+
+ abstract class DatabaseHandler(private val ctx: Context, private val dbName: String) :
+ MigrationHandler() {
+
+ abstract val sql: String
+
+ fun toCursor(): Cursor? {
+ val databasePath = ctx.getDatabasePath(dbName)
+ if (!databasePath.exists()) {
+ Log.d(TAG, "No migration needed for database: $dbName")
+ return null
+ }
+ val db = SQLiteDatabase.openDatabase(
+ databasePath,
+ SQLiteDatabase.OpenParams.Builder().addOpenFlags(SQLiteDatabase.OPEN_READONLY)
+ .build()
+ )
+ return db.rawQuery(sql, null)
+ }
+
+ override fun toBundle(): Bundle? {
+ throw UnsupportedOperationException()
+ }
+
+ override fun delete() {
+ val databasePath = ctx.getDatabasePath(dbName)
+ databasePath.delete()
+ Log.d(TAG, "$dbName: database deleted")
+ }
+ }
+
+ private class BluetoothDatabase(ctx: Context) : DatabaseHandler(ctx, DATABASE_NAME) {
+ companion object {
+ const val DATABASE_NAME = "bluetooth_db"
+ }
+ private val dbTable = "metadata"
+ override val sql = "select * from $dbTable"
+ }
+
+ private class OppDatabase(ctx: Context) : DatabaseHandler(ctx, DATABASE_NAME) {
+ companion object {
+ const val DATABASE_NAME = "btopp.db"
+ }
+ private val dbTable = "btopp"
+ override val sql = "select * from $dbTable"
+ }
+
+ /**
+ * Fetch legacy data describe by {@code arg} and perform {@code method} action on it
+ *
+ * @param method Action to perform. One of START_LEGACY_MIGRATION_CALL|FINISH_LEGACY_MIGRATION_CALL
+ * @param arg item on witch to perform the action specified by {@code method}
+ * @param extras unused
+ * @return A {@link Bundle} containing the results of the query.
+ */
+ override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
+ val migrationHandler = when (arg) {
+ OPP,
+ VOLUME_MAP,
+ BLUETOOTH_OPP_NAME,
+ BLUETOOTH_OPP_CHANNEL,
+ SIM_ACCESS_PERMISSION,
+ MESSAGE_ACCESS_PERMISSION,
+ PHONEBOOK_ACCESS_PERMISSION -> SharedPreferencesHandler(mContext, arg)
+ BLUETOOTH_SIGNED_DEFAULT -> {
+ val key = mContext.packageName + "_preferences"
+ SharedPreferencesHandler(mContext, key)
+ }
+ BluetoothDatabase.DATABASE_NAME -> BluetoothDatabase(mContext)
+ OppDatabase.DATABASE_NAME -> OppDatabase(mContext)
+ else -> throw UnsupportedOperationException()
+ }
+ return when (method) {
+ START_LEGACY_MIGRATION_CALL -> migrationHandler.toBundle()
+ FINISH_LEGACY_MIGRATION_CALL -> {
+ migrationHandler.delete()
+ return null
+ }
+ else -> throw UnsupportedOperationException()
+ }
+ }
+}
diff --git a/android/BluetoothLegacyMigration/OWNERS b/android/BluetoothLegacyMigration/OWNERS
new file mode 100644
index 0000000..8f5c903
--- /dev/null
+++ b/android/BluetoothLegacyMigration/OWNERS
@@ -0,0 +1,8 @@
+# Reviewers for /android/BluetoothLegacyMigration
+
+eruffieux@google.com
+rahulsabnis@google.com
+sattiraju@google.com
+siyuanh@google.com
+wescande@google.com
+zachoverflow@google.com
diff --git a/android/BluetoothLegacyMigration/res/mipmap-anydpi/bt_share.xml b/android/BluetoothLegacyMigration/res/mipmap-anydpi/bt_share.xml
new file mode 100644
index 0000000..ff00b0d
--- /dev/null
+++ b/android/BluetoothLegacyMigration/res/mipmap-anydpi/bt_share.xml
@@ -0,0 +1,28 @@
+<!--
+ Copyright (C) 2022 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="#e9ddd4" />
+ </background>
+</adaptive-icon>
diff --git a/android/app/jni/com_android_bluetooth_gatt.cpp b/android/app/jni/com_android_bluetooth_gatt.cpp
index 82aa164..c6196bc 100644
--- a/android/app/jni/com_android_bluetooth_gatt.cpp
+++ b/android/app/jni/com_android_bluetooth_gatt.cpp
@@ -199,6 +199,7 @@
*/
void btgattc_register_app_cb(int status, int clientIf, const Uuid& app_uuid) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onClientRegistered, status,
@@ -212,6 +213,7 @@
uint16_t periodic_adv_int,
std::vector<uint8_t> adv_data,
RawAddress* original_bda) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -233,6 +235,7 @@
void btgattc_open_cb(int conn_id, int status, int clientIf,
const RawAddress& bda) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -244,6 +247,7 @@
void btgattc_close_cb(int conn_id, int status, int clientIf,
const RawAddress& bda) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -254,6 +258,7 @@
}
void btgattc_search_complete_cb(int conn_id, int status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -263,6 +268,7 @@
void btgattc_register_for_notification_cb(int conn_id, int registered,
int status, uint16_t handle) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -271,6 +277,7 @@
}
void btgattc_notify_cb(int conn_id, const btgatt_notify_params_t& p_data) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -288,6 +295,7 @@
void btgattc_read_characteristic_cb(int conn_id, int status,
btgatt_read_params_t* p_data) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -308,6 +316,7 @@
void btgattc_write_characteristic_cb(int conn_id, int status, uint16_t handle,
uint16_t len, const uint8_t* value) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -319,6 +328,7 @@
}
void btgattc_execute_write_cb(int conn_id, int status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -328,6 +338,7 @@
void btgattc_read_descriptor_cb(int conn_id, int status,
const btgatt_read_params_t& p_data) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -346,6 +357,7 @@
void btgattc_write_descriptor_cb(int conn_id, int status, uint16_t handle,
uint16_t len, const uint8_t* value) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -358,6 +370,7 @@
void btgattc_remote_rssi_cb(int client_if, const RawAddress& bda, int rssi,
int status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -369,6 +382,7 @@
}
void btgattc_configure_mtu_cb(int conn_id, int status, int mtu) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onConfigureMTU, conn_id,
@@ -376,6 +390,7 @@
}
void btgattc_congestion_cb(int conn_id, bool congested) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onClientCongestion,
@@ -384,6 +399,7 @@
void btgattc_batchscan_reports_cb(int client_if, int status, int report_format,
int num_records, std::vector<uint8_t> data) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
ScopedLocalRef<jbyteArray> jb(sCallbackEnv.get(),
@@ -396,6 +412,7 @@
}
void btgattc_batchscan_threshold_cb(int client_if) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj,
@@ -403,6 +420,7 @@
}
void btgattc_track_adv_event_cb(btgatt_track_adv_info_t* p_adv_track_info) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -506,6 +524,7 @@
void btgattc_get_gatt_db_cb(int conn_id, const btgatt_db_element_t* db,
int count) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -525,6 +544,7 @@
void btgattc_phy_updated_cb(int conn_id, uint8_t tx_phy, uint8_t rx_phy,
uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -534,6 +554,7 @@
void btgattc_conn_updated_cb(int conn_id, uint16_t interval, uint16_t latency,
uint16_t timeout, uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -542,6 +563,7 @@
}
void btgattc_service_changed_cb(int conn_id) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -583,6 +605,7 @@
*/
void btgatts_register_app_cb(int status, int server_if, const Uuid& uuid) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onServerRegistered, status,
@@ -591,6 +614,7 @@
void btgatts_connection_cb(int conn_id, int server_if, int connected,
const RawAddress& bda) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -603,6 +627,7 @@
void btgatts_service_added_cb(int status, int server_if,
const btgatt_db_element_t* service,
size_t service_count) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -620,6 +645,7 @@
}
void btgatts_service_stopped_cb(int status, int server_if, int srvc_handle) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onServiceStopped, status,
@@ -627,6 +653,7 @@
}
void btgatts_service_deleted_cb(int status, int server_if, int srvc_handle) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onServiceDeleted, status,
@@ -637,6 +664,7 @@
const RawAddress& bda,
int attr_handle, int offset,
bool is_long) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -650,6 +678,7 @@
void btgatts_request_read_descriptor_cb(int conn_id, int trans_id,
const RawAddress& bda, int attr_handle,
int offset, bool is_long) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -666,6 +695,7 @@
bool need_rsp, bool is_prep,
const uint8_t* value,
size_t length) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -685,6 +715,7 @@
int offset, bool need_rsp,
bool is_prep, const uint8_t* value,
size_t length) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -701,6 +732,7 @@
void btgatts_request_exec_write_cb(int conn_id, int trans_id,
const RawAddress& bda, int exec_write) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -711,6 +743,7 @@
}
void btgatts_response_confirmation_cb(int status, int handle) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onResponseSendCompleted,
@@ -718,6 +751,7 @@
}
void btgatts_indication_sent_cb(int conn_id, int status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onNotificationSent,
@@ -725,6 +759,7 @@
}
void btgatts_congestion_cb(int conn_id, bool congested) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onServerCongestion,
@@ -732,6 +767,7 @@
}
void btgatts_mtu_changed_cb(int conn_id, int mtu) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onServerMtuChanged,
@@ -740,6 +776,7 @@
void btgatts_phy_updated_cb(int conn_id, uint8_t tx_phy, uint8_t rx_phy,
uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -749,6 +786,7 @@
void btgatts_conn_updated_cb(int conn_id, uint16_t interval, uint16_t latency,
uint16_t timeout, uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -890,6 +928,7 @@
void OnScannerRegistered(const Uuid app_uuid, uint8_t scannerId,
uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onScannerRegistered,
@@ -897,6 +936,7 @@
}
void OnSetScannerParameterComplete(uint8_t scannerId, uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(
@@ -907,6 +947,7 @@
uint8_t primary_phy, uint8_t secondary_phy,
uint8_t advertising_sid, int8_t tx_power, int8_t rssi,
uint16_t periodic_adv_int, std::vector<uint8_t> adv_data) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -932,6 +973,7 @@
}
void OnTrackAdvFoundLost(AdvertisingTrackInfo track_info) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -973,6 +1015,7 @@
void OnBatchScanReports(int client_if, int status, int report_format,
int num_records, std::vector<uint8_t> data) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
ScopedLocalRef<jbyteArray> jb(sCallbackEnv.get(),
@@ -986,6 +1029,7 @@
}
void OnBatchScanThresholdCrossed(int client_if) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj,
@@ -996,6 +1040,7 @@
uint8_t sid, uint8_t address_type,
RawAddress address, uint8_t phy,
uint16_t interval) override {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
if (!mPeriodicScanCallbacksObj) {
@@ -1013,6 +1058,7 @@
void OnPeriodicSyncReport(uint16_t sync_handle, int8_t tx_power, int8_t rssi,
uint8_t data_status,
std::vector<uint8_t> data) override {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -1027,6 +1073,7 @@
}
void OnPeriodicSyncLost(uint16_t sync_handle) override {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -1036,6 +1083,7 @@
void OnPeriodicSyncTransferred(int pa_source, uint8_t status,
RawAddress address) override {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
if (!mPeriodicScanCallbacksObj) {
@@ -1168,6 +1216,7 @@
static const bt_interface_t* btIf;
static void initializeNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_mutex> lock(callbacks_mutex);
if (btIf) return;
btIf = getBluetoothInterface();
@@ -1210,6 +1259,8 @@
}
static void cleanupNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_mutex> lock(callbacks_mutex);
+
if (!btIf) return;
if (sGattIf != NULL) {
@@ -1250,6 +1301,7 @@
void btgattc_register_scanner_cb(const Uuid& app_uuid, uint8_t scannerId,
uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onScannerRegistered,
@@ -1305,6 +1357,7 @@
static void readClientPhyCb(uint8_t clientIf, RawAddress bda, uint8_t tx_phy,
uint8_t rx_phy, uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -1451,6 +1504,7 @@
}
void set_scan_params_cmpl_cb(int client_if, uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onScanParamSetupCompleted,
@@ -1468,6 +1522,7 @@
void scan_filter_param_cb(uint8_t client_if, uint8_t avbl_space, uint8_t action,
uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj,
@@ -1547,6 +1602,7 @@
static void scan_filter_cfg_cb(uint8_t client_if, uint8_t filt_type,
uint8_t avbl_space, uint8_t action,
uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onScanFilterConfig, action,
@@ -1697,6 +1753,7 @@
}
void scan_enable_cb(uint8_t client_if, uint8_t action, uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onScanFilterEnableDisabled,
@@ -1729,6 +1786,7 @@
}
void batchscan_cfg_storage_cb(uint8_t client_if, uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(
@@ -1746,6 +1804,7 @@
}
void batchscan_enable_cb(uint8_t client_if, uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onBatchScanStartStopped,
@@ -1820,6 +1879,7 @@
static void readServerPhyCb(uint8_t serverIf, RawAddress bda, uint8_t tx_phy,
uint8_t rx_phy, uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -2000,7 +2060,7 @@
}
static void advertiseInitializeNative(JNIEnv* env, jobject object) {
- std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
+ std::unique_lock<std::shared_mutex> lock(callbacks_mutex);
if (mAdvertiseCallbacksObj != NULL) {
ALOGW("Cleaning up Advertise callback object");
env->DeleteGlobalRef(mAdvertiseCallbacksObj);
@@ -2011,7 +2071,7 @@
}
static void advertiseCleanupNative(JNIEnv* env, jobject object) {
- std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
+ std::unique_lock<std::shared_mutex> lock(callbacks_mutex);
if (mAdvertiseCallbacksObj != NULL) {
env->DeleteGlobalRef(mAdvertiseCallbacksObj);
mAdvertiseCallbacksObj = NULL;
@@ -2100,6 +2160,7 @@
static void ble_advertising_set_started_cb(int reg_id, uint8_t advertiser_id,
int8_t tx_power, uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mAdvertiseCallbacksObj,
@@ -2109,6 +2170,7 @@
static void ble_advertising_set_timeout_cb(uint8_t advertiser_id,
uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mAdvertiseCallbacksObj,
@@ -2160,6 +2222,7 @@
static void getOwnAddressCb(uint8_t advertiser_id, uint8_t address_type,
RawAddress address) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
@@ -2178,6 +2241,7 @@
static void callJniCallback(jmethodID method, uint8_t advertiser_id,
uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mAdvertiseCallbacksObj, method, advertiser_id,
@@ -2185,6 +2249,7 @@
}
static void enableSetCb(uint8_t advertiser_id, bool enable, uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mAdvertiseCallbacksObj,
@@ -2224,6 +2289,7 @@
static void setAdvertisingParametersNativeCb(uint8_t advertiser_id,
uint8_t status, int8_t tx_power) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mAdvertiseCallbacksObj,
@@ -2268,6 +2334,7 @@
static void enablePeriodicSetCb(uint8_t advertiser_id, bool enable,
uint8_t status) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
sCallbackEnv->CallVoidMethod(mAdvertiseCallbacksObj,
@@ -2295,6 +2362,7 @@
}
static void periodicScanInitializeNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_mutex> lock(callbacks_mutex);
if (mPeriodicScanCallbacksObj != NULL) {
ALOGW("Cleaning up periodic scan callback object");
env->DeleteGlobalRef(mPeriodicScanCallbacksObj);
@@ -2305,6 +2373,7 @@
}
static void periodicScanCleanupNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_mutex> lock(callbacks_mutex);
if (mPeriodicScanCallbacksObj != NULL) {
env->DeleteGlobalRef(mPeriodicScanCallbacksObj);
mPeriodicScanCallbacksObj = NULL;
diff --git a/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java b/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java
index 5710451..622f776 100644
--- a/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java
+++ b/android/app/src/com/android/bluetooth/BluetoothMethodProxy.java
@@ -17,6 +17,7 @@
package com.android.bluetooth;
import android.content.ContentResolver;
+import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@@ -75,6 +76,23 @@
}
/**
+ * Proxies {@link ContentResolver#delete(Uri, String, String[])}.
+ */
+ public int contentResolverDelete(ContentResolver contentResolver, final Uri url,
+ final String where,
+ final String[] selectionArgs) {
+ return contentResolver.delete(url, where, selectionArgs);
+ }
+
+ /**
+ * Proxies {@link ContentResolver#update(Uri, ContentValues, String, String[])}.
+ */
+ public int contentResolverUpdate(ContentResolver contentResolver, final Uri contentUri,
+ final ContentValues contentValues, String where, String[] selectionArgs) {
+ return contentResolver.update(contentUri, contentValues, where, selectionArgs);
+ }
+
+ /**
* Proxies {@link HeaderSet#getHeader}.
*/
public Object getHeader(HeaderSet headerSet, int headerId) throws IOException {
diff --git a/android/app/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java b/android/app/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java
index 508c68c..ecfd441 100644
--- a/android/app/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java
+++ b/android/app/src/com/android/bluetooth/avrcpcontroller/BrowseTree.java
@@ -23,6 +23,8 @@
import com.android.bluetooth.Utils;
+import com.google.common.annotations.VisibleForTesting;
+
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -57,7 +59,8 @@
public static final String PLAYER_PREFIX = "PLAYER";
// Static instance of Folder ID <-> Folder Instance (for navigation purposes)
- private final HashMap<String, BrowseNode> mBrowseMap = new HashMap<String, BrowseNode>();
+ @VisibleForTesting
+ final HashMap<String, BrowseNode> mBrowseMap = new HashMap<String, BrowseNode>();
private BrowseNode mCurrentBrowseNode;
private BrowseNode mCurrentBrowsedPlayer;
private BrowseNode mCurrentAddressedPlayer;
diff --git a/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java b/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
index c4be364..dc656d4 100644
--- a/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
+++ b/android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java
@@ -46,9 +46,10 @@
import com.android.bluetooth.le_audio.LeAudioService;
import com.android.internal.annotations.VisibleForTesting;
-import java.util.LinkedList;
+import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.RejectedExecutionException;
/**
* The active device manager is responsible for keeping track of the
@@ -130,14 +131,18 @@
private Handler mHandler = null;
private final AudioManager mAudioManager;
private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback;
+ private final AudioManagerOnModeChangedListener mAudioManagerOnModeChangedListener;
- private final List<BluetoothDevice> mA2dpConnectedDevices = new LinkedList<>();
- private final List<BluetoothDevice> mHfpConnectedDevices = new LinkedList<>();
- private final List<BluetoothDevice> mHearingAidConnectedDevices = new LinkedList<>();
- private final List<BluetoothDevice> mLeAudioConnectedDevices = new LinkedList<>();
- private final List<BluetoothDevice> mLeHearingAidConnectedDevices = new LinkedList<>();
+ private final List<BluetoothDevice> mA2dpConnectedDevices = new ArrayList<>();
+ private final List<BluetoothDevice> mHfpConnectedDevices = new ArrayList<>();
+ private final List<BluetoothDevice> mHearingAidConnectedDevices = new ArrayList<>();
+ private final List<BluetoothDevice> mLeAudioConnectedDevices = new ArrayList<>();
+ private final List<BluetoothDevice> mLeHearingAidConnectedDevices = new ArrayList<>();
+ private final List<BluetoothDevice> mPendingLeHearingAidActiveDevice = new ArrayList<>();
private BluetoothDevice mA2dpActiveDevice = null;
+ private BluetoothDevice mPendingA2dpActiveDevice = null;
private BluetoothDevice mHfpActiveDevice = null;
+ private BluetoothDevice mPendingHfpActiveDevice = null;
private BluetoothDevice mHearingAidActiveDevice = null;
private BluetoothDevice mLeAudioActiveDevice = null;
private BluetoothDevice mLeHearingAidActiveDevice = null;
@@ -244,16 +249,43 @@
if (mA2dpConnectedDevices.contains(device)) {
break; // The device is already connected
}
+ // New connected A2DP device
mA2dpConnectedDevices.add(device);
if (mHearingAidActiveDevice == null && mLeHearingAidActiveDevice == null) {
- // New connected device: select it as active
- setA2dpActiveDevice(device);
- setLeAudioActiveDevice(null);
+ // Lazy active A2DP if it is not being used.
+ // This will prevent the deactivation of LE audio
+ // earlier than the activation of HFP.
+ switch (mAudioManager.getMode()) {
+ case AudioManager.MODE_NORMAL:
+ break;
+ case AudioManager.MODE_RINGTONE: {
+ HeadsetService headsetService = mFactory.getHeadsetService();
+ if (mHfpActiveDevice == null && headsetService != null
+ && headsetService.isInbandRingingEnabled()) {
+ mPendingA2dpActiveDevice = device;
+ }
+ break;
+ }
+ case AudioManager.MODE_IN_CALL:
+ case AudioManager.MODE_IN_COMMUNICATION:
+ case AudioManager.MODE_CALL_SCREENING:
+ case AudioManager.MODE_CALL_REDIRECT:
+ case AudioManager.MODE_COMMUNICATION_REDIRECT: {
+ if (mHfpActiveDevice == null) {
+ mPendingA2dpActiveDevice = device;
+ }
+ }
+ }
+ if (mPendingA2dpActiveDevice == null) {
+ // select the device as active if not lazy active
+ setA2dpActiveDevice(device);
+ setLeAudioActiveDevice(null);
+ }
}
break;
}
if (prevState == BluetoothProfile.STATE_CONNECTED) {
- // Device disconnected
+ // A2DP device disconnected
if (DBG) {
Log.d(TAG,
"handleMessage(MESSAGE_A2DP_ACTION_CONNECTION_STATE_CHANGED): "
@@ -282,6 +314,9 @@
setHearingAidActiveDevice(null);
setLeAudioActiveDevice(null);
}
+ if (mHfpConnectedDevices.contains(device)) {
+ setHfpActiveDevice(device);
+ }
// Just assign locally the new value
mA2dpActiveDevice = device;
}
@@ -307,16 +342,37 @@
if (mHfpConnectedDevices.contains(device)) {
break; // The device is already connected
}
+ // New connected HFP device.
mHfpConnectedDevices.add(device);
if (mHearingAidActiveDevice == null && mLeHearingAidActiveDevice == null) {
- // New connected device: select it as active
- setHfpActiveDevice(device);
- setLeAudioActiveDevice(null);
+ // Lazy active HFP if it is not being used.
+ // This will prevent the deactivation of LE audio
+ // earlier than the activation of A2DP.
+ switch (mAudioManager.getMode()) {
+ case AudioManager.MODE_NORMAL:
+ if (mA2dpActiveDevice == null) {
+ mPendingHfpActiveDevice = device;
+ }
+ break;
+ case AudioManager.MODE_RINGTONE: {
+ HeadsetService headsetService = mFactory.getHeadsetService();
+ if (headsetService == null
+ || !headsetService.isInbandRingingEnabled()) {
+ mPendingHfpActiveDevice = device;
+ }
+ break;
+ }
+ }
+ if (mPendingHfpActiveDevice == null) {
+ // select the device as active if not lazy active
+ setHfpActiveDevice(device);
+ setLeAudioActiveDevice(null);
+ }
}
break;
}
if (prevState == BluetoothProfile.STATE_CONNECTED) {
- // Device disconnected
+ // HFP device disconnected
if (DBG) {
Log.d(TAG,
"handleMessage(MESSAGE_HFP_ACTION_CONNECTION_STATE_CHANGED): "
@@ -345,6 +401,9 @@
setHearingAidActiveDevice(null);
setLeAudioActiveDevice(null);
}
+ if (mA2dpConnectedDevices.contains(device)) {
+ setA2dpActiveDevice(device);
+ }
// Just assign locally the new value
mHfpActiveDevice = device;
}
@@ -370,7 +429,7 @@
break; // The device is already connected
}
mHearingAidConnectedDevices.add(device);
- // New connected device: select it as active
+ // New connected hearing aid device: select it as active
setHearingAidActiveDevice(device);
setA2dpActiveDevice(null);
setHfpActiveDevice(null);
@@ -378,7 +437,7 @@
break;
}
if (prevState == BluetoothProfile.STATE_CONNECTED) {
- // Device disconnected
+ // Hearing aid device disconnected
if (DBG) {
Log.d(TAG, "handleMessage(MESSAGE_HEARING_AID_ACTION_CONNECTION_STATE"
+ "_CHANGED): device " + device + " disconnected");
@@ -432,21 +491,28 @@
break; // The device is already connected
}
mLeAudioConnectedDevices.add(device);
- if (mHearingAidActiveDevice == null && mLeHearingAidActiveDevice == null) {
- // New connected device: select it as active
+ if (mHearingAidActiveDevice == null && mLeHearingAidActiveDevice == null
+ && mPendingLeHearingAidActiveDevice.isEmpty()) {
+ // New connected LE audio device: select it as active
setLeAudioActiveDevice(device);
setA2dpActiveDevice(null);
setHfpActiveDevice(null);
+ } else if (mPendingLeHearingAidActiveDevice.contains(device)) {
+ setLeHearingAidActiveDevice(device);
+ setHearingAidActiveDevice(null);
+ setA2dpActiveDevice(null);
+ setHfpActiveDevice(null);
}
break;
}
if (prevState == BluetoothProfile.STATE_CONNECTED) {
- // Device disconnected
+ // LE audio device disconnected
if (DBG) {
Log.d(TAG, "handleMessage(MESSAGE_LE_AUDIO_ACTION_CONNECTION_STATE"
+ "_CHANGED): device " + device + " disconnected");
}
mLeAudioConnectedDevices.remove(device);
+ mLeHearingAidConnectedDevices.remove(device);
if (Objects.equals(mLeAudioActiveDevice, device)) {
if (mLeAudioConnectedDevices.isEmpty()) {
setLeAudioActiveDevice(null);
@@ -499,34 +565,28 @@
}
mLeHearingAidConnectedDevices.add(device);
if (!mLeAudioConnectedDevices.contains(device)) {
- mLeAudioConnectedDevices.add(device);
- }
- // New connected device: select it as active
- setLeAudioActiveDevice(device);
- if (mLeAudioActiveDevice == device) {
- // setLeAudioActiveDevice succeed
+ mPendingLeHearingAidActiveDevice.add(device);
+ } else if (Objects.equals(mLeAudioActiveDevice, device)) {
mLeHearingAidActiveDevice = device;
+ } else {
+ // New connected LE hearing aid device: select it as active
+ setLeHearingAidActiveDevice(device);
+ setHearingAidActiveDevice(null);
+ setA2dpActiveDevice(null);
+ setHfpActiveDevice(null);
}
- setA2dpActiveDevice(null);
- setHfpActiveDevice(null);
break;
}
if (prevState == BluetoothProfile.STATE_CONNECTED) {
- // Device disconnected
+ // LE hearing aid device disconnected
if (DBG) {
Log.d(TAG, "handleMessage(MESSAGE_HAP_ACTION_CONNECTION_STATE"
+ "_CHANGED): device " + device + " disconnected");
}
mLeHearingAidConnectedDevices.remove(device);
- mLeAudioConnectedDevices.remove(device);
- // mLeAudioConnectedDevices should contain all of
- // mLeHearingAidConnectedDevices. Call setLeAudioActiveDevice(null)
- // only if there are no LE audio devices.
- if (Objects.equals(mLeHearingAidConnectedDevices, device)) {
- if (mLeAudioConnectedDevices.isEmpty()) {
- setLeAudioActiveDevice(null);
- }
- setFallbackDeviceActive();
+ mPendingLeHearingAidActiveDevice.remove(device);
+ if (Objects.equals(mLeHearingAidActiveDevice, device)) {
+ mLeHearingAidActiveDevice = null;
}
}
}
@@ -536,8 +596,8 @@
Intent intent = (Intent) msg.obj;
BluetoothDevice device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
- if (device != null && !mLeAudioConnectedDevices.contains(device)) {
- mLeAudioConnectedDevices.add(device);
+ if (device != null && !mLeHearingAidConnectedDevices.contains(device)) {
+ mLeHearingAidConnectedDevices.add(device);
}
if (DBG) {
Log.d(TAG, "handleMessage(MESSAGE_HAP_ACTION_ACTIVE_DEVICE_CHANGED): "
@@ -556,6 +616,40 @@
}
}
+ private class AudioManagerOnModeChangedListener implements AudioManager.OnModeChangedListener {
+ public void onModeChanged(int mode) {
+ switch (mode) {
+ case AudioManager.MODE_NORMAL: {
+ if (mPendingA2dpActiveDevice != null) {
+ setA2dpActiveDevice(mPendingA2dpActiveDevice);
+ setLeAudioActiveDevice(null);
+ }
+ break;
+ }
+ case AudioManager.MODE_RINGTONE: {
+ final HeadsetService headsetService = mFactory.getHeadsetService();
+ if (headsetService != null && headsetService.isInbandRingingEnabled()
+ && mPendingHfpActiveDevice != null) {
+ setHfpActiveDevice(mPendingHfpActiveDevice);
+ setLeAudioActiveDevice(null);
+ }
+ break;
+ }
+ case AudioManager.MODE_IN_CALL:
+ case AudioManager.MODE_IN_COMMUNICATION:
+ case AudioManager.MODE_CALL_SCREENING:
+ case AudioManager.MODE_CALL_REDIRECT:
+ case AudioManager.MODE_COMMUNICATION_REDIRECT: {
+ if (mPendingHfpActiveDevice != null) {
+ setHfpActiveDevice(mPendingHfpActiveDevice);
+ setLeAudioActiveDevice(null);
+ }
+ break;
+ }
+ }
+ }
+ }
+
/** Notifications of audio device connection and disconnection events. */
@SuppressLint("AndroidFrameworkRequiresPermission")
private class AudioManagerAudioDeviceCallback extends AudioDeviceCallback {
@@ -602,6 +696,7 @@
mFactory = factory;
mAudioManager = service.getSystemService(AudioManager.class);
mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback();
+ mAudioManagerOnModeChangedListener = new AudioManagerOnModeChangedListener();
}
void start() {
@@ -628,6 +723,11 @@
mAdapterService.registerReceiver(mReceiver, filter);
mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler);
+ mAudioManager.addOnModeChangedListener(command -> {
+ if (!mHandler.post(command)) {
+ throw new RejectedExecutionException(mHandler + " is shutting down");
+ }
+ }, mAudioManagerOnModeChangedListener);
}
void cleanup() {
@@ -670,6 +770,10 @@
return;
}
mA2dpActiveDevice = device;
+ mPendingA2dpActiveDevice = null;
+ if (mPendingHfpActiveDevice != null) {
+ setHfpActiveDevice(mPendingHfpActiveDevice);
+ }
}
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
@@ -685,6 +789,10 @@
return;
}
mHfpActiveDevice = device;
+ mPendingHfpActiveDevice = null;
+ if (mPendingA2dpActiveDevice != null) {
+ setA2dpActiveDevice(mPendingA2dpActiveDevice);
+ }
}
private void setHearingAidActiveDevice(BluetoothDevice device) {
@@ -715,6 +823,18 @@
mLeAudioActiveDevice = device;
if (device == null) {
mLeHearingAidActiveDevice = null;
+ mPendingLeHearingAidActiveDevice.remove(device);
+ }
+ }
+
+ private void setLeHearingAidActiveDevice(BluetoothDevice device) {
+ if (!Objects.equals(mLeAudioActiveDevice, device)) {
+ setLeAudioActiveDevice(device);
+ }
+ if (Objects.equals(mLeAudioActiveDevice, device)) {
+ // setLeAudioActiveDevice succeed
+ mLeHearingAidActiveDevice = device;
+ mPendingLeHearingAidActiveDevice.remove(device);
}
}
@@ -726,18 +846,34 @@
if (dbManager == null) {
return;
}
-
+ List<BluetoothDevice> connectedHearingAidDevices = new ArrayList<>();
if (!mHearingAidConnectedDevices.isEmpty()) {
+ connectedHearingAidDevices.addAll(mHearingAidConnectedDevices);
+ }
+ if (!mLeHearingAidConnectedDevices.isEmpty()) {
+ connectedHearingAidDevices.addAll(mLeHearingAidConnectedDevices);
+ }
+ if (!connectedHearingAidDevices.isEmpty()) {
BluetoothDevice device =
- dbManager.getMostRecentlyConnectedDevicesInList(mHearingAidConnectedDevices);
+ dbManager.getMostRecentlyConnectedDevicesInList(connectedHearingAidDevices);
if (device != null) {
- if (DBG) {
- Log.d(TAG, "set hearing aid device active: " + device);
+ if (mHearingAidConnectedDevices.contains(device)) {
+ if (DBG) {
+ Log.d(TAG, "set hearing aid device active: " + device);
+ }
+ setHearingAidActiveDevice(device);
+ setA2dpActiveDevice(null);
+ setHfpActiveDevice(null);
+ setLeAudioActiveDevice(null);
+ } else {
+ if (DBG) {
+ Log.d(TAG, "set LE hearing aid device active: " + device);
+ }
+ setLeHearingAidActiveDevice(device);
+ setHearingAidActiveDevice(null);
+ setA2dpActiveDevice(null);
+ setHfpActiveDevice(null);
}
- setHearingAidActiveDevice(device);
- setA2dpActiveDevice(null);
- setHfpActiveDevice(null);
- setLeAudioActiveDevice(null);
return;
}
}
@@ -754,7 +890,7 @@
headsetFallbackDevice = headsetService.getFallbackDevice();
}
- List<BluetoothDevice> connectedDevices = new LinkedList<>();
+ List<BluetoothDevice> connectedDevices = new ArrayList<>();
connectedDevices.addAll(mLeAudioConnectedDevices);
switch (mAudioManager.getMode()) {
case AudioManager.MODE_NORMAL:
@@ -821,8 +957,15 @@
mHfpConnectedDevices.clear();
mHfpActiveDevice = null;
+ mHearingAidConnectedDevices.clear();
mHearingAidActiveDevice = null;
+
+ mLeAudioConnectedDevices.clear();
mLeAudioActiveDevice = null;
+
+ mLeHearingAidConnectedDevices.clear();
+ mLeHearingAidActiveDevice = null;
+ mPendingLeHearingAidActiveDevice.clear();
}
@VisibleForTesting
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterApp.java b/android/app/src/com/android/bluetooth/btservice/AdapterApp.java
index 0c22c7c..99e3f4f 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterApp.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterApp.java
@@ -52,6 +52,11 @@
if (DBG) {
Log.d(TAG, "onCreate");
}
+ try {
+ DataMigration.run(this);
+ } catch (Exception e) {
+ Log.e(TAG, "Migration failure: ", e);
+ }
Config.init(this);
}
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
index 9199513..fc0b087 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
@@ -205,11 +205,11 @@
static final String LOCAL_MAC_ADDRESS_PERM = android.Manifest.permission.LOCAL_MAC_ADDRESS;
static final String RECEIVE_MAP_PERM = android.Manifest.permission.RECEIVE_BLUETOOTH_MAP;
- private static final String PHONEBOOK_ACCESS_PERMISSION_PREFERENCE_FILE =
+ static final String PHONEBOOK_ACCESS_PERMISSION_PREFERENCE_FILE =
"phonebook_access_permission";
- private static final String MESSAGE_ACCESS_PERMISSION_PREFERENCE_FILE =
+ static final String MESSAGE_ACCESS_PERMISSION_PREFERENCE_FILE =
"message_access_permission";
- private static final String SIM_ACCESS_PERMISSION_PREFERENCE_FILE = "sim_access_permission";
+ static final String SIM_ACCESS_PERMISSION_PREFERENCE_FILE = "sim_access_permission";
private static final int CONTROLLER_ENERGY_UPDATE_TIMEOUT_MILLIS = 30;
@@ -5105,6 +5105,8 @@
private static final String GATT_ROBUST_CACHING_SERVER_FLAG = "INIT_gatt_robust_caching_server";
private static final String IRK_ROTATION_FLAG = "INIT_irk_rotation";
private static final String PASS_PHY_UPDATE_CALLBACK_FLAG = "INIT_pass_phy_update_callback";
+ private static final String BTM_DM_FLUSH_DISCOVERY_QUEUE_ON_SEARCH_CANCEL =
+ "INIT_btm_dm_flush_discovery_queue_on_search_cancel";
/**
* Logging flags logic (only applies to DEBUG and VERBOSE levels):
@@ -5172,6 +5174,12 @@
DeviceConfig.NAMESPACE_BLUETOOTH, PASS_PHY_UPDATE_CALLBACK_FLAG, true)) {
initFlags.add(String.format("%s=%s", PASS_PHY_UPDATE_CALLBACK_FLAG, "true"));
}
+ if (DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_BLUETOOTH,
+ BTM_DM_FLUSH_DISCOVERY_QUEUE_ON_SEARCH_CANCEL, false)) {
+ initFlags.add(String.format("%s=%s",
+ BTM_DM_FLUSH_DISCOVERY_QUEUE_ON_SEARCH_CANCEL, "true"));
+ }
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH,
LOGGING_DEBUG_ENABLED_FOR_ALL_FLAG, false)) {
initFlags.add(String.format("%s=%s", LOGGING_DEBUG_ENABLED_FOR_ALL_FLAG, "true"));
diff --git a/android/app/src/com/android/bluetooth/btservice/DataMigration.java b/android/app/src/com/android/bluetooth/btservice/DataMigration.java
new file mode 100644
index 0000000..4846d60
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/btservice/DataMigration.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2022 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.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.bluetooth.btservice.storage.BluetoothDatabaseMigration;
+import com.android.bluetooth.opp.BluetoothOppProvider;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.List;
+
+/**
+ * @hide
+ */
+final class DataMigration {
+ private DataMigration(){}
+ private static final String TAG = "DataMigration";
+
+ @VisibleForTesting
+ static final String AUTHORITY = "bluetooth_legacy.provider";
+
+ @VisibleForTesting
+ static final String START_MIGRATION_CALL = "start_legacy_migration";
+ @VisibleForTesting
+ static final String FINISH_MIGRATION_CALL = "finish_legacy_migration";
+
+ @VisibleForTesting
+ static final String BLUETOOTH_DATABASE = "bluetooth_db";
+ @VisibleForTesting
+ static final String OPP_DATABASE = "btopp.db";
+
+ // AvrcpVolumeManager.VOLUME_MAP
+ private static final String VOLUME_MAP_PREFERENCE_FILE = "bluetooth_volume_map";
+ // com.android.blueotooth.opp.Constants.BLUETOOTHOPP_CHANNEL_PREFERENCE
+ private static final String BLUETOOTHOPP_CHANNEL_PREFERENCE = "btopp_channels";
+
+ // com.android.blueotooth.opp.Constants.BLUETOOTHOPP_NAME_PREFERENCE
+ private static final String BLUETOOTHOPP_NAME_PREFERENCE = "btopp_names";
+
+ // com.android.blueotooth.opp.OPP_PREFERENCE_FILE
+ private static final String OPP_PREFERENCE_FILE = "OPPMGR";
+
+ @VisibleForTesting
+ static final String[] sharedPreferencesKeys = {
+ // Bundles of Boolean
+ AdapterService.PHONEBOOK_ACCESS_PERMISSION_PREFERENCE_FILE,
+ AdapterService.MESSAGE_ACCESS_PERMISSION_PREFERENCE_FILE,
+ AdapterService.SIM_ACCESS_PERMISSION_PREFERENCE_FILE,
+
+ // Bundles of Integer
+ VOLUME_MAP_PREFERENCE_FILE,
+ BLUETOOTHOPP_CHANNEL_PREFERENCE,
+
+ // Bundles of String
+ BLUETOOTHOPP_NAME_PREFERENCE,
+
+ // Bundle of Boolean and String
+ OPP_PREFERENCE_FILE,
+ };
+
+ // Main key use for storing all the key in the associate bundle
+ @VisibleForTesting
+ static final String KEY_LIST = "key_list";
+
+ @VisibleForTesting
+ static final String BLUETOOTH_CONFIG = "bluetooth_config";
+ static final String MIGRATION_DONE_PROPERTY = "migration_done";
+ @VisibleForTesting
+ static final String MIGRATION_ATTEMPT_PROPERTY = "migration_attempt";
+
+ @VisibleForTesting
+ public static final int MIGRATION_STATUS_TO_BE_DONE = 0;
+ @VisibleForTesting
+ public static final int MIGRATION_STATUS_COMPLETED = 1;
+ @VisibleForTesting
+ public static final int MIGRATION_STATUS_MISSING_APK = 2;
+ @VisibleForTesting
+ public static final int MIGRATION_STATUS_MAX_ATTEMPT = 3;
+
+ @VisibleForTesting
+ static final int MAX_ATTEMPT = 3;
+
+ static int run(Context ctx) {
+ if (migrationStatus(ctx) == MIGRATION_STATUS_COMPLETED) {
+ Log.d(TAG, "Legacy migration skiped: already completed");
+ return MIGRATION_STATUS_COMPLETED;
+ }
+ if (!isMigrationApkInstalled(ctx)) {
+ Log.d(TAG, "Legacy migration skiped: no migration app installed");
+ markMigrationStatus(ctx, MIGRATION_STATUS_MISSING_APK);
+ return MIGRATION_STATUS_MISSING_APK;
+ }
+ if (!incrementeMigrationAttempt(ctx)) {
+ Log.d(TAG, "Legacy migration skiped: still failing after too many attempt");
+ markMigrationStatus(ctx, MIGRATION_STATUS_MAX_ATTEMPT);
+ return MIGRATION_STATUS_MAX_ATTEMPT;
+ }
+
+ for (String pref: sharedPreferencesKeys) {
+ sharedPreferencesMigration(pref, ctx);
+ }
+ // Migration for DefaultSharedPreferences used in PbapUtils. Contains Long
+ sharedPreferencesMigration(ctx.getPackageName() + "_preferences", ctx);
+
+ bluetoothDatabaseMigration(ctx);
+ oppDatabaseMigration(ctx);
+
+ markMigrationStatus(ctx, MIGRATION_STATUS_COMPLETED);
+ Log.d(TAG, "Legacy migration completed");
+ return MIGRATION_STATUS_COMPLETED;
+ }
+
+ @VisibleForTesting
+ static boolean bluetoothDatabaseMigration(Context ctx) {
+ final String logHeader = BLUETOOTH_DATABASE + ": ";
+ ContentResolver resolver = ctx.getContentResolver();
+ Cursor cursor = resolver.query(
+ Uri.parse("content://" + AUTHORITY + "/" + BLUETOOTH_DATABASE),
+ null, null, null, null);
+ if (cursor == null) {
+ Log.d(TAG, logHeader + "Nothing to migrate");
+ return true;
+ }
+ boolean status = BluetoothDatabaseMigration.run(ctx, cursor);
+ cursor.close();
+ if (status) {
+ resolver.call(AUTHORITY, FINISH_MIGRATION_CALL, BLUETOOTH_DATABASE, null);
+ Log.d(TAG, logHeader + "Migration complete. File is deleted");
+ } else {
+ Log.e(TAG, logHeader + "Invalid data. Incomplete migration. File is not deleted");
+ }
+ return status;
+ }
+
+ @VisibleForTesting
+ static boolean oppDatabaseMigration(Context ctx) {
+ final String logHeader = OPP_DATABASE + ": ";
+ ContentResolver resolver = ctx.getContentResolver();
+ Cursor cursor = resolver.query(
+ Uri.parse("content://" + AUTHORITY + "/" + OPP_DATABASE),
+ null, null, null, null);
+ if (cursor == null) {
+ Log.d(TAG, logHeader + "Nothing to migrate");
+ return true;
+ }
+ boolean status = BluetoothOppProvider.oppDatabaseMigration(ctx, cursor);
+ cursor.close();
+ if (status) {
+ resolver.call(AUTHORITY, FINISH_MIGRATION_CALL, OPP_DATABASE, null);
+ Log.d(TAG, logHeader + "Migration complete. File is deleted");
+ } else {
+ Log.e(TAG, logHeader + "Invalid data. Incomplete migration. File is not deleted");
+ }
+ return status;
+ }
+
+ private static boolean writeObjectToEditor(SharedPreferences.Editor editor, Bundle b,
+ String itemKey) {
+ Object value = b.get(itemKey);
+ if (value == null) {
+ Log.e(TAG, itemKey + ": No value associated with this itemKey");
+ return false;
+ }
+ if (value instanceof Boolean) {
+ editor.putBoolean(itemKey, (Boolean) value);
+ } else if (value instanceof Integer) {
+ editor.putInt(itemKey, (Integer) value);
+ } else if (value instanceof Long) {
+ editor.putLong(itemKey, (Long) value);
+ } else if (value instanceof String) {
+ editor.putString(itemKey, (String) value);
+ } else {
+ Log.e(TAG, itemKey + ": Failed to migrate: "
+ + value.getClass().getSimpleName() + ": Data type not handled");
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ static boolean sharedPreferencesMigration(String prefKey, Context ctx) {
+ final String logHeader = "SharedPreferencesMigration - " + prefKey + ": ";
+ ContentResolver resolver = ctx.getContentResolver();
+ Bundle b = resolver.call(AUTHORITY, START_MIGRATION_CALL, prefKey, null);
+ if (b == null) {
+ Log.d(TAG, logHeader + "Nothing to migrate");
+ return true;
+ }
+ List<String> keys = b.getStringArrayList(KEY_LIST);
+ if (keys == null) {
+ Log.e(TAG, logHeader + "Wrong format of bundle: No keys to migrate");
+ return false;
+ }
+ SharedPreferences pref = ctx.getSharedPreferences(prefKey, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = pref.edit();
+ boolean status = true;
+ for (String itemKey : keys) {
+ // prevent overriding any user settings if it's a new attempt
+ if (!pref.contains(itemKey)) {
+ status &= writeObjectToEditor(editor, b, itemKey);
+ } else {
+ Log.d(TAG, logHeader + itemKey + ": Already exists, not overriding data.");
+ }
+ }
+ editor.apply();
+ if (status) {
+ resolver.call(AUTHORITY, FINISH_MIGRATION_CALL, prefKey, null);
+ Log.d(TAG, logHeader + "Migration complete. File is deleted");
+ } else {
+ Log.e(TAG, logHeader + "Invalid data. Incomplete migration. File is not deleted");
+ }
+ return status;
+ }
+
+ @VisibleForTesting
+ static int migrationStatus(Context ctx) {
+ SharedPreferences pref = ctx.getSharedPreferences(BLUETOOTH_CONFIG, Context.MODE_PRIVATE);
+ return pref.getInt(MIGRATION_DONE_PROPERTY, MIGRATION_STATUS_TO_BE_DONE);
+ }
+
+ @VisibleForTesting
+ static boolean incrementeMigrationAttempt(Context ctx) {
+ SharedPreferences pref = ctx.getSharedPreferences(BLUETOOTH_CONFIG, Context.MODE_PRIVATE);
+ int currentAttempt = Math.min(pref.getInt(MIGRATION_ATTEMPT_PROPERTY, 0), MAX_ATTEMPT);
+ pref.edit()
+ .putInt(MIGRATION_ATTEMPT_PROPERTY, currentAttempt + 1)
+ .apply();
+ return currentAttempt < MAX_ATTEMPT;
+ }
+
+ @VisibleForTesting
+ static boolean isMigrationApkInstalled(Context ctx) {
+ ContentResolver resolver = ctx.getContentResolver();
+ ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY);
+ if (client != null) {
+ client.close();
+ return true;
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ static void markMigrationStatus(Context ctx, int status) {
+ ctx.getSharedPreferences(BLUETOOTH_CONFIG, Context.MODE_PRIVATE)
+ .edit()
+ .putInt(MIGRATION_DONE_PROPERTY, status)
+ .apply();
+ }
+}
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/BluetoothDatabaseMigration.java b/android/app/src/com/android/bluetooth/btservice/storage/BluetoothDatabaseMigration.java
new file mode 100644
index 0000000..a56c6a4
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/btservice/storage/BluetoothDatabaseMigration.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2022 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 android.bluetooth.BluetoothUtils;
+import android.content.Context;
+import android.database.Cursor;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Class for regrouping the migration that occur when going mainline
+ * @hide
+ */
+public final class BluetoothDatabaseMigration {
+ private static final String TAG = "BluetoothDatabaseMigration";
+ /**
+ * @hide
+ */
+ public static boolean run(Context ctx, Cursor cursor) {
+ boolean result = true;
+ MetadataDatabase database = MetadataDatabase.createDatabaseWithoutMigration(ctx);
+ while (cursor.moveToNext()) {
+ try {
+ final String primaryKey = cursor.getString(cursor.getColumnIndexOrThrow("address"));
+ String logKey = BluetoothUtils.toAnonymizedAddress(primaryKey);
+ if (logKey == null) { // handle non device address
+ logKey = primaryKey;
+ }
+
+ Metadata metadata = new Metadata(primaryKey);
+
+ metadata.migrated = fetchInt(cursor, "migrated") > 0;
+ migrate_a2dpSupportsOptionalCodecs(cursor, logKey, metadata);
+ migrate_a2dpOptionalCodecsEnabled(cursor, logKey, metadata);
+ metadata.last_active_time = fetchInt(cursor, "last_active_time");
+ metadata.is_active_a2dp_device = fetchInt(cursor, "is_active_a2dp_device") > 0;
+ migrate_connectionPolicy(cursor, logKey, metadata);
+ migrate_customizedMeta(cursor, metadata);
+
+ database.insert(metadata);
+ Log.d(TAG, "One item migrated: " + metadata);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to migrate one item: " + e);
+ result = false;
+ }
+ }
+ return result;
+ }
+
+ private static final List<Pair<Integer, String>> CONNECTION_POLICIES =
+ Arrays.asList(
+ new Pair(BluetoothProfile.A2DP, "a2dp_connection_policy"),
+ new Pair(BluetoothProfile.A2DP_SINK, "a2dp_sink_connection_policy"),
+ new Pair(BluetoothProfile.HEADSET, "hfp_connection_policy"),
+ new Pair(BluetoothProfile.HEADSET_CLIENT, "hfp_client_connection_policy"),
+ new Pair(BluetoothProfile.HID_HOST, "hid_host_connection_policy"),
+ new Pair(BluetoothProfile.PAN, "pan_connection_policy"),
+ new Pair(BluetoothProfile.PBAP, "pbap_connection_policy"),
+ new Pair(BluetoothProfile.PBAP_CLIENT, "pbap_client_connection_policy"),
+ new Pair(BluetoothProfile.MAP, "map_connection_policy"),
+ new Pair(BluetoothProfile.SAP, "sap_connection_policy"),
+ new Pair(BluetoothProfile.HEARING_AID, "hearing_aid_connection_policy"),
+ new Pair(BluetoothProfile.HAP_CLIENT, "hap_client_connection_policy"),
+ new Pair(BluetoothProfile.MAP_CLIENT, "map_client_connection_policy"),
+ new Pair(BluetoothProfile.LE_AUDIO, "le_audio_connection_policy"),
+ new Pair(BluetoothProfile.VOLUME_CONTROL, "volume_control_connection_policy"),
+ new Pair(BluetoothProfile.CSIP_SET_COORDINATOR,
+ "csip_set_coordinator_connection_policy"),
+ new Pair(BluetoothProfile.LE_CALL_CONTROL, "le_call_control_connection_policy"),
+ new Pair(BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT,
+ "bass_client_connection_policy"),
+ new Pair(BluetoothProfile.BATTERY, "battery_connection_policy")
+ );
+
+ private static final List<Pair<Integer, String>> CUSTOMIZED_META_KEYS =
+ Arrays.asList(
+ new Pair(BluetoothDevice.METADATA_MANUFACTURER_NAME, "manufacturer_name"),
+ new Pair(BluetoothDevice.METADATA_MODEL_NAME, "model_name"),
+ new Pair(BluetoothDevice.METADATA_SOFTWARE_VERSION, "software_version"),
+ new Pair(BluetoothDevice.METADATA_HARDWARE_VERSION, "hardware_version"),
+ new Pair(BluetoothDevice.METADATA_COMPANION_APP, "companion_app"),
+ new Pair(BluetoothDevice.METADATA_MAIN_ICON, "main_icon"),
+ new Pair(BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET, "is_untethered_headset"),
+ new Pair(BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON, "untethered_left_icon"),
+ new Pair(BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON, "untethered_right_icon"),
+ new Pair(BluetoothDevice.METADATA_UNTETHERED_CASE_ICON, "untethered_case_icon"),
+ new Pair(BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY,
+ "untethered_left_battery"),
+ new Pair(BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY,
+ "untethered_right_battery"),
+ new Pair(BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY,
+ "untethered_case_battery"),
+ new Pair(BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING,
+ "untethered_left_charging"),
+ new Pair(BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING,
+ "untethered_right_charging"),
+ new Pair(BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING,
+ "untethered_case_charging"),
+ new Pair(BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI,
+ "enhanced_settings_ui_uri"),
+ new Pair(BluetoothDevice.METADATA_DEVICE_TYPE, "device_type"),
+ new Pair(BluetoothDevice.METADATA_MAIN_BATTERY, "main_battery"),
+ new Pair(BluetoothDevice.METADATA_MAIN_CHARGING, "main_charging"),
+ new Pair(BluetoothDevice.METADATA_MAIN_LOW_BATTERY_THRESHOLD,
+ "main_low_battery_threshold"),
+ new Pair(BluetoothDevice.METADATA_UNTETHERED_LEFT_LOW_BATTERY_THRESHOLD,
+ "untethered_left_low_battery_threshold"),
+ new Pair(BluetoothDevice.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD,
+ "untethered_right_low_battery_threshold"),
+ new Pair(BluetoothDevice.METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD,
+ "untethered_case_low_battery_threshold"),
+ new Pair(BluetoothDevice.METADATA_SPATIAL_AUDIO, "spatial_audio"),
+ new Pair(BluetoothDevice.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+ "fastpair_customized")
+ );
+
+ private static int fetchInt(Cursor cursor, String key) {
+ return cursor.getInt(cursor.getColumnIndexOrThrow(key));
+ }
+
+ private static void migrate_a2dpSupportsOptionalCodecs(Cursor cursor, String logKey,
+ Metadata metadata) {
+ final String key = "a2dpSupportsOptionalCodecs";
+ final List<Integer> allowedValue = new ArrayList<>(Arrays.asList(
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN,
+ BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED,
+ BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED));
+ final int value = fetchInt(cursor, key);
+ if (!allowedValue.contains(value)) {
+ throw new IllegalArgumentException(logKey + ": Bad value for [" + key + "]: " + value);
+ }
+ metadata.a2dpSupportsOptionalCodecs = value;
+ }
+
+ private static void migrate_a2dpOptionalCodecsEnabled(Cursor cursor, String logKey,
+ Metadata metadata) {
+ final String key = "a2dpOptionalCodecsEnabled";
+ final List<Integer> allowedValue = new ArrayList<>(Arrays.asList(
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED,
+ BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED));
+ final int value = fetchInt(cursor, key);
+ if (!allowedValue.contains(value)) {
+ throw new IllegalArgumentException(logKey + ": Bad value for [" + key + "]: " + value);
+ }
+ metadata.a2dpOptionalCodecsEnabled = value;
+ }
+
+ private static void migrate_connectionPolicy(Cursor cursor, String logKey,
+ Metadata metadata) {
+ final List<Integer> allowedValue = new ArrayList<>(Arrays.asList(
+ BluetoothProfile.CONNECTION_POLICY_UNKNOWN,
+ BluetoothProfile.CONNECTION_POLICY_FORBIDDEN,
+ BluetoothProfile.CONNECTION_POLICY_ALLOWED));
+ for (Pair<Integer, String> p : CONNECTION_POLICIES) {
+ final int policy = cursor.getInt(cursor.getColumnIndexOrThrow(p.second));
+ if (allowedValue.contains(policy)) {
+ metadata.setProfileConnectionPolicy(p.first, policy);
+ } else {
+ throw new IllegalArgumentException(logKey + ": Bad value for ["
+ + BluetoothProfile.getProfileName(p.first)
+ + "]: " + policy);
+ }
+ }
+ }
+
+ private static void migrate_customizedMeta(Cursor cursor, Metadata metadata) {
+ for (Pair<Integer, String> p : CUSTOMIZED_META_KEYS) {
+ final byte[] blob = cursor.getBlob(cursor.getColumnIndexOrThrow(p.second));
+ // There is no specific pattern to check the custom meta data
+ metadata.setCustomizedMeta(p.first, blob);
+ }
+ }
+
+}
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
index 91e33e0..f79c5fb 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
@@ -21,17 +21,24 @@
import android.bluetooth.BluetoothA2dp.OptionalCodecsSupportStatus;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUtils;
import androidx.annotation.NonNull;
import androidx.room.Embedded;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.ArrayList;
import java.util.List;
+/**
+ * @hide
+ */
@Entity(tableName = "metadata")
-class Metadata {
+@VisibleForTesting
+public class Metadata {
@PrimaryKey
@NonNull
private String address;
@@ -62,7 +69,11 @@
is_active_a2dp_device = true;
}
- String getAddress() {
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public String getAddress() {
return address;
}
@@ -75,7 +86,7 @@
*/
@NonNull
public String getAnonymizedAddress() {
- return "XX:XX:XX" + getAddress().substring(8);
+ return BluetoothUtils.toAnonymizedAddress(address);
}
void setProfileConnectionPolicy(int profile, int connectionPolicy) {
@@ -148,7 +159,11 @@
}
}
- int getProfileConnectionPolicy(int profile) {
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public int getProfileConnectionPolicy(int profile) {
switch (profile) {
case BluetoothProfile.A2DP:
return profileConnectionPolicies.a2dp_connection_policy;
@@ -275,7 +290,11 @@
}
}
- byte[] getCustomizedMeta(int key) {
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public byte[] getCustomizedMeta(int key) {
byte[] value = null;
switch (key) {
case BluetoothDevice.METADATA_MANUFACTURER_NAME:
@@ -372,7 +391,8 @@
public String toString() {
StringBuilder builder = new StringBuilder();
- builder.append(address)
+ builder.append(getAnonymizedAddress())
+ .append(" last_active_time=" + last_active_time)
.append(" {profile connection policy(")
.append(profileConnectionPolicies)
.append("), optional codec(support=")
diff --git a/android/app/src/com/android/bluetooth/gatt/ScanManager.java b/android/app/src/com/android/bluetooth/gatt/ScanManager.java
index 675b899..8718036 100644
--- a/android/app/src/com/android/bluetooth/gatt/ScanManager.java
+++ b/android/app/src/com/android/bluetooth/gatt/ScanManager.java
@@ -282,6 +282,9 @@
if (client == null) {
client = mScanNative.getRegularScanClient(scannerId);
}
+ if (client == null) {
+ client = mScanNative.getSuspendedScanClient(scannerId);
+ }
sendMessage(MSG_STOP_BLE_SCAN, client);
}
@@ -1127,6 +1130,15 @@
return null;
}
+ ScanClient getSuspendedScanClient(int scannerId) {
+ for (ScanClient client : mSuspendedScanClients) {
+ if (client.scannerId == scannerId) {
+ return client;
+ }
+ }
+ return null;
+ }
+
void stopBatchScan(ScanClient client) {
mBatchClients.remove(client);
removeScanFilters(client.scannerId);
diff --git a/android/app/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java b/android/app/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java
index 8ff4b79..ba5bcf5 100644
--- a/android/app/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java
+++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java
@@ -29,6 +29,7 @@
import com.android.bluetooth.Utils;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.Arrays;
@@ -67,6 +68,16 @@
}
}
+ /**
+ * Set singleton instance.
+ */
+ @VisibleForTesting
+ static void setInstance(LeAudioNativeInterface instance) {
+ synchronized (INSTANCE_LOCK) {
+ sInstance = instance;
+ }
+ }
+
private byte[] getByteAddress(BluetoothDevice device) {
if (device == null) {
return Utils.getBytesFromAddress("00:00:00:00:00:00");
diff --git a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
index 856679c..155385b 100644
--- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
+++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
@@ -326,6 +326,11 @@
mLeAudioNativeIsInitialized = false;
mHfpHandoverDevice = null;
+ mActiveAudioOutDevice = null;
+ mActiveAudioInDevice = null;
+ mDatabaseManager = null;
+ mLeAudioCodecConfig = null;
+
// Set the service and BLE devices as inactive
setLeAudioService(null);
diff --git a/android/app/src/com/android/bluetooth/map/BluetoothMapContent.java b/android/app/src/com/android/bluetooth/map/BluetoothMapContent.java
index afa59bd..5c0a88c 100644
--- a/android/app/src/com/android/bluetooth/map/BluetoothMapContent.java
+++ b/android/app/src/com/android/bluetooth/map/BluetoothMapContent.java
@@ -37,6 +37,7 @@
import android.text.util.Rfc822Tokenizer;
import android.util.Log;
+import com.android.bluetooth.BluetoothMethodProxy;
import com.android.bluetooth.DeviceWorkArounds;
import com.android.bluetooth.SignedLongLong;
import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
@@ -1290,7 +1291,8 @@
String uriStr = new String(Mms.CONTENT_URI + "/" + id + "/part");
Uri uriAddress = Uri.parse(uriStr);
// TODO: maybe use a projection with only "ct" and "text"
- Cursor c = r.query(uriAddress, null, selection, null, null);
+ Cursor c = BluetoothMethodProxy.getInstance().contentResolverQuery(r, uriAddress, null,
+ selection, null, null);
try {
if (c != null && c.moveToFirst()) {
do {
@@ -1405,7 +1407,8 @@
String orderBy = Contacts.DISPLAY_NAME + " ASC";
Cursor c = null;
try {
- c = resolver.query(uri, projection, selection, null, orderBy);
+ c = BluetoothMethodProxy.getInstance().contentResolverQuery(resolver, uri, projection,
+ selection, null, orderBy);
if (c != null) {
int colIndex = c.getColumnIndex(Contacts.DISPLAY_NAME);
if (c.getCount() >= 1) {
@@ -1447,7 +1450,8 @@
Log.v(TAG, "whereClause is " + whereClause);
}
try {
- cr = r.query(sAllThreadsUri, RECIPIENT_ID_PROJECTION, whereClause, null, null);
+ cr = BluetoothMethodProxy.getInstance().contentResolverQuery(r, sAllThreadsUri,
+ RECIPIENT_ID_PROJECTION, whereClause, null, null);
if (cr != null && cr.moveToFirst()) {
recipientIds = cr.getString(0);
if (V) {
@@ -1479,7 +1483,8 @@
Log.v(TAG, "whereClause is " + whereClause);
}
try {
- cr = r.query(sAllCanonical, null, whereClause, null, null);
+ cr = BluetoothMethodProxy.getInstance().contentResolverQuery(r, sAllCanonical, null,
+ whereClause, null, null);
if (cr != null && cr.moveToFirst()) {
do {
//TODO: Multiple Recipeints are appended with ";" for now.
@@ -1511,7 +1516,8 @@
String[] projection = {Mms.Addr.ADDRESS};
Cursor c = null;
try {
- c = r.query(uriAddress, projection, selection, null, null); // TODO: Add projection
+ c = BluetoothMethodProxy.getInstance().contentResolverQuery(r, uriAddress, projection,
+ selection, null, null); // TODO: Add projection
int colIndex = c.getColumnIndex(Mms.Addr.ADDRESS);
if (c != null) {
if (c.moveToFirst()) {
diff --git a/android/app/src/com/android/bluetooth/mcp/McpService.java b/android/app/src/com/android/bluetooth/mcp/McpService.java
index bfccf7e..22d5ee2 100644
--- a/android/app/src/com/android/bluetooth/mcp/McpService.java
+++ b/android/app/src/com/android/bluetooth/mcp/McpService.java
@@ -206,10 +206,10 @@
* 2. authorized devices
* 3. Any LeAudio devices which are allowed to connect
*/
- if (mDeviceAuthorizations.getOrDefault(device, Utils.isPtsTestMode()
- ? BluetoothDevice.ACCESS_ALLOWED : BluetoothDevice.ACCESS_UNKNOWN)
- == BluetoothDevice.ACCESS_ALLOWED) {
- return BluetoothDevice.ACCESS_ALLOWED;
+ int authorization = mDeviceAuthorizations.getOrDefault(device, Utils.isPtsTestMode()
+ ? BluetoothDevice.ACCESS_ALLOWED : BluetoothDevice.ACCESS_UNKNOWN);
+ if (authorization != BluetoothDevice.ACCESS_UNKNOWN) {
+ return authorization;
}
LeAudioService leAudioService = LeAudioService.getLeAudioService();
diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppBatch.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppBatch.java
index 45e79d4..dff6fff 100644
--- a/android/app/src/com/android/bluetooth/opp/BluetoothOppBatch.java
+++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppBatch.java
@@ -37,6 +37,8 @@
import android.content.Context;
import android.util.Log;
+import com.android.bluetooth.BluetoothMethodProxy;
+
import java.util.ArrayList;
/**
@@ -148,7 +150,9 @@
if (info.mStatus < 200) {
if (info.mDirection == BluetoothShare.DIRECTION_INBOUND && info.mUri != null) {
- mContext.getContentResolver().delete(info.mUri, null, null);
+ BluetoothMethodProxy.getInstance().contentResolverDelete(
+ mContext.getContentResolver(), info.mUri, null, null
+ );
}
if (V) {
Log.v(TAG, "Cancel batch for info " + info.mId);
diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppPreference.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppPreference.java
index 3070947..740513f 100644
--- a/android/app/src/com/android/bluetooth/opp/BluetoothOppPreference.java
+++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppPreference.java
@@ -102,7 +102,8 @@
}
public String getName(BluetoothDevice remoteDevice) {
- if (remoteDevice.getIdentityAddress().equals("FF:FF:FF:00:00:00")) {
+ String identityAddress = remoteDevice.getIdentityAddress();
+ if (identityAddress != null && identityAddress.equals("FF:FF:FF:00:00:00")) {
return "localhost";
}
if (!mNames.isEmpty()) {
diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppProvider.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppProvider.java
index 4079a87..6505305 100644
--- a/android/app/src/com/android/bluetooth/opp/BluetoothOppProvider.java
+++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppProvider.java
@@ -44,6 +44,10 @@
import android.net.Uri;
import android.util.Log;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
/**
* This provider allows application to interact with Bluetooth OPP manager
*/
@@ -98,7 +102,7 @@
* when a new version of the provider needs an updated version of the
* database.
*/
- private final class DatabaseHelper extends SQLiteOpenHelper {
+ private static final class DatabaseHelper extends SQLiteOpenHelper {
DatabaseHelper(final Context context) {
super(context, DB_NAME, null, DB_VERSION);
@@ -137,7 +141,7 @@
}
- private void createTable(SQLiteDatabase db) {
+ private static void createTable(SQLiteDatabase db) {
try {
db.execSQL("CREATE TABLE " + DB_TABLE + "(" + BluetoothShare._ID
+ " INTEGER PRIMARY KEY AUTOINCREMENT," + BluetoothShare.URI + " TEXT, "
@@ -155,7 +159,7 @@
}
}
- private void dropTable(SQLiteDatabase db) {
+ private static void dropTable(SQLiteDatabase db) {
try {
db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
} catch (SQLException ex) {
@@ -198,6 +202,62 @@
}
}
+ private static void putString(String key, Cursor from, ContentValues to) {
+ to.put(key, from.getString(from.getColumnIndexOrThrow(key)));
+ }
+ private static void putInteger(String key, Cursor from, ContentValues to) {
+ to.put(key, from.getInt(from.getColumnIndexOrThrow(key)));
+ }
+ private static void putLong(String key, Cursor from, ContentValues to) {
+ to.put(key, from.getLong(from.getColumnIndexOrThrow(key)));
+ }
+
+ /**
+ * @hide
+ */
+ public static boolean oppDatabaseMigration(Context ctx, Cursor cursor) {
+ boolean result = true;
+ SQLiteDatabase db = new DatabaseHelper(ctx).getWritableDatabase();
+ while (cursor.moveToNext()) {
+ try {
+ ContentValues values = new ContentValues();
+
+ final List<String> stringKeys = new ArrayList<>(Arrays.asList(
+ BluetoothShare.URI,
+ BluetoothShare.FILENAME_HINT,
+ BluetoothShare.MIMETYPE,
+ BluetoothShare.DESTINATION));
+ for (String k : stringKeys) {
+ putString(k, cursor, values);
+ }
+
+ final List<String> integerKeys = new ArrayList<>(Arrays.asList(
+ BluetoothShare.VISIBILITY,
+ BluetoothShare.USER_CONFIRMATION,
+ BluetoothShare.DIRECTION,
+ BluetoothShare.STATUS,
+ Constants.MEDIA_SCANNED));
+ for (String k : integerKeys) {
+ putInteger(k, cursor, values);
+ }
+
+ final List<String> longKeys = new ArrayList<>(Arrays.asList(
+ BluetoothShare.TOTAL_BYTES,
+ BluetoothShare.TIMESTAMP));
+ for (String k : longKeys) {
+ putLong(k, cursor, values);
+ }
+
+ db.insert(DB_TABLE, null, values);
+ Log.d(TAG, "One item migrated: " + values);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to migrate one item: " + e);
+ result = false;
+ }
+ }
+ return result;
+ }
+
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppTransferActivity.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppTransferActivity.java
index bba6758..5f59890 100644
--- a/android/app/src/com/android/bluetooth/opp/BluetoothOppTransferActivity.java
+++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppTransferActivity.java
@@ -52,6 +52,8 @@
import com.android.bluetooth.R;
+import com.google.common.annotations.VisibleForTesting;
+
/**
* Handle all transfer related dialogs: -Ongoing transfer -Receiving one file
* dialog -Sending one file dialog -sending multiple files dialog -Complete
@@ -83,7 +85,8 @@
private TextView mLine1View, mLine2View, mLine3View, mLine5View;
- private int mWhichDialog;
+ @VisibleForTesting
+ int mWhichDialog;
private BluetoothAdapter mAdapter;
diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppUtility.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppUtility.java
index f1f1faa..89e51c8 100644
--- a/android/app/src/com/android/bluetooth/opp/BluetoothOppUtility.java
+++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppUtility.java
@@ -51,6 +51,7 @@
import android.util.EventLog;
import android.util.Log;
+import com.android.bluetooth.BluetoothMethodProxy;
import com.android.bluetooth.R;
import java.io.File;
@@ -88,7 +89,9 @@
public static BluetoothOppTransferInfo queryRecord(Context context, Uri uri) {
BluetoothOppTransferInfo info = new BluetoothOppTransferInfo();
- Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
+ Cursor cursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
+ context.getContentResolver(), uri, null, null, null, null
+ );
if (cursor != null) {
if (cursor.moveToFirst()) {
fillRecord(context, cursor, info);
@@ -157,10 +160,14 @@
public static ArrayList<String> queryTransfersInBatch(Context context, Long timeStamp) {
ArrayList<String> uris = new ArrayList();
final String where = BluetoothShare.TIMESTAMP + " == " + timeStamp;
- Cursor metadataCursor =
- context.getContentResolver().query(BluetoothShare.CONTENT_URI, new String[]{
- BluetoothShare._DATA
- }, where, null, BluetoothShare._ID);
+ Cursor metadataCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
+ context.getContentResolver(),
+ BluetoothShare.CONTENT_URI,
+ new String[]{BluetoothShare._DATA},
+ where,
+ null,
+ BluetoothShare._ID
+ );
if (metadataCursor == null) {
return null;
@@ -200,8 +207,10 @@
}
Uri path = null;
- Cursor metadataCursor = context.getContentResolver().query(uri, new String[]{
- BluetoothShare.URI}, null, null, null);
+ Cursor metadataCursor = BluetoothMethodProxy.getInstance().contentResolverQuery(
+ context.getContentResolver(), uri, new String[]{BluetoothShare.URI},
+ null, null, null
+ );
if (metadataCursor != null) {
try {
if (metadataCursor.moveToFirst()) {
@@ -307,7 +316,8 @@
public static void updateVisibilityToHidden(Context context, Uri uri) {
ContentValues updateValues = new ContentValues();
updateValues.put(BluetoothShare.VISIBILITY, BluetoothShare.VISIBILITY_HIDDEN);
- context.getContentResolver().update(uri, updateValues, null, null);
+ BluetoothMethodProxy.getInstance().contentResolverUpdate(context.getContentResolver(), uri,
+ updateValues, null, null);
}
/**
diff --git a/android/app/src/com/android/bluetooth/opp/Constants.java b/android/app/src/com/android/bluetooth/opp/Constants.java
index 84f735d..f1e5a30 100644
--- a/android/app/src/com/android/bluetooth/opp/Constants.java
+++ b/android/app/src/com/android/bluetooth/opp/Constants.java
@@ -38,6 +38,7 @@
import android.net.Uri;
import android.util.Log;
+import com.android.bluetooth.BluetoothMethodProxy;
import com.android.obex.HeaderSet;
import java.io.IOException;
@@ -229,7 +230,8 @@
Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + id);
ContentValues updateValues = new ContentValues();
updateValues.put(BluetoothShare.STATUS, status);
- context.getContentResolver().update(contentUri, updateValues, null, null);
+ BluetoothMethodProxy.getInstance().contentResolverUpdate(context.getContentResolver(),
+ contentUri, updateValues, null, null);
Constants.sendIntentIfCompleted(context, contentUri, status);
}
diff --git a/android/app/src/com/android/bluetooth/sap/SapRilReceiver.java b/android/app/src/com/android/bluetooth/sap/SapRilReceiver.java
index c21778b..0e8580b 100644
--- a/android/app/src/com/android/bluetooth/sap/SapRilReceiver.java
+++ b/android/app/src/com/android/bluetooth/sap/SapRilReceiver.java
@@ -369,4 +369,7 @@
mSapServerMsgHandler.sendMessage(newMsg);
}
+ AtomicLong getSapProxyCookie() {
+ return mSapProxyCookie;
+ }
}
diff --git a/android/app/src/com/android/bluetooth/sap/SapServer.java b/android/app/src/com/android/bluetooth/sap/SapServer.java
index 2514863..0ad1f8d 100644
--- a/android/app/src/com/android/bluetooth/sap/SapServer.java
+++ b/android/app/src/com/android/bluetooth/sap/SapServer.java
@@ -25,6 +25,8 @@
import android.util.Log;
import com.android.bluetooth.R;
+import com.android.bluetooth.Utils;
+import com.android.internal.annotations.VisibleForTesting;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@@ -51,26 +53,31 @@
public static final boolean DEBUG = SapService.DEBUG;
public static final boolean VERBOSE = SapService.VERBOSE;
- private enum SAP_STATE {
+ @VisibleForTesting
+ enum SAP_STATE {
DISCONNECTED, CONNECTING, CONNECTING_CALL_ONGOING, CONNECTED, CONNECTED_BUSY, DISCONNECTING;
}
- private SAP_STATE mState = SAP_STATE.DISCONNECTED;
+ @VisibleForTesting
+ SAP_STATE mState = SAP_STATE.DISCONNECTED;
private Context mContext = null;
/* RFCOMM socket I/O streams */
private BufferedOutputStream mRfcommOut = null;
private BufferedInputStream mRfcommIn = null;
/* References to the SapRilReceiver object */
- private SapRilReceiver mRilBtReceiver = null;
+ @VisibleForTesting
+ SapRilReceiver mRilBtReceiver = null;
/* The message handler members */
- private Handler mSapHandler = null;
+ @VisibleForTesting
+ Handler mSapHandler = null;
private HandlerThread mHandlerThread = null;
/* Reference to the SAP service - which created this instance of the SAP server */
private Handler mSapServiceHandler = null;
/* flag for when user forces disconnect of rfcomm */
- private boolean mIsLocalInitDisconnect = false;
+ @VisibleForTesting
+ boolean mIsLocalInitDisconnect = false;
private CountDownLatch mDeinitSignal = new CountDownLatch(1);
/* Message ID's handled by the message handler */
@@ -86,7 +93,8 @@
public static final String SAP_DISCONNECT_TYPE_EXTRA =
"com.android.bluetooth.sap.extra.DISCONNECT_TYPE";
public static final int NOTIFICATION_ID = android.R.drawable.stat_sys_data_bluetooth;
- private static final String SAP_NOTIFICATION_CHANNEL = "sap_notification_channel";
+ @VisibleForTesting
+ static final String SAP_NOTIFICATION_CHANNEL = "sap_notification_channel";
public static final int ISAP_GET_SERVICE_DELAY_MILLIS = 3 * 1000;
private static final int DISCONNECT_TIMEOUT_IMMEDIATE = 5000; /* ms */
private static final int DISCONNECT_TIMEOUT_RFCOMM = 2000; /* ms */
@@ -99,7 +107,8 @@
/* We store the mMaxMessageSize, as we need a copy of it when the init. sequence completes */
private int mMaxMsgSize = 0;
/* keep track of the current RIL test mode */
- private int mTestMode = SapMessage.INVALID_VALUE; // used to set the RIL in test mode
+ @VisibleForTesting
+ int mTestMode = SapMessage.INVALID_VALUE; // used to set the RIL in test mode
/**
* SapServer constructor
@@ -127,9 +136,11 @@
/**
* This handles the response from RIL.
*/
- private BroadcastReceiver mIntentReceiver;
+ @VisibleForTesting
+ BroadcastReceiver mIntentReceiver;
- private class SapServerBroadcastReceiver extends BroadcastReceiver {
+ @VisibleForTesting
+ class SapServerBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) {
@@ -177,12 +188,13 @@
* @param testMode Use SapMessage.TEST_MODE_XXX
*/
public void setTestMode(int testMode) {
- if (SapMessage.TEST) {
+ if (SapMessage.TEST || Utils.isInstrumentationTestMode()) {
mTestMode = testMode;
}
}
- private void sendDisconnectInd(int discType) {
+ @VisibleForTesting
+ void sendDisconnectInd(int discType) {
if (VERBOSE) {
Log.v(TAG, "in sendDisconnectInd()");
}
@@ -556,7 +568,8 @@
*
* @param msg the incoming SapMessage
*/
- private void onConnectRequest(SapMessage msg) {
+ @VisibleForTesting
+ void onConnectRequest(SapMessage msg) {
SapMessage reply = new SapMessage(SapMessage.ID_CONNECT_RESP);
if (mState == SAP_STATE.CONNECTING) {
@@ -601,7 +614,8 @@
}
}
- private void clearPendingRilResponses(SapMessage msg) {
+ @VisibleForTesting
+ void clearPendingRilResponses(SapMessage msg) {
if (mState == SAP_STATE.CONNECTED_BUSY) {
msg.setClearRilQueue(true);
}
@@ -611,7 +625,8 @@
* Send RFCOMM message to the Sap Server Handler Thread
* @param sapMsg The message to send
*/
- private void sendClientMessage(SapMessage sapMsg) {
+ @VisibleForTesting
+ void sendClientMessage(SapMessage sapMsg) {
Message newMsg = mSapHandler.obtainMessage(SAP_MSG_RFC_REPLY, sapMsg);
mSapHandler.sendMessage(newMsg);
}
@@ -620,7 +635,8 @@
* Send a RIL message to the SapServer message handler thread
* @param sapMsg
*/
- private void sendRilThreadMessage(SapMessage sapMsg) {
+ @VisibleForTesting
+ void sendRilThreadMessage(SapMessage sapMsg) {
Message newMsg = mSapHandler.obtainMessage(SAP_MSG_RIL_REQ, sapMsg);
mSapHandler.sendMessage(newMsg);
}
@@ -629,7 +645,8 @@
* Examine if a call is ongoing, by asking the telephony manager
* @return false if the phone is IDLE (can be used for SAP), true otherwise.
*/
- private boolean isCallOngoing() {
+ @VisibleForTesting
+ boolean isCallOngoing() {
TelephonyManager tManager = mContext.getSystemService(TelephonyManager.class);
if (tManager.getCallState() == TelephonyManager.CALL_STATE_IDLE) {
return false;
@@ -642,7 +659,8 @@
* We add thread protection, as we access the state from two threads.
* @param newState
*/
- private void changeState(SAP_STATE newState) {
+ @VisibleForTesting
+ void changeState(SAP_STATE newState) {
if (DEBUG) {
Log.i(TAG_HANDLER, "Changing state from " + mState.name() + " to " + newState.name());
}
@@ -706,7 +724,7 @@
startDisconnectTimer(SapMessage.DISC_RFCOMM, DISCONNECT_TIMEOUT_RFCOMM);
break;
case SAP_PROXY_DEAD:
- if ((long) msg.obj == mRilBtReceiver.mSapProxyCookie.get()) {
+ if ((long) msg.obj == mRilBtReceiver.getSapProxyCookie().get()) {
mRilBtReceiver.notifyShutdown(); /* Only needed in case of a connection error */
mRilBtReceiver.resetSapProxy();
@@ -726,7 +744,8 @@
* Close the in/out rfcomm streams, to trigger a shutdown of the SapServer main thread.
* Use this after completing the deinit sequence.
*/
- private void shutdown() {
+ @VisibleForTesting
+ void shutdown() {
if (DEBUG) {
Log.i(TAG_HANDLER, "in Shutdown()");
@@ -749,7 +768,8 @@
clearNotification();
}
- private void startDisconnectTimer(int discType, int timeMs) {
+ @VisibleForTesting
+ void startDisconnectTimer(int discType, int timeMs) {
stopDisconnectTimer();
synchronized (this) {
@@ -769,7 +789,8 @@
}
}
- private void stopDisconnectTimer() {
+ @VisibleForTesting
+ void stopDisconnectTimer() {
synchronized (this) {
if (mPendingDiscIntent != null) {
AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
@@ -789,7 +810,8 @@
* here before they go to the client
* @param sapMsg the message to send to the SAP client
*/
- private void handleRfcommReply(SapMessage sapMsg) {
+ @VisibleForTesting
+ void handleRfcommReply(SapMessage sapMsg) {
if (sapMsg != null) {
if (DEBUG) {
@@ -908,7 +930,8 @@
}
}
- private void handleRilInd(SapMessage sapMsg) {
+ @VisibleForTesting
+ void handleRilInd(SapMessage sapMsg) {
if (sapMsg == null) {
return;
}
@@ -939,7 +962,8 @@
* This is only to be called from the handlerThread, else use sendRilThreadMessage();
* @param sapMsg
*/
- private void sendRilMessage(SapMessage sapMsg) {
+ @VisibleForTesting
+ void sendRilMessage(SapMessage sapMsg) {
if (VERBOSE) {
Log.i(TAG_HANDLER,
"sendRilMessage() - " + SapMessage.getMsgTypeName(sapMsg.getMsgType()));
@@ -975,7 +999,8 @@
/**
* Only call this from the sapHandler thread.
*/
- private void sendReply(SapMessage msg) {
+ @VisibleForTesting
+ void sendReply(SapMessage msg) {
if (VERBOSE) {
Log.i(TAG_HANDLER,
"sendReply() RFCOMM - " + SapMessage.getMsgTypeName(msg.getMsgType()));
@@ -992,7 +1017,8 @@
}
}
- private static String getMessageName(int messageId) {
+ @VisibleForTesting
+ static String getMessageName(int messageId) {
switch (messageId) {
case SAP_MSG_RFC_REPLY:
return "SAP_MSG_REPLY";
diff --git a/android/app/tests/unit/AndroidTest.xml b/android/app/tests/unit/AndroidTest.xml
index 981e0a8..4966540 100644
--- a/android/app/tests/unit/AndroidTest.xml
+++ b/android/app/tests/unit/AndroidTest.xml
@@ -20,6 +20,9 @@
<option name="cleanup-apks" value="true" />
<option name="test-file-name" value="BluetoothInstrumentationTests.apk" />
</target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
+ <option name="force-root" value="true" />
+ </target_preparer>
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="run-command" value="settings put global ble_scan_always_enabled 0" />
<option name="run-command" value="su u$(am get-current-user)_system svc bluetooth disable" />
@@ -32,6 +35,7 @@
<option name="test-tag" value="BluetoothInstrumentationTests" />
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.bluetooth.tests" />
+ <option name="test-filter-dir" value="/data/data/{PACKAGE}/cache" />
<option name="hidden-api-checks" value="false"/>
</test>
diff --git a/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayerTest.java b/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayerTest.java
new file mode 100644
index 0000000..30e9c62
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/AvrcpPlayerTest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2022 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 com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.net.Uri;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class AvrcpPlayerTest {
+ private static final int TEST_PLAYER_ID = 1;
+ private static final int TEST_PLAYER_TYPE = AvrcpPlayer.TYPE_VIDEO;
+ private static final int TEST_PLAYER_SUB_TYPE = AvrcpPlayer.SUB_TYPE_AUDIO_BOOK;
+ private static final String TEST_NAME = "test_name";
+ private static final int TEST_FEATURE = AvrcpPlayer.FEATURE_PLAY;
+ private static final int TEST_PLAY_STATUS = PlaybackStateCompat.STATE_STOPPED;
+ private static final int TEST_PLAY_TIME = 1;
+
+ private final AvrcpItem mAvrcpItem = new AvrcpItem.Builder().build();
+ private final byte[] mTestAddress = new byte[]{01, 01, 01, 01, 01, 01};
+ private BluetoothAdapter mAdapter;
+ private BluetoothDevice mTestDevice = null;
+
+ @Mock
+ private PlayerApplicationSettings mPlayerApplicationSettings;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ mTestDevice = mAdapter.getRemoteDevice(mTestAddress);
+ }
+
+ @Test
+ public void buildAvrcpPlayer() {
+ AvrcpPlayer.Builder builder = new AvrcpPlayer.Builder();
+ builder.setDevice(mTestDevice);
+ builder.setPlayerId(TEST_PLAYER_ID);
+ builder.setPlayerType(TEST_PLAYER_TYPE);
+ builder.setPlayerSubType(TEST_PLAYER_SUB_TYPE);
+ builder.setName(TEST_NAME);
+ builder.setSupportedFeature(TEST_FEATURE);
+ builder.setPlayStatus(TEST_PLAY_STATUS);
+ builder.setCurrentTrack(mAvrcpItem);
+
+ AvrcpPlayer avrcpPlayer = builder.build();
+
+ assertThat(avrcpPlayer.getDevice()).isEqualTo(mTestDevice);
+ assertThat(avrcpPlayer.getId()).isEqualTo(TEST_PLAYER_ID);
+ assertThat(avrcpPlayer.getName()).isEqualTo(TEST_NAME);
+ assertThat(avrcpPlayer.supportsFeature(TEST_FEATURE)).isTrue();
+ assertThat(avrcpPlayer.getPlayStatus()).isEqualTo(TEST_PLAY_STATUS);
+ assertThat(avrcpPlayer.getCurrentTrack()).isEqualTo(mAvrcpItem);
+ assertThat(avrcpPlayer.getPlaybackState().getActions()).isEqualTo(
+ PlaybackStateCompat.ACTION_PREPARE | PlaybackStateCompat.ACTION_PLAY);
+ }
+
+ @Test
+ public void setAndGetPlayTime() {
+ AvrcpPlayer avrcpPlayer = new AvrcpPlayer.Builder().build();
+
+ avrcpPlayer.setPlayTime(TEST_PLAY_TIME);
+
+ assertThat(avrcpPlayer.getPlayTime()).isEqualTo(TEST_PLAY_TIME);
+ }
+
+ @Test
+ public void setPlayStatus() {
+ AvrcpPlayer avrcpPlayer = new AvrcpPlayer.Builder().build();
+ avrcpPlayer.setPlayTime(TEST_PLAY_TIME);
+
+ avrcpPlayer.setPlayStatus(PlaybackStateCompat.STATE_PLAYING);
+ assertThat(avrcpPlayer.getPlaybackState().getPlaybackSpeed()).isEqualTo(1);
+
+ avrcpPlayer.setPlayStatus(PlaybackStateCompat.STATE_PAUSED);
+ assertThat(avrcpPlayer.getPlaybackState().getPlaybackSpeed()).isEqualTo(0);
+
+ avrcpPlayer.setPlayStatus(PlaybackStateCompat.STATE_FAST_FORWARDING);
+ assertThat(avrcpPlayer.getPlaybackState().getPlaybackSpeed()).isEqualTo(3);
+
+ avrcpPlayer.setPlayStatus(PlaybackStateCompat.STATE_REWINDING);
+ assertThat(avrcpPlayer.getPlaybackState().getPlaybackSpeed()).isEqualTo(-3);
+ }
+
+ @Test
+ public void setSupportedPlayerApplicationSettings() {
+ when(mPlayerApplicationSettings.supportsSetting(
+ PlayerApplicationSettings.REPEAT_STATUS)).thenReturn(true);
+ when(mPlayerApplicationSettings.supportsSetting(
+ PlayerApplicationSettings.SHUFFLE_STATUS)).thenReturn(true);
+ AvrcpPlayer avrcpPlayer = new AvrcpPlayer.Builder().build();
+ long expectedActions =
+ PlaybackStateCompat.ACTION_PREPARE | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
+ | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE;
+
+ avrcpPlayer.setSupportedPlayerApplicationSettings(mPlayerApplicationSettings);
+
+ assertThat(avrcpPlayer.getPlaybackState().getActions()).isEqualTo(expectedActions);
+ }
+
+ @Test
+ public void supportsSetting() {
+ int settingType = 1;
+ int settingValue = 1;
+ when(mPlayerApplicationSettings.supportsSetting(settingType, settingValue)).thenReturn(
+ true);
+ AvrcpPlayer avrcpPlayer = new AvrcpPlayer.Builder().build();
+
+ avrcpPlayer.setSupportedPlayerApplicationSettings(mPlayerApplicationSettings);
+
+ assertThat(avrcpPlayer.supportsSetting(settingType, settingValue)).isTrue();
+ }
+
+ @Test
+ public void updateAvailableActions() {
+ byte[] supportedFeatures = new byte[16];
+ setSupportedFeature(supportedFeatures, AvrcpPlayer.FEATURE_STOP);
+ setSupportedFeature(supportedFeatures, AvrcpPlayer.FEATURE_PAUSE);
+ setSupportedFeature(supportedFeatures, AvrcpPlayer.FEATURE_REWIND);
+ setSupportedFeature(supportedFeatures, AvrcpPlayer.FEATURE_FAST_FORWARD);
+ setSupportedFeature(supportedFeatures, AvrcpPlayer.FEATURE_FORWARD);
+ setSupportedFeature(supportedFeatures, AvrcpPlayer.FEATURE_PREVIOUS);
+ long expectedActions = PlaybackStateCompat.ACTION_PREPARE | PlaybackStateCompat.ACTION_STOP
+ | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_REWIND
+ | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
+
+ AvrcpPlayer avrcpPlayer = new AvrcpPlayer.Builder().setSupportedFeatures(
+ supportedFeatures).build();
+
+ assertThat(avrcpPlayer.getPlaybackState().getActions()).isEqualTo(expectedActions);
+ }
+
+ @Test
+ public void toString_returnsInfo() {
+ AvrcpPlayer avrcpPlayer = new AvrcpPlayer.Builder().setPlayerId(TEST_PLAYER_ID).setName(
+ TEST_NAME).setCurrentTrack(mAvrcpItem).build();
+
+ assertThat(avrcpPlayer.toString()).isEqualTo(
+ "<AvrcpPlayer id=" + TEST_PLAYER_ID + " name=" + TEST_NAME + " track="
+ + mAvrcpItem + " playState=" + avrcpPlayer.getPlaybackState() + ">");
+ }
+
+ @Test
+ public void notifyImageDownload() {
+ String uuid = "1111";
+ Uri uri = Uri.parse("http://test.com");
+ AvrcpItem trackWithDifferentUuid = new AvrcpItem.Builder().build();
+ AvrcpItem trackWithSameUuid = new AvrcpItem.Builder().build();
+ trackWithSameUuid.setCoverArtUuid(uuid);
+ AvrcpPlayer avrcpPlayer = new AvrcpPlayer.Builder().build();
+
+ assertThat(avrcpPlayer.notifyImageDownload(uuid, uri)).isFalse();
+
+ avrcpPlayer.updateCurrentTrack(trackWithDifferentUuid);
+ assertThat(avrcpPlayer.notifyImageDownload(uuid, uri)).isFalse();
+
+ avrcpPlayer.updateCurrentTrack(trackWithSameUuid);
+ assertThat(avrcpPlayer.notifyImageDownload(uuid, uri)).isTrue();
+ }
+
+ private void setSupportedFeature(byte[] supportedFeatures, int feature) {
+ int byteNumber = feature / 8;
+ byte bitMask = (byte) (1 << (feature % 8));
+ supportedFeatures[byteNumber] = (byte) (supportedFeatures[byteNumber] | bitMask);
+ }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/BrowseTreeTest.java b/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/BrowseTreeTest.java
new file mode 100644
index 0000000..53af271
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/avrcpcontroller/BrowseTreeTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2022 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 com.google.common.truth.Truth.assertThat;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+
+import com.android.bluetooth.avrcpcontroller.BrowseTree.BrowseNode;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Set;
+
+public class BrowseTreeTest {
+ private static final String ILLEGAL_ID = "illegal_id";
+ private static final String TEST_HANDLE = "test_handle";
+ private static final String TEST_NODE_ID = "test_node_id";
+
+ private final byte[] mTestAddress = new byte[]{01, 01, 01, 01, 01, 01};
+ private BluetoothAdapter mAdapter;
+ private BluetoothDevice mTestDevice = null;
+
+ @Before
+ public void setUp() {
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ mTestDevice = mAdapter.getRemoteDevice(mTestAddress);
+ }
+
+ @Test
+ public void constructor_withoutDevice() {
+ BrowseTree browseTree = new BrowseTree(null);
+
+ assertThat(browseTree.mRootNode.mItem.getDevice()).isEqualTo(null);
+ }
+
+ @Test
+ public void constructor_withDevice() {
+ BrowseTree browseTree = new BrowseTree(mTestDevice);
+
+ assertThat(browseTree.mRootNode.mItem.getDevice()).isEqualTo(mTestDevice);
+ }
+
+ @Test
+ public void clear() {
+ BrowseTree browseTree = new BrowseTree(mTestDevice);
+
+ browseTree.clear();
+
+ assertThat(browseTree.mBrowseMap).isEmpty();
+ }
+
+ @Test
+ public void getTrackFromNowPlayingList() {
+ BrowseTree browseTree = new BrowseTree(mTestDevice);
+ BrowseNode trackInNowPlayingList = browseTree.new BrowseNode(new AvrcpItem.Builder()
+ .setUuid(ILLEGAL_ID).setTitle(ILLEGAL_ID).setBrowsable(true).build());
+
+ browseTree.mNowPlayingNode.addChild(trackInNowPlayingList);
+
+ assertThat(browseTree.getTrackFromNowPlayingList(0)).isEqualTo(
+ trackInNowPlayingList);
+ }
+
+ @Test
+ public void onConnected() {
+ BrowseTree browseTree = new BrowseTree(null);
+
+ assertThat(browseTree.mRootNode.getChildrenCount()).isEqualTo(0);
+
+ browseTree.onConnected(mTestDevice);
+
+ assertThat(browseTree.mRootNode.getChildrenCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void findBrowseNodeByID() {
+ BrowseTree browseTree = new BrowseTree(mTestDevice);
+
+ assertThat(browseTree.findBrowseNodeByID(ILLEGAL_ID)).isNull();
+ assertThat(browseTree.findBrowseNodeByID(BrowseTree.ROOT)).isEqualTo(browseTree.mRootNode);
+ }
+
+ @Test
+ public void setAndGetCurrentBrowsedFolder() {
+ BrowseTree browseTree = new BrowseTree(mTestDevice);
+
+ assertThat(browseTree.setCurrentBrowsedFolder(ILLEGAL_ID)).isFalse();
+ assertThat(browseTree.setCurrentBrowsedFolder(BrowseTree.NOW_PLAYING_PREFIX)).isTrue();
+ assertThat(browseTree.getCurrentBrowsedFolder()).isEqualTo(browseTree.mNowPlayingNode);
+ }
+
+ @Test
+ public void setAndGetCurrentBrowsedPlayer() {
+ BrowseTree browseTree = new BrowseTree(mTestDevice);
+
+ assertThat(browseTree.setCurrentBrowsedPlayer(ILLEGAL_ID, 0, 0)).isFalse();
+ assertThat(
+ browseTree.setCurrentBrowsedPlayer(BrowseTree.NOW_PLAYING_PREFIX, 2, 1)).isTrue();
+ assertThat(browseTree.getCurrentBrowsedPlayer()).isEqualTo(browseTree.mNowPlayingNode);
+ }
+
+ @Test
+ public void setAndGetCurrentAddressedPlayer() {
+ BrowseTree browseTree = new BrowseTree(mTestDevice);
+
+ assertThat(browseTree.setCurrentAddressedPlayer(ILLEGAL_ID)).isFalse();
+ assertThat(browseTree.setCurrentAddressedPlayer(BrowseTree.NOW_PLAYING_PREFIX)).isTrue();
+ assertThat(browseTree.getCurrentAddressedPlayer()).isEqualTo(browseTree.mNowPlayingNode);
+ }
+
+ @Test
+ public void indicateCoverArtUsedAndUnused() {
+ BrowseTree browseTree = new BrowseTree(mTestDevice);
+ assertThat(browseTree.getNodesUsingCoverArt(TEST_HANDLE)).isEmpty();
+
+ browseTree.indicateCoverArtUsed(TEST_NODE_ID, TEST_HANDLE);
+
+ assertThat(browseTree.getNodesUsingCoverArt(TEST_HANDLE).get(0)).isEqualTo(TEST_NODE_ID);
+
+ browseTree.indicateCoverArtUnused(TEST_NODE_ID, TEST_HANDLE);
+
+ assertThat(browseTree.getNodesUsingCoverArt(TEST_HANDLE)).isEmpty();
+ assertThat(browseTree.getAndClearUnusedCoverArt().get(0)).isEqualTo(TEST_HANDLE);
+ }
+
+ @Test
+ public void notifyImageDownload() {
+ BrowseTree browseTree = new BrowseTree(null);
+ String testDeviceId = BrowseTree.PLAYER_PREFIX + mTestDevice.getAddress();
+
+ browseTree.onConnected(mTestDevice);
+ browseTree.indicateCoverArtUsed(TEST_NODE_ID, TEST_HANDLE);
+ browseTree.indicateCoverArtUsed(testDeviceId, TEST_HANDLE);
+ Set<BrowseTree.BrowseNode> parents = browseTree.notifyImageDownload(TEST_HANDLE, null);
+
+ assertThat(parents.contains(browseTree.mRootNode)).isTrue();
+ }
+
+ @Test
+ public void getEldestChild_whenNodesAreNotAncestorDescendantRelation() {
+ BrowseTree browseTree = new BrowseTree(null);
+
+ browseTree.onConnected(mTestDevice);
+
+ assertThat(BrowseTree.getEldestChild(browseTree.mNowPlayingNode,
+ browseTree.mRootNode)).isNull();
+ }
+
+ @Test
+ public void getEldestChild_whenNodesAreAncestorDescendantRelation() {
+ BrowseTree browseTree = new BrowseTree(null);
+
+ browseTree.onConnected(mTestDevice);
+
+ assertThat(BrowseTree.getEldestChild(browseTree.mRootNode,
+ browseTree.mRootNode.getChild(0))).isEqualTo(browseTree.mRootNode.getChild(0));
+ }
+
+ @Test
+ public void getNextStepFolder() {
+ BrowseTree browseTree = new BrowseTree(null);
+ BrowseNode nodeOutOfMap = browseTree.new BrowseNode(new AvrcpItem.Builder()
+ .setUuid(ILLEGAL_ID).setTitle(ILLEGAL_ID).setBrowsable(true).build());
+
+ browseTree.onConnected(mTestDevice);
+
+ assertThat(browseTree.getNextStepToFolder(null)).isNull();
+ assertThat(browseTree.getNextStepToFolder(browseTree.mRootNode)).isEqualTo(
+ browseTree.mRootNode);
+ assertThat(browseTree.getNextStepToFolder(browseTree.mRootNode.getChild(0))).isEqualTo(
+ browseTree.mRootNode.getChild(0));
+ assertThat(browseTree.getNextStepToFolder(nodeOutOfMap)).isNull();
+
+ browseTree.setCurrentBrowsedPlayer(BrowseTree.NOW_PLAYING_PREFIX, 2, 1);
+ assertThat(browseTree.getNextStepToFolder(browseTree.mRootNode.getChild(0))).isEqualTo(
+ browseTree.mNavigateUpNode);
+ }
+
+ @Test
+ public void toString_returnsSizeInfo() {
+ BrowseTree browseTree = new BrowseTree(mTestDevice);
+
+ assertThat(browseTree.toString()).isEqualTo("Size: " + browseTree.mBrowseMap.size());
+ }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
index 1671e65..40123b6 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java
@@ -16,7 +16,13 @@
package com.android.bluetooth.btservice;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.isNull;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
@@ -48,9 +54,12 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
@MediumTest
@RunWith(AndroidJUnit4.class)
@@ -62,7 +71,10 @@
private BluetoothDevice mA2dpHeadsetDevice;
private BluetoothDevice mHearingAidDevice;
private BluetoothDevice mLeAudioDevice;
+ private BluetoothDevice mLeHearingAidDevice;
private BluetoothDevice mSecondaryAudioDevice;
+ private ArrayList<BluetoothDevice> mDeviceConnectionStack;
+ private BluetoothDevice mMostRecentDevice;
private ActiveDeviceManager mActiveDeviceManager;
private static final int TIMEOUT_MS = 1000;
@@ -85,6 +97,7 @@
// Set up mocks and test assets
MockitoAnnotations.initMocks(this);
TestUtils.setAdapterService(mAdapterService);
+
when(mAdapterService.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mAudioManager);
when(mAdapterService.getSystemServiceName(AudioManager.class))
.thenReturn(Context.AUDIO_SERVICE);
@@ -93,12 +106,6 @@
when(mServiceFactory.getHeadsetService()).thenReturn(mHeadsetService);
when(mServiceFactory.getHearingAidService()).thenReturn(mHearingAidService);
when(mServiceFactory.getLeAudioService()).thenReturn(mLeAudioService);
- when(mA2dpService.setActiveDevice(any())).thenReturn(true);
- when(mHeadsetService.setActiveDevice(any())).thenReturn(true);
- when(mHearingAidService.setActiveDevice(any())).thenReturn(true);
- when(mLeAudioService.setActiveDevice(any())).thenReturn(true);
- when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any()))
- .thenReturn(mSecondaryAudioDevice);
mActiveDeviceManager = new ActiveDeviceManager(mAdapterService, mServiceFactory);
mActiveDeviceManager.start();
@@ -110,7 +117,46 @@
mA2dpHeadsetDevice = TestUtils.getTestDevice(mAdapter, 2);
mHearingAidDevice = TestUtils.getTestDevice(mAdapter, 3);
mLeAudioDevice = TestUtils.getTestDevice(mAdapter, 4);
- mSecondaryAudioDevice = TestUtils.getTestDevice(mAdapter, 5);
+ mLeHearingAidDevice = TestUtils.getTestDevice(mAdapter, 5);
+ mSecondaryAudioDevice = TestUtils.getTestDevice(mAdapter, 6);
+ mDeviceConnectionStack = new ArrayList<>();
+ mMostRecentDevice = null;
+
+ when(mA2dpService.setActiveDevice(any())).thenReturn(true);
+ when(mHeadsetService.setActiveDevice(any())).thenReturn(true);
+ when(mHearingAidService.setActiveDevice(any())).thenReturn(true);
+ when(mLeAudioService.setActiveDevice(any())).thenReturn(true);
+
+ when(mA2dpService.getFallbackDevice()).thenAnswer(invocation -> {
+ if (!mDeviceConnectionStack.isEmpty() && Objects.equals(mA2dpDevice,
+ mDeviceConnectionStack.get(mDeviceConnectionStack.size() - 1))) {
+ return mA2dpDevice;
+ }
+ return null;
+ });
+ when(mHeadsetService.getFallbackDevice()).thenAnswer(invocation -> {
+ if (!mDeviceConnectionStack.isEmpty() && Objects.equals(mHeadsetDevice,
+ mDeviceConnectionStack.get(mDeviceConnectionStack.size() - 1))) {
+ return mHeadsetDevice;
+ }
+ return null;
+ });
+ when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any())).thenAnswer(
+ invocation -> {
+ List<BluetoothDevice> devices = invocation.getArgument(0);
+ if (devices == null || devices.size() == 0) {
+ return null;
+ } else if (devices.contains(mLeHearingAidDevice)) {
+ return mLeHearingAidDevice;
+ } else if (devices.contains(mHearingAidDevice)) {
+ return mHearingAidDevice;
+ } else if (mMostRecentDevice != null && devices.contains(mMostRecentDevice)) {
+ return mMostRecentDevice;
+ } else {
+ return devices.get(0);
+ }
+ }
+ );
}
@After
@@ -182,14 +228,15 @@
*/
@Test
public void a2dpSecondDeviceDisconnected_fallbackDeviceActive() {
- a2dpConnected(mSecondaryAudioDevice);
- verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice);
-
a2dpConnected(mA2dpDevice);
verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice);
- a2dpDisconnected(mA2dpDevice);
+ a2dpConnected(mSecondaryAudioDevice);
verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice);
+
+ Mockito.clearInvocations(mA2dpService);
+ a2dpDisconnected(mSecondaryAudioDevice);
+ verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice);
}
/**
@@ -197,6 +244,8 @@
*/
@Test
public void onlyHeadsetConnected_setHeadsetActive() {
+ when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_IN_CALL);
+
headsetConnected(mHeadsetDevice);
verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mHeadsetDevice);
}
@@ -206,6 +255,8 @@
*/
@Test
public void secondHeadsetConnected_setSecondHeadsetActive() {
+ when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_IN_CALL);
+
headsetConnected(mHeadsetDevice);
verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mHeadsetDevice);
@@ -218,6 +269,8 @@
*/
@Test
public void lastHeadsetDisconnected_clearHeadsetActive() {
+ when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_IN_CALL);
+
headsetConnected(mHeadsetDevice);
verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mHeadsetDevice);
@@ -230,6 +283,8 @@
*/
@Test
public void headsetActiveDeviceSelected_setActive() {
+ when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_IN_CALL);
+
headsetConnected(mHeadsetDevice);
verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mHeadsetDevice);
@@ -243,21 +298,23 @@
Assert.assertEquals(mHeadsetDevice, mActiveDeviceManager.getHfpActiveDevice());
}
-
/**
* Two Headsets are connected and the current active is then disconnected.
* Should then set active device to fallback device.
*/
@Test
public void headsetSecondDeviceDisconnected_fallbackDeviceActive() {
- headsetConnected(mSecondaryAudioDevice);
- verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice);
+ when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_IN_CALL);
headsetConnected(mHeadsetDevice);
verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mHeadsetDevice);
- headsetDisconnected(mHeadsetDevice);
+ headsetConnected(mSecondaryAudioDevice);
verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice);
+
+ Mockito.clearInvocations(mHeadsetService);
+ headsetDisconnected(mSecondaryAudioDevice);
+ verify(mHeadsetService, timeout(TIMEOUT_MS)).setActiveDevice(mHeadsetDevice);
}
/**
@@ -392,14 +449,15 @@
*/
@Test
public void leAudioSecondDeviceDisconnected_fallbackDeviceActive() {
- leAudioConnected(mSecondaryAudioDevice);
- verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice);
-
leAudioConnected(mLeAudioDevice);
verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice);
- leAudioDisconnected(mLeAudioDevice);
+ leAudioConnected(mSecondaryAudioDevice);
verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice);
+
+ Mockito.clearInvocations(mLeAudioService);
+ leAudioDisconnected(mSecondaryAudioDevice);
+ verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice);
}
/**
@@ -463,7 +521,7 @@
public void leAudioActive_setHeadsetActiveExplicitly() {
Assume.assumeTrue("Ignore test when LeAudioService is not enabled",
LeAudioService.isEnabled());
-
+ when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_IN_CALL);
leAudioActiveDeviceChanged(mLeAudioDevice);
headsetConnected(mA2dpHeadsetDevice);
headsetActiveDeviceChanged(mA2dpHeadsetDevice);
@@ -481,23 +539,18 @@
*/
@Test
public void leAudioAndA2dpConnectedThenA2dpDisconnected_fallbackToLeAudio() {
+ when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);
+
leAudioConnected(mLeAudioDevice);
verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice);
a2dpConnected(mA2dpDevice);
verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice);
- when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);
- when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any())).thenAnswer(
- invocation -> {
- List<BluetoothDevice> devices = invocation.getArgument(0);
- return (devices != null && devices.contains(mLeAudioDevice))
- ? mLeAudioDevice : null;
- }
- );
+ Mockito.clearInvocations(mLeAudioService);
a2dpDisconnected(mA2dpDevice);
verify(mA2dpService, timeout(TIMEOUT_MS).atLeast(1)).setActiveDevice(isNull());
- verify(mLeAudioService, timeout(TIMEOUT_MS).times(2)).setActiveDevice(mLeAudioDevice);
+ verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice);
}
/**
@@ -506,23 +559,18 @@
*/
@Test
public void a2dpAndLeAudioConnectedThenLeAudioDisconnected_fallbackToA2dp() {
+ when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);
+
a2dpConnected(mA2dpDevice);
verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice);
leAudioConnected(mLeAudioDevice);
verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice);
- when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);
- when(mA2dpService.getFallbackDevice()).thenReturn(mA2dpDevice);
- when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any())).thenAnswer(
- invocation -> {
- List<BluetoothDevice> devices = invocation.getArgument(0);
- return (devices != null && devices.contains(mA2dpDevice)) ? mA2dpDevice : null;
- }
- );
+ Mockito.clearInvocations(mA2dpService);
leAudioDisconnected(mLeAudioDevice);
verify(mLeAudioService, timeout(TIMEOUT_MS).atLeast(1)).setActiveDevice(isNull());
- verify(mA2dpService, timeout(TIMEOUT_MS).times(2)).setActiveDevice(mA2dpDevice);
+ verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice);
}
/**
@@ -531,14 +579,15 @@
*/
@Test
public void hearingAidSecondDeviceDisconnected_fallbackDeviceActive() {
- hearingAidConnected(mSecondaryAudioDevice);
- verify(mHearingAidService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice);
-
hearingAidConnected(mHearingAidDevice);
verify(mHearingAidService, timeout(TIMEOUT_MS)).setActiveDevice(mHearingAidDevice);
- leAudioDisconnected(mHearingAidDevice);
+ hearingAidConnected(mSecondaryAudioDevice);
verify(mHearingAidService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice);
+
+ Mockito.clearInvocations(mHearingAidService);
+ hearingAidDisconnected(mSecondaryAudioDevice);
+ verify(mHearingAidService, timeout(TIMEOUT_MS)).setActiveDevice(mHearingAidDevice);
}
/**
@@ -547,6 +596,8 @@
*/
@Test
public void activeDeviceDisconnected_fallbackToHearingAid() {
+ when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);
+
hearingAidConnected(mHearingAidDevice);
verify(mHearingAidService, timeout(TIMEOUT_MS)).setActiveDevice(mHearingAidDevice);
@@ -560,25 +611,8 @@
verify(mLeAudioService, never()).setActiveDevice(mLeAudioDevice);
verify(mA2dpService, never()).setActiveDevice(mA2dpDevice);
- when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);
- when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any())).thenAnswer(
- invocation -> {
- List<BluetoothDevice> devices = invocation.getArgument(0);
- if (devices == null) {
- return null;
- } else if (devices.contains(mA2dpDevice)) {
- return mA2dpDevice;
- } else if (devices.contains(mLeAudioDevice)) {
- return mLeAudioDevice;
- } else if (devices.contains(mHearingAidDevice)) {
- return mHearingAidDevice;
- } else {
- return devices.get(0);
- }
- }
- );
a2dpDisconnected(mA2dpDevice);
- verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(isNull());
+ verify(mA2dpService, timeout(TIMEOUT_MS).atLeast(1)).setActiveDevice(isNull());
verify(mHearingAidService, timeout(TIMEOUT_MS).times(2))
.setActiveDevice(mHearingAidDevice);
}
@@ -587,9 +621,13 @@
* One LE Hearing Aid is connected.
*/
@Test
- public void onlyLeHearingAIdConnected_setHeadsetActive() {
- leAudioConnected(mLeAudioDevice);
- verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice);
+ public void onlyLeHearingAidConnected_setLeAudioActive() {
+ leHearingAidConnected(mLeHearingAidDevice);
+ TestUtils.waitForLooperToFinishScheduledTask(mActiveDeviceManager.getHandlerLooper());
+ verify(mLeAudioService, never()).setActiveDevice(mLeHearingAidDevice);
+
+ leAudioConnected(mLeHearingAidDevice);
+ verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeHearingAidDevice);
}
/**
@@ -598,8 +636,9 @@
*/
@Test
public void leAudioConnectedAfterLeHearingAid_setLeAudioActiveShouldNotBeCalled() {
- leHearingAidConnected(mSecondaryAudioDevice);
- verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mSecondaryAudioDevice);
+ leHearingAidConnected(mLeHearingAidDevice);
+ leAudioConnected(mLeHearingAidDevice);
+ verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeHearingAidDevice);
leAudioConnected(mLeAudioDevice);
TestUtils.waitForLooperToFinishScheduledTask(mActiveDeviceManager.getHandlerLooper());
@@ -607,6 +646,36 @@
}
/**
+ * Test connect/disconnect of devices.
+ * Hearing Aid, LE Hearing Aid, A2DP connected, then LE hearing Aid and hearing aid
+ * disconnected.
+ */
+ @Test
+ public void activeDeviceChange_withHearingAidLeHearingAidAndA2dpDevices() {
+ when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);
+
+ hearingAidConnected(mHearingAidDevice);
+ verify(mHearingAidService, timeout(TIMEOUT_MS)).setActiveDevice(mHearingAidDevice);
+
+ leHearingAidConnected(mLeHearingAidDevice);
+ leAudioConnected(mLeHearingAidDevice);
+ verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeHearingAidDevice);
+
+ a2dpConnected(mA2dpDevice);
+ TestUtils.waitForLooperToFinishScheduledTask(mActiveDeviceManager.getHandlerLooper());
+ verify(mA2dpService, never()).setActiveDevice(mA2dpDevice);
+
+ Mockito.clearInvocations(mHearingAidService, mA2dpService);
+ leHearingAidDisconnected(mLeHearingAidDevice);
+ leAudioDisconnected(mLeHearingAidDevice);
+ verify(mHearingAidService, timeout(TIMEOUT_MS)).setActiveDevice(mHearingAidDevice);
+ verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(isNull());
+
+ hearingAidDisconnected(mHearingAidDevice);
+ verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice);
+ }
+
+ /**
* A wired audio device is connected. Then all active devices are set to null.
*/
@Test
@@ -626,6 +695,9 @@
* Helper to indicate A2dp connected for a device.
*/
private void a2dpConnected(BluetoothDevice device) {
+ mDeviceConnectionStack.add(device);
+ mMostRecentDevice = 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);
@@ -637,6 +709,10 @@
* Helper to indicate A2dp disconnected for a device.
*/
private void a2dpDisconnected(BluetoothDevice device) {
+ mDeviceConnectionStack.remove(device);
+ mMostRecentDevice = (mDeviceConnectionStack.size() > 0)
+ ? mDeviceConnectionStack.get(mDeviceConnectionStack.size() - 1) : null;
+
Intent intent = new Intent(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTED);
@@ -648,6 +724,10 @@
* Helper to indicate A2dp active device changed for a device.
*/
private void a2dpActiveDeviceChanged(BluetoothDevice device) {
+ mDeviceConnectionStack.remove(device);
+ mDeviceConnectionStack.add(device);
+ mMostRecentDevice = device;
+
Intent intent = new Intent(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
mActiveDeviceManager.getBroadcastReceiver().onReceive(mContext, intent);
@@ -657,6 +737,9 @@
* Helper to indicate Headset connected for a device.
*/
private void headsetConnected(BluetoothDevice device) {
+ mDeviceConnectionStack.add(device);
+ mMostRecentDevice = 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);
@@ -668,6 +751,10 @@
* Helper to indicate Headset disconnected for a device.
*/
private void headsetDisconnected(BluetoothDevice device) {
+ mDeviceConnectionStack.remove(device);
+ mMostRecentDevice = (mDeviceConnectionStack.size() > 0)
+ ? mDeviceConnectionStack.get(mDeviceConnectionStack.size() - 1) : null;
+
Intent intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTED);
@@ -679,6 +766,10 @@
* Helper to indicate Headset active device changed for a device.
*/
private void headsetActiveDeviceChanged(BluetoothDevice device) {
+ mDeviceConnectionStack.remove(device);
+ mDeviceConnectionStack.add(device);
+ mMostRecentDevice = device;
+
Intent intent = new Intent(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
mActiveDeviceManager.getBroadcastReceiver().onReceive(mContext, intent);
@@ -688,6 +779,8 @@
* Helper to indicate Hearing Aid connected for a device.
*/
private void hearingAidConnected(BluetoothDevice device) {
+ mMostRecentDevice = device;
+
Intent intent = new Intent(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_DISCONNECTED);
@@ -699,6 +792,10 @@
* Helper to indicate Hearing Aid disconnected for a device.
*/
private void hearingAidDisconnected(BluetoothDevice device) {
+ mDeviceConnectionStack.remove(device);
+ mMostRecentDevice = (mDeviceConnectionStack.size() > 0)
+ ? mDeviceConnectionStack.get(mDeviceConnectionStack.size() - 1) : null;
+
Intent intent = new Intent(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTED);
@@ -710,15 +807,21 @@
* Helper to indicate Hearing Aid active device changed for a device.
*/
private void hearingAidActiveDeviceChanged(BluetoothDevice device) {
+ mMostRecentDevice = device;
+
Intent intent = new Intent(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
mActiveDeviceManager.getBroadcastReceiver().onReceive(mContext, intent);
+ mDeviceConnectionStack.remove(device);
+ mDeviceConnectionStack.add(device);
}
/**
* Helper to indicate LE Audio connected for a device.
*/
private void leAudioConnected(BluetoothDevice device) {
+ mMostRecentDevice = device;
+
Intent intent = new Intent(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_DISCONNECTED);
@@ -730,6 +833,10 @@
* Helper to indicate LE Audio disconnected for a device.
*/
private void leAudioDisconnected(BluetoothDevice device) {
+ mDeviceConnectionStack.remove(device);
+ mMostRecentDevice = (mDeviceConnectionStack.size() > 0)
+ ? mDeviceConnectionStack.get(mDeviceConnectionStack.size() - 1) : null;
+
Intent intent = new Intent(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTED);
@@ -741,6 +848,10 @@
* Helper to indicate LE Audio active device changed for a device.
*/
private void leAudioActiveDeviceChanged(BluetoothDevice device) {
+ mDeviceConnectionStack.remove(device);
+ mDeviceConnectionStack.add(device);
+ mMostRecentDevice = device;
+
Intent intent = new Intent(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
mActiveDeviceManager.getBroadcastReceiver().onReceive(mContext, intent);
@@ -750,6 +861,9 @@
* Helper to indicate LE Hearing Aid connected for a device.
*/
private void leHearingAidConnected(BluetoothDevice device) {
+ mDeviceConnectionStack.add(device);
+ mMostRecentDevice = device;
+
Intent intent = new Intent(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_DISCONNECTED);
@@ -761,6 +875,10 @@
* Helper to indicate LE Hearing Aid disconnected for a device.
*/
private void leHearingAidDisconnected(BluetoothDevice device) {
+ mDeviceConnectionStack.remove(device);
+ mMostRecentDevice = (mDeviceConnectionStack.size() > 0)
+ ? mDeviceConnectionStack.get(mDeviceConnectionStack.size() - 1) : null;
+
Intent intent = new Intent(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTED);
@@ -772,6 +890,10 @@
* Helper to indicate LE Audio Hearing Aid device changed for a device.
*/
private void leHearingAidActiveDeviceChanged(BluetoothDevice device) {
+ mDeviceConnectionStack.remove(device);
+ mDeviceConnectionStack.add(device);
+ mMostRecentDevice = device;
+
Intent intent = new Intent(BluetoothHapClient.ACTION_HAP_DEVICE_AVAILABLE);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
mActiveDeviceManager.getBroadcastReceiver().onReceive(mContext, intent);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
index 042480d..7902b5f 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
@@ -87,6 +87,7 @@
import libcore.util.HexEncoding;
import org.junit.After;
+import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
@@ -214,6 +215,8 @@
AsyncTask.setDefaultExecutor((r) -> {
InstrumentationRegistry.getInstrumentation().runOnMainSync(r);
});
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity();
InstrumentationRegistry.getInstrumentation().runOnMainSync(
() -> mAdapterService = new AdapterService());
@@ -238,6 +241,8 @@
mPermissionManager = InstrumentationRegistry.getTargetContext()
.getSystemService(PermissionManager.class);
+ when(mMockContext.getCacheDir()).thenReturn(InstrumentationRegistry.getTargetContext()
+ .getCacheDir());
when(mMockContext.getApplicationInfo()).thenReturn(mMockApplicationInfo);
when(mMockContext.getContentResolver()).thenReturn(mMockContentResolver);
when(mMockContext.getApplicationContext()).thenReturn(mMockContext);
@@ -328,6 +333,11 @@
mAdapterService.cleanup();
}
+ @AfterClass
+ public static void tearDownOnce() {
+ AsyncTask.setDefaultExecutor(AsyncTask.SERIAL_EXECUTOR);
+ }
+
private void verifyStateChange(int prevState, int currState, int callNumber, int timeoutMs) {
try {
verify(mIBluetoothCallback, timeout(timeoutMs)
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/DataMigrationTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/DataMigrationTest.java
new file mode 100644
index 0000000..3206f6f
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/DataMigrationTest.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2022 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 android.bluetooth.BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED;
+import static android.bluetooth.BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED;
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockCursor;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.btservice.storage.Metadata;
+import com.android.bluetooth.btservice.storage.MetadataDatabase;
+import com.android.bluetooth.opp.BluetoothShare;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DataMigrationTest {
+ private static final String TAG = "DataMigrationTest";
+
+ private static final String AUTHORITY = "bluetooth_legacy.provider";
+
+ private MockContentResolver mMockContentResolver;
+
+ private Context mTargetContext;
+ private SharedPreferences mPrefs;
+
+ @Mock private Context mMockContext;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ mPrefs = mTargetContext.getSharedPreferences("TestPref", Context.MODE_PRIVATE);
+
+ mMockContentResolver = new MockContentResolver(mTargetContext);
+ when(mMockContext.getContentResolver()).thenReturn(mMockContentResolver);
+ when(mMockContext.getCacheDir()).thenReturn(mTargetContext.getCacheDir());
+
+ when(mMockContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mPrefs);
+
+ mTargetContext.deleteSharedPreferences("TestPref");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mTargetContext.deleteSharedPreferences("TestPref");
+ mTargetContext.deleteDatabase("TestBluetoothDb");
+ mTargetContext.deleteDatabase("TestOppDb");
+ }
+
+ private void assertRunStatus(int status) {
+ assertThat(DataMigration.run(mMockContext)).isEqualTo(status);
+ assertThat(DataMigration.migrationStatus(mMockContext)).isEqualTo(status);
+ }
+
+ /**
+ * Test: execute Empty migration
+ */
+ @Test
+ public void testEmptyMigration() {
+ BluetoothLegacyContentProvider fakeContentProvider =
+ new BluetoothLegacyContentProvider(mMockContext);
+ mMockContentResolver.addProvider(AUTHORITY, fakeContentProvider);
+
+ final int nCallCount = DataMigration.sharedPreferencesKeys.length
+ + 1; // +1 for default preferences
+ final int nBundleCount = 2; // `bluetooth_db` && `btopp.db`
+
+ assertRunStatus(DataMigration.MIGRATION_STATUS_COMPLETED);
+ assertThat(fakeContentProvider.mCallCount).isEqualTo(nCallCount);
+ assertThat(fakeContentProvider.mBundleCount).isEqualTo(nBundleCount);
+
+ // run it twice to trigger an already completed migration
+ assertRunStatus(DataMigration.MIGRATION_STATUS_COMPLETED);
+ // ContentProvider should not have any more calls made than previously
+ assertThat(fakeContentProvider.mCallCount).isEqualTo(nCallCount);
+ assertThat(fakeContentProvider.mBundleCount).isEqualTo(nBundleCount);
+ }
+
+ private static class BluetoothLegacyContentProvider extends MockContentProvider {
+ BluetoothLegacyContentProvider(Context ctx) {
+ super(ctx);
+ }
+ int mCallCount = 0;
+ int mBundleCount = 0;
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ mBundleCount++;
+ return null;
+ }
+ @Override
+ public Bundle call(String method, String arg, Bundle extras) {
+ mCallCount++;
+ return null;
+ }
+ }
+
+ /**
+ * Test: execute migration without having a content provided registered
+ */
+ @Test
+ public void testMissingProvider() {
+ assertThat(DataMigration.isMigrationApkInstalled(mMockContext)).isFalse();
+
+ assertRunStatus(DataMigration.MIGRATION_STATUS_MISSING_APK);
+
+ mMockContentResolver.addProvider(AUTHORITY, new MockContentProvider(mMockContext));
+ assertThat(DataMigration.isMigrationApkInstalled(mMockContext)).isTrue();
+ }
+
+ /**
+ * Test: execute migration after too many attempt
+ */
+ @Test
+ public void testTooManyAttempt() {
+ SharedPreferences pref = mMockContext
+ .getSharedPreferences(DataMigration.BLUETOOTH_CONFIG,
+ Context.MODE_PRIVATE);
+
+ assertThat(pref.getInt(DataMigration.MIGRATION_ATTEMPT_PROPERTY, -1))
+ .isEqualTo(-1);
+
+ for (int i = 0; i < DataMigration.MAX_ATTEMPT; i++) {
+ assertThat(DataMigration.incrementeMigrationAttempt(mMockContext))
+ .isTrue();
+ assertThat(pref.getInt(DataMigration.MIGRATION_ATTEMPT_PROPERTY, -1))
+ .isEqualTo(i + 1);
+ }
+ assertThat(DataMigration.incrementeMigrationAttempt(mMockContext))
+ .isFalse();
+ assertThat(pref.getInt(DataMigration.MIGRATION_ATTEMPT_PROPERTY, -1))
+ .isEqualTo(DataMigration.MAX_ATTEMPT + 1);
+
+ mMockContentResolver.addProvider(AUTHORITY, new MockContentProvider(mMockContext));
+ assertRunStatus(DataMigration.MIGRATION_STATUS_MAX_ATTEMPT);
+ }
+
+ /**
+ * Test: execute migration of SharedPreferences
+ */
+ @Test
+ public void testSharedPreferencesMigration() {
+ BluetoothLegacySharedPreferencesContentProvider fakeContentProvider =
+ new BluetoothLegacySharedPreferencesContentProvider(mMockContext);
+ mMockContentResolver.addProvider(AUTHORITY, fakeContentProvider);
+
+ assertThat(DataMigration.sharedPreferencesMigration("Boolean", mMockContext)).isTrue();
+ assertThat(mPrefs.getBoolean("keyBoolean", false)).isTrue();
+ assertThat(fakeContentProvider.mCallCount).isEqualTo(2);
+
+ assertThat(DataMigration.sharedPreferencesMigration("Long", mMockContext)).isTrue();
+ assertThat(mPrefs.getLong("keyLong", -1)).isEqualTo(42);
+ assertThat(fakeContentProvider.mCallCount).isEqualTo(4);
+
+ assertThat(DataMigration.sharedPreferencesMigration("Int", mMockContext)).isTrue();
+ assertThat(mPrefs.getInt("keyInt", -1)).isEqualTo(42);
+ assertThat(fakeContentProvider.mCallCount).isEqualTo(6);
+
+ assertThat(DataMigration.sharedPreferencesMigration("String", mMockContext)).isTrue();
+ assertThat(mPrefs.getString("keyString", "Not42")).isEqualTo("42");
+ assertThat(fakeContentProvider.mCallCount).isEqualTo(8);
+
+ // Check not overriding an existing value:
+ mPrefs.edit().putString("keyString2", "already 42").apply();
+ assertThat(DataMigration.sharedPreferencesMigration("String2", mMockContext)).isTrue();
+ assertThat(mPrefs.getString("keyString2", "Not42")).isEqualTo("already 42");
+ assertThat(fakeContentProvider.mCallCount).isEqualTo(10);
+
+ assertThat(DataMigration.sharedPreferencesMigration("Invalid", mMockContext)).isFalse();
+
+ assertThat(DataMigration.sharedPreferencesMigration("null", mMockContext)).isFalse();
+
+ assertThat(DataMigration.sharedPreferencesMigration("empty", mMockContext)).isFalse();
+
+ assertThat(DataMigration
+ .sharedPreferencesMigration("anything else", mMockContext)).isTrue();
+ }
+
+ private static class BluetoothLegacySharedPreferencesContentProvider
+ extends MockContentProvider {
+ BluetoothLegacySharedPreferencesContentProvider(Context ctx) {
+ super(ctx);
+ }
+ String mLastMethod = null;
+ int mCallCount = 0;
+ int mBundleCount = 0;
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ mBundleCount++;
+ return null;
+ }
+ @Override
+ public Bundle call(String method, String arg, Bundle extras) {
+ mCallCount++;
+ mLastMethod = method;
+ assertThat(method).isNotNull();
+ assertThat(arg).isNotNull();
+ assertThat(extras).isNull();
+ final String key = "key" + arg;
+ Bundle b = new Bundle();
+ b.putStringArrayList(DataMigration.KEY_LIST, new ArrayList<String>(Arrays.asList(key)));
+ switch(arg) {
+ case "Boolean":
+ b.putBoolean(key, true);
+ break;
+ case "Long":
+ b.putLong(key, Long.valueOf(42));
+ break;
+ case "Int":
+ b.putInt(key, 42);
+ break;
+ case "String":
+ b.putString(key, "42");
+ break;
+ case "String2":
+ b.putString(key, "42");
+ break;
+ case "null":
+ b.putObject(key, null);
+ break;
+ case "Invalid":
+ // Put anything different from Boolean/Long/Integer/String
+ b.putFloat(key, 42f);
+ break;
+ case "empty":
+ // Do not put anything in the bundle and remove the key
+ b = new Bundle();
+ break;
+ default:
+ return null;
+ }
+ return b;
+ }
+ }
+
+ /**
+ * Test: execute migration of BLUETOOTH_DATABASE and OPP_DATABASE without correct data
+ */
+ @Test
+ public void testIncompleteDbMigration() {
+ when(mMockContext.getDatabasePath("btopp.db"))
+ .thenReturn(mTargetContext.getDatabasePath("TestOppDb"));
+ when(mMockContext.getDatabasePath("bluetooth_db"))
+ .thenReturn(mTargetContext.getDatabasePath("TestBluetoothDb"));
+
+ BluetoothLegacyDbContentProvider fakeContentProvider =
+ new BluetoothLegacyDbContentProvider(mMockContext);
+ mMockContentResolver.addProvider(AUTHORITY, fakeContentProvider);
+
+ fakeContentProvider.mCursor = new FakeCursor(FAKE_SAMPLE);
+ assertThat(DataMigration.bluetoothDatabaseMigration(mMockContext)).isFalse();
+
+ fakeContentProvider.mCursor = new FakeCursor(FAKE_SAMPLE);
+ assertThat(DataMigration.oppDatabaseMigration(mMockContext)).isFalse();
+ }
+
+ private static final List<Pair<String, Object>> FAKE_SAMPLE =
+ Arrays.asList(
+ new Pair("wrong_key", "wrong_content")
+ );
+
+ /**
+ * Test: execute migration of BLUETOOTH_DATABASE
+ */
+ @Test
+ public void testBluetoothDbMigration() {
+ when(mMockContext.getDatabasePath("bluetooth_db"))
+ .thenReturn(mTargetContext.getDatabasePath("TestBluetoothDb"));
+
+ BluetoothLegacyDbContentProvider fakeContentProvider =
+ new BluetoothLegacyDbContentProvider(mMockContext);
+ mMockContentResolver.addProvider(AUTHORITY, fakeContentProvider);
+
+ Cursor c = new FakeCursor(BLUETOOTH_DATABASE_SAMPLE);
+ fakeContentProvider.mCursor = c;
+ assertThat(DataMigration.bluetoothDatabaseMigration(mMockContext)).isTrue();
+
+ MetadataDatabase database = MetadataDatabase.createDatabaseWithoutMigration(mMockContext);
+ Metadata metadata = database.load().get(0);
+
+ Log.d(TAG, "Metadata migrated: " + metadata);
+
+ assertWithMessage("Address mismatch")
+ .that(metadata.getAddress()).isEqualTo("my_address");
+ assertWithMessage("Connection policy mismatch")
+ .that(metadata.getProfileConnectionPolicy(BluetoothProfile.A2DP))
+ .isEqualTo(CONNECTION_POLICY_FORBIDDEN);
+ assertWithMessage("Custom metadata mismatch")
+ .that(metadata.getCustomizedMeta(BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING))
+ .isEqualTo(CUSTOM_META);
+ }
+
+ private static final byte[] CUSTOM_META = new byte[]{ 42, 43, 44};
+
+ private static final List<Pair<String, Object>> BLUETOOTH_DATABASE_SAMPLE =
+ Arrays.asList(
+ new Pair("address", "my_address"),
+ new Pair("migrated", 1),
+ new Pair("a2dpSupportsOptionalCodecs", OPTIONAL_CODECS_NOT_SUPPORTED),
+ new Pair("a2dpOptionalCodecsEnabled", OPTIONAL_CODECS_PREF_DISABLED),
+ new Pair("last_active_time", 42),
+ new Pair("is_active_a2dp_device", 1),
+
+ // connection_policy
+ new Pair("a2dp_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("a2dp_sink_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("hfp_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("hfp_client_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("hid_host_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("pan_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("pbap_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("pbap_client_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("map_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("sap_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("hearing_aid_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("hap_client_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("map_client_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("le_audio_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("volume_control_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("csip_set_coordinator_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("le_call_control_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("bass_client_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+ new Pair("battery_connection_policy", CONNECTION_POLICY_FORBIDDEN),
+
+ // Custom meta-data
+ new Pair("manufacturer_name", CUSTOM_META),
+ new Pair("model_name", CUSTOM_META),
+ new Pair("software_version", CUSTOM_META),
+ new Pair("hardware_version", CUSTOM_META),
+ new Pair("companion_app", CUSTOM_META),
+ new Pair("main_icon", CUSTOM_META),
+ new Pair("is_untethered_headset", CUSTOM_META),
+ new Pair("untethered_left_icon", CUSTOM_META),
+ new Pair("untethered_right_icon", CUSTOM_META),
+ new Pair("untethered_case_icon", CUSTOM_META),
+ new Pair("untethered_left_battery", CUSTOM_META),
+ new Pair("untethered_right_battery", CUSTOM_META),
+ new Pair("untethered_case_battery", CUSTOM_META),
+ new Pair("untethered_left_charging", CUSTOM_META),
+ new Pair("untethered_right_charging", CUSTOM_META),
+ new Pair("untethered_case_charging", CUSTOM_META),
+ new Pair("enhanced_settings_ui_uri", CUSTOM_META),
+ new Pair("device_type", CUSTOM_META),
+ new Pair("main_battery", CUSTOM_META),
+ new Pair("main_charging", CUSTOM_META),
+ new Pair("main_low_battery_threshold", CUSTOM_META),
+ new Pair("untethered_left_low_battery_threshold", CUSTOM_META),
+ new Pair("untethered_right_low_battery_threshold", CUSTOM_META),
+ new Pair("untethered_case_low_battery_threshold", CUSTOM_META),
+ new Pair("spatial_audio", CUSTOM_META),
+ new Pair("fastpair_customized", CUSTOM_META)
+ );
+
+ /**
+ * Test: execute migration of OPP_DATABASE
+ */
+ @Test
+ public void testOppDbMigration() {
+ when(mMockContext.getDatabasePath("btopp.db"))
+ .thenReturn(mTargetContext.getDatabasePath("TestOppDb"));
+
+ BluetoothLegacyDbContentProvider fakeContentProvider =
+ new BluetoothLegacyDbContentProvider(mMockContext);
+ mMockContentResolver.addProvider(AUTHORITY, fakeContentProvider);
+
+ Cursor c = new FakeCursor(OPP_DATABASE_SAMPLE);
+ fakeContentProvider.mCursor = c;
+ assertThat(DataMigration.oppDatabaseMigration(mMockContext)).isTrue();
+ }
+
+ private static final List<Pair<String, Object>> OPP_DATABASE_SAMPLE =
+ Arrays.asList(
+ // String
+ new Pair(BluetoothShare.URI, "content"),
+ new Pair(BluetoothShare.FILENAME_HINT, "content"),
+ new Pair(BluetoothShare.MIMETYPE, "content"),
+ new Pair(BluetoothShare.DESTINATION, "content"),
+
+ // Int
+ new Pair(BluetoothShare.VISIBILITY, 42),
+ new Pair(BluetoothShare.USER_CONFIRMATION, 42),
+ new Pair(BluetoothShare.DIRECTION, 42),
+ new Pair(BluetoothShare.STATUS, 42),
+ new Pair("scanned" /* Constants.MEDIA_SCANNED */, 42),
+
+ // Long
+ new Pair(BluetoothShare.TOTAL_BYTES, 42L),
+ new Pair(BluetoothShare.TIMESTAMP, 42L)
+ );
+
+ private static class BluetoothLegacyDbContentProvider extends MockContentProvider {
+ BluetoothLegacyDbContentProvider(Context ctx) {
+ super(ctx);
+ }
+ String mLastMethod = null;
+ Cursor mCursor = null;
+ int mCallCount = 0;
+ int mBundleCount = 0;
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ mBundleCount++;
+ return mCursor;
+ }
+ @Override
+ public Bundle call(String method, String arg, Bundle extras) {
+ mCallCount++;
+ return null;
+ }
+ }
+
+ private static class FakeCursor extends MockCursor {
+ int mNumItem = 1;
+ List<Pair<String, Object>> mRows;
+
+ FakeCursor(List<Pair<String, Object>> rows) {
+ mRows = rows;
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ return (String) (mRows.get(columnIndex).second);
+ }
+
+ @Override
+ public byte[] getBlob(int columnIndex) {
+ return (byte[]) (mRows.get(columnIndex).second);
+ }
+
+ @Override
+ public int getInt(int columnIndex) {
+ return (int) (mRows.get(columnIndex).second);
+ }
+
+ @Override
+ public long getLong(int columnIndex) {
+ return (long) (mRows.get(columnIndex).second);
+ }
+
+ @Override
+ public boolean moveToNext() {
+ return mNumItem-- > 0;
+ }
+
+ @Override
+ public int getCount() {
+ return 1;
+ }
+
+ @Override
+ public int getColumnIndexOrThrow(String columnName) {
+ for (int i = 0; i < mRows.size(); i++) {
+ if (columnName.equals(mRows.get(i).first)) {
+ return i;
+ }
+ }
+ throw new IllegalArgumentException("No such column: " + columnName);
+ }
+
+ @Override
+ public int getColumnIndex(String columnName) {
+ for (int i = 0; i < mRows.size(); i++) {
+ if (columnName.equals(mRows.get(i).first)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public void close() {}
+ }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java
index 8ab98d3..cf74dfd 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hearingaid/HearingAidServiceTest.java
@@ -180,6 +180,11 @@
Assert.assertEquals(newState, intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
Assert.assertEquals(prevState, intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
-1));
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+ // ActiveDeviceManager calls setActiveDevice when connected.
+ mService.setActiveDevice(device);
+ }
+
}
private void verifyNoConnectionStateIntent(int timeoutMs, BluetoothDevice device) {
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java
index d74b622..0219258 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java
@@ -172,6 +172,8 @@
doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
doReturn(true, false).when(mAdapterService).isStartedProfile(anyString());
doReturn(true).when(mAdapterService).isLeAudioBroadcastSourceSupported();
+ doReturn((long)(1 << BluetoothProfile.LE_AUDIO_BROADCAST) | (1 << BluetoothProfile.LE_AUDIO))
+ .when(mAdapterService).getSupportedProfilesBitMask();
mAdapter = BluetoothAdapter.getDefaultAdapter();
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java
index a11ee69..a77b3fd 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java
@@ -180,8 +180,8 @@
doAnswer(invocation -> mBondedDevices.toArray(new BluetoothDevice[]{})).when(
mAdapterService).getBondedDevices();
+ LeAudioNativeInterface.setInstance(mNativeInterface);
startService();
- mService.mLeAudioNativeInterface = mNativeInterface;
mService.mAudioManager = mAudioManager;
mService.mVolumeControlService = mVolumeControlService;
@@ -217,6 +217,8 @@
.getBondState(any(BluetoothDevice.class));
doReturn(new ParcelUuid[]{BluetoothUuid.LE_AUDIO}).when(mAdapterService)
.getRemoteUuids(any(BluetoothDevice.class));
+
+ verify(mNativeInterface, timeout(3000).times(1)).init(any());
}
@After
@@ -231,6 +233,7 @@
mTargetContext.unregisterReceiver(mLeAudioIntentReceiver);
mDeviceQueueMap.clear();
TestUtils.clearAdapterService(mAdapterService);
+ LeAudioNativeInterface.setInstance(null);
}
private void startService() throws TimeoutException {
@@ -1034,7 +1037,7 @@
@Test
public void testGetActiveDevices() {
int groupId = 1;
- int direction = 1;
+ int direction = 2;
int snkAudioLocation = 3;
int srcAudioLocation = 4;
int availableContexts = 5;
@@ -1085,8 +1088,7 @@
mService.messageFromNative(groupStatusChangedEvent);
}
- private void injectAudioConfChanged(int groupId, Integer availableContexts) {
- int direction = 1;
+ private void injectAudioConfChanged(int groupId, Integer availableContexts, int direction) {
int snkAudioLocation = 3;
int srcAudioLocation = 4;
int eventType = LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED;
@@ -1109,7 +1111,7 @@
doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
injectAudioConfChanged(testGroupId, BluetoothLeAudio.CONTEXT_TYPE_MEDIA |
- BluetoothLeAudio.CONTEXT_TYPE_CONVERSATIONAL);
+ BluetoothLeAudio.CONTEXT_TYPE_CONVERSATIONAL, 3);
injectGroupStatusChange(testGroupId, BluetoothLeAudio.GROUP_STATUS_ACTIVE);
String action = BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED;
@@ -1131,7 +1133,7 @@
String action = BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED;
Integer contexts = BluetoothLeAudio.CONTEXT_TYPE_MEDIA |
BluetoothLeAudio.CONTEXT_TYPE_CONVERSATIONAL;
- injectAudioConfChanged(testGroupId, contexts);
+ injectAudioConfChanged(testGroupId, contexts, 3);
Intent intent = TestUtils.waitForNoIntent(TIMEOUT_MS, mDeviceQueueMap.get(mSingleDevice));
assertThat(intent).isNull();
@@ -1146,7 +1148,7 @@
String action = BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED;
- injectAudioConfChanged(testGroupId, 0);
+ injectAudioConfChanged(testGroupId, 0, 3);
Intent intent = TestUtils.waitForNoIntent(TIMEOUT_MS, mDeviceQueueMap.get(mSingleDevice));
assertThat(intent).isNull();
}
@@ -1192,7 +1194,7 @@
connectTestDevice(mSingleDevice, testGroupId);
injectAudioConfChanged(testGroupId, BluetoothLeAudio.CONTEXT_TYPE_MEDIA |
- BluetoothLeAudio.CONTEXT_TYPE_CONVERSATIONAL);
+ BluetoothLeAudio.CONTEXT_TYPE_CONVERSATIONAL, 3);
sendEventAndVerifyIntentForGroupStatusChanged(testGroupId, LeAudioStackEvent.GROUP_STATUS_ACTIVE);
sendEventAndVerifyIntentForGroupStatusChanged(testGroupId, LeAudioStackEvent.GROUP_STATUS_INACTIVE);
@@ -1285,7 +1287,7 @@
@Test
public void testLeadGroupDeviceDisconnects() {
int groupId = 1;
- int direction = 1;
+ int direction = 2;
int snkAudioLocation = 3;
int srcAudioLocation = 4;
int availableContexts = 5;
@@ -1350,7 +1352,7 @@
@Test
public void testLeadGroupDeviceReconnects() {
int groupId = 1;
- int direction = 1;
+ int direction = 2;
int snkAudioLocation = 3;
int srcAudioLocation = 4;
int availableContexts = 5;
@@ -1417,6 +1419,7 @@
public void testVolumeCache() {
int groupId = 1;
int volume = 100;
+ int direction = 2;
int availableContexts = 4;
doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
@@ -1429,7 +1432,7 @@
ArgumentCaptor.forClass(BluetoothProfileConnectionInfo.class);
//Add location support.
- injectAudioConfChanged(groupId, availableContexts);
+ injectAudioConfChanged(groupId, availableContexts, direction);
doReturn(-1).when(mVolumeControlService).getAudioDeviceGroupVolume(groupId);
//Set group and device as active.
diff --git a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapAppParamsTest.java b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapAppParamsTest.java
new file mode 100644
index 0000000..f3d9361
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapAppParamsTest.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2022 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.map;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.SignedLongLong;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothMapAppParamsTest {
+ public static final long TEST_PARAMETER_MASK = 1;
+ public static final int TEST_MAX_LIST_COUNT = 3;
+ public static final int TEST_START_OFFSET = 1;
+ public static final int TEST_FILTER_MESSAGE_TYPE = 1;
+ public static final int TEST_FILTER_PRIORITY = 1;
+ public static final int TEST_ATTACHMENT = 1;
+ public static final int TEST_CHARSET = 1;
+ public static final int TEST_CHAT_STATE = 1;
+ public static final long TEST_ID_HIGH = 1;
+ public static final long TEST_ID_LOW = 1;
+ public static final int TEST_CONVO_LISTING_SIZE = 1;
+ public static final long TEST_COUNT_LOW = 1;
+ public static final long TEST_COUNT_HIGH = 1;
+ public static final long TEST_CONVO_PARAMETER_MASK = 1;
+ public static final String TEST_FILTER_CONVO_ID = "1111";
+ public static final long TEST_FILTER_LAST_ACTIVITY_BEGIN = 0;
+ public static final long TEST_FILTER_LAST_ACTIVITY_END = 0;
+ public static final String TEST_FILTER_MSG_HANDLE = "1";
+ public static final String TEST_FILTER_ORIGINATOR = "test_filter_originator";
+ public static final long TEST_FILTER_PERIOD_BEGIN = 0;
+ public static final long TEST_FILTER_PERIOD_END = 0;
+ public static final int TEST_FILTER_PRESENCE = 1;
+ public static final int TEST_FILTER_READ_STATUS = 1;
+ public static final String TEST_FILTER_RECIPIENT = "test_filter_recipient";
+ public static final int TEST_FOLDER_LISTING_SIZE = 1;
+ public static final int TEST_FILTER_UID_PRESENT = 1;
+ public static final int TEST_FRACTION_DELIVER = 1;
+ public static final int TEST_FRACTION_REQUEST = 1;
+ public static final long TEST_LAST_ACTIVITY = 0;
+ public static final int TEST_MAS_INSTANCE_ID = 1;
+ public static final int TEST_MESSAGE_LISTING_SIZE = 1;
+ public static final long TEST_MSE_TIME = 0;
+ public static final int TEST_NEW_MESSAGE = 1;
+ public static final long TEST_NOTIFICATION_FILTER = 1;
+ public static final int TEST_NOTIFICATION_STATUS = 1;
+ public static final int TEST_PRESENCE_AVAILABILITY = 1;
+ public static final String TEST_PRESENCE_STATUS = "test_presence_status";
+ public static final int TEST_RETRY = 1;
+ public static final int TEST_STATUS_INDICATOR = 1;
+ public static final int TEST_STATUS_VALUE = 1;
+ public static final int TEST_SUBJECT_LENGTH = 1;
+ public static final int TEST_TRANSPARENT = 1;
+
+ @Test
+ public void settersAndGetters() throws Exception {
+ ByteBuffer ret = ByteBuffer.allocate(16);
+ ret.putLong(TEST_COUNT_HIGH);
+ ret.putLong(TEST_COUNT_LOW);
+
+ BluetoothMapAppParams appParams = new BluetoothMapAppParams();
+ appParams.setParameterMask(TEST_PARAMETER_MASK);
+ appParams.setMaxListCount(TEST_MAX_LIST_COUNT);
+ appParams.setStartOffset(TEST_START_OFFSET);
+ appParams.setFilterMessageType(TEST_FILTER_MESSAGE_TYPE);
+ appParams.setFilterPriority(TEST_FILTER_PRIORITY);
+ appParams.setAttachment(TEST_ATTACHMENT);
+ appParams.setCharset(TEST_CHARSET);
+ appParams.setChatState(TEST_CHAT_STATE);
+ appParams.setChatStateConvoId(TEST_ID_HIGH, TEST_ID_LOW);
+ appParams.setConvoListingSize(TEST_CONVO_LISTING_SIZE);
+ appParams.setConvoListingVerCounter(TEST_COUNT_LOW, TEST_COUNT_HIGH);
+ appParams.setConvoParameterMask(TEST_CONVO_PARAMETER_MASK);
+ appParams.setDatabaseIdentifier(TEST_ID_HIGH, TEST_ID_LOW);
+ appParams.setFilterConvoId(TEST_FILTER_CONVO_ID);
+ appParams.setFilterMsgHandle(TEST_FILTER_MSG_HANDLE);
+ appParams.setFilterOriginator(TEST_FILTER_ORIGINATOR);
+ appParams.setFilterPresence(TEST_FILTER_PRESENCE);
+ appParams.setFilterReadStatus(TEST_FILTER_READ_STATUS);
+ appParams.setFilterRecipient(TEST_FILTER_RECIPIENT);
+ appParams.setFolderListingSize(TEST_FOLDER_LISTING_SIZE);
+ appParams.setFilterUidPresent(TEST_FILTER_UID_PRESENT);
+ appParams.setFolderVerCounter(TEST_COUNT_LOW, TEST_COUNT_HIGH);
+ appParams.setFractionDeliver(TEST_FRACTION_DELIVER);
+ appParams.setFractionRequest(TEST_FRACTION_REQUEST);
+ appParams.setMasInstanceId(TEST_MAS_INSTANCE_ID);
+ appParams.setMessageListingSize(TEST_MESSAGE_LISTING_SIZE);
+ appParams.setNewMessage(TEST_NEW_MESSAGE);
+ appParams.setNotificationFilter(TEST_NOTIFICATION_FILTER);
+ appParams.setNotificationStatus(TEST_NOTIFICATION_STATUS);
+ appParams.setPresenceAvailability(TEST_PRESENCE_AVAILABILITY);
+ appParams.setPresenceStatus(TEST_PRESENCE_STATUS);
+ appParams.setRetry(TEST_RETRY);
+ appParams.setStatusIndicator(TEST_STATUS_INDICATOR);
+ appParams.setStatusValue(TEST_STATUS_VALUE);
+ appParams.setSubjectLength(TEST_SUBJECT_LENGTH);
+ appParams.setTransparent(TEST_TRANSPARENT);
+
+ assertThat(appParams.getParameterMask()).isEqualTo(TEST_PARAMETER_MASK);
+ assertThat(appParams.getMaxListCount()).isEqualTo(TEST_MAX_LIST_COUNT);
+ assertThat(appParams.getStartOffset()).isEqualTo(TEST_START_OFFSET);
+ assertThat(appParams.getFilterMessageType()).isEqualTo(TEST_FILTER_MESSAGE_TYPE);
+ assertThat(appParams.getFilterPriority()).isEqualTo(TEST_FILTER_PRIORITY);
+ assertThat(appParams.getAttachment()).isEqualTo(TEST_ATTACHMENT);
+ assertThat(appParams.getCharset()).isEqualTo(TEST_CHARSET);
+ assertThat(appParams.getChatState()).isEqualTo(TEST_CHAT_STATE);
+ assertThat(appParams.getChatStateConvoId()).isEqualTo(new SignedLongLong(
+ TEST_ID_HIGH, TEST_ID_LOW));
+ assertThat(appParams.getChatStateConvoIdByteArray()).isEqualTo(ret.array());
+ assertThat(appParams.getChatStateConvoIdString()).isEqualTo(new String(ret.array()));
+ assertThat(appParams.getConvoListingSize()).isEqualTo(TEST_CONVO_LISTING_SIZE);
+ assertThat(appParams.getConvoListingVerCounter()).isEqualTo(
+ ret.array());
+ assertThat(appParams.getConvoParameterMask()).isEqualTo(TEST_CONVO_PARAMETER_MASK);
+ assertThat(appParams.getDatabaseIdentifier()).isEqualTo(ret.array());
+ assertThat(appParams.getFilterConvoId()).isEqualTo(
+ SignedLongLong.fromString(TEST_FILTER_CONVO_ID));
+ assertThat(appParams.getFilterConvoIdString()).isEqualTo(BluetoothMapUtils.getLongAsString(
+ SignedLongLong.fromString(TEST_FILTER_CONVO_ID).getLeastSignificantBits()));
+ assertThat(appParams.getFilterMsgHandle()).isEqualTo(
+ BluetoothMapUtils.getLongFromString(TEST_FILTER_MSG_HANDLE));
+ assertThat(appParams.getFilterMsgHandleString()).isEqualTo(
+ BluetoothMapUtils.getLongAsString(appParams.getFilterMsgHandle()));
+ assertThat(appParams.getFilterOriginator()).isEqualTo(TEST_FILTER_ORIGINATOR);
+ assertThat(appParams.getFilterPresence()).isEqualTo(TEST_FILTER_PRESENCE);
+ assertThat(appParams.getFilterReadStatus()).isEqualTo(TEST_FILTER_READ_STATUS);
+ assertThat(appParams.getFilterRecipient()).isEqualTo(TEST_FILTER_RECIPIENT);
+ assertThat(appParams.getFolderListingSize()).isEqualTo(TEST_FOLDER_LISTING_SIZE);
+ assertThat(appParams.getFilterUidPresent()).isEqualTo(TEST_FILTER_UID_PRESENT);
+ assertThat(appParams.getFolderVerCounter()).isEqualTo(ret.array());
+ assertThat(appParams.getFractionDeliver()).isEqualTo(TEST_FRACTION_DELIVER);
+ assertThat(appParams.getFractionRequest()).isEqualTo(TEST_FRACTION_REQUEST);
+ assertThat(appParams.getMasInstanceId()).isEqualTo(TEST_MAS_INSTANCE_ID);
+ assertThat(appParams.getMessageListingSize()).isEqualTo(TEST_MESSAGE_LISTING_SIZE);
+ assertThat(appParams.getNewMessage()).isEqualTo(TEST_NEW_MESSAGE);
+ assertThat(appParams.getNotificationFilter()).isEqualTo(TEST_NOTIFICATION_FILTER);
+ assertThat(appParams.getNotificationStatus()).isEqualTo(TEST_NOTIFICATION_STATUS);
+ assertThat(appParams.getPresenceAvailability()).isEqualTo(TEST_PRESENCE_AVAILABILITY);
+ assertThat(appParams.getPresenceStatus()).isEqualTo(TEST_PRESENCE_STATUS);
+ assertThat(appParams.getRetry()).isEqualTo(TEST_RETRY);
+ assertThat(appParams.getStatusIndicator()).isEqualTo(TEST_STATUS_INDICATOR);
+ assertThat(appParams.getStatusValue()).isEqualTo(TEST_STATUS_VALUE);
+ assertThat(appParams.getSubjectLength()).isEqualTo(TEST_SUBJECT_LENGTH);
+ assertThat(appParams.getTransparent()).isEqualTo(TEST_TRANSPARENT);
+ }
+
+ @Test
+ public void setAndGetFilterLastActivity_withString() throws Exception {
+ BluetoothMapAppParams appParams = new BluetoothMapAppParams();
+ appParams.setFilterLastActivityBegin(TEST_FILTER_LAST_ACTIVITY_BEGIN);
+ appParams.setFilterLastActivityEnd(TEST_FILTER_LAST_ACTIVITY_END);
+ String lastActivityBeginString = appParams.getFilterLastActivityBeginString();
+ String lastActivityEndString = appParams.getFilterLastActivityEndString();
+
+ appParams.setFilterLastActivityBegin(lastActivityBeginString);
+ appParams.setFilterLastActivityEnd(lastActivityEndString);
+
+ assertThat(appParams.getFilterLastActivityBegin()).isEqualTo(
+ TEST_FILTER_LAST_ACTIVITY_BEGIN);
+ assertThat(appParams.getFilterLastActivityBegin()).isEqualTo(TEST_FILTER_LAST_ACTIVITY_END);
+ }
+
+ @Test
+ public void setAndGetLastActivity_withString() throws Exception {
+ BluetoothMapAppParams appParams = new BluetoothMapAppParams();
+ appParams.setLastActivity(TEST_LAST_ACTIVITY);
+ String lastActivityString = appParams.getLastActivityString();
+
+ appParams.setLastActivity(lastActivityString);
+
+ assertThat(appParams.getLastActivity()).isEqualTo(TEST_LAST_ACTIVITY);
+ }
+
+ @Test
+ public void setAndGetFilterPeriod_withString() throws Exception {
+ BluetoothMapAppParams appParams = new BluetoothMapAppParams();
+ appParams.setFilterPeriodBegin(TEST_FILTER_PERIOD_BEGIN);
+ appParams.setFilterPeriodEnd(TEST_FILTER_PERIOD_END);
+ String filterPeriodBeginString = appParams.getFilterPeriodBeginString();
+ String filterPeriodEndString = appParams.getFilterPeriodEndString();
+
+ appParams.setFilterPeriodBegin(filterPeriodBeginString);
+ appParams.setFilterPeriodEnd(filterPeriodEndString);
+
+ assertThat(appParams.getFilterPeriodBegin()).isEqualTo(TEST_FILTER_PERIOD_BEGIN);
+ assertThat(appParams.getFilterPeriodEnd()).isEqualTo(TEST_FILTER_PERIOD_END);
+ }
+
+ @Test
+ public void setAndGetMseTime_withString() throws Exception {
+ BluetoothMapAppParams appParams = new BluetoothMapAppParams();
+ appParams.setMseTime(TEST_MSE_TIME);
+ String mseTimeString = appParams.getMseTimeString();
+
+ appParams.setMseTime(mseTimeString);
+
+ assertThat(appParams.getMseTime()).isEqualTo(TEST_MSE_TIME);
+ }
+
+ @Test
+ public void setters_withIllegalArguments() {
+ BluetoothMapAppParams appParams = new BluetoothMapAppParams();
+ int ILLEGAL_PARAMETER_INT = -2;
+ long ILLEGAL_PARAMETER_LONG = -2;
+
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setAttachment(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setCharset(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setChatState(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setConvoListingSize(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setConvoParameterMask(ILLEGAL_PARAMETER_LONG));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setFilterMessageType(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setFilterPresence(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setFilterPriority(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setFilterReadStatus(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setFilterUidPresent(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setFolderListingSize(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setFractionDeliver(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setFractionRequest(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setMasInstanceId(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setMaxListCount(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setMessageListingSize(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setNewMessage(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setNotificationFilter(ILLEGAL_PARAMETER_LONG));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setNotificationStatus(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setParameterMask(ILLEGAL_PARAMETER_LONG));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setPresenceAvailability(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setRetry(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setStartOffset(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setStatusIndicator(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setStatusValue(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setSubjectLength(ILLEGAL_PARAMETER_INT));
+ assertThrows(IllegalArgumentException.class,
+ () -> appParams.setTransparent(ILLEGAL_PARAMETER_INT));
+ }
+
+ @Test
+ public void setters_withIllegalStrings() {
+ BluetoothMapAppParams appParams = new BluetoothMapAppParams();
+
+ appParams.setFilterConvoId(" ");
+ appParams.setFilterMsgHandle("=");
+
+ assertThat(appParams.getFilterConvoId()).isNull();
+ assertThat(appParams.getFilterMsgHandle()).isEqualTo(-1);
+ }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapContentTest.java b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapContentTest.java
new file mode 100644
index 0000000..8f78989
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapContentTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2022 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.map;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.provider.ContactsContract;
+import android.provider.Telephony;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.BluetoothMethodProxy;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+@RunWith(AndroidJUnit4.class)
+public class BluetoothMapContentTest {
+ private static final String TEST_TEXT = "text";
+
+ @Mock
+ private ContentResolver mContentResolver;
+ @Spy
+ private BluetoothMethodProxy mMapMethodProxy = BluetoothMethodProxy.getInstance();
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ BluetoothMethodProxy.setInstanceForTesting(mMapMethodProxy);
+ }
+
+ @After
+ public void tearDown() {
+ BluetoothMethodProxy.setInstanceForTesting(null);
+ }
+
+ @Test
+ public void getTextPartsMms() {
+ final long id = 1111;
+ Cursor cursor = mock(Cursor.class);
+ when(cursor.moveToFirst()).thenReturn(true);
+ when(cursor.getColumnIndex("ct")).thenReturn(1);
+ when(cursor.getString(1)).thenReturn("text/plain");
+ when(cursor.getColumnIndex("text")).thenReturn(2);
+ when(cursor.getString(2)).thenReturn(TEST_TEXT);
+ doReturn(cursor).when(mMapMethodProxy).contentResolverQuery(any(), any(), any(), any(),
+ any(), any());
+
+ assertThat(BluetoothMapContent.getTextPartsMms(mContentResolver, id)).isEqualTo(TEST_TEXT);
+ }
+
+ @Test
+ public void getContactNameFromPhone() {
+ String phoneName = "testPhone";
+ Cursor cursor = mock(Cursor.class);
+ when(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)).thenReturn(1);
+ when(cursor.getCount()).thenReturn(1);
+ when(cursor.getString(1)).thenReturn(TEST_TEXT);
+ doReturn(cursor).when(mMapMethodProxy).contentResolverQuery(any(), any(), any(), any(),
+ any(), any());
+
+ assertThat(
+ BluetoothMapContent.getContactNameFromPhone(phoneName, mContentResolver)).isEqualTo(
+ TEST_TEXT);
+ }
+
+ @Test
+ public void getCanonicalAddressSms() {
+ int threadId = 0;
+ Cursor cursor = mock(Cursor.class);
+ when(cursor.moveToFirst()).thenReturn(true);
+ when(cursor.getString(0)).thenReturn("recipientIdOne recipientIdTwo");
+ when(cursor.getColumnIndex(Telephony.CanonicalAddressesColumns.ADDRESS)).thenReturn(1);
+ when(cursor.getString(1)).thenReturn("recipientAddress");
+ doReturn(cursor).when(mMapMethodProxy).contentResolverQuery(any(), any(), any(), any(),
+ any(), any());
+
+ assertThat(
+ BluetoothMapContent.getCanonicalAddressSms(mContentResolver, threadId)).isEqualTo(
+ "recipientAddress");
+ }
+
+ @Test
+ public void getAddressMms() {
+ long id = 1111;
+ int type = 0;
+ Cursor cursor = mock(Cursor.class);
+ when(cursor.moveToFirst()).thenReturn(true);
+ when(cursor.getColumnIndex(Telephony.Mms.Addr.ADDRESS)).thenReturn(1);
+ when(cursor.getString(1)).thenReturn(TEST_TEXT);
+ doReturn(cursor).when(mMapMethodProxy).contentResolverQuery(any(), any(), any(), any(),
+ any(), any());
+
+ assertThat(BluetoothMapContent.getAddressMms(mContentResolver, id, type)).isEqualTo(
+ TEST_TEXT);
+ }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapSmsPduTest.java b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapSmsPduTest.java
new file mode 100644
index 0000000..56c791c
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapSmsPduTest.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2022 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.map;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.telephony.SmsMessage;
+import android.telephony.TelephonyManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.map.BluetoothMapSmsPdu.SmsPdu;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothMapSmsPduTest {
+ private static final String TEST_TEXT = "test";
+ // Text below size 160 only need one SMS part
+ private static final String TEST_TEXT_WITH_TWO_SMS_PARTS = "a".repeat(161);
+ private static final String TEST_DESTINATION_ADDRESS = "12";
+ private static final int TEST_TYPE = BluetoothMapSmsPdu.SMS_TYPE_GSM;
+ private static final long TEST_DATE = 1;
+
+ private byte[] TEST_DATA;
+ private int TEST_ENCODING;
+ private int TEST_LANGUAGE_TABLE;
+
+ @Mock
+ private Context mTargetContext;
+ @Mock
+ private TelephonyManager mTelephonyManager;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ when(mTargetContext.getSystemServiceName(TelephonyManager.class)).thenReturn(
+ "TELEPHONY_SERVICE");
+ when(mTargetContext.getSystemService("TELEPHONY_SERVICE")).thenReturn(mTelephonyManager);
+
+ int[] ted = SmsMessage.calculateLength((CharSequence) TEST_TEXT, false);
+ TEST_ENCODING = ted[3];
+ TEST_LANGUAGE_TABLE = ted[4];
+ TEST_DATA = SmsMessage.getSubmitPdu(null, TEST_DESTINATION_ADDRESS, TEST_TEXT,
+ false).encodedMessage;
+ }
+
+ @Test
+ public void constructor_withDataAndType() {
+ SmsPdu smsPdu = new SmsPdu(TEST_DATA, TEST_TYPE);
+ int offsetExpected = 2 + ((TEST_DATA[2] + 1) & 0xff) / 2 + 5;
+
+ assertThat(smsPdu.getData()).isEqualTo(TEST_DATA);
+ assertThat(smsPdu.getEncoding()).isEqualTo(-1);
+ assertThat(smsPdu.getLanguageTable()).isEqualTo(-1);
+ assertThat(smsPdu.getLanguageShiftTable()).isEqualTo(-1);
+ assertThat(smsPdu.getUserDataMsgOffset()).isEqualTo(offsetExpected);
+ assertThat(smsPdu.getUserDataMsgSize()).isEqualTo(TEST_DATA.length - (offsetExpected));
+ }
+
+ @Test
+ public void constructor_withAllParameters() {
+ SmsPdu smsPdu = new SmsPdu(TEST_DATA, TEST_ENCODING, TEST_TYPE, TEST_LANGUAGE_TABLE);
+
+ assertThat(smsPdu.getData()).isEqualTo(TEST_DATA);
+ assertThat(smsPdu.getEncoding()).isEqualTo(TEST_ENCODING);
+ assertThat(smsPdu.getType()).isEqualTo(TEST_TYPE);
+ assertThat(smsPdu.getLanguageTable()).isEqualTo(TEST_LANGUAGE_TABLE);
+ }
+
+ @Test
+ public void getSubmitPdus_withTypeGSM_whenMsgCountIsMoreThanOne() throws Exception {
+ when(mTelephonyManager.getCurrentPhoneType()).thenReturn(TelephonyManager.PHONE_TYPE_GSM);
+
+ ArrayList<SmsPdu> pdus = BluetoothMapSmsPdu.getSubmitPdus(mTargetContext,
+ TEST_TEXT_WITH_TWO_SMS_PARTS, null);
+
+ assertThat(pdus.size()).isEqualTo(2);
+ assertThat(pdus.get(0).getType()).isEqualTo(BluetoothMapSmsPdu.SMS_TYPE_GSM);
+
+ BluetoothMapbMessageSms messageSmsToEncode = new BluetoothMapbMessageSms();
+ messageSmsToEncode.setType(BluetoothMapUtils.TYPE.SMS_GSM);
+ messageSmsToEncode.setFolder("placeholder");
+ messageSmsToEncode.setStatus(true);
+ messageSmsToEncode.setSmsBodyPdus(pdus);
+
+ byte[] encodedMessageSms = messageSmsToEncode.encode();
+ InputStream inputStream = new ByteArrayInputStream(encodedMessageSms);
+ BluetoothMapbMessage messageParsed = BluetoothMapbMessage.parse(inputStream,
+ BluetoothMapAppParams.CHARSET_NATIVE);
+
+ assertThat(messageParsed).isInstanceOf(BluetoothMapbMessageSms.class);
+ BluetoothMapbMessageSms messageSmsParsed = (BluetoothMapbMessageSms) messageParsed;
+ assertThat(messageSmsParsed.getSmsBody()).isEqualTo(TEST_TEXT_WITH_TWO_SMS_PARTS);
+ }
+
+ @Test
+ public void getSubmitPdus_withTypeCDMA() throws Exception {
+ when(mTelephonyManager.getCurrentPhoneType()).thenReturn(TelephonyManager.PHONE_TYPE_CDMA);
+
+ ArrayList<SmsPdu> pdus = BluetoothMapSmsPdu.getSubmitPdus(mTargetContext, TEST_TEXT, null);
+
+ assertThat(pdus.size()).isEqualTo(1);
+ assertThat(pdus.get(0).getType()).isEqualTo(BluetoothMapSmsPdu.SMS_TYPE_CDMA);
+
+ BluetoothMapbMessageSms messageSmsToEncode = new BluetoothMapbMessageSms();
+ messageSmsToEncode.setType(BluetoothMapUtils.TYPE.SMS_CDMA);
+ messageSmsToEncode.setFolder("placeholder");
+ messageSmsToEncode.setStatus(true);
+ messageSmsToEncode.setSmsBodyPdus(pdus);
+
+ byte[] encodedMessageSms = messageSmsToEncode.encode();
+ InputStream inputStream = new ByteArrayInputStream(encodedMessageSms);
+ BluetoothMapbMessage messageParsed = BluetoothMapbMessage.parse(inputStream,
+ BluetoothMapAppParams.CHARSET_NATIVE);
+
+ assertThat(messageParsed).isInstanceOf(BluetoothMapbMessageSms.class);
+ }
+
+ @Test
+ public void getDeliverPdus_withTypeGSM() throws Exception {
+ when(mTelephonyManager.getCurrentPhoneType()).thenReturn(TelephonyManager.PHONE_TYPE_GSM);
+
+ ArrayList<SmsPdu> pdus = BluetoothMapSmsPdu.getDeliverPdus(mTargetContext, TEST_TEXT,
+ TEST_DESTINATION_ADDRESS, TEST_DATE);
+
+ assertThat(pdus.size()).isEqualTo(1);
+ assertThat(pdus.get(0).getType()).isEqualTo(BluetoothMapSmsPdu.SMS_TYPE_GSM);
+
+ BluetoothMapbMessageSms messageSmsToEncode = new BluetoothMapbMessageSms();
+ messageSmsToEncode.setType(BluetoothMapUtils.TYPE.SMS_GSM);
+ messageSmsToEncode.setFolder("placeholder");
+ messageSmsToEncode.setStatus(true);
+ messageSmsToEncode.setSmsBodyPdus(pdus);
+
+ byte[] encodedMessageSms = messageSmsToEncode.encode();
+ InputStream inputStream = new ByteArrayInputStream(encodedMessageSms);
+
+ assertThrows(IllegalArgumentException.class, () -> BluetoothMapbMessage.parse(inputStream,
+ BluetoothMapAppParams.CHARSET_NATIVE));
+ }
+
+ @Test
+ public void getDeliverPdus_withTypeCDMA() throws Exception {
+ when(mTelephonyManager.getCurrentPhoneType()).thenReturn(TelephonyManager.PHONE_TYPE_CDMA);
+
+ ArrayList<SmsPdu> pdus = BluetoothMapSmsPdu.getDeliverPdus(mTargetContext, TEST_TEXT,
+ TEST_DESTINATION_ADDRESS, TEST_DATE);
+
+ assertThat(pdus.size()).isEqualTo(1);
+ assertThat(pdus.get(0).getType()).isEqualTo(BluetoothMapSmsPdu.SMS_TYPE_CDMA);
+
+ BluetoothMapbMessageSms messageSmsToEncode = new BluetoothMapbMessageSms();
+ messageSmsToEncode.setType(BluetoothMapUtils.TYPE.SMS_CDMA);
+ messageSmsToEncode.setFolder("placeholder");
+ messageSmsToEncode.setStatus(true);
+ messageSmsToEncode.setSmsBodyPdus(pdus);
+
+ byte[] encodedMessageSms = messageSmsToEncode.encode();
+ InputStream inputStream = new ByteArrayInputStream(encodedMessageSms);
+
+ assertThrows(IllegalArgumentException.class, () -> BluetoothMapbMessage.parse(inputStream,
+ BluetoothMapAppParams.CHARSET_NATIVE));
+ }
+
+ @Test
+ public void getEncodingString() {
+ SmsPdu smsPduGsm7bitWithLanguageTableZero = new SmsPdu(TEST_DATA, SmsMessage.ENCODING_7BIT,
+ BluetoothMapSmsPdu.SMS_TYPE_GSM, 0);
+ assertThat(smsPduGsm7bitWithLanguageTableZero.getEncodingString()).isEqualTo("G-7BIT");
+
+ SmsPdu smsPduGsm7bitWithLanguageTableOne = new SmsPdu(TEST_DATA, SmsMessage.ENCODING_7BIT,
+ BluetoothMapSmsPdu.SMS_TYPE_GSM, 1);
+ assertThat(smsPduGsm7bitWithLanguageTableOne.getEncodingString()).isEqualTo("G-7BITEXT");
+
+ SmsPdu smsPduGsm8bit = new SmsPdu(TEST_DATA, SmsMessage.ENCODING_8BIT,
+ BluetoothMapSmsPdu.SMS_TYPE_GSM, 0);
+ assertThat(smsPduGsm8bit.getEncodingString()).isEqualTo("G-8BIT");
+
+ SmsPdu smsPduGsm16bit = new SmsPdu(TEST_DATA, SmsMessage.ENCODING_16BIT,
+ BluetoothMapSmsPdu.SMS_TYPE_GSM, 0);
+ assertThat(smsPduGsm16bit.getEncodingString()).isEqualTo("G-16BIT");
+
+ SmsPdu smsPduGsmUnknown = new SmsPdu(TEST_DATA, SmsMessage.ENCODING_UNKNOWN,
+ BluetoothMapSmsPdu.SMS_TYPE_GSM, 0);
+ assertThat(smsPduGsmUnknown.getEncodingString()).isEqualTo("");
+
+ SmsPdu smsPduCdma7bit = new SmsPdu(TEST_DATA, SmsMessage.ENCODING_7BIT,
+ BluetoothMapSmsPdu.SMS_TYPE_CDMA, 0);
+ assertThat(smsPduCdma7bit.getEncodingString()).isEqualTo("C-7ASCII");
+
+ SmsPdu smsPduCdma8bit = new SmsPdu(TEST_DATA, SmsMessage.ENCODING_8BIT,
+ BluetoothMapSmsPdu.SMS_TYPE_CDMA, 0);
+ assertThat(smsPduCdma8bit.getEncodingString()).isEqualTo("C-8BIT");
+
+ SmsPdu smsPduCdma16bit = new SmsPdu(TEST_DATA, SmsMessage.ENCODING_16BIT,
+ BluetoothMapSmsPdu.SMS_TYPE_CDMA, 0);
+ assertThat(smsPduCdma16bit.getEncodingString()).isEqualTo("C-UNICODE");
+
+ SmsPdu smsPduCdmaKsc5601 = new SmsPdu(TEST_DATA, SmsMessage.ENCODING_KSC5601,
+ BluetoothMapSmsPdu.SMS_TYPE_CDMA, 0);
+ assertThat(smsPduCdmaKsc5601.getEncodingString()).isEqualTo("C-KOREAN");
+
+ SmsPdu smsPduCdmaUnknown = new SmsPdu(TEST_DATA, SmsMessage.ENCODING_UNKNOWN,
+ BluetoothMapSmsPdu.SMS_TYPE_CDMA, 0);
+ assertThat(smsPduCdmaUnknown.getEncodingString()).isEqualTo("");
+ }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapbMessageEmailTest.java b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapbMessageEmailTest.java
new file mode 100644
index 0000000..e2a5cb4
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapbMessageEmailTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2022 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.map;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.map.BluetoothMapUtils.TYPE;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothMapbMessageEmailTest {
+ public static final String TEST_EMAIL_BODY = "test_email_body";
+
+ @Test
+ public void setAndGetEmailBody() {
+ BluetoothMapbMessageEmail messageEmail = new BluetoothMapbMessageEmail();
+ messageEmail.setEmailBody(TEST_EMAIL_BODY);
+ assertThat(messageEmail.getEmailBody()).isEqualTo(TEST_EMAIL_BODY);
+ }
+
+ @Test
+ public void encodeToByteArray_thenCreateByParsing() throws Exception {
+ BluetoothMapbMessageEmail messageEmailToEncode = new BluetoothMapbMessageEmail();
+ messageEmailToEncode.setType(TYPE.EMAIL);
+ messageEmailToEncode.setFolder("placeholder");
+ messageEmailToEncode.setStatus(true);
+ messageEmailToEncode.setEmailBody(TEST_EMAIL_BODY);
+
+ byte[] encodedMessageEmail = messageEmailToEncode.encode();
+ InputStream inputStream = new ByteArrayInputStream(encodedMessageEmail);
+
+ BluetoothMapbMessage messageParsed = BluetoothMapbMessage.parse(inputStream,
+ BluetoothMapAppParams.CHARSET_UTF8);
+ assertThat(messageParsed).isInstanceOf(BluetoothMapbMessageEmail.class);
+ BluetoothMapbMessageEmail messageEmailParsed = (BluetoothMapbMessageEmail) messageParsed;
+ assertThat(messageEmailParsed.getEmailBody()).isEqualTo(TEST_EMAIL_BODY);
+ }
+
+ @Test
+ public void encodeToByteArray_withEmptyBody_thenCreateByParsing() throws Exception {
+ BluetoothMapbMessageEmail messageEmailToEncode = new BluetoothMapbMessageEmail();
+ messageEmailToEncode.setType(TYPE.EMAIL);
+ messageEmailToEncode.setFolder("placeholder");
+ messageEmailToEncode.setStatus(true);
+
+ byte[] encodedMessageEmail = messageEmailToEncode.encode();
+ InputStream inputStream = new ByteArrayInputStream(encodedMessageEmail);
+
+ BluetoothMapbMessage messageParsed = BluetoothMapbMessage.parse(inputStream,
+ BluetoothMapAppParams.CHARSET_UTF8);
+ assertThat(messageParsed).isInstanceOf(BluetoothMapbMessageEmail.class);
+ BluetoothMapbMessageEmail messageEmailParsed = (BluetoothMapbMessageEmail) messageParsed;
+ assertThat(messageEmailParsed.getEmailBody()).isEqualTo("");
+ }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapbMessageSmsTest.java b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapbMessageSmsTest.java
new file mode 100644
index 0000000..40607b2
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/map/BluetoothMapbMessageSmsTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2022 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.map;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.map.BluetoothMapSmsPdu.SmsPdu;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothMapbMessageSmsTest {
+ private static final String TEST_SMS_BODY = "test_sms_body";
+ private static final String TEST_MESSAGE = "test";
+ private static final String TEST_ADDRESS = "12";
+
+ private Context mTargetContext;
+ private ArrayList<SmsPdu> TEST_SMS_BODY_PDUS;
+
+ @Before
+ public void setUp() throws Exception {
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ TEST_SMS_BODY_PDUS = BluetoothMapSmsPdu.getSubmitPdus(mTargetContext, TEST_MESSAGE,
+ TEST_ADDRESS);
+ }
+
+ @Test
+ public void settersAndGetters() {
+ BluetoothMapbMessageSms messageSms = new BluetoothMapbMessageSms();
+ messageSms.setSmsBody(TEST_SMS_BODY);
+ messageSms.setSmsBodyPdus(TEST_SMS_BODY_PDUS);
+
+ assertThat(messageSms.getSmsBody()).isEqualTo(TEST_SMS_BODY);
+ assertThat(messageSms.mEncoding).isEqualTo(TEST_SMS_BODY_PDUS.get(0).getEncodingString());
+ }
+
+ @Test
+ public void parseMsgInit() {
+ BluetoothMapbMessageSms messageSms = new BluetoothMapbMessageSms();
+ messageSms.parseMsgInit();
+ assertThat(messageSms.getSmsBody()).isEqualTo("");
+ }
+
+ @Test
+ public void encodeToByteArray_thenAddByParsing() throws Exception {
+ BluetoothMapbMessageSms messageSmsToEncode = new BluetoothMapbMessageSms();
+ messageSmsToEncode.setType(BluetoothMapUtils.TYPE.SMS_GSM);
+ messageSmsToEncode.setFolder("placeholder");
+ messageSmsToEncode.setStatus(true);
+ messageSmsToEncode.setSmsBodyPdus(TEST_SMS_BODY_PDUS);
+
+ byte[] encodedMessageSms = messageSmsToEncode.encode();
+ InputStream inputStream = new ByteArrayInputStream(encodedMessageSms);
+
+ BluetoothMapbMessage messageParsed = BluetoothMapbMessage.parse(inputStream,
+ BluetoothMapAppParams.CHARSET_NATIVE);
+ assertThat(messageParsed).isInstanceOf(BluetoothMapbMessageSms.class);
+ BluetoothMapbMessageSms messageSmsParsed = (BluetoothMapbMessageSms) messageParsed;
+ assertThat(messageSmsParsed.getSmsBody()).isEqualTo(TEST_MESSAGE);
+ }
+
+ @Test
+ public void encodeToByteArray_withEmptyMessage_thenAddByParsing() throws Exception {
+ BluetoothMapbMessageSms messageSmsToEncode = new BluetoothMapbMessageSms();
+ messageSmsToEncode.setType(BluetoothMapUtils.TYPE.SMS_GSM);
+ messageSmsToEncode.setFolder("placeholder");
+ messageSmsToEncode.setStatus(true);
+
+ byte[] encodedMessageSms = messageSmsToEncode.encode();
+ InputStream inputStream = new ByteArrayInputStream(encodedMessageSms);
+
+ BluetoothMapbMessage messageParsed = BluetoothMapbMessage.parse(inputStream,
+ BluetoothMapAppParams.CHARSET_UTF8);
+ assertThat(messageParsed).isInstanceOf(BluetoothMapbMessageSms.class);
+ BluetoothMapbMessageSms messageSmsParsed = (BluetoothMapbMessageSms) messageParsed;
+ assertThat(messageSmsParsed.getSmsBody()).isEqualTo("");
+ }
+}
\ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppBatchTest.java b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppBatchTest.java
index 93d6e82..a37d049 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppBatchTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppBatchTest.java
@@ -17,6 +17,9 @@
package com.android.bluetooth.opp;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
import android.content.Context;
@@ -24,9 +27,12 @@
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.bluetooth.BluetoothMethodProxy;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mockito;
@MediumTest
@RunWith(AndroidJUnit4.class)
@@ -85,37 +91,18 @@
}
@Test
- public void cancelBatch_throwUnknownUri() {
- // Array can be access and edit by the inner class
- final boolean[] batchCancelCalled = {false};
- mBluetoothOppBatch.registerListener(new BluetoothOppBatch.BluetoothOppBatchListener() {
- @Override
- public void onShareAdded(int id) {
- }
+ public void cancelBatch_cancelSuccessfully() {
- @Override
- public void onShareDeleted(int id) {
- }
-
- @Override
- public void onBatchCanceled() {
- batchCancelCalled[0] = true;
- }
- });
+ BluetoothMethodProxy proxy = spy(BluetoothMethodProxy.getInstance());
+ BluetoothMethodProxy.setInstanceForTesting(proxy);
+ doReturn(0).when(proxy).contentResolverDelete(any(), any(), any(), any());
+ doReturn(0).when(proxy).contentResolverUpdate(any(), any(), any(), any(), any());
assertThat(mBluetoothOppBatch.getPendingShare()).isEqualTo(mInitShareInfo);
- try {
- mBluetoothOppBatch.cancelBatch();
- assertThat(mBluetoothOppBatch.isEmpty()).isTrue();
- assertThat(batchCancelCalled[0]).isTrue();
- } catch (IllegalArgumentException e) {
- // the id for BluetoothOppShareInfo id is made up, so the link is invalid,
- // leading to IllegalArgumentException. In this case, cancelBatch() failed
- assertThat(e).hasMessageThat().isEqualTo(
- "Unknown URI content://com.android.bluetooth.opp/btopp/0");
- assertThat(mBluetoothOppBatch.isEmpty()).isFalse();
- assertThat(batchCancelCalled[0]).isFalse();
- }
+ mBluetoothOppBatch.cancelBatch();
+ assertThat(mBluetoothOppBatch.isEmpty()).isTrue();
+
+ BluetoothMethodProxy.setInstanceForTesting(null);
}
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppServiceTest.java
index e1f67aa..d011a6d 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppServiceTest.java
@@ -45,9 +45,11 @@
private BluetoothOppService mService = null;
private BluetoothAdapter mAdapter = null;
- @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
+ @Rule
+ public final ServiceTestRule mServiceRule = new ServiceTestRule();
- @Mock private AdapterService mAdapterService;
+ @Mock
+ private AdapterService mAdapterService;
@Before
public void setUp() throws Exception {
diff --git a/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppTestUtils.java b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppTestUtils.java
new file mode 100644
index 0000000..915ff0e
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppTestUtils.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 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.opp;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+
+import android.database.Cursor;
+
+import org.mockito.internal.util.MockUtil;
+
+import java.util.List;
+import java.util.Objects;
+
+public class BluetoothOppTestUtils {
+
+ /**
+ * A class containing the data to be return by a cursor. Intended to be use with setUpMockCursor
+ *
+ * @attr columnName is name of column to be used as a parameter in cursor.getColumnIndexOrThrow
+ * @attr mIndex should be returned from cursor.getColumnIndexOrThrow
+ * @attr mValue should be returned from cursor.getInt() or cursor.getString() or
+ * cursor.getLong()
+ */
+ public static class CursorMockData {
+ public final String mColumnName;
+ public final int mColumnIndex;
+ public final Object mValue;
+
+ public CursorMockData(String columnName, int index, Object value) {
+ mColumnName = columnName;
+ mColumnIndex = index;
+ mValue = value;
+ }
+ }
+
+ /**
+ * Set up a mock single-row Cursor that work for common use cases in the OPP package.
+ * It mocks the database column index and value of the cell in that column of the current row
+ *
+ * <pre>
+ * cursorMockDataList.add(
+ * new CursorMockData(BluetoothShare.DIRECTION, 2, BluetoothShare.DIRECTION_INBOUND
+ * );
+ * ...
+ * setUpMockCursor(cursor, cursorMockDataList);
+ * // This will return 2
+ * int index = cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION);
+ * int direction = cursor.getInt(index); // This will return BluetoothShare.DIRECTION_INBOUND
+ * </pre>
+ *
+ * @param cursor a mock/spy cursor to be setup
+ * @param cursorMockDataList a list representing what cursor will return
+ */
+ public static void setUpMockCursor(
+ Cursor cursor, List<CursorMockData> cursorMockDataList) {
+ assert(MockUtil.isMock(cursor));
+
+ doAnswer(invocation -> {
+ String name = invocation.getArgument(0);
+ return cursorMockDataList.stream().filter(
+ mockCursorData -> Objects.equals(mockCursorData.mColumnName, name)
+ ).findFirst().orElse(new CursorMockData("", -1, null)).mColumnIndex;
+ }).when(cursor).getColumnIndexOrThrow(anyString());
+
+ doAnswer(invocation -> {
+ int index = invocation.getArgument(0);
+ return cursorMockDataList.stream().filter(
+ mockCursorData -> mockCursorData.mColumnIndex == index
+ ).findFirst().orElse(new CursorMockData("", -1, -1)).mValue;
+ }).when(cursor).getInt(anyInt());
+
+ doAnswer(invocation -> {
+ int index = invocation.getArgument(0);
+ return cursorMockDataList.stream().filter(
+ mockCursorData -> mockCursorData.mColumnIndex == index
+ ).findFirst().orElse(new CursorMockData("", -1, -1)).mValue;
+ }).when(cursor).getLong(anyInt());
+
+ doAnswer(invocation -> {
+ int index = invocation.getArgument(0);
+ return cursorMockDataList.stream().filter(
+ mockCursorData -> mockCursorData.mColumnIndex == index
+ ).findFirst().orElse(new CursorMockData("", -1, null)).mValue;
+ }).when(cursor).getString(anyInt());
+
+ doReturn(true).when(cursor).moveToFirst();
+ doReturn(true).when(cursor).moveToLast();
+ doReturn(true).when(cursor).moveToNext();
+ doReturn(true).when(cursor).moveToPrevious();
+ doReturn(true).when(cursor).moveToPosition(anyInt());
+ }
+}
+
diff --git a/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppTransferActivityTest.java b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppTransferActivityTest.java
new file mode 100644
index 0000000..23ae99d
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppTransferActivityTest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2022 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.opp;
+
+
+import static android.content.pm.PackageManager.DONT_KILL_APP;
+import static android.service.pm.PackageProto.UserInfoProto.COMPONENT_ENABLED_STATE_DEFAULT;
+import static android.service.pm.PackageProto.UserInfoProto.COMPONENT_ENABLED_STATE_ENABLED;
+
+import static com.android.bluetooth.opp.BluetoothOppTestUtils.CursorMockData;
+import static com.android.bluetooth.opp.BluetoothOppTransferActivity.DIALOG_RECEIVE_COMPLETE_FAIL;
+import static com.android.bluetooth.opp.BluetoothOppTransferActivity.DIALOG_RECEIVE_COMPLETE_SUCCESS;
+import static com.android.bluetooth.opp.BluetoothOppTransferActivity.DIALOG_RECEIVE_ONGOING;
+import static com.android.bluetooth.opp.BluetoothOppTransferActivity.DIALOG_SEND_COMPLETE_FAIL;
+import static com.android.bluetooth.opp.BluetoothOppTransferActivity.DIALOG_SEND_COMPLETE_SUCCESS;
+import static com.android.bluetooth.opp.BluetoothOppTransferActivity.DIALOG_SEND_ONGOING;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.BluetoothMethodProxy;
+import com.android.bluetooth.pbap.BluetoothPbapActivity;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothOppTransferActivityTest {
+ @Mock
+ Cursor mCursor;
+ @Spy
+ BluetoothMethodProxy mBluetoothMethodProxy;
+
+ List<CursorMockData> mCursorMockDataList;
+
+ Intent mIntent;
+ Context mTargetContext;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mBluetoothMethodProxy = Mockito.spy(BluetoothMethodProxy.getInstance());
+ BluetoothMethodProxy.setInstanceForTesting(mBluetoothMethodProxy);
+
+ Uri dataUrl = Uri.parse("content://com.android.bluetooth.opp.test/random");
+
+ mTargetContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ mIntent = new Intent();
+ mIntent.setClass(mTargetContext, BluetoothOppTransferActivity.class);
+ mIntent.setData(dataUrl);
+
+ doReturn(mCursor).when(mBluetoothMethodProxy).contentResolverQuery(any(), eq(dataUrl),
+ eq(null), eq(null),
+ eq(null), eq(null));
+
+ doReturn(1).when(mBluetoothMethodProxy).contentResolverUpdate(any(), eq(dataUrl),
+ any(), eq(null), eq(null));
+
+ int idValue = 1234;
+ Long timestampValue = 123456789L;
+ String destinationValue = "AA:BB:CC:00:11:22";
+ String fileTypeValue = "text/plain";
+
+ mCursorMockDataList = new ArrayList<>(List.of(
+ new CursorMockData(BluetoothShare._ID, 0, idValue),
+ new CursorMockData(BluetoothShare.MIMETYPE, 5, fileTypeValue),
+ new CursorMockData(BluetoothShare.TIMESTAMP, 6, timestampValue),
+ new CursorMockData(BluetoothShare.DESTINATION, 7, destinationValue),
+ new CursorMockData(BluetoothShare._DATA, 8, null),
+ new CursorMockData(BluetoothShare.FILENAME_HINT, 9, null),
+ new CursorMockData(BluetoothShare.URI, 10, "content://textfile.txt"),
+ new CursorMockData(BluetoothShare.USER_CONFIRMATION, 11,
+ BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED)
+ ));
+
+ enableActivity(true);
+ }
+
+ @After
+ public void tearDown() {
+ BluetoothMethodProxy.setInstanceForTesting(null);
+ enableActivity(false);
+ }
+
+ @Test
+ public void onCreate_showSendOnGoingDialog() {
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.STATUS, 1, BluetoothShare.STATUS_PENDING));
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.DIRECTION, 2, BluetoothShare.DIRECTION_OUTBOUND)
+ );
+ mCursorMockDataList.add(new CursorMockData(BluetoothShare.TOTAL_BYTES, 3, 100));
+ mCursorMockDataList.add(new CursorMockData(BluetoothShare.CURRENT_BYTES, 4, 0));
+ BluetoothOppTestUtils.setUpMockCursor(mCursor, mCursorMockDataList);
+
+ ActivityScenario<BluetoothOppTransferActivity> activityScenario = ActivityScenario.launch(
+ mIntent);
+
+ activityScenario.onActivity(activity -> {
+ assertThat(activity.mWhichDialog).isEqualTo(DIALOG_SEND_ONGOING);
+ });
+ }
+
+ @Test
+ public void onCreate_showSendCompleteSuccessDialog() {
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.STATUS, 1, BluetoothShare.STATUS_SUCCESS)
+ );
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.DIRECTION, 2, BluetoothShare.DIRECTION_OUTBOUND)
+ );
+ mCursorMockDataList.add(new CursorMockData(BluetoothShare.TOTAL_BYTES, 3, 100));
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.CURRENT_BYTES, 4, 100));
+ BluetoothOppTestUtils.setUpMockCursor(mCursor, mCursorMockDataList);
+
+ ActivityScenario<BluetoothOppTransferActivity> activityScenario = ActivityScenario.launch(
+ mIntent);
+
+ activityScenario.onActivity(activity -> {
+ assertThat(activity.mWhichDialog).isEqualTo(DIALOG_SEND_COMPLETE_SUCCESS);
+ });
+ }
+
+ @Test
+ public void onCreate_showSendCompleteFailDialog() {
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.STATUS, 1, BluetoothShare.STATUS_FORBIDDEN));
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.DIRECTION, 2, BluetoothShare.DIRECTION_OUTBOUND)
+ );
+ mCursorMockDataList.add(new CursorMockData(BluetoothShare.TOTAL_BYTES, 3, 100));
+ mCursorMockDataList.add(new CursorMockData(BluetoothShare.CURRENT_BYTES, 4, 42));
+ BluetoothOppTestUtils.setUpMockCursor(mCursor, mCursorMockDataList);
+
+ ActivityScenario<BluetoothOppTransferActivity> activityScenario = ActivityScenario.launch(
+ mIntent);
+
+ activityScenario.onActivity(
+ activity -> assertThat(activity.mWhichDialog).isEqualTo(DIALOG_SEND_COMPLETE_FAIL));
+ }
+
+ @Test
+ public void onCreate_showReceiveOnGoingDialog() {
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.STATUS, 1, BluetoothShare.STATUS_PENDING));
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.DIRECTION, 2, BluetoothShare.DIRECTION_INBOUND)
+ );
+ mCursorMockDataList.add(new CursorMockData(BluetoothShare.TOTAL_BYTES, 3, 100));
+ mCursorMockDataList.add(new CursorMockData(BluetoothShare.CURRENT_BYTES, 4, 0));
+ BluetoothOppTestUtils.setUpMockCursor(mCursor, mCursorMockDataList);
+
+ ActivityScenario<BluetoothOppTransferActivity> activityScenario = ActivityScenario.launch(
+ mIntent);
+
+ activityScenario.onActivity(
+ activity -> assertThat(activity.mWhichDialog).isEqualTo(DIALOG_RECEIVE_ONGOING));
+ }
+
+ @Test
+ public void onCreate_showReceiveCompleteSuccessDialog() {
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.STATUS, 1, BluetoothShare.STATUS_SUCCESS));
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.DIRECTION, 2, BluetoothShare.DIRECTION_INBOUND)
+ );
+ mCursorMockDataList.add(new CursorMockData(BluetoothShare.TOTAL_BYTES, 3, 100));
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.CURRENT_BYTES, 4, 100)
+ );
+
+ BluetoothOppTestUtils.setUpMockCursor(mCursor, mCursorMockDataList);
+ ActivityScenario<BluetoothOppTransferActivity> activityScenario = ActivityScenario.launch(
+ mIntent);
+
+ activityScenario.onActivity(activity -> assertThat(activity.mWhichDialog).isEqualTo(
+ DIALOG_RECEIVE_COMPLETE_SUCCESS));
+ }
+
+ @Test
+ public void onCreate_showReceiveCompleteFailDialog() {
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.STATUS, 1, BluetoothShare.STATUS_FORBIDDEN));
+ mCursorMockDataList.add(
+ new CursorMockData(BluetoothShare.DIRECTION, 2, BluetoothShare.DIRECTION_INBOUND)
+ );
+ mCursorMockDataList.add(new CursorMockData(BluetoothShare.TOTAL_BYTES, 3, 100));
+ mCursorMockDataList.add(new CursorMockData(BluetoothShare.CURRENT_BYTES, 4, 42));
+
+ BluetoothOppTestUtils.setUpMockCursor(mCursor, mCursorMockDataList);
+ ActivityScenario<BluetoothOppTransferActivity> activityScenario = ActivityScenario.launch(
+ mIntent);
+
+ activityScenario.onActivity(activity -> assertThat(activity.mWhichDialog).isEqualTo(
+ DIALOG_RECEIVE_COMPLETE_FAIL));
+ }
+
+ private void enableActivity(boolean enable) {
+ int enabledState = enable ? COMPONENT_ENABLED_STATE_ENABLED
+ : COMPONENT_ENABLED_STATE_DEFAULT;
+
+ mTargetContext.getPackageManager().setApplicationEnabledSetting(
+ mTargetContext.getPackageName(), enabledState, DONT_KILL_APP);
+
+ ComponentName activityName = new ComponentName(mTargetContext,
+ BluetoothOppTransferActivity.class);
+ mTargetContext.getPackageManager().setComponentEnabledSetting(
+ activityName, enabledState, DONT_KILL_APP);
+ }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppUtilityTest.java b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppUtilityTest.java
new file mode 100644
index 0000000..9703245
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/opp/BluetoothOppUtilityTest.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2022 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.opp;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.bluetooth.BluetoothMethodProxy;
+import com.android.bluetooth.opp.BluetoothOppTestUtils.CursorMockData;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class BluetoothOppUtilityTest {
+
+ private static final Uri CORRECT_FORMAT_BUT_INVALID_FILE_URI = Uri.parse(
+ "content://com.android.bluetooth.opp/btopp/0123455343467");
+ private static final Uri INCORRECT_FORMAT_URI = Uri.parse("www.google.com");
+
+ Context mContext;
+ @Mock
+ Cursor mCursor;
+
+ @Spy
+ BluetoothMethodProxy mCallProxy = BluetoothMethodProxy.getInstance();
+
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ BluetoothMethodProxy.setInstanceForTesting(mCallProxy);
+ }
+
+ @After
+ public void tearDown() {
+ BluetoothMethodProxy.setInstanceForTesting(null);
+ }
+
+ @Test
+ public void isBluetoothShareUri_correctlyCheckUri() {
+ assertThat(BluetoothOppUtility.isBluetoothShareUri(INCORRECT_FORMAT_URI)).isFalse();
+ assertThat(BluetoothOppUtility.isBluetoothShareUri(CORRECT_FORMAT_BUT_INVALID_FILE_URI))
+ .isTrue();
+ }
+
+ @Test
+ public void queryRecord_withInvalidFileUrl_returnsNull() {
+ doReturn(null).when(mCallProxy).contentResolverQuery(any(),
+ eq(CORRECT_FORMAT_BUT_INVALID_FILE_URI), eq(null), eq(null),
+ eq(null), eq(null));
+ assertThat(BluetoothOppUtility.queryRecord(mContext,
+ CORRECT_FORMAT_BUT_INVALID_FILE_URI)).isNull();
+ }
+
+ @Test
+ public void queryRecord_mockCursor_returnsInstance() {
+ String destinationValue = "AA:BB:CC:00:11:22";
+
+ doReturn(mCursor).when(mCallProxy).contentResolverQuery(any(),
+ eq(CORRECT_FORMAT_BUT_INVALID_FILE_URI), eq(null), eq(null),
+ eq(null), eq(null));
+ doReturn(true).when(mCursor).moveToFirst();
+ doReturn(destinationValue).when(mCursor).getString(anyInt());
+ assertThat(BluetoothOppUtility.queryRecord(mContext,
+ CORRECT_FORMAT_BUT_INVALID_FILE_URI)).isInstanceOf(BluetoothOppTransferInfo.class);
+ }
+
+ @Test
+ public void queryTransfersInBatch_returnsCorrectUrlArrayList() {
+ long timestampValue = 123456;
+ String where = BluetoothShare.TIMESTAMP + " == " + timestampValue;
+ AtomicInteger cnt = new AtomicInteger(1);
+
+ doReturn(mCursor).when(mCallProxy).contentResolverQuery(any(),
+ eq(BluetoothShare.CONTENT_URI), eq(new String[]{
+ BluetoothShare._DATA
+ }), eq(where), eq(null), eq(BluetoothShare._ID));
+
+
+ doAnswer(invocation -> cnt.incrementAndGet() > 5).when(mCursor).isAfterLast();
+ doReturn(CORRECT_FORMAT_BUT_INVALID_FILE_URI.toString()).when(mCursor)
+ .getString(0);
+
+ ArrayList<String> answer = BluetoothOppUtility.queryTransfersInBatch(mContext,
+ timestampValue);
+ for (String url : answer) {
+ assertThat(url).isEqualTo(CORRECT_FORMAT_BUT_INVALID_FILE_URI.toString());
+ }
+ }
+
+ @Test
+ public void fillRecord_filledAllProperties() {
+ int idValue = 1234;
+ int directionValue = BluetoothShare.DIRECTION_OUTBOUND;
+ long totalBytesValue = 10;
+ long currentBytesValue = 1;
+ int statusValue = BluetoothShare.STATUS_PENDING;
+ Long timestampValue = 123456789L;
+ String destinationValue = "AA:BB:CC:00:11:22";
+ String fileNameValue = "Unknown file";
+ String deviceNameValue = "Unknown device"; // bt device name
+ String fileTypeValue = "text/plain";
+
+ List<CursorMockData> cursorMockDataList = List.of(
+ new CursorMockData(BluetoothShare._ID, 0, idValue),
+ new CursorMockData(BluetoothShare.STATUS, 1, statusValue),
+ new CursorMockData(BluetoothShare.DIRECTION, 2, directionValue),
+ new CursorMockData(BluetoothShare.TOTAL_BYTES, 3, totalBytesValue),
+ new CursorMockData(BluetoothShare.CURRENT_BYTES, 4, currentBytesValue),
+ new CursorMockData(BluetoothShare.TIMESTAMP, 5, timestampValue),
+ new CursorMockData(BluetoothShare.DESTINATION, 6, destinationValue),
+ new CursorMockData(BluetoothShare._DATA, 7, null),
+ new CursorMockData(BluetoothShare.FILENAME_HINT, 8, null),
+ new CursorMockData(BluetoothShare.MIMETYPE, 9, fileTypeValue)
+ );
+
+ BluetoothOppTestUtils.setUpMockCursor(mCursor, cursorMockDataList);
+
+ BluetoothOppTransferInfo info = new BluetoothOppTransferInfo();
+ BluetoothOppUtility.fillRecord(mContext, mCursor, info);
+
+ assertThat(info.mID).isEqualTo(idValue);
+ assertThat(info.mStatus).isEqualTo(statusValue);
+ assertThat(info.mDirection).isEqualTo(directionValue);
+ assertThat(info.mTotalBytes).isEqualTo(totalBytesValue);
+ assertThat(info.mCurrentBytes).isEqualTo(currentBytesValue);
+ assertThat(info.mTimeStamp).isEqualTo(timestampValue);
+ assertThat(info.mDestAddr).isEqualTo(destinationValue);
+ assertThat(info.mFileUri).isEqualTo(null);
+ assertThat(info.mFileType).isEqualTo(fileTypeValue);
+ assertThat(info.mDeviceName).isEqualTo(deviceNameValue);
+ assertThat(info.mHandoverInitiated).isEqualTo(false);
+ assertThat(info.mFileName).isEqualTo(fileNameValue);
+ }
+
+ @Test
+ public void fileExists_returnFalse() {
+ assertThat(
+ BluetoothOppUtility.fileExists(mContext, CORRECT_FORMAT_BUT_INVALID_FILE_URI)
+ ).isFalse();
+ }
+
+ @Test
+ public void isRecognizedFileType_withWrongFileUriAndMimeType_returnFalse() {
+ assertThat(
+ BluetoothOppUtility.isRecognizedFileType(mContext,
+ CORRECT_FORMAT_BUT_INVALID_FILE_URI,
+ "aWrongMimeType")
+ ).isFalse();
+ }
+
+ @Test
+ public void formatProgressText() {
+ assertThat(BluetoothOppUtility.formatProgressText(100, 42)).isEqualTo("42%");
+ }
+
+ @Test
+ public void formatResultText() {
+ assertThat(BluetoothOppUtility.formatResultText(1, 2, mContext)).isEqualTo(
+ "1 successful, 2 unsuccessful.");
+ }
+
+ @Test
+ public void getStatusDescription_returnCorrectString() {
+ String deviceName = "randomName";
+ assertThat(BluetoothOppUtility.getStatusDescription(mContext,
+ BluetoothShare.STATUS_PENDING, deviceName)).isEqualTo(
+ "File transfer not started yet.");
+ assertThat(BluetoothOppUtility.getStatusDescription(mContext,
+ BluetoothShare.STATUS_RUNNING, deviceName)).isEqualTo(
+ "File transfer is ongoing.");
+ assertThat(BluetoothOppUtility.getStatusDescription(mContext,
+ BluetoothShare.STATUS_SUCCESS, deviceName)).isEqualTo(
+ "File transfer completed successfully.");
+ assertThat(BluetoothOppUtility.getStatusDescription(mContext,
+ BluetoothShare.STATUS_NOT_ACCEPTABLE, deviceName)).isEqualTo(
+ "Content isn\'t supported.");
+ assertThat(BluetoothOppUtility.getStatusDescription(mContext,
+ BluetoothShare.STATUS_FORBIDDEN, deviceName)).isEqualTo(
+ "Transfer forbidden by target device.");
+ assertThat(BluetoothOppUtility.getStatusDescription(mContext,
+ BluetoothShare.STATUS_CANCELED, deviceName)).isEqualTo(
+ "Transfer canceled by user.");
+ assertThat(BluetoothOppUtility.getStatusDescription(mContext,
+ BluetoothShare.STATUS_FILE_ERROR, deviceName)).isEqualTo("Storage issue.");
+ assertThat(BluetoothOppUtility.getStatusDescription(mContext,
+ BluetoothShare.STATUS_CONNECTION_ERROR, deviceName)).isEqualTo(
+ "Connection unsuccessful.");
+ assertThat(BluetoothOppUtility.getStatusDescription(mContext,
+ BluetoothShare.STATUS_ERROR_NO_SDCARD, deviceName)).isEqualTo(
+ BluetoothOppUtility.deviceHasNoSdCard() ?
+ "No USB storage." :
+ "No SD card. Insert an SD card to save transferred files."
+ );
+ assertThat(BluetoothOppUtility.getStatusDescription(mContext,
+ BluetoothShare.STATUS_ERROR_SDCARD_FULL, deviceName)).isEqualTo(
+ BluetoothOppUtility.deviceHasNoSdCard() ?
+ "There isn\'t enough space in USB storage to save the file." :
+ "There isn\'t enough space on the SD card to save the file."
+ );
+ assertThat(BluetoothOppUtility.getStatusDescription(mContext,
+ BluetoothShare.STATUS_BAD_REQUEST, deviceName)).isEqualTo(
+ "Request can\'t be handled correctly.");
+ assertThat(BluetoothOppUtility.getStatusDescription(mContext, 12345465,
+ deviceName)).isEqualTo("Unknown error.");
+ }
+
+ @Test
+ public void originalUri_trimBeforeAt() {
+ Uri originalUri = Uri.parse("com.android.bluetooth.opp.BluetoothOppSendFileInfo");
+ Uri uri = Uri.parse("com.android.bluetooth.opp.BluetoothOppSendFileInfo@dfe15a6");
+ assertThat(BluetoothOppUtility.originalUri(uri)).isEqualTo(originalUri);
+ }
+
+ @Test
+ public void fileInfo_testFileInfoFunctions() {
+ assertThat(
+ BluetoothOppUtility.getSendFileInfo(CORRECT_FORMAT_BUT_INVALID_FILE_URI)
+ ).isEqualTo(
+ BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR
+ );
+ assertThat(BluetoothOppUtility.generateUri(CORRECT_FORMAT_BUT_INVALID_FILE_URI,
+ BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR).toString()
+ ).contains(
+ CORRECT_FORMAT_BUT_INVALID_FILE_URI.toString());
+ try {
+ BluetoothOppUtility.putSendFileInfo(CORRECT_FORMAT_BUT_INVALID_FILE_URI,
+ BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR);
+ BluetoothOppUtility.closeSendFileInfo(CORRECT_FORMAT_BUT_INVALID_FILE_URI);
+ } catch (Exception e) {
+ assertWithMessage("Exception should not happen.").fail();
+ }
+ }
+
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/sap/SapServerTest.java b/android/app/tests/unit/src/com/android/bluetooth/sap/SapServerTest.java
new file mode 100644
index 0000000..09ac038
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/sap/SapServerTest.java
@@ -0,0 +1,714 @@
+/*
+ * Copyright 2022 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.sap;
+
+import static com.android.bluetooth.sap.SapMessage.CON_STATUS_ERROR_CONNECTION;
+import static com.android.bluetooth.sap.SapMessage.CON_STATUS_OK;
+import static com.android.bluetooth.sap.SapMessage.CON_STATUS_OK_ONGOING_CALL;
+import static com.android.bluetooth.sap.SapMessage.DISC_GRACEFULL;
+import static com.android.bluetooth.sap.SapMessage.ID_CONNECT_REQ;
+import static com.android.bluetooth.sap.SapMessage.ID_CONNECT_RESP;
+import static com.android.bluetooth.sap.SapMessage.ID_DISCONNECT_IND;
+import static com.android.bluetooth.sap.SapMessage.ID_DISCONNECT_RESP;
+import static com.android.bluetooth.sap.SapMessage.ID_ERROR_RESP;
+import static com.android.bluetooth.sap.SapMessage.ID_RIL_UNSOL_DISCONNECT_IND;
+import static com.android.bluetooth.sap.SapMessage.ID_STATUS_IND;
+import static com.android.bluetooth.sap.SapMessage.TEST_MODE_ENABLE;
+import static com.android.bluetooth.sap.SapServer.SAP_MSG_RFC_REPLY;
+import static com.android.bluetooth.sap.SapServer.SAP_MSG_RIL_CONNECT;
+import static com.android.bluetooth.sap.SapServer.SAP_MSG_RIL_IND;
+import static com.android.bluetooth.sap.SapServer.SAP_MSG_RIL_REQ;
+import static com.android.bluetooth.sap.SapServer.SAP_PROXY_DEAD;
+import static com.android.bluetooth.sap.SapServer.SAP_RIL_SOCK_CLOSED;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.hardware.radio.V1_0.ISap;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.RemoteException;
+import android.telephony.TelephonyManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicLong;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SapServerTest {
+ private static final long TIMEOUT_MS = 1_000;
+
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+
+ @Spy
+ private Context mTargetContext =
+ new ContextWrapper(InstrumentationRegistry.getInstrumentation().getTargetContext());
+
+ @Spy
+ private TestHandlerCallback mCallback = new TestHandlerCallback();
+
+ @Mock
+ private InputStream mInputStream;
+
+ @Mock
+ private OutputStream mOutputStream;
+
+ private SapServer mSapServer;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mHandlerThread = new HandlerThread("SapServerTest");
+ mHandlerThread.start();
+
+ mHandler = new Handler(mHandlerThread.getLooper(), mCallback);
+ mSapServer = spy(new SapServer(mHandler, mTargetContext, mInputStream, mOutputStream));
+ }
+
+ @After
+ public void tearDown() {
+ mHandlerThread.quit();
+ }
+
+ @Test
+ public void setNotification() {
+ NotificationManager notificationManager = mock(NotificationManager.class);
+ when(mTargetContext.getSystemService(NotificationManager.class))
+ .thenReturn(notificationManager);
+
+ ArgumentCaptor<Notification> captor = ArgumentCaptor.forClass(Notification.class);
+ int type = DISC_GRACEFULL;
+ int flags = PendingIntent.FLAG_CANCEL_CURRENT;
+ mSapServer.setNotification(type, flags);
+
+ verify(notificationManager).notify(eq(SapServer.NOTIFICATION_ID), captor.capture());
+ Notification notification = captor.getValue();
+ assertThat(notification.getChannelId()).isEqualTo(SapServer.SAP_NOTIFICATION_CHANNEL);
+ }
+
+ @Test
+ public void clearNotification() {
+ NotificationManager notificationManager = mock(NotificationManager.class);
+ when(mTargetContext.getSystemService(NotificationManager.class))
+ .thenReturn(notificationManager);
+
+ mSapServer.clearNotification();
+
+ verify(notificationManager).cancel(SapServer.NOTIFICATION_ID);
+ }
+
+ @Test
+ public void setTestMode() {
+ int testMode = TEST_MODE_ENABLE;
+ mSapServer.setTestMode(testMode);
+
+ assertThat(mSapServer.mTestMode).isEqualTo(testMode);
+ }
+
+ @Test
+ public void onConnectRequest_whenStateIsConnecting_callsSendRilMessage() {
+ SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+ ISap mockSapProxy = mock(ISap.class);
+ Object lock = new Object();
+ when(mockReceiver.getSapProxyLock()).thenReturn(lock);
+ when(mockReceiver.getSapProxy()).thenReturn(mockSapProxy);
+ mSapServer.mRilBtReceiver = mockReceiver;
+ mSapServer.mSapHandler = mHandler;
+
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTING);
+ SapMessage msg = new SapMessage(ID_STATUS_IND);
+ mSapServer.onConnectRequest(msg);
+
+ verify(mSapServer).sendRilMessage(msg);
+ }
+
+ @Test
+ public void onConnectRequest_whenStateIsConnected_sendsErrorConnectionClientMessage() {
+ mSapServer.mSapHandler = mHandler;
+
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTED);
+ mSapServer.onConnectRequest(mock(SapMessage.class));
+
+ verify(mSapServer).sendClientMessage(argThat(
+ sapMsg -> sapMsg.getMsgType() == ID_CONNECT_RESP
+ && sapMsg.getConnectionStatus() == CON_STATUS_ERROR_CONNECTION));
+ }
+
+ @Test
+ public void onConnectRequest_whenStateIsCallOngoing_sendsErrorConnectionClientMessage() {
+ mSapServer.mSapHandler = mHandler;
+
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTING_CALL_ONGOING);
+ mSapServer.onConnectRequest(mock(SapMessage.class));
+
+ verify(mSapServer, atLeastOnce()).sendClientMessage(argThat(
+ sapMsg -> sapMsg.getMsgType() == ID_CONNECT_RESP
+ && sapMsg.getConnectionStatus() == CON_STATUS_ERROR_CONNECTION));
+ }
+
+ @Test
+ public void getMessageName() {
+ assertThat(SapServer.getMessageName(SAP_MSG_RFC_REPLY)).isEqualTo("SAP_MSG_REPLY");
+ assertThat(SapServer.getMessageName(SAP_MSG_RIL_CONNECT)).isEqualTo("SAP_MSG_RIL_CONNECT");
+ assertThat(SapServer.getMessageName(SAP_MSG_RIL_REQ)).isEqualTo("SAP_MSG_RIL_REQ");
+ assertThat(SapServer.getMessageName(SAP_MSG_RIL_IND)).isEqualTo("SAP_MSG_RIL_IND");
+ assertThat(SapServer.getMessageName(-1)).isEqualTo("Unknown message ID");
+ }
+
+ @Test
+ public void sendReply() throws Exception {
+ SapMessage msg = mock(SapMessage.class);
+ mSapServer.sendReply(msg);
+
+ verify(msg).write(any(OutputStream.class));
+ }
+
+ @Test
+ public void sendRilMessage_success() throws Exception {
+ SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+ ISap mockSapProxy = mock(ISap.class);
+ Object lock = new Object();
+ when(mockReceiver.getSapProxyLock()).thenReturn(lock);
+ when(mockReceiver.getSapProxy()).thenReturn(mockSapProxy);
+ mSapServer.mRilBtReceiver = mockReceiver;
+ mSapServer.mSapHandler = mHandler;
+
+ SapMessage msg = mock(SapMessage.class);
+ mSapServer.sendRilMessage(msg);
+
+ verify(msg).send(mockSapProxy);
+ }
+
+ @Test
+ public void sendRilMessage_whenSapProxyIsNull_sendsErrorClientMessage() throws Exception {
+ SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+ Object lock = new Object();
+ when(mockReceiver.getSapProxyLock()).thenReturn(lock);
+ when(mockReceiver.getSapProxy()).thenReturn(null);
+ mSapServer.mRilBtReceiver = mockReceiver;
+ mSapServer.mSapHandler = mHandler;
+
+ SapMessage msg = mock(SapMessage.class);
+ mSapServer.sendRilMessage(msg);
+
+ verify(mSapServer).sendClientMessage(
+ argThat(sapMsg -> sapMsg.getMsgType() == ID_ERROR_RESP));
+ }
+
+ @Test
+ public void sendRilMessage_whenIAEIsThrown_sendsErrorClientMessage() throws Exception {
+ SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+ Object lock = new Object();
+ ISap mockSapProxy = mock(ISap.class);
+ when(mockReceiver.getSapProxyLock()).thenReturn(lock);
+ when(mockReceiver.getSapProxy()).thenReturn(mockSapProxy);
+ mSapServer.mRilBtReceiver = mockReceiver;
+ mSapServer.mSapHandler = mHandler;
+
+ SapMessage msg = mock(SapMessage.class);
+ doThrow(new IllegalArgumentException()).when(msg).send(any());
+ mSapServer.sendRilMessage(msg);
+
+ verify(mSapServer).sendClientMessage(
+ argThat(sapMsg -> sapMsg.getMsgType() == ID_ERROR_RESP));
+ }
+
+ @Test
+ public void sendRilMessage_whenRemoteExceptionIsThrown_sendsErrorClientMessage()
+ throws Exception {
+ SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+ Object lock = new Object();
+ ISap mockSapProxy = mock(ISap.class);
+ when(mockReceiver.getSapProxyLock()).thenReturn(lock);
+ when(mockReceiver.getSapProxy()).thenReturn(mockSapProxy);
+ mSapServer.mRilBtReceiver = mockReceiver;
+ mSapServer.mSapHandler = mHandler;
+
+ SapMessage msg = mock(SapMessage.class);
+ doThrow(new RemoteException()).when(msg).send(any());
+ mSapServer.sendRilMessage(msg);
+
+ verify(mSapServer).sendClientMessage(
+ argThat(sapMsg -> sapMsg.getMsgType() == ID_ERROR_RESP));
+ verify(mockReceiver).notifyShutdown();
+ verify(mockReceiver).resetSapProxy();
+ }
+
+ @Test
+ public void handleRilInd_whenMessageIsNull() {
+ try {
+ mSapServer.handleRilInd(null);
+ } catch (Exception e) {
+ assertWithMessage("Exception should not happen.").fail();
+ }
+ }
+
+ @Test
+ public void handleRilInd_whenStateIsConnected_callsSendClientMessage() {
+ int disconnectionType = DISC_GRACEFULL;
+ SapMessage msg = mock(SapMessage.class);
+ when(msg.getMsgType()).thenReturn(ID_RIL_UNSOL_DISCONNECT_IND);
+ when(msg.getDisconnectionType()).thenReturn(disconnectionType);
+ mSapServer.mSapHandler = mHandler;
+
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTED);
+ mSapServer.handleRilInd(msg);
+
+ verify(mSapServer).sendClientMessage(argThat(
+ sapMsg -> sapMsg.getMsgType() == ID_DISCONNECT_IND
+ && sapMsg.getDisconnectionType() == disconnectionType));
+ }
+
+ @Test
+ public void handleRilInd_whenStateIsDisconnected_callsSendDisconnectInd() {
+ int disconnectionType = DISC_GRACEFULL;
+ NotificationManager notificationManager = mock(NotificationManager.class);
+ when(mTargetContext.getSystemService(NotificationManager.class))
+ .thenReturn(notificationManager);
+ SapMessage msg = mock(SapMessage.class);
+ when(msg.getMsgType()).thenReturn(ID_RIL_UNSOL_DISCONNECT_IND);
+ when(msg.getDisconnectionType()).thenReturn(disconnectionType);
+ mSapServer.mSapHandler = mHandler;
+
+ mSapServer.changeState(SapServer.SAP_STATE.DISCONNECTED);
+ mSapServer.handleRilInd(msg);
+
+ verify(mSapServer).sendDisconnectInd(disconnectionType);
+ }
+
+ @Test
+ public void handleRfcommReply_whenMessageIsNull() {
+ try {
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTED_BUSY);
+ mSapServer.handleRfcommReply(null);
+ } catch (Exception e) {
+ assertWithMessage("Exception should not happen.").fail();
+ }
+ }
+
+ @Test
+ public void handleRfcommReply_connectRespMsg_whenInCallOngoingState() {
+ SapMessage msg = mock(SapMessage.class);
+ when(msg.getMsgType()).thenReturn(ID_CONNECT_RESP);
+
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTING_CALL_ONGOING);
+ when(msg.getConnectionStatus()).thenReturn(CON_STATUS_OK);
+ mSapServer.handleRfcommReply(msg);
+
+ assertThat(mSapServer.mState).isEqualTo(SapServer.SAP_STATE.CONNECTED);
+ }
+
+ @Test
+ public void handleRfcommReply_connectRespMsg_whenNotInCallOngoingState_okStatus() {
+ SapMessage msg = mock(SapMessage.class);
+ when(msg.getMsgType()).thenReturn(ID_CONNECT_RESP);
+
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTED);
+ when(msg.getConnectionStatus()).thenReturn(CON_STATUS_OK);
+ mSapServer.handleRfcommReply(msg);
+
+ assertThat(mSapServer.mState).isEqualTo(SapServer.SAP_STATE.CONNECTED);
+ }
+
+ @Test
+ public void handleRfcommReply_connectRespMsg_whenNotInCallOngoingState_ongoingCallStatus() {
+ SapMessage msg = mock(SapMessage.class);
+ when(msg.getMsgType()).thenReturn(ID_CONNECT_RESP);
+
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTED);
+ when(msg.getConnectionStatus()).thenReturn(CON_STATUS_OK_ONGOING_CALL);
+ mSapServer.handleRfcommReply(msg);
+
+ assertThat(mSapServer.mState).isEqualTo(SapServer.SAP_STATE.CONNECTING_CALL_ONGOING);
+ }
+
+ @Test
+ public void handleRfcommReply_connectRespMsg_whenNotInCallOngoingState_errorStatus() {
+ AlarmManager alarmManager = mock(AlarmManager.class);
+ when(mTargetContext.getSystemService(AlarmManager.class)).thenReturn(alarmManager);
+ SapMessage msg = mock(SapMessage.class);
+ when(msg.getMsgType()).thenReturn(ID_CONNECT_RESP);
+
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTED);
+ when(msg.getConnectionStatus()).thenReturn(CON_STATUS_ERROR_CONNECTION);
+ mSapServer.handleRfcommReply(msg);
+
+ verify(mSapServer).startDisconnectTimer(anyInt(), anyInt());
+ }
+
+ @Test
+ public void handleRfcommReply_disconnectRespMsg_whenInDisconnectingState() {
+ SapMessage msg = mock(SapMessage.class);
+ when(msg.getMsgType()).thenReturn(ID_DISCONNECT_RESP);
+
+ mSapServer.changeState(SapServer.SAP_STATE.DISCONNECTING);
+ mSapServer.handleRfcommReply(msg);
+
+ assertThat(mSapServer.mState).isEqualTo(SapServer.SAP_STATE.DISCONNECTED);
+ }
+
+ @Test
+ public void handleRfcommReply_disconnectRespMsg_whenInConnectedState_shutDown() {
+ SapMessage msg = mock(SapMessage.class);
+ when(msg.getMsgType()).thenReturn(ID_DISCONNECT_RESP);
+
+ mSapServer.mIsLocalInitDisconnect = true;
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTED);
+ mSapServer.handleRfcommReply(msg);
+
+ verify(mSapServer).shutdown();
+ }
+
+ @Test
+ public void handleRfcommReply_disconnectRespMsg_whenInConnectedState_startsDisconnectTimer() {
+ AlarmManager alarmManager = mock(AlarmManager.class);
+ when(mTargetContext.getSystemService(AlarmManager.class)).thenReturn(alarmManager);
+ SapMessage msg = mock(SapMessage.class);
+ when(msg.getMsgType()).thenReturn(ID_DISCONNECT_RESP);
+
+ mSapServer.mIsLocalInitDisconnect = false;
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTED);
+ mSapServer.handleRfcommReply(msg);
+
+ verify(mSapServer).startDisconnectTimer(anyInt(), anyInt());
+ }
+
+ @Test
+ public void handleRfcommReply_statusIndMsg_whenInDisonnectingState_doesNotSendMessage()
+ throws Exception {
+ SapMessage msg = mock(SapMessage.class);
+ when(msg.getMsgType()).thenReturn(ID_STATUS_IND);
+
+ mSapServer.changeState(SapServer.SAP_STATE.DISCONNECTING);
+ mSapServer.handleRfcommReply(msg);
+
+ verify(msg, never()).send(any());
+ }
+
+ @Test
+ public void handleRfcommReply_statusIndMsg_whenInConnectedState_setsNotification() {
+ NotificationManager notificationManager = mock(NotificationManager.class);
+ when(mTargetContext.getSystemService(NotificationManager.class))
+ .thenReturn(notificationManager);
+ SapMessage msg = mock(SapMessage.class);
+ when(msg.getMsgType()).thenReturn(ID_STATUS_IND);
+
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTED);
+ mSapServer.handleRfcommReply(msg);
+
+ verify(notificationManager).notify(eq(SapServer.NOTIFICATION_ID), any());
+ }
+
+ @Test
+ public void startDisconnectTimer_and_stopDisconnectTimer() {
+ AlarmManager alarmManager = mock(AlarmManager.class);
+ when(mTargetContext.getSystemService(AlarmManager.class)).thenReturn(alarmManager);
+
+ mSapServer.startDisconnectTimer(SapMessage.DISC_FORCED, 1_000);
+ verify(alarmManager).set(anyInt(), anyLong(), any(PendingIntent.class));
+
+ mSapServer.stopDisconnectTimer();
+ verify(alarmManager).cancel(any(PendingIntent.class));
+ }
+
+ @Test
+ public void isCallOngoing() {
+ TelephonyManager telephonyManager = mock(TelephonyManager.class);
+ when(mTargetContext.getSystemService(TelephonyManager.class)).thenReturn(telephonyManager);
+
+ when(telephonyManager.getCallState()).thenReturn(TelephonyManager.CALL_STATE_OFFHOOK);
+ assertThat(mSapServer.isCallOngoing()).isTrue();
+
+ when(telephonyManager.getCallState()).thenReturn(TelephonyManager.CALL_STATE_IDLE);
+ assertThat(mSapServer.isCallOngoing()).isFalse();
+ }
+
+ @Test
+ public void sendRilThreadMessage() {
+ mSapServer.mSapHandler = mHandler;
+
+ SapMessage msg = new SapMessage(ID_STATUS_IND);
+ mSapServer.sendRilThreadMessage(msg);
+
+ verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RIL_REQ), argThat(
+ new ArgumentMatcher<Object>() {
+ @Override
+ public boolean matches(Object arg) {
+ return msg == arg;
+ }
+ }
+ ));
+ }
+
+ @Test
+ public void sendClientMessage() {
+ mSapServer.mSapHandler = mHandler;
+
+ SapMessage msg = new SapMessage(ID_STATUS_IND);
+ mSapServer.sendClientMessage(msg);
+
+ verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
+ new ArgumentMatcher<Object>() {
+ @Override
+ public boolean matches(Object arg) {
+ return msg == arg;
+ }
+ }
+ ));
+ }
+
+ // TODO: Find a good way to run() method.
+
+ @Test
+ public void clearPendingRilResponses_whenInConnectedBusyState_setsClearRilQueueAsTrue() {
+ SapMessage msg = mock(SapMessage.class);
+
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTED_BUSY);
+ mSapServer.clearPendingRilResponses(msg);
+
+ verify(msg).setClearRilQueue(true);
+ }
+
+ @Test
+ public void handleMessage_forRfcReplyMsg_callsHandleRfcommReply() {
+ SapMessage sapMsg = mock(SapMessage.class);
+ when(sapMsg.getMsgType()).thenReturn(ID_CONNECT_RESP);
+ when(sapMsg.getConnectionStatus()).thenReturn(CON_STATUS_OK);
+ mSapServer.changeState(SapServer.SAP_STATE.DISCONNECTED);
+
+ Message message = Message.obtain();
+ message.what = SAP_MSG_RFC_REPLY;
+ message.obj = sapMsg;
+
+ try {
+ mSapServer.handleMessage(message);
+
+ verify(mSapServer).handleRfcommReply(sapMsg);
+ } finally {
+ message.recycle();
+ }
+ }
+
+ @Test
+ public void handleMessage_forRilConnectMsg_callsSendRilMessage() throws Exception {
+ SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+ Object lock = new Object();
+ when(mockReceiver.getSapProxyLock()).thenReturn(lock);
+ mSapServer.mRilBtReceiver = mockReceiver;
+ mSapServer.mSapHandler = mHandler;
+ mSapServer.setTestMode(TEST_MODE_ENABLE);
+
+ Message message = Message.obtain();
+ message.what = SAP_MSG_RIL_CONNECT;
+
+ try {
+ mSapServer.handleMessage(message);
+
+ verify(mSapServer).sendRilMessage(
+ argThat(sapMsg -> sapMsg.getMsgType() == ID_CONNECT_REQ));
+ } finally {
+ message.recycle();
+ }
+ }
+
+ @Test
+ public void handleMessage_forRilReqMsg_callsSendRilMessage() throws Exception {
+ SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+ ISap mockSapProxy = mock(ISap.class);
+ Object lock = new Object();
+ when(mockReceiver.getSapProxyLock()).thenReturn(lock);
+ when(mockReceiver.getSapProxy()).thenReturn(mockSapProxy);
+ mSapServer.mRilBtReceiver = mockReceiver;
+ mSapServer.mSapHandler = mHandler;
+
+ SapMessage sapMsg = mock(SapMessage.class);
+ when(sapMsg.getMsgType()).thenReturn(ID_CONNECT_REQ);
+
+ Message message = Message.obtain();
+ message.what = SAP_MSG_RIL_REQ;
+ message.obj = sapMsg;
+
+ try {
+ mSapServer.handleMessage(message);
+
+ verify(mSapServer).sendRilMessage(sapMsg);
+ } finally {
+ message.recycle();
+ }
+ }
+
+ @Test
+ public void handleMessage_forRilIndMsg_callsHandleRilInd() throws Exception {
+ SapMessage sapMsg = mock(SapMessage.class);
+ when(sapMsg.getMsgType()).thenReturn(ID_RIL_UNSOL_DISCONNECT_IND);
+ when(sapMsg.getDisconnectionType()).thenReturn(DISC_GRACEFULL);
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTED);
+ mSapServer.mSapHandler = mHandler;
+
+ Message message = Message.obtain();
+ message.what = SAP_MSG_RIL_IND;
+ message.obj = sapMsg;
+
+ try {
+ mSapServer.handleMessage(message);
+
+ verify(mSapServer).handleRilInd(sapMsg);
+ } finally {
+ message.recycle();
+ }
+ }
+
+ @Test
+ public void handleMessage_forRilSocketClosedMsg_startsDisconnectTimer() throws Exception {
+ AlarmManager alarmManager = mock(AlarmManager.class);
+ when(mTargetContext.getSystemService(AlarmManager.class)).thenReturn(alarmManager);
+
+ Message message = Message.obtain();
+ message.what = SAP_RIL_SOCK_CLOSED;
+
+ try {
+ mSapServer.handleMessage(message);
+
+ verify(mSapServer).startDisconnectTimer(anyInt(), anyInt());
+ } finally {
+ message.recycle();
+ }
+ }
+
+ @Test
+ public void handleMessage_forProxyDeadMsg_notifiesShutDown() throws Exception {
+ SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+ AtomicLong cookie = new AtomicLong(23);
+ when(mockReceiver.getSapProxyCookie()).thenReturn(cookie);
+ mSapServer.mRilBtReceiver = mockReceiver;
+
+ Message message = Message.obtain();
+ message.what = SAP_PROXY_DEAD;
+ message.obj = cookie.get();
+
+ try {
+ mSapServer.handleMessage(message);
+
+ verify(mockReceiver).notifyShutdown();
+ verify(mockReceiver).resetSapProxy();
+ } finally {
+ message.recycle();
+ }
+ }
+
+ @Test
+ public void onReceive_phoneStateChangedAction_whenStateIsCallOngoing_callsOnConnectRequest() {
+ mSapServer.mIntentReceiver = mSapServer.new SapServerBroadcastReceiver();
+ mSapServer.mSapHandler = mHandler;
+ Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
+ intent.putExtra(TelephonyManager.EXTRA_STATE, TelephonyManager.EXTRA_STATE_IDLE);
+
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTING_CALL_ONGOING);
+ assertThat(mSapServer.mState).isEqualTo(SapServer.SAP_STATE.CONNECTING_CALL_ONGOING);
+ mSapServer.mIntentReceiver.onReceive(mTargetContext, intent);
+
+ verify(mSapServer).onConnectRequest(
+ argThat(sapMsg -> sapMsg.getMsgType() == ID_CONNECT_REQ));
+ }
+
+ @Test
+ public void onReceive_SapDisconnectedAction_forDiscRfcommType_callsShutDown() {
+ mSapServer.mIntentReceiver = mSapServer.new SapServerBroadcastReceiver();
+
+ int disconnectType = SapMessage.DISC_RFCOMM;
+ Intent intent = new Intent(SapServer.SAP_DISCONNECT_ACTION);
+ intent.putExtra(SapServer.SAP_DISCONNECT_TYPE_EXTRA, disconnectType);
+ mSapServer.mIntentReceiver.onReceive(mTargetContext, intent);
+
+ verify(mSapServer).shutdown();
+ }
+
+ @Test
+ public void onReceive_SapDisconnectedAction_forNonDiscRfcommType_callsSendDisconnectInd() {
+ mSapServer.mIntentReceiver = mSapServer.new SapServerBroadcastReceiver();
+ mSapServer.mSapHandler = mHandler;
+
+ int disconnectType = SapMessage.DISC_GRACEFULL;
+ Intent intent = new Intent(SapServer.SAP_DISCONNECT_ACTION);
+ intent.putExtra(SapServer.SAP_DISCONNECT_TYPE_EXTRA, disconnectType);
+ mSapServer.changeState(SapServer.SAP_STATE.CONNECTED);
+ mSapServer.mIntentReceiver.onReceive(mTargetContext, intent);
+
+ verify(mSapServer).sendDisconnectInd(disconnectType);
+ }
+
+ @Test
+ public void onReceive_unknownAction_doesNothing() {
+ Intent intent = new Intent("random intent action");
+
+ try {
+ mSapServer.mIntentReceiver.onReceive(mTargetContext, intent);
+ } catch (Exception e) {
+ assertWithMessage("Exception should not happen.").fail();
+ }
+ }
+
+ public static class TestHandlerCallback implements Handler.Callback {
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ receiveMessage(msg.what, msg.obj);
+ return true;
+ }
+
+ public void receiveMessage(int what, Object obj) {}
+ }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java
index ae817b5..8282b82 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java
@@ -107,6 +107,8 @@
Looper.prepare();
}
+ getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
+
MockitoAnnotations.initMocks(this);
TestUtils.setAdapterService(mAdapterService);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java
index 8454136..ac0cc79 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java
@@ -82,6 +82,8 @@
mAdapter = BluetoothAdapter.getDefaultAdapter();
mContext = getInstrumentation().getTargetContext();
+ getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
+
// Default TbsGatt mock behavior
doReturn(true).when(mTbsGatt).init(mGtbsCcidCaptor.capture(), mGtbsUciCaptor.capture(),
mDefaultGtbsUriSchemesCaptor.capture(), anyBoolean(), anyBoolean(),
diff --git a/android/app/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java
index c187a77..e74b36a 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/telephony/BluetoothInCallServiceTest.java
@@ -39,6 +39,7 @@
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
+import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ServiceTestRule;
import androidx.test.runner.AndroidJUnit4;
@@ -120,6 +121,7 @@
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
// Create the service Intent.
Intent serviceIntent =
diff --git a/android/app/tests/unit/src/com/android/bluetooth/util/GsmAlphabetTest.java b/android/app/tests/unit/src/com/android/bluetooth/util/GsmAlphabetTest.java
index cb4fd61..8d271e8 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/util/GsmAlphabetTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/util/GsmAlphabetTest.java
@@ -18,8 +18,11 @@
import static com.google.common.truth.Truth.assertThat;
+import androidx.test.InstrumentationRegistry;
+
import com.android.internal.telephony.uicc.IccUtils;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -29,6 +32,11 @@
private static final String GSM_EXTENDED_CHARS = "{|}\\[~]\f\u20ac";
+ @Before
+ public void setUp() throws Exception {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
+ }
+
@Test
public void gsm7BitPackedToString() throws Exception {
byte[] packed;
diff --git a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlStateMachineTest.java
index b56982f..2082abd 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlStateMachineTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlStateMachineTest.java
@@ -68,6 +68,7 @@
@Before
public void setUp() throws Exception {
mTargetContext = InstrumentationRegistry.getTargetContext();
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
// Set up mocks and test assets
MockitoAnnotations.initMocks(this);
TestUtils.setAdapterService(mAdapterService);
diff --git a/android/pandora/avatar_experimental/Android.bp b/android/pandora/avatar_experimental/Android.bp
index 61e7b07..721e764 100644
--- a/android/pandora/avatar_experimental/Android.bp
+++ b/android/pandora/avatar_experimental/Android.bp
@@ -24,7 +24,6 @@
"avatar/controllers/*.py",
],
libs: [
- "mobly",
"pandora_experimental-python",
"libprotobuf-python",
"bumble",
diff --git a/android/pandora/mmi2grpc/mmi2grpc/avrcp.py b/android/pandora/mmi2grpc/mmi2grpc/avrcp.py
index 6d1ae39..c63a58a 100644
--- a/android/pandora/mmi2grpc/mmi2grpc/avrcp.py
+++ b/android/pandora/mmi2grpc/mmi2grpc/avrcp.py
@@ -26,6 +26,7 @@
from pandora_experimental.avrcp_grpc import AVRCP
from pandora_experimental.host_grpc import Host
from pandora_experimental.host_pb2 import Connection
+from pandora_experimental.mediaplayer_grpc import MediaPlayer
class AVRCPProxy(ProfileProxy):
@@ -44,6 +45,7 @@
self.host = Host(channel)
self.a2dp = A2DP(channel)
self.avrcp = AVRCP(channel)
+ self.mediaplayer = MediaPlayer(channel)
@assert_description
def TSC_AVDTP_mmi_iut_accept_connect(self, test: str, pts_addr: bytes, **kwargs):
@@ -59,15 +61,13 @@
the IUT connects to PTS to establish pairing.
"""
+ self.connection = self.host.WaitConnection(address=pts_addr).connection
if ("TG" in test and "TG/VLH" not in test) or "CT/VLH" in test:
-
- self.connection = self.host.WaitConnection(address=pts_addr).connection
try:
self.source = self.a2dp.WaitSource(connection=self.connection).source
except RpcError:
pass
else:
- self.connection = self.host.WaitConnection(address=pts_addr).connection
try:
self.sink = self.a2dp.WaitSink(connection=self.connection).sink
except RpcError:
@@ -149,8 +149,10 @@
Take action to disconnect all A2DP and/or AVRCP connections.
"""
- self.a2dp.Close(source=self.source)
- self.source = None
+ if self.connection is None:
+ self.connection = self.host.GetConnection(address=pts_addr).connection
+ self.host.Disconnect(connection=self.connection)
+
return "OK"
@assert_description
@@ -221,6 +223,7 @@
Tester and the L2CAP_ConnectReq from the Lower Tester, the IUT issues an
L2CAP_ConnectRsp to the Lower Tester.
"""
+
return "OK"
@assert_description
@@ -362,7 +365,7 @@
return "OK"
@assert_description
- def TSC_AVDTP_mmi_iut_initiate_connect(self, pts_addr: bytes, **kwargs):
+ def TSC_AVDTP_mmi_iut_initiate_connect(self, test: str, pts_addr: bytes, **kwargs):
"""
Create an AVDTP signaling channel.
@@ -370,7 +373,9 @@
connection with PTS.
"""
self.connection = self.host.Connect(address=pts_addr).connection
- self.source = self.a2dp.OpenSource(connection=self.connection).source
+ if ("TG" in test and "TG/VLH" not in test) or "CT/VLH" in test:
+ self.source = self.a2dp.OpenSource(connection=self.connection).source
+
return "OK"
@assert_description
@@ -541,4 +546,201 @@
continue once the IUT has responded.
"""
- return "OK"
\ No newline at end of file
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_accept_set_browsed_player(self, **kwargs):
+ """
+ Take action to send a valid response to the [Set Browsed Player] command
+ sent by the PTS.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_accept_get_folder_items_virtual_file_system(self, **kwargs):
+ """
+ Take action to send a valid response to the [Get Folder Items] with the
+ scope <Virtual File System> command sent by the PTS.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_user_confirm_virtual_file_system(self, **kwargs):
+ """
+ Are the following items found in the current folder?
+
+ Folder:
+ com.android.pandora
+
+
+ Note: Some media elements and folders may not be
+ listed above.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_accept_change_path_down(self, **kwargs):
+ """
+ Take action to send a valid response to the [Change Path] <Down> command
+ sent by the PTS.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_accept_change_path_up(self, **kwargs):
+ """
+ Take action to send a valid response to the [Change Path] <Up> command
+ sent by the PTS.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_user_action_track_playing(self, **kwargs):
+ """
+ Place the IUT into a state where a track is currently playing, then
+ press 'OK' to continue.
+ """
+ self.mediaplayer.Play()
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_accept_get_item_attributes(self, **kwargs):
+ """
+ Take action to send a valid response to the [Get Item Attributes]
+ command sent by the PTS.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_reject_set_addressed_player_invalid_player_id(self, **kwargs):
+ """
+ PTS has sent a Set Addressed Player command with an invalid Player Id.
+ The IUT must respond with the error code: Invalid Player Id (0x11).
+ Description: Verify that the IUT can properly reject a Set Addressed
+ Player command that contains an invalid player id.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_reject_get_folder_items_out_of_range(self, **kwargs):
+ """
+ PTS has sent a Get Folder Items command with invalid values for Start
+ and End. The IUT must respond with the error code: Range Out Of Bounds
+ (0x0B).
+
+ Description: Verify that the IUT can properly reject a Get
+ Folder Items command that contains an invalid start and end index.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_reject_change_path_down_invalid_uid(self, **kwargs):
+ """
+ PTS has sent a Change Path Down command with an invalid folder UID. The
+ IUT must respond with the error code: Does Not Exist (0x09).
+ Description: Verify that the IUT can properly reject an Change Path Down
+ command that contains an invalid UID.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_reject_get_item_attributes_invalid_uid_counter(self, **kwargs):
+ """
+ PTS has sent a Get Item Attributes command with an invalid UID Counter.
+ The IUT must respond with the error code: UID Changed (0x05).
+ Description: Verify that the IUT can properly reject a Get Item
+ Attributes command that contains an invalid UID Counter.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_accept_play_item(self, **kwargs):
+ """
+ Take action to send a valid response to the [Play Item] command sent by
+ the PTS.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_reject_play_item_invalid_uid(self, **kwargs):
+ """
+ PTS has sent a Play Item command with an invalid UID. The IUT must
+ respond with the error code: Does Not Exist (0x09).
+
+ Description: Verify
+ that the IUT can properly reject a Play Item command that contains an
+ invalid UID.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_initiate_register_notification_changed_track_changed(self, **kwargs):
+ """
+ Take action to trigger a [Register Notification, Changed] response for
+ <Track Changed> to the PTS from the IUT. This can be accomplished by
+ changing the currently playing track on the IUT.
+
+ Description: Verify
+ that the Implementation Under Test (IUT) can update database by sending
+ a valid Track Changed Notification to the PTS.
+ """
+
+ self.mediaplayer.Play()
+ self.mediaplayer.Forward()
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_user_action_change_track(self, **kwargs):
+ """
+ Take action to change the currently playing track.
+ """
+ self.mediaplayer.Forward()
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_reject_set_browsed_player_invalid_player_id(self, **kwargs):
+ """
+ PTS has sent a Set Browsed Player command with an invalid Player Id.
+ The IUT must respond with the error code: Invalid Player Id (0x11).
+ Description: Verify that the IUT can properly reject a Set Browsed
+ Player command that contains an invalid player id.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_reject_register_notification_notify_invalid_event_id(self, **kwargs):
+ """
+ PTS has sent a Register Notification command with an invalid Event Id.
+ The IUT must respond with the error code: Invalid Parameter (0x01).
+ Description: Verify that the IUT can properly reject a Register
+ Notification command that contains an invalid event Id.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_user_action_play_large_metadata_media(self, **kwargs):
+ """
+ Start playing a media item with more than 512 bytes worth of metadata,
+ then press 'OK'.
+ """
+
+ self.mediaplayer.SetLargeMetadata()
+
+ return "OK"
diff --git a/android/pandora/mmi2grpc/mmi2grpc/hfp.py b/android/pandora/mmi2grpc/mmi2grpc/hfp.py
index a34e48f..7179f0a 100644
--- a/android/pandora/mmi2grpc/mmi2grpc/hfp.py
+++ b/android/pandora/mmi2grpc/mmi2grpc/hfp.py
@@ -18,17 +18,17 @@
from pandora_experimental.hfp_grpc import HFP
from pandora_experimental.host_grpc import Host
+from pandora_experimental.security_grpc import Security
import sys
import threading
+import time
# Standard time to wait before asking for waitConnection
WAIT_DELAY_BEFORE_CONNECTION = 2
# The tests needs the MMI to accept pairing confirmation request.
-NEEDS_WAIT_CONNECTION_BEFORE_TEST = {
- 'HFP/AG/WBS/BV-01-I',
-}
+NEEDS_WAIT_CONNECTION_BEFORE_TEST = {'HFP/AG/WBS/BV-01-I', 'HFP/AG/SLC/BV-05-I'}
class HFPProxy(ProfileProxy):
@@ -37,6 +37,7 @@
super().__init__(channel)
self.hfp = HFP(channel)
self.host = Host(channel)
+ self.security = Security(channel)
self.connection = None
@@ -65,7 +66,7 @@
(IUT), then click Ok.
"""
- self.host.DeletePairing(address=pts_addr)
+ self.security.DeletePairing(address=pts_addr)
return "OK"
@assert_description
@@ -100,13 +101,29 @@
return "OK"
@assert_description
+ def TSC_iut_connectable(self, pts_addr: str, test: str, **kwargs):
+ """
+ Make the Implementation Under Test (IUT) connectable, then click Ok.
+ """
+
+ if "HFP/AG/SLC/BV-03-C" in test:
+ self.connection = self.host.WaitConnection(pts_addr).connection
+
+ return "OK"
+
+ @assert_description
def TSC_iut_disable_slc(self, pts_addr: bytes, **kwargs):
"""
Click Ok, then disable the service level connection using the
Implementation Under Test (IUT).
"""
- self.hfp.DisableSlc(connection=self.connection)
+ def go():
+ time.sleep(2)
+ self.hfp.DisableSlc(connection=self.connection)
+
+ threading.Thread(target=go).start()
+
return "OK"
@assert_description
diff --git a/android/pandora/server/AndroidManifest.xml b/android/pandora/server/AndroidManifest.xml
index a8c5743..089a2a5 100644
--- a/android/pandora/server/AndroidManifest.xml
+++ b/android/pandora/server/AndroidManifest.xml
@@ -20,6 +20,13 @@
<application>
<uses-library android:name="android.test.runner" />
+
+ <service android:name=".MediaPlayerBrowserService"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.media.browse.MediaBrowserService"/>
+ </intent-filter>
+ </service>
</application>
<uses-permission android:name="android.permission.INTERNET" />
diff --git a/android/pandora/server/configs/PtsBotTest.xml b/android/pandora/server/configs/PtsBotTest.xml
index 8af3995..600ddc9 100644
--- a/android/pandora/server/configs/PtsBotTest.xml
+++ b/android/pandora/server/configs/PtsBotTest.xml
@@ -37,7 +37,7 @@
<option name="profile" value="AVRCP" />
<option name="profile" value="GAP" />
<option name="profile" value="GATT" />
- <option name="profile" value="HFP/AG/PSI" />
+ <option name="profile" value="HFP/AG" />
<option name="profile" value="HID/HOS" />
<option name="profile" value="HOGP" />
<option name="profile" value="L2CAP/LE" />
diff --git a/android/pandora/server/configs/PtsBotTestMts.xml b/android/pandora/server/configs/PtsBotTestMts.xml
index 193bd34..df3ad53 100644
--- a/android/pandora/server/configs/PtsBotTestMts.xml
+++ b/android/pandora/server/configs/PtsBotTestMts.xml
@@ -37,7 +37,7 @@
<option name="profile" value="AVRCP" />
<option name="profile" value="GAP" />
<option name="profile" value="GATT" />
- <option name="profile" value="HFP/AG/PSI" />
+ <option name="profile" value="HFP/AG" />
<option name="profile" value="HID/HOS" />
<option name="profile" value="HOGP" />
<option name="profile" value="L2CAP/LE" />
diff --git a/android/pandora/server/configs/pts_bot_tests_config.json b/android/pandora/server/configs/pts_bot_tests_config.json
index 0b39a04..dcdc90c 100644
--- a/android/pandora/server/configs/pts_bot_tests_config.json
+++ b/android/pandora/server/configs/pts_bot_tests_config.json
@@ -68,6 +68,7 @@
"AVDTP/SRC/ACP/SIG/SMG/BI-26-C",
"AVDTP/SRC/ACP/SIG/SMG/BI-33-C",
"AVDTP/SRC/ACP/SIG/SMG/BV-06-C",
+ "AVDTP/SRC/ACP/SIG/SMG/BV-08-C",
"AVDTP/SRC/ACP/SIG/SMG/BV-10-C",
"AVDTP/SRC/ACP/SIG/SMG/BV-12-C",
"AVDTP/SRC/ACP/SIG/SMG/BV-16-C",
@@ -91,6 +92,7 @@
"AVDTP/SRC/INT/SIG/SMG/BV-25-C",
"AVDTP/SRC/INT/SIG/SMG/BV-28-C",
"AVDTP/SRC/INT/SIG/SMG/BV-31-C",
+ "AVDTP/SRC/INT/SIG/SMG/BI-35-C",
"AVDTP/SRC/INT/SIG/SYN/BV-05-C",
"AVDTP/SRC/INT/TRA/BTR/BV-01-C",
"AVRCP/CT/CEC/BV-02-I",
@@ -105,15 +107,36 @@
"AVRCP/TG/ICC/BV-02-I",
"AVRCP/TG/INV/BI-01-C",
"AVRCP/TG/INV/BI-02-C",
+ "AVRCP/TG/MCN/CB/BI-01-C",
+ "AVRCP/TG/MCN/CB/BI-03-C",
+ "AVRCP/TG/MCN/CB/BI-04-C",
+ "AVRCP/TG/MCN/CB/BI-05-C",
+ "AVRCP/TG/MCN/CB/BV-01-I",
+ "AVRCP/TG/MCN/CB/BV-02-C",
+ "AVRCP/TG/MCN/CB/BV-02-I",
+ "AVRCP/TG/MCN/CB/BV-03-I",
+ "AVRCP/TG/MCN/CB/BV-05-C",
+ "AVRCP/TG/MCN/CB/BV-06-C",
+ "AVRCP/TG/MCN/CB/BV-06-I",
+ "AVRCP/TG/MCN/CB/BV-08-C",
"AVRCP/TG/MDI/BV-02-C",
"AVRCP/TG/MDI/BV-04-C",
"AVRCP/TG/MDI/BV-05-C",
+ "AVRCP/TG/MPS/BI-01-C",
+ "AVRCP/TG/MPS/BI-02-C",
"AVRCP/TG/MPS/BV-01-I",
"AVRCP/TG/MPS/BV-02-C",
"AVRCP/TG/MPS/BV-02-I",
"AVRCP/TG/MPS/BV-03-I",
+ "AVRCP/TG/MPS/BV-04-C",
+ "AVRCP/TG/MPS/BV-06-C",
+ "AVRCP/TG/MPS/BV-09-C",
+ "AVRCP/TG/NFY/BI-01-C",
+ "AVRCP/TG/NFY/BV-02-C",
"AVRCP/TG/PTT/BV-01-I",
"AVRCP/TG/PTT/BV-05-I",
+ "AVRCP/TG/RCR/BV-02-C",
+ "AVRCP/TG/RCR/BV-04-C",
"GAP/ADV/BV-01-C",
"GAP/ADV/BV-02-C",
"GAP/ADV/BV-03-C",
@@ -212,7 +235,13 @@
"GATT/SR/GAW/BI-12-C",
"GATT/SR/UNS/BI-01-C",
"GATT/SR/UNS/BI-02-C",
- "HFP/AG/PSI/BV-05-I",
+ "HFP/AG/DIS/BV-01-I",
+ "HFP/AG/HFI/BV-02-I",
+ "HFP/AG/PSI/BV-03-C",
+ "HFP/AG/SLC/BV-09-I",
+ "HFP/AG/SLC/BV-10-I",
+ "HFP/AG/TCA/BV-04-I",
+ "HFP/AG/WBS/BV-01-I",
"HID/HOS/DAT/BV-01-C",
"HID/HOS/HCE/BV-01-I",
"HID/HOS/HCE/BV-03-I",
@@ -381,10 +410,8 @@
"AVDTP/SNK/INT/SIG/SYN/BV-04-C",
"AVDTP/SRC/ACP/SIG/SMG/BI-11-C",
"AVDTP/SRC/ACP/SIG/SMG/BI-23-C",
- "AVDTP/SRC/ACP/SIG/SMG/BV-08-C",
"AVDTP/SRC/ACP/SIG/SMG/BV-14-C",
"AVDTP/SRC/ACP/SIG/SMG/ESR05/BV-14-C",
- "AVDTP/SRC/INT/SIG/SMG/BI-35-C",
"AVDTP/SRC/INT/SIG/SMG/BV-11-C",
"AVDTP/SRC/INT/SIG/SMG/BV-13-C",
"AVDTP/SRC/INT/SIG/SMG/BV-23-C",
@@ -393,20 +420,8 @@
"AVRCP/CT/CRC/BV-01-I",
"AVRCP/CT/PTH/BV-01-C",
"AVRCP/CT/PTT/BV-01-I",
- "AVRCP/TG/MCN/CB/BI-01-C",
"AVRCP/TG/MCN/CB/BI-02-C",
- "AVRCP/TG/MCN/CB/BI-03-C",
- "AVRCP/TG/MCN/CB/BI-04-C",
- "AVRCP/TG/MCN/CB/BI-05-C",
- "AVRCP/TG/MCN/CB/BV-01-I",
- "AVRCP/TG/MCN/CB/BV-02-C",
- "AVRCP/TG/MCN/CB/BV-02-I",
- "AVRCP/TG/MCN/CB/BV-03-I",
"AVRCP/TG/MCN/CB/BV-04-I",
- "AVRCP/TG/MCN/CB/BV-05-C",
- "AVRCP/TG/MCN/CB/BV-06-C",
- "AVRCP/TG/MCN/CB/BV-06-I",
- "AVRCP/TG/MCN/CB/BV-08-C",
"AVRCP/TG/MCN/CB/BV-09-C",
"AVRCP/TG/MCN/NP/BI-01-C",
"AVRCP/TG/MCN/NP/BV-01-I",
@@ -417,18 +432,9 @@
"AVRCP/TG/MCN/NP/BV-06-I",
"AVRCP/TG/MCN/NP/BV-07-C",
"AVRCP/TG/MCN/NP/BV-09-C",
- "AVRCP/TG/MPS/BI-01-C",
- "AVRCP/TG/MPS/BI-02-C",
- "AVRCP/TG/MPS/BV-04-C",
- "AVRCP/TG/MPS/BV-06-C",
- "AVRCP/TG/MPS/BV-09-C",
- "AVRCP/TG/NFY/BI-01-C",
- "AVRCP/TG/NFY/BV-02-C",
"AVRCP/TG/NFY/BV-04-C",
"AVRCP/TG/NFY/BV-06-C",
"AVRCP/TG/NFY/BV-07-C",
- "AVRCP/TG/RCR/BV-02-C",
- "AVRCP/TG/RCR/BV-04-C",
"GAP/BOND/BON/BV-01-C",
"GAP/BOND/BON/BV-02-C",
"GAP/BOND/BON/BV-03-C",
@@ -554,13 +560,43 @@
"GATT/SR/GAW/BV-09-C",
"GATT/SR/GAW/BV-10-C",
"GATT/SR/GAW/BV-11-C",
- "HFP/AG/DIS/BV-01-I",
- "HFP/AG/HFI/BI-03-I",
- "HFP/AG/HFI/BV-02-I",
+ "HFP/AG/TRS/BV-01-C",
"HFP/AG/PSI/BV-01-C",
- "HFP/AG/PSI/BV-02-C",
"HFP/AG/PSI/BV-04-I",
- "HFP/AG/PSI/BV-05-I",
+ "HFP/AG/ACS/BV-04-I",
+ "HFP/AG/ACS/BV-08-I",
+ "HFP/AG/ACS/BV-11-I",
+ "HFP/AG/ACS/BI-14-I",
+ "HFP/AG/ACS/BV-16-I",
+ "HFP/AG/ACR/BV-01-I",
+ "HFP/AG/ACR/BV-02-I",
+ "HFP/AG/CLI/BV-01-I",
+ "HFP/AG/ICA/BV-04-I",
+ "HFP/AG/ICA/BV-07-I",
+ "HFP/AG/ICA/BV-08-I",
+ "HFP/AG/ICA/BV-09-I",
+ "HFP/AG/TCA/BV-01-I",
+ "HFP/AG/TCA/BV-02-I",
+ "HFP/AG/TCA/BV-03-I",
+ "HFP/AG/TCA/BV-05-I",
+ "HFP/AG/ATH/BV-03-I",
+ "HFP/AG/ATH/BV-04-I",
+ "HFP/AG/ATH/BV-06-I",
+ "HFP/AG/ATA/BV-01-I",
+ "HFP/AG/ATA/BV-02-I",
+ "HFP/AG/OCN/BV-01-I",
+ "HFP/AG/OCM/BV-01-I",
+ "HFP/AG/OCM/BV-02-I",
+ "HFP/AG/OCL/BV-01-I",
+ "HFP/AG/OCL/BV-02-I",
+ "HFP/AG/TWC/BV-02-I",
+ "HFP/AG/TWC/BV-03-I",
+ "HFP/AG/TWC/BV-05-I",
+ "HFP/AG/ENO/BV-01-I",
+ "HFP/AG/VRA/BV-01-I",
+ "HFP/AG/TDC/BV-01-I",
+ "HFP/AG/ECS/BV-03-I",
+ "HFP/AG/NUM/BV-01-I",
"HFP/AG/SLC/BV-01-C",
"HFP/AG/SLC/BV-02-C",
"HFP/AG/SLC/BV-03-C",
@@ -568,13 +604,23 @@
"HFP/AG/SLC/BV-05-I",
"HFP/AG/SLC/BV-06-I",
"HFP/AG/SLC/BV-07-I",
- "HFP/AG/SLC/BV-09-I",
- "HFP/AG/SLC/BV-10-I",
- "HFP/AG/TCA/BV-01-I",
- "HFP/AG/TCA/BV-02-I",
- "HFP/AG/TCA/BV-03-I",
- "HFP/AG/TCA/BV-04-I",
- "HFP/AG/TCA/BV-05-I",
+ "HFP/AG/ACC/BV-08-I",
+ "HFP/AG/ACC/BV-09-I",
+ "HFP/AG/ACC/BV-10-I",
+ "HFP/AG/ACC/BV-11-I",
+ "HFP/AG/ACC/BI-12-I",
+ "HFP/AG/ACC/BI-13-I",
+ "HFP/AG/ACC/BI-14-I",
+ "HFP/AG/ACC/BV-15-I",
+ "HFP/AG/SDP/BV-01-I",
+ "HFP/AG/IIA/BV-01-I",
+ "HFP/AG/IIA/BV-02-I",
+ "HFP/AG/IID/BV-01-I",
+ "HFP/AG/IID/BV-03-I",
+ "HFP/AG/IIC/BV-01-I",
+ "HFP/AG/IIC/BV-02-I",
+ "HFP/AG/IIC/BV-03-I",
+ "HFP/AG/HFI/BI-03-I",
"HID/HOS/HCR/BV-01-I",
"L2CAP/LE/CFC/BV-07-C",
"L2CAP/LE/CFC/BV-11-C",
@@ -1600,7 +1646,9 @@
"ATT": {},
"AVCTP": {},
"AVDTP": {},
- "AVRCP": {},
+ "AVRCP": {
+ "TSPX_player_feature_bitmask": "0000000000B7010C0A00000000000000"
+ },
"BNEP": {},
"DID": {},
"GAP": {},
diff --git a/android/pandora/server/src/com/android/pandora/A2dp.kt b/android/pandora/server/src/com/android/pandora/A2dp.kt
index b2a082f..72e848f 100644
--- a/android/pandora/server/src/com/android/pandora/A2dp.kt
+++ b/android/pandora/server/src/com/android/pandora/A2dp.kt
@@ -70,28 +70,6 @@
scope.cancel()
}
- fun buildAudioTrack(): AudioTrack? {
- audioTrack =
- AudioTrack.Builder()
- .setAudioAttributes(
- AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_MEDIA)
- .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
- .build()
- )
- .setAudioFormat(
- AudioFormat.Builder()
- .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
- .setSampleRate(44100)
- .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
- .build()
- )
- .setTransferMode(AudioTrack.MODE_STREAM)
- .setBufferSizeInBytes(44100 * 2 * 2)
- .build()
- return audioTrack
- }
-
override fun openSource(
request: OpenSourceRequest,
responseObserver: StreamObserver<OpenSourceResponse>
@@ -332,5 +310,4 @@
.build()
}
}
-
}
diff --git a/android/pandora/server/src/com/android/pandora/Avrcp.kt b/android/pandora/server/src/com/android/pandora/Avrcp.kt
index bd6d81f..f8cc95b 100644
--- a/android/pandora/server/src/com/android/pandora/Avrcp.kt
+++ b/android/pandora/server/src/com/android/pandora/Avrcp.kt
@@ -18,15 +18,11 @@
import android.bluetooth.BluetoothManager
import android.content.Context
-
-import pandora.AVRCPGrpc.AVRCPImplBase
-import pandora.AvrcpProto.*
-import com.google.protobuf.Empty
-
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
-
+import pandora.AVRCPGrpc.AVRCPImplBase
+import pandora.AvrcpProto.*
@kotlinx.coroutines.ExperimentalCoroutinesApi
class Avrcp(val context: Context) : AVRCPImplBase() {
diff --git a/android/pandora/server/src/com/android/pandora/MediaPlayer.kt b/android/pandora/server/src/com/android/pandora/MediaPlayer.kt
new file mode 100644
index 0000000..7942a3b
--- /dev/null
+++ b/android/pandora/server/src/com/android/pandora/MediaPlayer.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2022 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.pandora
+
+import android.content.Context
+import android.content.Intent
+import android.media.*
+import com.google.protobuf.Empty
+import io.grpc.stub.StreamObserver
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import pandora.MediaPlayerGrpc.MediaPlayerImplBase
+import pandora.MediaPlayerProto.*
+
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class MediaPlayer(val context: Context) : MediaPlayerImplBase() {
+ private val TAG = "PandoraMediaPlayer"
+
+ private val scope: CoroutineScope
+
+ init {
+ // Init the CoroutineScope
+ scope = CoroutineScope(Dispatchers.Default)
+ context.startService(Intent(context, MediaPlayerBrowserService::class.java))
+ }
+
+ override fun play(request: Empty, responseObserver: StreamObserver<Empty>) {
+ grpcUnary<Empty>(scope, responseObserver) {
+ MediaPlayerBrowserService.instance.play()
+ Empty.getDefaultInstance()
+ }
+ }
+
+ override fun stop(request: Empty, responseObserver: StreamObserver<Empty>) {
+ grpcUnary<Empty>(scope, responseObserver) {
+ MediaPlayerBrowserService.instance.stop()
+ Empty.getDefaultInstance()
+ }
+ }
+
+ override fun pause(request: Empty, responseObserver: StreamObserver<Empty>) {
+ grpcUnary<Empty>(scope, responseObserver) {
+ MediaPlayerBrowserService.instance.pause()
+ Empty.getDefaultInstance()
+ }
+ }
+
+ override fun rewind(request: Empty, responseObserver: StreamObserver<Empty>) {
+ grpcUnary<Empty>(scope, responseObserver) {
+ MediaPlayerBrowserService.instance.rewind()
+ Empty.getDefaultInstance()
+ }
+ }
+
+ override fun fastForward(request: Empty, responseObserver: StreamObserver<Empty>) {
+ grpcUnary<Empty>(scope, responseObserver) {
+ MediaPlayerBrowserService.instance.fastForward()
+ Empty.getDefaultInstance()
+ }
+ }
+
+ override fun forward(request: Empty, responseObserver: StreamObserver<Empty>) {
+ grpcUnary<Empty>(scope, responseObserver) {
+ MediaPlayerBrowserService.instance.forward()
+ Empty.getDefaultInstance()
+ }
+ }
+
+ override fun backward(request: Empty, responseObserver: StreamObserver<Empty>) {
+ grpcUnary<Empty>(scope, responseObserver) {
+ MediaPlayerBrowserService.instance.backward()
+ Empty.getDefaultInstance()
+ }
+ }
+
+ override fun setLargeMetadata(request: Empty, responseObserver: StreamObserver<Empty>) {
+ grpcUnary<Empty>(scope, responseObserver) {
+ MediaPlayerBrowserService.instance.setLargeMetadata()
+ Empty.getDefaultInstance()
+ }
+ }
+
+ fun deinit() {
+ // Deinit the CoroutineScope
+ scope.cancel()
+ // Stop service
+ context.stopService(Intent(context, MediaPlayerBrowserService::class.java))
+ }
+}
diff --git a/android/pandora/server/src/com/android/pandora/MediaPlayerBrowserService.kt b/android/pandora/server/src/com/android/pandora/MediaPlayerBrowserService.kt
new file mode 100644
index 0000000..8ff3034
--- /dev/null
+++ b/android/pandora/server/src/com/android/pandora/MediaPlayerBrowserService.kt
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2022 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.pandora
+
+import android.content.Intent
+import android.media.*
+import android.media.browse.MediaBrowser.MediaItem
+import android.media.session.*
+import android.os.Bundle
+import android.service.media.MediaBrowserService
+import android.service.media.MediaBrowserService.BrowserRoot
+import android.util.Log
+
+/* MediaBrowserService to handle MediaButton and Browsing */
+class MediaPlayerBrowserService : MediaBrowserService() {
+ private val TAG = "PandoraMediaPlayerBrowserService"
+
+ private lateinit var mediaSession: MediaSession
+ private lateinit var playbackStateBuilder: PlaybackState.Builder
+ private val alphanumeric = ('A'..'Z') + ('a'..'z') + ('0'..'9')
+ private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaItem>>()
+ private var metadataItems = mutableMapOf<String, MediaMetadata>()
+ private var queue = mutableListOf<MediaSession.QueueItem>()
+ private var currentTrack = -1
+
+ override fun onCreate() {
+ super.onCreate()
+ setupMediaSession()
+ initBrowseFolderList()
+ instance = this
+ }
+
+ fun deinit() {
+ // Releasing MediaSession instance
+ mediaSession.release()
+ }
+
+ private fun setupMediaSession() {
+ mediaSession = MediaSession(this, "MediaSession")
+
+ mediaSession.setFlags(
+ MediaSession.FLAG_HANDLES_MEDIA_BUTTONS or MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS
+ )
+ mediaSession.setCallback(mSessionCallback)
+ playbackStateBuilder =
+ PlaybackState.Builder()
+ .setState(PlaybackState.STATE_NONE, 0, 1.0f)
+ .setActions(getAvailableActions())
+ mediaSession.setPlaybackState(playbackStateBuilder.build())
+ mediaSession.setMetadata(null)
+ mediaSession.setQueue(queue)
+ mediaSession.setQueueTitle(NOW_PLAYING_PREFIX)
+ mediaSession.isActive = true
+ sessionToken = mediaSession.sessionToken
+ }
+
+ private fun getAvailableActions(): Long =
+ PlaybackState.ACTION_SKIP_TO_PREVIOUS or
+ PlaybackState.ACTION_SKIP_TO_NEXT or
+ PlaybackState.ACTION_FAST_FORWARD or
+ PlaybackState.ACTION_REWIND or
+ PlaybackState.ACTION_PLAY or
+ PlaybackState.ACTION_STOP or
+ PlaybackState.ACTION_PAUSE
+
+ private fun setPlaybackState(state: Int) {
+ playbackStateBuilder.setState(state, 0, 1.0f)
+ mediaSession.setPlaybackState(playbackStateBuilder.build())
+ }
+
+ fun play() {
+ setPlaybackState(PlaybackState.STATE_PLAYING)
+ if (currentTrack == -1) {
+ currentTrack = 1
+ initQueue()
+ mediaSession.setQueue(queue)
+ mediaSession.setMetadata(metadataItems.get("" + currentTrack))
+ }
+ }
+
+ fun stop() {
+ setPlaybackState(PlaybackState.STATE_STOPPED)
+ mediaSession.setMetadata(null)
+ }
+
+ fun pause() {
+ setPlaybackState(PlaybackState.STATE_PAUSED)
+ }
+
+ fun rewind() {
+ setPlaybackState(PlaybackState.STATE_REWINDING)
+ }
+
+ fun fastForward() {
+ setPlaybackState(PlaybackState.STATE_FAST_FORWARDING)
+ }
+
+ fun forward() {
+ if (currentTrack == QUEUE_SIZE || currentTrack == -1) currentTrack = 1 else currentTrack += 1
+ setPlaybackState(PlaybackState.STATE_SKIPPING_TO_NEXT)
+ mediaSession.setMetadata(metadataItems.get("" + currentTrack))
+ }
+
+ fun backward() {
+ if (currentTrack == 1 || currentTrack == -1) currentTrack = QUEUE_SIZE else currentTrack -= 1
+ setPlaybackState(PlaybackState.STATE_SKIPPING_TO_PREVIOUS)
+ mediaSession.setMetadata(metadataItems.get("" + currentTrack))
+ }
+
+ fun setLargeMetadata() {
+ mediaSession.setMetadata(
+ MediaMetadata.Builder()
+ .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "MEDIA_ID")
+ .putString(MediaMetadata.METADATA_KEY_TITLE, generateAlphanumericString(512))
+ .putString(MediaMetadata.METADATA_KEY_ARTIST, generateAlphanumericString(512))
+ .build()
+ )
+ }
+
+ private fun generateAlphanumericString(length: Int): String {
+ return buildString { repeat(length) { append(alphanumeric.random()) } }
+ }
+
+ private val mSessionCallback: MediaSession.Callback =
+ object : MediaSession.Callback() {
+ override fun onPlay() {
+ Log.i(TAG, "onPlay")
+ play()
+ }
+
+ override fun onPause() {
+ Log.i(TAG, "onPause")
+ pause()
+ }
+
+ override fun onSkipToPrevious() {
+ Log.i(TAG, "onSkipToPrevious")
+ // TODO : Need to handle to play previous audio in the list
+ }
+
+ override fun onSkipToNext() {
+ Log.i(TAG, "onSkipToNext")
+ // TODO : Need to handle to play next audio in the list
+ }
+
+ override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
+ Log.i(TAG, "MediaSessionCallback——》onMediaButtonEvent $mediaButtonEvent")
+ return super.onMediaButtonEvent(mediaButtonEvent)
+ }
+ }
+
+ override fun onGetRoot(p0: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
+ Log.i(TAG, "onGetRoot")
+ return BrowserRoot(ROOT, null)
+ }
+
+ override fun onLoadChildren(parentId: String, result: Result<MutableList<MediaItem>>) {
+ Log.i(TAG, "onLoadChildren")
+ if (parentId == ROOT) {
+ val map = mediaIdToChildren[ROOT]
+ Log.i(TAG, "onloadchildren $map")
+ result.sendResult(map)
+ } else if (parentId == NOW_PLAYING_PREFIX) {
+ result.sendResult(mediaIdToChildren[NOW_PLAYING_PREFIX])
+ } else {
+ Log.i(TAG, "onloadchildren inside else")
+ result.sendResult(null)
+ }
+ }
+
+ private fun initMediaItems() {
+ var mediaItems = mutableListOf<MediaItem>()
+ for (item in 1..QUEUE_SIZE) {
+ val metaData: MediaMetadata =
+ MediaMetadata.Builder()
+ .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, NOW_PLAYING_PREFIX + item)
+ .putString(MediaMetadata.METADATA_KEY_TITLE, "Title$item")
+ .putString(MediaMetadata.METADATA_KEY_ARTIST, "Artist$item")
+ .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, item.toLong())
+ .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, QUEUE_SIZE.toLong())
+ .build()
+ val mediaItem = MediaItem(metaData.description, MediaItem.FLAG_PLAYABLE)
+ mediaItems.add(mediaItem)
+ metadataItems.put("" + item, metaData)
+ }
+ mediaIdToChildren[NOW_PLAYING_PREFIX] = mediaItems
+ }
+
+ private fun initQueue() {
+ for ((key, value) in metadataItems.entries) {
+ val mediaItem = MediaItem(value.description, MediaItem.FLAG_PLAYABLE)
+ queue.add(MediaSession.QueueItem(mediaItem.description, key.toLong()))
+ }
+ }
+
+ private fun initBrowseFolderList() {
+ var rootList = mediaIdToChildren[ROOT] ?: mutableListOf()
+
+ val emptyFolderMetaData =
+ MediaMetadata.Builder()
+ .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, EMPTY_FOLDER)
+ .putString(MediaMetadata.METADATA_KEY_TITLE, EMPTY_FOLDER)
+ .putLong(
+ MediaMetadata.METADATA_KEY_BT_FOLDER_TYPE,
+ MediaDescription.BT_FOLDER_TYPE_PLAYLISTS
+ )
+ .build()
+ val emptyFolderMediaItem = MediaItem(emptyFolderMetaData.description, MediaItem.FLAG_BROWSABLE)
+
+ val playlistMetaData =
+ MediaMetadata.Builder()
+ .apply {
+ putString(MediaMetadata.METADATA_KEY_MEDIA_ID, NOW_PLAYING_PREFIX)
+ putString(MediaMetadata.METADATA_KEY_TITLE, NOW_PLAYING_PREFIX)
+ putLong(
+ MediaMetadata.METADATA_KEY_BT_FOLDER_TYPE,
+ MediaDescription.BT_FOLDER_TYPE_PLAYLISTS
+ )
+ }
+ .build()
+
+ val playlistsMediaItem = MediaItem(playlistMetaData.description, MediaItem.FLAG_BROWSABLE)
+
+ rootList += emptyFolderMediaItem
+ rootList += playlistsMediaItem
+ mediaIdToChildren[ROOT] = rootList
+ initMediaItems()
+ }
+
+ companion object {
+ lateinit var instance: MediaPlayerBrowserService
+ const val ROOT = "__ROOT__"
+ const val EMPTY_FOLDER = "@empty@"
+ const val NOW_PLAYING_PREFIX = "NowPlayingId"
+ const val QUEUE_SIZE = 6
+ }
+}
diff --git a/android/pandora/server/src/com/android/pandora/Server.kt b/android/pandora/server/src/com/android/pandora/Server.kt
index df70116..bdcd1ff3 100644
--- a/android/pandora/server/src/com/android/pandora/Server.kt
+++ b/android/pandora/server/src/com/android/pandora/Server.kt
@@ -37,8 +37,9 @@
private var hfp: Hfp
private var hid: Hid
private var l2cap: L2cap
+ private var mediaplayer: MediaPlayer
private var security: Security
- private var androidInternal : AndroidInternal
+ private var androidInternal: AndroidInternal
private var grpcServer: GrpcServer
init {
@@ -48,6 +49,7 @@
hfp = Hfp(context)
hid = Hid(context)
l2cap = L2cap(context)
+ mediaplayer = MediaPlayer(context)
security = Security(context)
androidInternal = AndroidInternal()
@@ -59,6 +61,7 @@
.addService(hfp)
.addService(hid)
.addService(l2cap)
+ .addService(mediaplayer)
.addService(security)
.addService(androidInternal)
@@ -92,6 +95,7 @@
hfp.deinit()
hid.deinit()
l2cap.deinit()
+ mediaplayer.deinit()
security.deinit()
androidInternal.deinit()
}
diff --git a/android/pandora/server/src/com/android/pandora/Utils.kt b/android/pandora/server/src/com/android/pandora/Utils.kt
index 091ce60..c3863c7 100644
--- a/android/pandora/server/src/com/android/pandora/Utils.kt
+++ b/android/pandora/server/src/com/android/pandora/Utils.kt
@@ -24,6 +24,7 @@
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.media.*
import android.net.MacAddress
import android.os.ParcelFileDescriptor
import android.util.Log
@@ -318,3 +319,24 @@
fun BluetoothDevice.toByteString() =
ByteString.copyFrom(MacAddress.fromString(this.address).toByteArray())!!
+
+/** Creates Audio track instance and returns the reference. */
+fun buildAudioTrack(): AudioTrack? {
+ return AudioTrack.Builder()
+ .setAudioAttributes(
+ AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .build()
+ )
+ .setAudioFormat(
+ AudioFormat.Builder()
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+ .setSampleRate(44100)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
+ .build()
+ )
+ .setTransferMode(AudioTrack.MODE_STREAM)
+ .setBufferSizeInBytes(44100 * 2 * 2)
+ .build()
+}
diff --git a/android/pandora/test/Android.bp b/android/pandora/test/Android.bp
index 93d9cbf..4e783f6 100644
--- a/android/pandora/test/Android.bp
+++ b/android/pandora/test/Android.bp
@@ -32,3 +32,14 @@
},
data: ["config.yml"],
}
+
+python_binary_host {
+ name: "avatar_runner",
+ main: "runner.py",
+ srcs: [
+ "runner.py",
+ ],
+ libs: [
+ "libavatar_experimental",
+ ],
+}
diff --git a/android/pandora/test/AndroidTest.xml b/android/pandora/test/AndroidTest.xml
index e198daa..9f6ce9f 100644
--- a/android/pandora/test/AndroidTest.xml
+++ b/android/pandora/test/AndroidTest.xml
@@ -21,6 +21,7 @@
<option name="install-arg" value="-g" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.PythonVirtualenvPreparer">
+ <option name="dep-module" value="mobly" />
<option name="dep-module" value="grpcio" />
<option name="dep-module" value="pyee" />
<option name="dep-module" value="ansicolors" />
diff --git a/android/pandora/test/connect.py b/android/pandora/test/connect.py
index 84c7bf3d..55cf92d 100644
--- a/android/pandora/test/connect.py
+++ b/android/pandora/test/connect.py
@@ -52,7 +52,8 @@
if __name__ == '__main__':
# MoblyBinaryHostTest pass test_runner arguments after a "--"
# to make it work with rewrite argv to skip the "--"
- index = sys.argv.index('--')
- sys.argv = sys.argv[:1] + sys.argv[index + 1:]
+ if '--' in sys.argv:
+ index = sys.argv.index('--')
+ sys.argv = sys.argv[:1] + sys.argv[index + 1:]
logging.basicConfig(level=logging.DEBUG)
test_runner.main()
diff --git a/android/pandora/test/runner.py b/android/pandora/test/runner.py
new file mode 100644
index 0000000..613b1fc
--- /dev/null
+++ b/android/pandora/test/runner.py
@@ -0,0 +1,114 @@
+# Copyright 2022 Google LLC
+#
+# 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
+#
+# https://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.
+
+import os
+import sys
+import logging
+import argparse
+import subprocess
+
+from re import sub
+from pathlib import Path
+from genericpath import exists
+from multiprocessing import Process
+
+ANDROID_BUILD_TOP = os.getenv("ANDROID_BUILD_TOP")
+TARGET_PRODUCT = os.getenv("TARGET_PRODUCT")
+TARGET_BUILD_VARIANT = os.getenv("TARGET_BUILD_VARIANT")
+ANDROID_PRODUCT_OUT = os.getenv("ANDROID_PRODUCT_OUT")
+PANDORA_CF_APK = Path(
+ f'{ANDROID_BUILD_TOP}/out/target/product/vsoc_x86_64/testcases/PandoraServer/x86_64/PandoraServer.apk'
+)
+
+
+def build_pandora_server():
+ target = TARGET_PRODUCT if TARGET_BUILD_VARIANT == "release" else f'{TARGET_PRODUCT}-{TARGET_BUILD_VARIANT}'
+ logging.debug(f'build_pandora_server: {target}')
+ pandora_server_cmd = f'source build/envsetup.sh && lunch {target} && make PandoraServer'
+ subprocess.run(pandora_server_cmd,
+ cwd=ANDROID_BUILD_TOP,
+ shell=True,
+ executable='/bin/bash',
+ check=True)
+
+
+def install_pandora_server(serial):
+ logging.debug('Install PandoraServer.apk')
+ pandora_apk_path = Path(
+ f'{ANDROID_PRODUCT_OUT}/testcases/PandoraServer/x86_64/PandoraServer.apk')
+ if not pandora_apk_path.exists():
+ logging.error(
+ f"PandoraServer apk is not build or the path is wrong: {pandora_apk_path}"
+ )
+ sys.exit(1)
+ install_apk_cmd = ['adb', 'install', '-r', '-g', str(pandora_apk_path)]
+ if args.serial != "":
+ install_apk_cmd.append(f'-s {serial}')
+ subprocess.run(install_apk_cmd, check=True)
+
+
+def instrument_pandora_server():
+ logging.debug('instrument_pandora_server')
+ instrument_cmd = 'adb shell am instrument --no-hidden-api-checks -w com.android.pandora/.Main'
+ instrument_process = Process(
+ target=lambda: subprocess.run(instrument_cmd, shell=True, check=True))
+ instrument_process.start()
+ return instrument_process
+
+
+def run_test(args):
+ logging.debug(f'run_test config: {args.config} test: {args.test}')
+ test_cmd = ['python3', args.test, '-c', args.config]
+ if args.verbose:
+ test_cmd.append('--verbose')
+ p = subprocess.Popen(test_cmd)
+ p.wait(timeout=args.timeout)
+ p.terminate()
+
+
+def run(args):
+ if not PANDORA_CF_APK.exists() or args.build:
+ build_pandora_server()
+ install_pandora_server(args.serial)
+ instrument_process = instrument_pandora_server()
+ run_test(args)
+ instrument_process.terminate()
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("test", type=str, help="Test script path")
+ parser.add_argument("config", type=str, help="Test config file path")
+ parser.add_argument("-b",
+ "--build",
+ action="store_true",
+ help="Build the PandoraServer.apk")
+ parser.add_argument("-s",
+ "--serial",
+ type=str,
+ default="",
+ help="Use device with given serial")
+ parser.add_argument("-m",
+ "--timeout",
+ type=int,
+ default=1800000,
+ help="Mobly test timeout")
+ parser.add_argument("-v",
+ "--verbose",
+ action="store_true",
+ help="Set console logger level to DEBUG")
+ args = parser.parse_args()
+ console_level = logging.DEBUG if args.verbose else logging.INFO
+ logging.basicConfig(level=console_level)
+ run(args)
diff --git a/pandora/interfaces/Android.bp b/pandora/interfaces/Android.bp
index 38a344a..cc76f5f 100644
--- a/pandora/interfaces/Android.bp
+++ b/pandora/interfaces/Android.bp
@@ -69,6 +69,7 @@
"pandora_experimental/hid.proto",
"pandora_experimental/host.proto",
"pandora_experimental/l2cap.proto",
+ "pandora_experimental/mediaplayer.proto",
"pandora_experimental/security.proto",
],
out: [
@@ -88,6 +89,8 @@
"pandora_experimental/host_pb2.py",
"pandora_experimental/l2cap_grpc.py",
"pandora_experimental/l2cap_pb2.py",
+ "pandora_experimental/mediaplayer_grpc.py",
+ "pandora_experimental/mediaplayer_pb2.py",
"pandora_experimental/security_grpc.py",
"pandora_experimental/security_pb2.py",
]
@@ -98,4 +101,4 @@
srcs: [
":pandora_experimental-python-src",
],
-}
+}
\ No newline at end of file
diff --git a/pandora/interfaces/pandora_experimental/avrcp.proto b/pandora/interfaces/pandora_experimental/avrcp.proto
index b079f90..d2a7060 100644
--- a/pandora/interfaces/pandora_experimental/avrcp.proto
+++ b/pandora/interfaces/pandora_experimental/avrcp.proto
@@ -4,9 +4,5 @@
package pandora;
-import "pandora_experimental/host.proto";
-
-
service AVRCP {
-
-}
+}
\ No newline at end of file
diff --git a/pandora/interfaces/pandora_experimental/mediaplayer.proto b/pandora/interfaces/pandora_experimental/mediaplayer.proto
new file mode 100644
index 0000000..e6cf1f2
--- /dev/null
+++ b/pandora/interfaces/pandora_experimental/mediaplayer.proto
@@ -0,0 +1,19 @@
+syntax = "proto3";
+
+option java_outer_classname = "MediaPlayerProto";
+
+package pandora;
+
+import "google/protobuf/empty.proto";
+
+
+service MediaPlayer {
+ rpc Play(google.protobuf.Empty) returns (google.protobuf.Empty);
+ rpc Stop(google.protobuf.Empty) returns (google.protobuf.Empty);
+ rpc Pause(google.protobuf.Empty) returns (google.protobuf.Empty);
+ rpc Rewind(google.protobuf.Empty) returns (google.protobuf.Empty);
+ rpc FastForward(google.protobuf.Empty) returns (google.protobuf.Empty);
+ rpc Forward(google.protobuf.Empty) returns (google.protobuf.Empty);
+ rpc Backward(google.protobuf.Empty) returns (google.protobuf.Empty);
+ rpc SetLargeMetadata(google.protobuf.Empty) returns (google.protobuf.Empty);
+}
\ No newline at end of file
diff --git a/service/Android.bp b/service/Android.bp
index 0aa9dba..f1dff44 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -20,6 +20,7 @@
name: "services.bluetooth-sources",
srcs: [
"java/**/*.java",
+ ":statslog-bluetooth-java-gen",
],
visibility: [
"//frameworks/base/services",
@@ -62,6 +63,7 @@
"framework-annotations-lib",
"framework-bluetooth-pre-jarjar",
"app-compat-annotations",
+ "framework-statsd.stubs.module_lib",
],
static_libs: [
@@ -69,6 +71,8 @@
"androidx.appcompat_appcompat",
"modules-utils-shell-command-handler",
"bluetooth-nano-protos",
+ "bluetooth-protos-lite",
+ "libprotobuf-java-lite",
],
apex_available: [
diff --git a/service/java/com/android/server/bluetooth/BluetoothAirplaneModeListener.java b/service/java/com/android/server/bluetooth/BluetoothAirplaneModeListener.java
index cd8949b..67d7f12 100644
--- a/service/java/com/android/server/bluetooth/BluetoothAirplaneModeListener.java
+++ b/service/java/com/android/server/bluetooth/BluetoothAirplaneModeListener.java
@@ -24,9 +24,11 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.SystemClock;
import android.provider.Settings;
import android.util.Log;
+import com.android.bluetooth.BluetoothStatsLog;
import com.android.internal.annotations.VisibleForTesting;
/**
@@ -68,6 +70,19 @@
@VisibleForTesting static final int MAX_TOAST_COUNT = 10; // 10 times
+ /* Tracks the bluetooth state before entering airplane mode*/
+ private boolean mIsBluetoothOnBeforeApmToggle = false;
+ /* Tracks the bluetooth state after entering airplane mode*/
+ private boolean mIsBluetoothOnAfterApmToggle = false;
+ /* Tracks whether user toggled bluetooth in airplane mode */
+ private boolean mUserToggledBluetoothDuringApm = false;
+ /* Tracks whether user toggled bluetooth in airplane mode within one minute */
+ private boolean mUserToggledBluetoothDuringApmWithinMinute = false;
+ /* Tracks whether media profile was connected before entering airplane mode */
+ private boolean mIsMediaProfileConnectedBeforeApmToggle = false;
+ /* Tracks when airplane mode has been enabled */
+ private long mApmEnabledTime = 0;
+
private final BluetoothManagerService mBluetoothManager;
private final BluetoothAirplaneModeHandler mHandler;
private final Context mContext;
@@ -138,61 +153,77 @@
@VisibleForTesting
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
void handleAirplaneModeChange() {
- if (shouldSkipAirplaneModeChange()) {
- Log.i(TAG, "Ignore airplane mode change");
- // Airplane mode enabled when Bluetooth is being used for audio/headering aid.
- // Bluetooth is not disabled in such case, only state is changed to
- // BLUETOOTH_ON_AIRPLANE mode.
- mAirplaneHelper.setSettingsInt(Settings.Global.BLUETOOTH_ON,
- BluetoothManagerService.BLUETOOTH_ON_AIRPLANE);
- if (!isApmEnhancementEnabled() || !isBluetoothToggledOnApm()) {
- if (shouldPopToast()) {
- mAirplaneHelper.showToastMessage();
- }
- } else {
- if (isWifiEnabledOnApm() && isFirstTimeNotification(APM_WIFI_BT_NOTIFICATION)) {
- try {
- sendApmNotification("bluetooth_and_wifi_stays_on_title",
- "bluetooth_and_wifi_stays_on_message",
- APM_WIFI_BT_NOTIFICATION);
- } catch (Exception e) {
- Log.e(TAG, "APM enhancement BT and Wi-Fi stays on notification not shown");
- }
- } else if (!isWifiEnabledOnApm() && isFirstTimeNotification(APM_BT_NOTIFICATION)) {
- try {
- sendApmNotification("bluetooth_stays_on_title",
- "bluetooth_stays_on_message",
- APM_BT_NOTIFICATION);
- } catch (Exception e) {
- Log.e(TAG, "APM enhancement BT stays on notification not shown");
- }
- }
- }
+ if (mAirplaneHelper == null) {
return;
}
- if (mAirplaneHelper != null) {
- mAirplaneHelper.onAirplaneModeChanged(mBluetoothManager);
+ if (mAirplaneHelper.isAirplaneModeOn()) {
+ mApmEnabledTime = SystemClock.elapsedRealtime();
+ mIsBluetoothOnBeforeApmToggle = mAirplaneHelper.isBluetoothOn();
+ mIsBluetoothOnAfterApmToggle = shouldSkipAirplaneModeChange();
+ mIsMediaProfileConnectedBeforeApmToggle = mAirplaneHelper.isMediaProfileConnected();
+ if (mIsBluetoothOnAfterApmToggle) {
+ Log.i(TAG, "Ignore airplane mode change");
+ // Airplane mode enabled when Bluetooth is being used for audio/headering aid.
+ // Bluetooth is not disabled in such case, only state is changed to
+ // BLUETOOTH_ON_AIRPLANE mode.
+ mAirplaneHelper.setSettingsInt(Settings.Global.BLUETOOTH_ON,
+ BluetoothManagerService.BLUETOOTH_ON_AIRPLANE);
+ if (!isApmEnhancementEnabled() || !isBluetoothToggledOnApm()) {
+ if (shouldPopToast()) {
+ mAirplaneHelper.showToastMessage();
+ }
+ } else {
+ if (isWifiEnabledOnApm() && isFirstTimeNotification(APM_WIFI_BT_NOTIFICATION)) {
+ try {
+ sendApmNotification("bluetooth_and_wifi_stays_on_title",
+ "bluetooth_and_wifi_stays_on_message",
+ APM_WIFI_BT_NOTIFICATION);
+ } catch (Exception e) {
+ Log.e(TAG,
+ "APM enhancement BT and Wi-Fi stays on notification not shown");
+ }
+ } else if (!isWifiEnabledOnApm() && isFirstTimeNotification(
+ APM_BT_NOTIFICATION)) {
+ try {
+ sendApmNotification("bluetooth_stays_on_title",
+ "bluetooth_stays_on_message",
+ APM_BT_NOTIFICATION);
+ } catch (Exception e) {
+ Log.e(TAG, "APM enhancement BT stays on notification not shown");
+ }
+ }
+ }
+ return;
+ }
+ } else {
+ BluetoothStatsLog.write(BluetoothStatsLog.AIRPLANE_MODE_SESSION_REPORTED,
+ BluetoothStatsLog.AIRPLANE_MODE_SESSION_REPORTED__PACKAGE_NAME__BLUETOOTH,
+ mIsBluetoothOnBeforeApmToggle,
+ mIsBluetoothOnAfterApmToggle,
+ mAirplaneHelper.isBluetoothOn(),
+ isBluetoothToggledOnApm(),
+ mUserToggledBluetoothDuringApm,
+ mUserToggledBluetoothDuringApmWithinMinute,
+ mIsMediaProfileConnectedBeforeApmToggle);
+ mUserToggledBluetoothDuringApm = false;
+ mUserToggledBluetoothDuringApmWithinMinute = false;
}
+ mAirplaneHelper.onAirplaneModeChanged(mBluetoothManager);
}
@VisibleForTesting
boolean shouldSkipAirplaneModeChange() {
- if (mAirplaneHelper == null) {
- return false;
- }
boolean apmEnhancementUsed = isApmEnhancementEnabled() && isBluetoothToggledOnApm();
// APM feature disabled or user has not used the feature yet by changing BT state in APM
// BT will only remain on in APM when media profile is connected
if (!apmEnhancementUsed && mAirplaneHelper.isBluetoothOn()
- && mAirplaneHelper.isAirplaneModeOn()
&& mAirplaneHelper.isMediaProfileConnected()) {
return true;
}
// APM feature enabled and user has used the feature by changing BT state in APM
// BT will only remain on in APM based on user's last action in APM
if (apmEnhancementUsed && mAirplaneHelper.isBluetoothOn()
- && mAirplaneHelper.isAirplaneModeOn()
&& mAirplaneHelper.isBluetoothOnAPM()) {
return true;
}
@@ -200,7 +231,6 @@
// BT will only remain on in APM if the default value is set to on
if (isApmEnhancementEnabled() && !isBluetoothToggledOnApm()
&& mAirplaneHelper.isBluetoothOn()
- && mAirplaneHelper.isAirplaneModeOn()
&& mAirplaneHelper.isBluetoothOnAPM()) {
return true;
}
@@ -245,4 +275,15 @@
mAirplaneHelper.setSettingsSecureInt(notificationState,
NOTIFICATION_SHOWN);
}
+
+ /**
+ * Helper method to update whether user toggled Bluetooth in airplane mode
+ */
+ public void updateBluetoothToggledTime() {
+ if (!mUserToggledBluetoothDuringApm) {
+ mUserToggledBluetoothDuringApmWithinMinute =
+ SystemClock.elapsedRealtime() - mApmEnabledTime < 60000;
+ }
+ mUserToggledBluetoothDuringApm = true;
+ }
}
diff --git a/service/java/com/android/server/bluetooth/BluetoothManagerService.java b/service/java/com/android/server/bluetooth/BluetoothManagerService.java
index a249286..f7bcc69 100644
--- a/service/java/com/android/server/bluetooth/BluetoothManagerService.java
+++ b/service/java/com/android/server/bluetooth/BluetoothManagerService.java
@@ -1379,20 +1379,23 @@
synchronized (mReceiver) {
mQuietEnableExternal = false;
mEnableExternal = true;
- if (isAirplaneModeOn() && isApmEnhancementOn()) {
- setSettingsSecureInt(BLUETOOTH_APM_STATE, BLUETOOTH_ON_APM);
- setSettingsSecureInt(APM_USER_TOGGLED_BLUETOOTH, USED);
- if (isFirstTimeNotification(APM_BT_ENABLED_NOTIFICATION)) {
- final long callingIdentity = Binder.clearCallingIdentity();
- try {
- mBluetoothAirplaneModeListener.sendApmNotification(
- "bluetooth_enabled_apm_title",
- "bluetooth_enabled_apm_message",
- APM_BT_ENABLED_NOTIFICATION);
- } catch (Exception e) {
- Log.e(TAG, "APM enhancement BT enabled notification not shown");
- } finally {
- Binder.restoreCallingIdentity(callingIdentity);
+ if (isAirplaneModeOn()) {
+ mBluetoothAirplaneModeListener.updateBluetoothToggledTime();
+ if (isApmEnhancementOn()) {
+ setSettingsSecureInt(BLUETOOTH_APM_STATE, BLUETOOTH_ON_APM);
+ setSettingsSecureInt(APM_USER_TOGGLED_BLUETOOTH, USED);
+ if (isFirstTimeNotification(APM_BT_ENABLED_NOTIFICATION)) {
+ final long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ mBluetoothAirplaneModeListener.sendApmNotification(
+ "bluetooth_enabled_apm_title",
+ "bluetooth_enabled_apm_message",
+ APM_BT_ENABLED_NOTIFICATION);
+ } catch (Exception e) {
+ Log.e(TAG, "APM enhancement BT enabled notification not shown");
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
}
}
}
@@ -1436,9 +1439,12 @@
}
synchronized (mReceiver) {
- if (isAirplaneModeOn() && isApmEnhancementOn()) {
- setSettingsSecureInt(BLUETOOTH_APM_STATE, BLUETOOTH_OFF_APM);
- setSettingsSecureInt(APM_USER_TOGGLED_BLUETOOTH, USED);
+ if (isAirplaneModeOn()) {
+ mBluetoothAirplaneModeListener.updateBluetoothToggledTime();
+ if (isApmEnhancementOn()) {
+ setSettingsSecureInt(BLUETOOTH_APM_STATE, BLUETOOTH_OFF_APM);
+ setSettingsSecureInt(APM_USER_TOGGLED_BLUETOOTH, USED);
+ }
}
if (persist) {
diff --git a/service/tests/src/com/android/server/BluetoothAirplaneModeListenerTest.java b/service/tests/src/com/android/server/BluetoothAirplaneModeListenerTest.java
index 92c9851..cc3dd45 100644
--- a/service/tests/src/com/android/server/BluetoothAirplaneModeListenerTest.java
+++ b/service/tests/src/com/android/server/BluetoothAirplaneModeListenerTest.java
@@ -84,9 +84,6 @@
Assert.assertFalse(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());
when(mHelper.isMediaProfileConnected()).thenReturn(true);
- Assert.assertFalse(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());
-
- when(mHelper.isAirplaneModeOn()).thenReturn(true);
Assert.assertTrue(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());
}
diff --git a/system/bta/dm/bta_dm_act.cc b/system/bta/dm/bta_dm_act.cc
index bde70de..661504b 100644
--- a/system/bta/dm/bta_dm_act.cc
+++ b/system/bta/dm/bta_dm_act.cc
@@ -38,6 +38,7 @@
#include "btif/include/stack_manager.h"
#include "device/include/controller.h"
#include "device/include/interop.h"
+#include "gd/common/init_flags.h"
#include "main/shim/acl_api.h"
#include "main/shim/btm_api.h"
#include "main/shim/dumpsys.h"
@@ -47,6 +48,7 @@
#include "osi/include/fixed_queue.h"
#include "osi/include/log.h"
#include "osi/include/osi.h"
+#include "osi/include/properties.h"
#include "stack/btm/btm_ble_int.h"
#include "stack/btm/btm_dev.h"
#include "stack/btm/btm_sec.h"
@@ -87,7 +89,8 @@
static uint8_t bta_dm_new_link_key_cback(const RawAddress& bd_addr,
DEV_CLASS dev_class,
tBTM_BD_NAME bd_name,
- const LinkKey& key, uint8_t key_type);
+ const LinkKey& key, uint8_t key_type,
+ bool is_ctkd);
static void bta_dm_authentication_complete_cback(const RawAddress& bd_addr,
DEV_CLASS dev_class,
tBTM_BD_NAME bd_name,
@@ -915,6 +918,10 @@
bta_dm_search_cb.transport = p_data->discover.transport;
bta_dm_search_cb.name_discover_done = false;
+
+ LOG_INFO("bta_dm_discovery: starting service discovery to %s , transport: %s",
+ PRIVATE_ADDRESS(p_data->discover.bd_addr),
+ bt_transport_text(p_data->discover.transport).c_str());
bta_dm_discover_device(p_data->discover.bd_addr);
}
@@ -1455,6 +1462,9 @@
tBTA_DM_MSG* p_pending_discovery =
(tBTA_DM_MSG*)osi_malloc(sizeof(tBTA_DM_API_DISCOVER));
memcpy(p_pending_discovery, p_data, sizeof(tBTA_DM_API_DISCOVER));
+
+ LOG_INFO("bta_dm_discovery: queuing service discovery to %s",
+ p_pending_discovery->discover.bd_addr.ToString().c_str());
fixed_queue_enqueue(bta_dm_search_cb.pending_discovery_queue,
p_pending_discovery);
}
@@ -1507,7 +1517,10 @@
******************************************************************************/
void bta_dm_search_clear_queue() {
osi_free_and_reset((void**)&bta_dm_search_cb.p_pending_search);
- fixed_queue_flush(bta_dm_search_cb.pending_discovery_queue, osi_free);
+ if (bluetooth::common::InitFlags::
+ IsBtmDmFlushDiscoveryQueueOnSearchCancel()) {
+ fixed_queue_flush(bta_dm_search_cb.pending_discovery_queue, osi_free);
+ }
}
/*******************************************************************************
@@ -1747,6 +1760,8 @@
if (transport == BT_TRANSPORT_LE) {
if (bta_dm_search_cb.services_to_search & BTA_BLE_SERVICE_MASK) {
+ LOG_INFO("bta_dm_discovery: starting GATT discovery on %s",
+ PRIVATE_ADDRESS(bta_dm_search_cb.peer_bdaddr));
// set the raw data buffer here
memset(g_disc_raw_data_buf, 0, sizeof(g_disc_raw_data_buf));
/* start GATT for service discovery */
@@ -1754,6 +1769,8 @@
return;
}
} else {
+ LOG_INFO("bta_dm_discovery: starting SDP discovery on %s",
+ PRIVATE_ADDRESS(bta_dm_search_cb.peer_bdaddr));
bta_dm_search_cb.sdp_results = false;
bta_dm_find_services(bta_dm_search_cb.peer_bdaddr);
return;
@@ -2075,7 +2092,8 @@
static uint8_t bta_dm_new_link_key_cback(const RawAddress& bd_addr,
UNUSED_ATTR DEV_CLASS dev_class,
tBTM_BD_NAME bd_name,
- const LinkKey& key, uint8_t key_type) {
+ const LinkKey& key, uint8_t key_type,
+ bool is_ctkd) {
tBTA_DM_SEC sec_event;
tBTA_DM_AUTH_CMPL* p_auth_cmpl;
tBTA_DM_SEC_EVT event = BTA_DM_AUTH_CMPL_EVT;
@@ -2092,6 +2110,8 @@
p_auth_cmpl->key_type = key_type;
p_auth_cmpl->success = true;
p_auth_cmpl->key = key;
+ p_auth_cmpl->is_ctkd = is_ctkd;
+
sec_event.auth_cmpl.fail_reason = HCI_SUCCESS;
// Report the BR link key based on the BR/EDR address and type
diff --git a/system/bta/gatt/bta_gattc_act.cc b/system/bta/gatt/bta_gattc_act.cc
index ecb9a87..2026291 100644
--- a/system/bta/gatt/bta_gattc_act.cc
+++ b/system/bta/gatt/bta_gattc_act.cc
@@ -711,7 +711,7 @@
/** Configure MTU size on the GATT connection */
void bta_gattc_cfg_mtu(tBTA_GATTC_CLCB* p_clcb, const tBTA_GATTC_DATA* p_data) {
- if (!bta_gattc_enqueue(p_clcb, p_data)) return;
+ if (bta_gattc_enqueue(p_clcb, p_data) == ENQUEUED_FOR_LATER) return;
tGATT_STATUS status =
GATTC_ConfigureMTU(p_clcb->bta_conn_id, p_data->api_mtu.mtu);
@@ -723,6 +723,7 @@
bta_gattc_cmpl_sendmsg(p_clcb->bta_conn_id, GATTC_OPTYPE_CONFIG, status,
NULL);
+ bta_gattc_continue(p_clcb);
}
}
@@ -855,6 +856,8 @@
* referenced by p_clcb->p_q_cmd
*/
if (p_q_cmd != p_clcb->p_q_cmd) osi_free_and_reset((void**)&p_q_cmd);
+ } else {
+ bta_gattc_continue(p_clcb);
}
if (p_clcb->p_rcb->p_cback) {
@@ -866,7 +869,7 @@
/** Read an attribute */
void bta_gattc_read(tBTA_GATTC_CLCB* p_clcb, const tBTA_GATTC_DATA* p_data) {
- if (!bta_gattc_enqueue(p_clcb, p_data)) return;
+ if (bta_gattc_enqueue(p_clcb, p_data) == ENQUEUED_FOR_LATER) return;
tGATT_STATUS status;
if (p_data->api_read.handle != 0) {
@@ -893,13 +896,14 @@
bta_gattc_cmpl_sendmsg(p_clcb->bta_conn_id, GATTC_OPTYPE_READ, status,
NULL);
+ bta_gattc_continue(p_clcb);
}
}
/** read multiple */
void bta_gattc_read_multi(tBTA_GATTC_CLCB* p_clcb,
const tBTA_GATTC_DATA* p_data) {
- if (!bta_gattc_enqueue(p_clcb, p_data)) return;
+ if (bta_gattc_enqueue(p_clcb, p_data) == ENQUEUED_FOR_LATER) return;
tGATT_READ_PARAM read_param;
memset(&read_param, 0, sizeof(tGATT_READ_PARAM));
@@ -918,12 +922,13 @@
bta_gattc_cmpl_sendmsg(p_clcb->bta_conn_id, GATTC_OPTYPE_READ, status,
NULL);
+ bta_gattc_continue(p_clcb);
}
}
/** Write an attribute */
void bta_gattc_write(tBTA_GATTC_CLCB* p_clcb, const tBTA_GATTC_DATA* p_data) {
- if (!bta_gattc_enqueue(p_clcb, p_data)) return;
+ if (bta_gattc_enqueue(p_clcb, p_data) == ENQUEUED_FOR_LATER) return;
tGATT_STATUS status = GATT_SUCCESS;
tGATT_VALUE attr;
@@ -947,12 +952,13 @@
bta_gattc_cmpl_sendmsg(p_clcb->bta_conn_id, GATTC_OPTYPE_WRITE, status,
NULL);
+ bta_gattc_continue(p_clcb);
}
}
/** send execute write */
void bta_gattc_execute(tBTA_GATTC_CLCB* p_clcb, const tBTA_GATTC_DATA* p_data) {
- if (!bta_gattc_enqueue(p_clcb, p_data)) return;
+ if (bta_gattc_enqueue(p_clcb, p_data) == ENQUEUED_FOR_LATER) return;
tGATT_STATUS status =
GATTC_ExecuteWrite(p_clcb->bta_conn_id, p_data->api_exec.is_execute);
@@ -962,6 +968,7 @@
bta_gattc_cmpl_sendmsg(p_clcb->bta_conn_id, GATTC_OPTYPE_EXE_WRITE, status,
NULL);
+ bta_gattc_continue(p_clcb);
}
}
diff --git a/system/bta/gatt/bta_gattc_cache.cc b/system/bta/gatt/bta_gattc_cache.cc
index a477e1f..0123908 100644
--- a/system/bta/gatt/bta_gattc_cache.cc
+++ b/system/bta/gatt/bta_gattc_cache.cc
@@ -313,6 +313,7 @@
!GATT_HANDLE_IS_VALID(end_handle)) {
LOG(ERROR) << "invalid start_handle=" << loghex(start_handle)
<< ", end_handle=" << loghex(end_handle);
+ p_sdp_rec = SDP_FindServiceInDb(cb_data->p_sdp_db, 0, p_sdp_rec);
continue;
}
diff --git a/system/bta/gatt/bta_gattc_int.h b/system/bta/gatt/bta_gattc_int.h
index 1df5e53..2cdc5c5 100644
--- a/system/bta/gatt/bta_gattc_int.h
+++ b/system/bta/gatt/bta_gattc_int.h
@@ -25,6 +25,7 @@
#define BTA_GATTC_INT_H
#include <cstdint>
+#include <deque>
#include "bt_target.h" // Must be first to define build configuration
#include "bta/gatt/database.h"
@@ -254,6 +255,7 @@
tBTA_GATTC_RCB* p_rcb; /* pointer to the registration CB */
tBTA_GATTC_SERV* p_srcb; /* server cache CB */
const tBTA_GATTC_DATA* p_q_cmd; /* command in queue waiting for execution */
+ std::deque<const tBTA_GATTC_DATA*> p_q_cmd_queue;
// request during discover state
#define BTA_GATTC_DISCOVER_REQ_NONE 0
@@ -423,8 +425,16 @@
extern tBTA_GATTC_CLCB* bta_gattc_find_int_conn_clcb(tBTA_GATTC_DATA* p_msg);
extern tBTA_GATTC_CLCB* bta_gattc_find_int_disconn_clcb(tBTA_GATTC_DATA* p_msg);
-extern bool bta_gattc_enqueue(tBTA_GATTC_CLCB* p_clcb,
- const tBTA_GATTC_DATA* p_data);
+enum BtaEnqueuedResult_t {
+ ENQUEUED_READY_TO_SEND,
+ ENQUEUED_FOR_LATER,
+};
+
+extern BtaEnqueuedResult_t bta_gattc_enqueue(tBTA_GATTC_CLCB* p_clcb,
+ const tBTA_GATTC_DATA* p_data);
+extern bool bta_gattc_is_data_queued(tBTA_GATTC_CLCB* p_clcb,
+ const tBTA_GATTC_DATA* p_data);
+extern void bta_gattc_continue(tBTA_GATTC_CLCB* p_clcb);
extern bool bta_gattc_check_notif_registry(tBTA_GATTC_RCB* p_clreg,
tBTA_GATTC_SERV* p_srcb,
diff --git a/system/bta/gatt/bta_gattc_main.cc b/system/bta/gatt/bta_gattc_main.cc
index bba20b5..b624c5a 100644
--- a/system/bta/gatt/bta_gattc_main.cc
+++ b/system/bta/gatt/bta_gattc_main.cc
@@ -328,7 +328,7 @@
action = state_table[event][i];
if (action != BTA_GATTC_IGNORE) {
(*bta_gattc_action[action])(p_clcb, p_data);
- if (p_clcb->p_q_cmd == p_data) {
+ if (bta_gattc_is_data_queued(p_clcb, p_data)) {
/* buffer is queued, don't free in the bta dispatcher.
* we free it ourselves when a completion event is received.
*/
diff --git a/system/bta/gatt/bta_gattc_utils.cc b/system/bta/gatt/bta_gattc_utils.cc
index 1412702..a3ff62d 100644
--- a/system/bta/gatt/bta_gattc_utils.cc
+++ b/system/bta/gatt/bta_gattc_utils.cc
@@ -24,6 +24,8 @@
#define LOG_TAG "bt_bta_gattc"
+#include <base/logging.h>
+
#include <cstdint>
#include "bt_target.h" // Must be first to define build configuration
@@ -31,12 +33,11 @@
#include "device/include/controller.h"
#include "gd/common/init_flags.h"
#include "osi/include/allocator.h"
+#include "osi/include/log.h"
#include "types/bt_transport.h"
#include "types/hci_role.h"
#include "types/raw_address.h"
-#include <base/logging.h>
-
static uint8_t ble_acceptlist_size() {
const controller_t* controller = controller_get_interface();
if (!controller->supports_ble()) {
@@ -146,6 +147,7 @@
p_clcb->status = GATT_SUCCESS;
p_clcb->transport = transport;
p_clcb->bda = remote_bda;
+ p_clcb->p_q_cmd = NULL;
p_clcb->p_rcb = bta_gattc_cl_get_regcb(client_if);
@@ -217,8 +219,29 @@
p_srcb->gatt_database.Clear();
}
- osi_free_and_reset((void**)&p_clcb->p_q_cmd);
- memset(p_clcb, 0, sizeof(tBTA_GATTC_CLCB));
+ while (!p_clcb->p_q_cmd_queue.empty()) {
+ auto p_q_cmd = p_clcb->p_q_cmd_queue.front();
+ p_clcb->p_q_cmd_queue.pop_front();
+ osi_free_and_reset((void**)&p_q_cmd);
+ }
+
+ if (p_clcb->p_q_cmd != NULL) {
+ osi_free_and_reset((void**)&p_clcb->p_q_cmd);
+ }
+
+ /* Clear p_clcb. Some of the fields are already reset e.g. p_q_cmd_queue and
+ * p_q_cmd. */
+ p_clcb->bta_conn_id = 0;
+ p_clcb->bda = {};
+ p_clcb->transport = 0;
+ p_clcb->p_rcb = NULL;
+ p_clcb->p_srcb = NULL;
+ p_clcb->request_during_discovery = 0;
+ p_clcb->auto_update = 0;
+ p_clcb->disc_active = 0;
+ p_clcb->in_use = 0;
+ p_clcb->state = BTA_GATTC_IDLE_ST;
+ p_clcb->status = GATT_SUCCESS;
}
/*******************************************************************************
@@ -315,24 +338,57 @@
}
return p_tcb;
}
+
+void bta_gattc_continue(tBTA_GATTC_CLCB* p_clcb) {
+ if (p_clcb->p_q_cmd != NULL) {
+ LOG_INFO("Already scheduled another request for conn_id = 0x%04x",
+ p_clcb->bta_conn_id);
+ return;
+ }
+
+ if (p_clcb->p_q_cmd_queue.empty()) {
+ LOG_INFO("Nothing to do for conn_id = 0x%04x", p_clcb->bta_conn_id);
+ return;
+ }
+
+ const tBTA_GATTC_DATA* p_q_cmd = p_clcb->p_q_cmd_queue.front();
+ p_clcb->p_q_cmd_queue.pop_front();
+ bta_gattc_sm_execute(p_clcb, p_q_cmd->hdr.event, p_q_cmd);
+}
+
+bool bta_gattc_is_data_queued(tBTA_GATTC_CLCB* p_clcb,
+ const tBTA_GATTC_DATA* p_data) {
+ if (p_clcb->p_q_cmd == p_data) {
+ return true;
+ }
+
+ auto it = std::find(p_clcb->p_q_cmd_queue.begin(),
+ p_clcb->p_q_cmd_queue.end(), p_data);
+ return it != p_clcb->p_q_cmd_queue.end();
+}
/*******************************************************************************
*
* Function bta_gattc_enqueue
*
* Description enqueue a client request in clcb.
*
- * Returns success or failure.
+ * Returns BtaEnqueuedResult_t
*
******************************************************************************/
-bool bta_gattc_enqueue(tBTA_GATTC_CLCB* p_clcb, const tBTA_GATTC_DATA* p_data) {
+BtaEnqueuedResult_t bta_gattc_enqueue(tBTA_GATTC_CLCB* p_clcb,
+ const tBTA_GATTC_DATA* p_data) {
if (p_clcb->p_q_cmd == NULL) {
p_clcb->p_q_cmd = p_data;
- return true;
+ return ENQUEUED_READY_TO_SEND;
}
- LOG(ERROR) << __func__ << ": already has a pending command";
- /* skip the callback now. ----- need to send callback ? */
- return false;
+ LOG_INFO(
+ "Already has a pending command to executer. Queuing for later %s conn "
+ "id=0x%04x",
+ p_clcb->bda.ToString().c_str(), p_clcb->bta_conn_id);
+ p_clcb->p_q_cmd_queue.push_back(p_data);
+
+ return ENQUEUED_FOR_LATER;
}
/*******************************************************************************
diff --git a/system/bta/include/bta_api.h b/system/bta/include/bta_api.h
index 98182f4..18a71f3 100644
--- a/system/bta/include/bta_api.h
+++ b/system/bta/include/bta_api.h
@@ -304,6 +304,7 @@
fail_reason; /* The HCI reason/error code for when success=false */
tBLE_ADDR_TYPE addr_type; /* Peer device address type */
tBT_DEVICE_TYPE dev_type;
+ bool is_ctkd; /* True if key is derived using CTKD procedure */
} tBTA_DM_AUTH_CMPL;
/* Structure associated with BTA_DM_LINK_UP_EVT */
diff --git a/system/bta/le_audio/client.cc b/system/bta/le_audio/client.cc
index 9f6ff28..948efcd 100644
--- a/system/bta/le_audio/client.cc
+++ b/system/bta/le_audio/client.cc
@@ -18,6 +18,8 @@
#include <base/bind.h>
#include <base/strings/string_number_conversions.h>
+#include <deque>
+
#include "advertise_data_parser.h"
#include "audio_hal_interface/le_audio_software.h"
#include "bta/csis/csis_types.h"
@@ -66,6 +68,7 @@
using bluetooth::le_audio::GroupStreamStatus;
using le_audio::CodecManager;
using le_audio::ContentControlIdKeeper;
+using le_audio::DeviceConnectState;
using le_audio::LeAudioDevice;
using le_audio::LeAudioDeviceGroup;
using le_audio::LeAudioDeviceGroups;
@@ -471,7 +474,7 @@
if (group_id == bluetooth::groups::kGroupUnknown) return;
LOG(INFO) << __func__ << "Set member adding ...";
- leAudioDevices_.Add(address, true);
+ leAudioDevices_.Add(address, DeviceConnectState::CONNECTING_BY_USER);
leAudioDevice = leAudioDevices_.FindByAddress(address);
} else {
if (leAudioDevice->group_id_ != bluetooth::groups::kGroupUnknown) {
@@ -739,13 +742,15 @@
return false;
}
+ if (group->GetState() != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
+ stream_setup_start_timestamp_ =
+ bluetooth::common::time_get_os_boottime_us();
+ }
+
bool result = groupStateMachine_->StartStream(
group, static_cast<LeAudioContextType>(final_context_type),
adjusted_metadata_context_type,
GetAllCcids(adjusted_metadata_context_type));
- if (result)
- stream_setup_start_timestamp_ =
- bluetooth::common::time_get_os_boottime_us();
return result;
}
@@ -974,6 +979,7 @@
} else {
/* In case there was an active group. Stop the stream */
GroupStop(active_group_id_);
+ callbacks_->OnGroupStatus(active_group_id_, GroupStatus::INACTIVE);
}
active_group_id_ = group_id;
@@ -988,7 +994,7 @@
if (leAudioDevice->conn_id_ != GATT_INVALID_CONN_ID) {
Disconnect(address);
- leAudioDevice->removing_device_ = true;
+ leAudioDevice->SetConnectionState(DeviceConnectState::REMOVING);
return;
}
@@ -1007,9 +1013,10 @@
void Connect(const RawAddress& address) override {
LeAudioDevice* leAudioDevice = leAudioDevices_.FindByAddress(address);
if (!leAudioDevice) {
- leAudioDevices_.Add(address, true);
+ leAudioDevices_.Add(address, DeviceConnectState::CONNECTING_BY_USER);
+
} else {
- leAudioDevice->connecting_actively_ = true;
+ leAudioDevice->SetConnectionState(DeviceConnectState::CONNECTING_BY_USER);
le_audio::MetricsCollector::Get()->OnConnectionStateChanged(
leAudioDevice->group_id_, address, ConnectionState::CONNECTING,
@@ -1058,7 +1065,7 @@
source_audio_location, sink_supported_context_types,
source_supported_context_types);
- leAudioDevices_.Add(address, false);
+ leAudioDevices_.Add(address, DeviceConnectState::DISCONNECTED);
leAudioDevice = leAudioDevices_.FindByAddress(address);
int group_id = DeviceGroups::Get()->GetGroupId(
@@ -1073,6 +1080,10 @@
le_audio::types::kLeAudioDirectionSink;
}
+ callbacks_->OnSinkAudioLocationAvailable(
+ leAudioDevice->address_,
+ leAudioDevice->snk_audio_locations_.to_ulong());
+
leAudioDevice->src_audio_locations_ = source_audio_location;
if (source_audio_location != 0) {
leAudioDevice->audio_directions_ |=
@@ -1105,6 +1116,9 @@
}
if (autoconnect) {
+ leAudioDevice->SetConnectionState(
+ DeviceConnectState::CONNECTING_AUTOCONNECT);
+ leAudioDevice->autoconnect_flag_ = true;
BTA_GATTC_Open(gatt_if_, address, false, false);
}
}
@@ -1150,6 +1164,8 @@
<< " to background connect to connected group: "
<< leAudioDevice->group_id_;
+ leAudioDevice->SetConnectionState(
+ DeviceConnectState::CONNECTING_AUTOCONNECT);
BTA_GATTC_Open(gatt_if_, leAudioDevice->address_, false, false);
}
@@ -1163,9 +1179,9 @@
}
/* cancel pending direct connect */
- if (leAudioDevice->connecting_actively_) {
+ if (leAudioDevice->GetConnectionState() ==
+ DeviceConnectState::CONNECTING_BY_USER) {
BTA_GATTC_CancelOpen(gatt_if_, address, true);
- leAudioDevice->connecting_actively_ = false;
}
/* Removes all registrations for connection */
@@ -1175,6 +1191,7 @@
/* User is disconnecting the device, we shall remove the autoconnect flag
*/
btif_storage_set_leaudio_autoconnect(address, false);
+ leAudioDevice->autoconnect_flag_ = false;
auto group = aseGroups_.FindById(leAudioDevice->group_id_);
if (group &&
@@ -1200,6 +1217,8 @@
return;
}
+ leAudioDevice->SetConnectionState(DeviceConnectState::DISCONNECTING);
+
if (acl_force_disconnect) {
leAudioDevice->DisconnectAcl();
return;
@@ -1509,7 +1528,13 @@
if (status != GATT_SUCCESS) {
/* autoconnect connection failed, that's ok */
- if (!leAudioDevice->connecting_actively_) return;
+ if (leAudioDevice->GetConnectionState() ==
+ DeviceConnectState::CONNECTING_AUTOCONNECT) {
+ leAudioDevice->SetConnectionState(DeviceConnectState::DISCONNECTED);
+ return;
+ }
+
+ leAudioDevice->SetConnectionState(DeviceConnectState::DISCONNECTED);
LOG(ERROR) << "Failed to connect to LeAudio leAudioDevice, status: "
<< +status;
@@ -1527,7 +1552,15 @@
BTM_RequestPeerSCA(leAudioDevice->address_, transport);
- leAudioDevice->connecting_actively_ = false;
+ if (leAudioDevice->GetConnectionState() ==
+ DeviceConnectState::CONNECTING_AUTOCONNECT) {
+ leAudioDevice->SetConnectionState(
+ DeviceConnectState::CONNECTED_AUTOCONNECT_GETTING_READY);
+ } else {
+ leAudioDevice->SetConnectionState(
+ DeviceConnectState::CONNECTED_BY_USER_GETTING_READY);
+ }
+
leAudioDevice->conn_id_ = conn_id;
leAudioDevice->mtu_ = mtu;
@@ -1545,13 +1578,8 @@
}
if (BTM_IsLinkKeyKnown(address, BT_TRANSPORT_LE)) {
- int result = BTM_SetEncryption(
- address, BT_TRANSPORT_LE,
- [](const RawAddress* bd_addr, tBT_TRANSPORT transport,
- void* p_ref_data, tBTM_STATUS status) {
- if (instance) instance->OnEncryptionComplete(*bd_addr, status);
- },
- nullptr, BTM_BLE_SEC_ENCRYPT);
+ int result = BTM_SetEncryption(address, BT_TRANSPORT_LE, nullptr, nullptr,
+ BTM_BLE_SEC_ENCRYPT);
LOG(INFO) << __func__
<< "Encryption required. Request result: " << result;
@@ -1633,13 +1661,22 @@
if (status != BTM_SUCCESS) {
LOG(ERROR) << "Encryption failed"
<< " status: " << int{status};
- BTA_GATTC_Close(leAudioDevice->conn_id_);
- if (leAudioDevice->connecting_actively_) {
+ if (leAudioDevice->GetConnectionState() ==
+ DeviceConnectState::CONNECTED_BY_USER_GETTING_READY) {
callbacks_->OnConnectionState(ConnectionState::DISCONNECTED, address);
le_audio::MetricsCollector::Get()->OnConnectionStateChanged(
leAudioDevice->group_id_, address, ConnectionState::CONNECTED,
le_audio::ConnectionStatus::FAILED);
}
+
+ leAudioDevice->SetConnectionState(DeviceConnectState::DISCONNECTING);
+
+ BTA_GATTC_Close(leAudioDevice->conn_id_);
+ return;
+ }
+
+ if (leAudioDevice->encrypted_) {
+ LOG(INFO) << __func__ << " link already encrypted, nothing to do";
return;
}
@@ -1649,11 +1686,6 @@
if (leAudioDevice->known_service_handles_)
RegisterKnownNotifications(leAudioDevice);
- if (leAudioDevice->encrypted_) {
- LOG(INFO) << __func__ << " link already encrypted, nothing to do";
- return;
- }
-
leAudioDevice->encrypted_ = true;
/* If we know services and read is not ongoing, this is reconnection and
@@ -1678,6 +1710,7 @@
return;
}
+ BtaGattQueue::Clean(leAudioDevice->conn_id_);
LeAudioDeviceGroup* group = aseGroups_.FindById(leAudioDevice->group_id_);
groupStateMachine_->ProcessHciNotifAclDisconnected(group, leAudioDevice);
@@ -1694,7 +1727,7 @@
leAudioDevice->group_id_, address, ConnectionState::DISCONNECTED,
le_audio::ConnectionStatus::SUCCESS);
- if (leAudioDevice->removing_device_) {
+ if (leAudioDevice->GetConnectionState() == DeviceConnectState::REMOVING) {
if (leAudioDevice->group_id_ != bluetooth::groups::kGroupUnknown) {
auto group = aseGroups_.FindById(leAudioDevice->group_id_);
group_remove_node(group, address, true);
@@ -1704,7 +1737,11 @@
}
/* Attempt background re-connect if disconnect was not intended locally */
if (reason != GATT_CONN_TERMINATE_LOCAL_HOST) {
+ leAudioDevice->SetConnectionState(
+ DeviceConnectState::CONNECTING_AUTOCONNECT);
BTA_GATTC_Open(gatt_if_, address, false, false);
+ } else {
+ leAudioDevice->SetConnectionState(DeviceConnectState::DISCONNECTED);
}
}
@@ -1782,6 +1819,11 @@
return;
}
+ if (!leAudioDevice->encrypted_) {
+ LOG_DEBUG("Wait for device to be encrypted");
+ return;
+ }
+
if (!leAudioDevice->known_service_handles_)
BTA_GATTC_ServiceSearchRequest(
leAudioDevice->conn_id_,
@@ -2223,6 +2265,8 @@
* codec configuration */
group->SetPendingConfiguration();
groupStateMachine_->StopStream(group);
+ stream_setup_start_timestamp_ =
+ bluetooth::common::time_get_os_boottime_us();
return;
}
@@ -2230,6 +2274,9 @@
LOG_WARN("Could not add device %s to the group %d streaming. ",
leAudioDevice->address_.ToString().c_str(), group->group_id_);
scheduleAttachDeviceToTheStream(leAudioDevice->address_);
+ } else {
+ stream_setup_start_timestamp_ =
+ bluetooth::common::time_get_os_boottime_us();
}
}
@@ -2258,22 +2305,29 @@
}
void connectionReady(LeAudioDevice* leAudioDevice) {
+ LOG_DEBUG("%s, %s", leAudioDevice->address_.ToString().c_str(),
+ bluetooth::common::ToString(leAudioDevice->GetConnectionState())
+ .c_str());
callbacks_->OnConnectionState(ConnectionState::CONNECTED,
leAudioDevice->address_);
+ if (leAudioDevice->GetConnectionState() ==
+ DeviceConnectState::CONNECTED_BY_USER_GETTING_READY &&
+ (leAudioDevice->autoconnect_flag_ == false)) {
+ btif_storage_set_leaudio_autoconnect(leAudioDevice->address_, true);
+ leAudioDevice->autoconnect_flag_ = true;
+ }
+
+ leAudioDevice->SetConnectionState(DeviceConnectState::CONNECTED);
+ le_audio::MetricsCollector::Get()->OnConnectionStateChanged(
+ leAudioDevice->group_id_, leAudioDevice->address_,
+ ConnectionState::CONNECTED, le_audio::ConnectionStatus::SUCCESS);
+
if (leAudioDevice->group_id_ != bluetooth::groups::kGroupUnknown) {
LeAudioDeviceGroup* group = aseGroups_.FindById(leAudioDevice->group_id_);
UpdateContextAndLocations(group, leAudioDevice);
AttachToStreamingGroupIfNeeded(leAudioDevice);
}
-
- if (leAudioDevice->first_connection_) {
- btif_storage_set_leaudio_autoconnect(leAudioDevice->address_, true);
- leAudioDevice->first_connection_ = false;
- }
- le_audio::MetricsCollector::Get()->OnConnectionStateChanged(
- leAudioDevice->group_id_, leAudioDevice->address_,
- ConnectionState::CONNECTED, le_audio::ConnectionStatus::SUCCESS);
}
bool IsAseAcceptingAudioData(struct ase* ase) {
@@ -2838,16 +2892,16 @@
std::stringstream stream;
if (print_audio_state) {
if (sender) {
- stream << " audio sender state: " << audio_sender_state_ << "\n";
+ stream << "\taudio sender state: " << audio_sender_state_ << "\n";
} else {
- stream << " audio receiver state: " << audio_receiver_state_ << "\n";
+ stream << "\taudio receiver state: " << audio_receiver_state_ << "\n";
}
}
- stream << " num_channels: " << +conf->num_channels << "\n"
- << " sample rate: " << +conf->sample_rate << "\n"
- << " bits pers sample: " << +conf->bits_per_sample << "\n"
- << " data_interval_us: " << +conf->data_interval_us << "\n";
+ stream << "\tsample rate: " << +conf->sample_rate
+ << ",\tchan: " << +conf->num_channels
+ << ",\tbits: " << +conf->bits_per_sample
+ << ",\tdata_interval_us: " << +conf->data_interval_us << "\n";
dprintf(fd, "%s", stream.str().c_str());
}
@@ -2880,17 +2934,27 @@
void Dump(int fd) {
dprintf(fd, " Active group: %d\n", active_group_id_);
- dprintf(fd, " configuration content type: 0x%08hx\n",
+ dprintf(fd, " configuration: %s (0x%08hx)\n",
+ bluetooth::common::ToString(configuration_context_type_).c_str(),
configuration_context_type_);
+ dprintf(fd, " metadata context type mask: ");
+
+ for (auto ctx : le_audio::types::kLeAudioContextAllTypesArray) {
+ if (static_cast<uint16_t>(ctx) & metadata_context_types_.to_ulong()) {
+ dprintf(fd, "%s, ", bluetooth::common::ToString(ctx).c_str());
+ }
+ }
+ dprintf(fd, "\n");
dprintf(fd, " TBS state: %s\n", in_call_ ? " In call" : "No calls");
- dprintf(
- fd, " stream setup time if started: %d ms\n",
- (int)((stream_setup_end_timestamp_ - stream_setup_start_timestamp_) /
- 1000));
+ dprintf(fd, " Start time: ");
+ for (auto t : stream_start_history_queue_) {
+ dprintf(fd, ", %d ms", static_cast<int>(t));
+ }
+ dprintf(fd, "\n");
printCurrentStreamConfiguration(fd);
dprintf(fd, " ----------------\n ");
dprintf(fd, " LE Audio Groups:\n");
- aseGroups_.Dump(fd);
+ aseGroups_.Dump(fd, active_group_id_);
dprintf(fd, "\n Not grouped devices:\n");
leAudioDevices_.Dump(fd, bluetooth::groups::kGroupUnknown);
}
@@ -3827,6 +3891,23 @@
}
}
+ void take_stream_time(void) {
+ if (stream_setup_start_timestamp_ == 0) {
+ return;
+ }
+
+ if (stream_start_history_queue_.size() == 10) {
+ stream_start_history_queue_.pop_back();
+ }
+
+ stream_setup_end_timestamp_ = bluetooth::common::time_get_os_boottime_us();
+ stream_start_history_queue_.emplace_front(
+ (stream_setup_end_timestamp_ - stream_setup_start_timestamp_) / 1000);
+
+ stream_setup_end_timestamp_ = 0;
+ stream_setup_start_timestamp_ = 0;
+ }
+
void StatusReportCb(int group_id, GroupStreamStatus status) {
LOG_INFO("status: %d , audio_sender_state %s, audio_receiver_state %s",
static_cast<int>(status),
@@ -3845,8 +3926,8 @@
if (audio_receiver_state_ == AudioState::READY_TO_START)
StartReceivingAudio(group_id);
- stream_setup_end_timestamp_ =
- bluetooth::common::time_get_os_boottime_us();
+ take_stream_time();
+
le_audio::MetricsCollector::Get()->OnStreamStarted(
active_group_id_, configuration_context_type_);
break;
@@ -3884,8 +3965,6 @@
*/
FALLTHROUGH;
case GroupStreamStatus::IDLE: {
- stream_setup_end_timestamp_ = 0;
- stream_setup_start_timestamp_ = 0;
if (group && group->IsPendingConfiguration()) {
SuspendedForReconfiguration();
auto adjusted_metedata_context_type =
@@ -3898,6 +3977,8 @@
return;
}
}
+ stream_setup_end_timestamp_ = 0;
+ stream_setup_start_timestamp_ = 0;
CancelStreamingRequest();
if (group) {
NotifyUpperLayerGroupTurnedIdleDuringCall(group->group_id_);
@@ -3933,6 +4014,7 @@
AudioContexts metadata_context_types_;
uint64_t stream_setup_start_timestamp_;
uint64_t stream_setup_end_timestamp_;
+ std::deque<uint64_t> stream_start_history_queue_;
/* Microphone (s) */
AudioState audio_receiver_state_;
diff --git a/system/bta/le_audio/devices.cc b/system/bta/le_audio/devices.cc
index 76c34e2..ca423ed 100644
--- a/system/bta/le_audio/devices.cc
+++ b/system/bta/le_audio/devices.cc
@@ -54,6 +54,42 @@
using le_audio::types::LeAudioLc3Config;
namespace le_audio {
+std::ostream& operator<<(std::ostream& os, const DeviceConnectState& state) {
+ const char* char_value_ = "UNKNOWN";
+
+ switch (state) {
+ case DeviceConnectState::CONNECTED:
+ char_value_ = "CONNECTED";
+ break;
+ case DeviceConnectState::DISCONNECTED:
+ char_value_ = "DISCONNECTED";
+ break;
+ case DeviceConnectState::REMOVING:
+ char_value_ = "REMOVING";
+ break;
+ case DeviceConnectState::DISCONNECTING:
+ char_value_ = "DISCONNECTING";
+ break;
+ case DeviceConnectState::CONNECTING_BY_USER:
+ char_value_ = "CONNECTING_BY_USER";
+ break;
+ case DeviceConnectState::CONNECTED_BY_USER_GETTING_READY:
+ char_value_ = "CONNECTED_BY_USER_GETTING_READY";
+ break;
+ case DeviceConnectState::CONNECTING_AUTOCONNECT:
+ char_value_ = "CONNECTING_AUTOCONNECT";
+ break;
+ case DeviceConnectState::CONNECTED_AUTOCONNECT_GETTING_READY:
+ char_value_ = "CONNECTED_AUTOCONNECT_GETTING_READY";
+ break;
+ }
+
+ os << char_value_ << " ("
+ << "0x" << std::setfill('0') << std::setw(2) << static_cast<int>(state)
+ << ")";
+ return os;
+}
+
/* LeAudioDeviceGroup Class methods implementation */
void LeAudioDeviceGroup::AddNode(
const std::shared_ptr<LeAudioDevice>& leAudioDevice) {
@@ -973,7 +1009,11 @@
LOG_INFO("device: %s", leAudioDevice->address_.ToString().c_str());
struct ase* ase = leAudioDevice->GetFirstActiveAse();
- ASSERT_LOG(ase, " Shouldn't be called without an active ASE");
+ if (!ase) {
+ LOG_ERROR(" Device %s shouldn't be called without an active ASE",
+ leAudioDevice->address_.ToString().c_str());
+ return false;
+ }
for (; ase != nullptr; ase = leAudioDevice->GetNextActiveAse(ase)) {
uint8_t cis_id = kInvalidCisId;
@@ -1536,8 +1576,16 @@
continue;
}
- /* For the moment, we configure only connected devices. */
- if (device->conn_id_ == GATT_INVALID_CONN_ID) continue;
+ /* For the moment, we configure only connected devices and when it is
+ * ready to stream i.e. All ASEs are discovered and device is reported as
+ * connected
+ */
+ if (device->GetConnectionState() != DeviceConnectState::CONNECTED) {
+ LOG_WARN(
+ "Device %s, in the state %s", device->address_.ToString().c_str(),
+ bluetooth::common::ToString(device->GetConnectionState()).c_str());
+ continue;
+ }
if (!device->ConfigureAses(ent, context_type, &active_ase_num,
group_snk_audio_locations,
@@ -1859,67 +1907,72 @@
}
LeAudioDeviceGroup::~LeAudioDeviceGroup(void) { this->Cleanup(); }
-void LeAudioDeviceGroup::Dump(int fd) {
+void LeAudioDeviceGroup::Dump(int fd, int active_group_id) {
+ bool is_active = (group_id_ == active_group_id);
std::stringstream stream;
auto* active_conf = GetActiveConfiguration();
+ uint32_t context_type_mask = GetActiveContexts().to_ulong();
- stream << " == Group id: " << group_id_ << " == \n"
- << " state: " << GetState() << "\n"
- << " target state: " << GetTargetState() << "\n"
- << " cig state: " << cig_state_ << "\n"
- << " number of devices: " << Size() << "\n"
- << " number of connected devices: " << NumOfConnected() << "\n"
- << " active context types: "
- << loghex(GetActiveContexts().to_ulong()) << "\n"
- << " current context type: "
- << static_cast<int>(GetCurrentContextType()) << "\n"
- << " active stream configuration name: "
+ stream << "\n == Group id: " << group_id_
+ << " == " << (is_active ? ",\tActive\n" : ",\tInactive\n")
+ << " state: " << GetState()
+ << ",\ttarget state: " << GetTargetState()
+ << ",\tcig state: " << cig_state_ << "\n"
+ << " active context type mask: " << context_type_mask;
+
+ for (auto ctx : types::kLeAudioContextAllTypesArray) {
+ if (static_cast<uint16_t>(ctx) & static_cast<uint16_t>(context_type_mask)) {
+ stream << ", " << bluetooth::common::ToString(ctx).c_str();
+ }
+ }
+
+ stream << ",\n current context type: "
+ << bluetooth::common::ToString(GetCurrentContextType()).c_str() << "\n"
+ << " active configuration name: "
<< (active_conf ? active_conf->name : " not set") << "\n"
- << " Last used stream configuration: \n"
- << " pending_configuration: " << stream_conf.pending_configuration
+ << " stream configuration: "
+ << (stream_conf.conf != nullptr ? stream_conf.conf->name : " unknown ")
<< "\n"
- << " codec id : " << +(stream_conf.id.coding_format) << "\n"
- << " name: "
- << (stream_conf.conf != nullptr ? stream_conf.conf->name : " null ")
+ << " codec id: " << +(stream_conf.id.coding_format)
+ << ",\tpending_configuration: " << stream_conf.pending_configuration
<< "\n"
- << " number of sinks in the configuration "
- << stream_conf.sink_num_of_devices << "\n"
- << " number of sink_streams connected: "
- << stream_conf.sink_streams.size() << "\n"
- << " number of sources in the configuration "
- << stream_conf.source_num_of_devices << "\n"
- << " number of source_streams connected: "
- << stream_conf.source_streams.size() << "\n"
+ << " num of devices(connected): " << Size() << "("
+ << NumOfConnected() << ")\n"
+ << ", num of sinks(connected): " << stream_conf.sink_num_of_devices
+ << "(" << stream_conf.sink_streams.size() << ")\n"
+ << " num of sources(connected): "
+ << stream_conf.source_num_of_devices << "("
+ << stream_conf.source_streams.size() << ")\n"
<< " allocated CISes: " << static_cast<int>(cises_.size());
if (cises_.size() > 0) {
- stream << "\n\t === CISes === ";
+ stream << "\n\t == CISes == ";
for (auto cis : cises_) {
stream << "\n\t cis id: " << static_cast<int>(cis.id)
- << ", type: " << static_cast<int>(cis.type)
- << ", conn_handle: " << static_cast<int>(cis.conn_handle)
- << ", addr: " << cis.addr;
+ << ",\ttype: " << static_cast<int>(cis.type)
+ << ",\tconn_handle: " << static_cast<int>(cis.conn_handle)
+ << ",\taddr: " << cis.addr;
}
+ stream << "\n\t ====";
}
if (GetFirstActiveDevice() != nullptr) {
uint32_t sink_delay;
- stream << "\n presentation_delay for sink (speaker): ";
- if (GetPresentationDelay(&sink_delay, le_audio::types::kLeAudioDirectionSink)) {
- stream << sink_delay << " us";
+ if (GetPresentationDelay(&sink_delay,
+ le_audio::types::kLeAudioDirectionSink)) {
+ stream << "\n presentation_delay for sink (speaker): " << sink_delay
+ << " us";
}
- stream << "\n presentation_delay for source (microphone): ";
+
uint32_t source_delay;
- if (GetPresentationDelay(&source_delay, le_audio::types::kLeAudioDirectionSource)) {
- stream << source_delay << " us";
+ if (GetPresentationDelay(&source_delay,
+ le_audio::types::kLeAudioDirectionSource)) {
+ stream << "\n presentation_delay for source (microphone): "
+ << source_delay << " us";
}
- stream << "\n";
- } else {
- stream << "\n presentation_delay for sink (speaker):\n"
- << " presentation_delay for source (microphone): \n";
}
- stream << " === devices: ===";
+ stream << "\n == devices: ==";
dprintf(fd, "%s", stream.str().c_str());
@@ -1929,6 +1982,17 @@
}
/* LeAudioDevice Class methods implementation */
+void LeAudioDevice::SetConnectionState(DeviceConnectState state) {
+ LOG_DEBUG(" %s --> %s",
+ bluetooth::common::ToString(connection_state_).c_str(),
+ bluetooth::common::ToString(state).c_str());
+ connection_state_ = state;
+}
+
+DeviceConnectState LeAudioDevice::GetConnectionState(void) {
+ return connection_state_;
+}
+
void LeAudioDevice::ClearPACs(void) {
snk_pacs_.clear();
src_pacs_.clear();
@@ -2341,32 +2405,29 @@
}
void LeAudioDevice::Dump(int fd) {
+ uint16_t acl_handle = BTM_GetHCIConnHandle(address_, BT_TRANSPORT_LE);
+
std::stringstream stream;
stream << std::boolalpha;
- stream << "\n\taddress: " << address_
- << (conn_id_ == GATT_INVALID_CONN_ID ? "\n\t Not connected "
- : "\n\t Connected conn_id =")
+ stream << "\n\taddress: " << address_ << ": " << connection_state_ << ": "
<< (conn_id_ == GATT_INVALID_CONN_ID ? "" : std::to_string(conn_id_))
- << "\n\t set member: " << csis_member_
- << "\n\t known_service_handles_: " << known_service_handles_
- << "\n\t notify_connected_after_read_: "
- << notify_connected_after_read_
- << "\n\t removing_device_: " << removing_device_
- << "\n\t first_connection_: " << first_connection_
- << "\n\t encrypted_: " << encrypted_
- << "\n\t connecting_actively_: " << connecting_actively_
- << "\n\t number of ases_: " << static_cast<int>(ases_.size());
+ << ", acl_handle: " << std::to_string(acl_handle) << ",\t"
+ << (encrypted_ ? "Encrypted" : "Unecrypted")
+ << ",mtu: " << std::to_string(mtu_)
+ << "\n\tnumber of ases_: " << static_cast<int>(ases_.size());
if (ases_.size() > 0) {
- stream << "\n\t == ASE == ";
+ stream << "\n\t == ASEs == ";
for (auto& ase : ases_) {
stream << "\n\t id: " << static_cast<int>(ase.id)
- << ", active: " << ase.active << ", direction: "
+ << ",\tactive: " << ase.active << ", dir: "
<< (ase.direction == types::kLeAudioDirectionSink ? "sink"
: "source")
- << ", allocated cis id: " << static_cast<int>(ase.cis_id);
+ << ",\tcis_id: " << static_cast<int>(ase.cis_id) << ",\tstate: "
+ << bluetooth::common::ToString(ase.data_path_state);
}
}
+ stream << "\n\t ====";
dprintf(fd, "%s", stream.str().c_str());
}
@@ -2504,9 +2565,9 @@
groups_.clear();
}
-void LeAudioDeviceGroups::Dump(int fd) {
+void LeAudioDeviceGroups::Dump(int fd, int active_group_id) {
for (auto& g : groups_) {
- g->Dump(fd);
+ g->Dump(fd, active_group_id);
}
}
@@ -2534,7 +2595,7 @@
}
/* LeAudioDevices Class methods implementation */
-void LeAudioDevices::Add(const RawAddress& address, bool first_connection,
+void LeAudioDevices::Add(const RawAddress& address, DeviceConnectState state,
int group_id) {
auto device = FindByAddress(address);
if (device != nullptr) {
@@ -2544,7 +2605,7 @@
}
leAudioDevices_.emplace_back(
- std::make_shared<LeAudioDevice>(address, first_connection, group_id));
+ std::make_shared<LeAudioDevice>(address, state, group_id));
}
void LeAudioDevices::Remove(const RawAddress& address) {
diff --git a/system/bta/le_audio/devices.h b/system/bta/le_audio/devices.h
index 90fa87e..1bdd6fc 100644
--- a/system/bta/le_audio/devices.h
+++ b/system/bta/le_audio/devices.h
@@ -34,6 +34,31 @@
#include "raw_address.h"
namespace le_audio {
+
+/* Enums */
+enum class DeviceConnectState : uint8_t {
+ /* Initial state */
+ DISCONNECTED,
+ /* When ACL connected, encrypted, CCC registered and initial characteristics
+ read is completed */
+ CONNECTED,
+ /* Used when device is unbonding (RemoveDevice() API is called) */
+ REMOVING,
+ /* Disconnecting */
+ DISCONNECTING,
+ /* 2 states below are used when user creates connection. Connect API is
+ called. */
+ CONNECTING_BY_USER,
+ /* Always used after CONNECTING_BY_USER */
+ CONNECTED_BY_USER_GETTING_READY,
+ /* 2 states are used when autoconnect was used for the connection.*/
+ CONNECTING_AUTOCONNECT,
+ /* Always used after CONNECTING_AUTOCONNECT */
+ CONNECTED_AUTOCONNECT_GETTING_READY,
+};
+
+std::ostream& operator<<(std::ostream& os, const DeviceConnectState& state);
+
/* Class definitions */
/* LeAudioDevice class represents GATT server device with ASCS, PAC services as
@@ -50,15 +75,11 @@
public:
RawAddress address_;
+ DeviceConnectState connection_state_;
bool known_service_handles_;
bool notify_connected_after_read_;
- bool removing_device_;
-
- /* we are making active attempt to connect to this device, 'direct connect'.
- * This is true only during initial phase of first connection. */
- bool first_connection_;
- bool connecting_actively_;
bool closing_stream_for_disconnection_;
+ bool autoconnect_flag_;
uint16_t conn_id_;
uint16_t mtu_;
bool encrypted_;
@@ -84,15 +105,14 @@
alarm_t* link_quality_timer;
uint16_t link_quality_timer_data;
- LeAudioDevice(const RawAddress& address_, bool first_connection,
+ LeAudioDevice(const RawAddress& address_, DeviceConnectState state,
int group_id = bluetooth::groups::kGroupUnknown)
: address_(address_),
+ connection_state_(state),
known_service_handles_(false),
notify_connected_after_read_(false),
- removing_device_(false),
- first_connection_(first_connection),
- connecting_actively_(first_connection),
closing_stream_for_disconnection_(false),
+ autoconnect_flag_(false),
conn_id_(GATT_INVALID_CONN_ID),
mtu_(0),
encrypted_(false),
@@ -102,6 +122,8 @@
link_quality_timer(nullptr) {}
~LeAudioDevice(void);
+ void SetConnectionState(DeviceConnectState state);
+ DeviceConnectState GetConnectionState(void);
void ClearPACs(void);
void RegisterPACs(std::vector<struct types::acs_ac_record>* apr_db,
std::vector<struct types::acs_ac_record>* apr);
@@ -171,7 +193,7 @@
*/
class LeAudioDevices {
public:
- void Add(const RawAddress& address, bool first_connection,
+ void Add(const RawAddress& address, le_audio::DeviceConnectState state,
int group_id = bluetooth::groups::kGroupUnknown);
void Remove(const RawAddress& address);
LeAudioDevice* FindByAddress(const RawAddress& address);
@@ -335,7 +357,7 @@
bool IsInTransition(void);
bool IsReleasing(void);
- void Dump(int fd);
+ void Dump(int fd, int active_group_id);
private:
uint32_t transport_latency_mtos_us_;
@@ -381,7 +403,7 @@
size_t Size();
bool IsAnyInTransition();
void Cleanup(void);
- void Dump(int fd);
+ void Dump(int fd, int active_group_id);
private:
std::vector<std::unique_ptr<LeAudioDeviceGroup>> groups_;
diff --git a/system/bta/le_audio/devices_test.cc b/system/bta/le_audio/devices_test.cc
index 2077e37..75b6559 100644
--- a/system/bta/le_audio/devices_test.cc
+++ b/system/bta/le_audio/devices_test.cc
@@ -35,6 +35,7 @@
namespace internal {
namespace {
+using ::le_audio::DeviceConnectState;
using ::le_audio::LeAudioDevice;
using ::le_audio::LeAudioDeviceGroup;
using ::le_audio::LeAudioDevices;
@@ -72,23 +73,23 @@
TEST_F(LeAudioDevicesTest, test_add) {
RawAddress test_address_0 = GetTestAddress(0);
ASSERT_EQ((size_t)0, devices_->Size());
- devices_->Add(test_address_0, true);
+ devices_->Add(test_address_0, DeviceConnectState::CONNECTING_BY_USER);
ASSERT_EQ((size_t)1, devices_->Size());
- devices_->Add(GetTestAddress(1), true, 1);
+ devices_->Add(GetTestAddress(1), DeviceConnectState::CONNECTING_BY_USER, 1);
ASSERT_EQ((size_t)2, devices_->Size());
- devices_->Add(test_address_0, true);
+ devices_->Add(test_address_0, DeviceConnectState::CONNECTING_BY_USER);
ASSERT_EQ((size_t)2, devices_->Size());
- devices_->Add(GetTestAddress(1), true, 2);
+ devices_->Add(GetTestAddress(1), DeviceConnectState::CONNECTING_BY_USER, 2);
ASSERT_EQ((size_t)2, devices_->Size());
}
TEST_F(LeAudioDevicesTest, test_remove) {
RawAddress test_address_0 = GetTestAddress(0);
- devices_->Add(test_address_0, true);
+ devices_->Add(test_address_0, DeviceConnectState::CONNECTING_BY_USER);
RawAddress test_address_1 = GetTestAddress(1);
- devices_->Add(test_address_1, true);
+ devices_->Add(test_address_1, DeviceConnectState::CONNECTING_BY_USER);
RawAddress test_address_2 = GetTestAddress(2);
- devices_->Add(test_address_2, true);
+ devices_->Add(test_address_2, DeviceConnectState::CONNECTING_BY_USER);
ASSERT_EQ((size_t)3, devices_->Size());
devices_->Remove(test_address_0);
ASSERT_EQ((size_t)2, devices_->Size());
@@ -100,11 +101,11 @@
TEST_F(LeAudioDevicesTest, test_find_by_address_success) {
RawAddress test_address_0 = GetTestAddress(0);
- devices_->Add(test_address_0, true);
+ devices_->Add(test_address_0, DeviceConnectState::CONNECTING_BY_USER);
RawAddress test_address_1 = GetTestAddress(1);
- devices_->Add(test_address_1, false);
+ devices_->Add(test_address_1, DeviceConnectState::DISCONNECTED);
RawAddress test_address_2 = GetTestAddress(2);
- devices_->Add(test_address_2, true);
+ devices_->Add(test_address_2, DeviceConnectState::CONNECTING_BY_USER);
LeAudioDevice* device = devices_->FindByAddress(test_address_1);
ASSERT_NE(nullptr, device);
ASSERT_EQ(test_address_1, device->address_);
@@ -112,20 +113,20 @@
TEST_F(LeAudioDevicesTest, test_find_by_address_failed) {
RawAddress test_address_0 = GetTestAddress(0);
- devices_->Add(test_address_0, true);
+ devices_->Add(test_address_0, DeviceConnectState::CONNECTING_BY_USER);
RawAddress test_address_2 = GetTestAddress(2);
- devices_->Add(test_address_2, true);
+ devices_->Add(test_address_2, DeviceConnectState::CONNECTING_BY_USER);
LeAudioDevice* device = devices_->FindByAddress(GetTestAddress(1));
ASSERT_EQ(nullptr, device);
}
TEST_F(LeAudioDevicesTest, test_get_by_address_success) {
RawAddress test_address_0 = GetTestAddress(0);
- devices_->Add(test_address_0, true);
+ devices_->Add(test_address_0, DeviceConnectState::CONNECTING_BY_USER);
RawAddress test_address_1 = GetTestAddress(1);
- devices_->Add(test_address_1, false);
+ devices_->Add(test_address_1, DeviceConnectState::DISCONNECTED);
RawAddress test_address_2 = GetTestAddress(2);
- devices_->Add(test_address_2, true);
+ devices_->Add(test_address_2, DeviceConnectState::CONNECTING_BY_USER);
std::shared_ptr<LeAudioDevice> device =
devices_->GetByAddress(test_address_1);
ASSERT_NE(nullptr, device);
@@ -134,28 +135,28 @@
TEST_F(LeAudioDevicesTest, test_get_by_address_failed) {
RawAddress test_address_0 = GetTestAddress(0);
- devices_->Add(test_address_0, true);
+ devices_->Add(test_address_0, DeviceConnectState::CONNECTING_BY_USER);
RawAddress test_address_2 = GetTestAddress(2);
- devices_->Add(test_address_2, true);
+ devices_->Add(test_address_2, DeviceConnectState::CONNECTING_BY_USER);
std::shared_ptr<LeAudioDevice> device =
devices_->GetByAddress(GetTestAddress(1));
ASSERT_EQ(nullptr, device);
}
TEST_F(LeAudioDevicesTest, test_find_by_conn_id_success) {
- devices_->Add(GetTestAddress(1), true);
+ devices_->Add(GetTestAddress(1), DeviceConnectState::CONNECTING_BY_USER);
RawAddress test_address_0 = GetTestAddress(0);
- devices_->Add(test_address_0, true);
- devices_->Add(GetTestAddress(4), true);
+ devices_->Add(test_address_0, DeviceConnectState::CONNECTING_BY_USER);
+ devices_->Add(GetTestAddress(4), DeviceConnectState::CONNECTING_BY_USER);
LeAudioDevice* device = devices_->FindByAddress(test_address_0);
device->conn_id_ = 0x0005;
ASSERT_EQ(device, devices_->FindByConnId(0x0005));
}
TEST_F(LeAudioDevicesTest, test_find_by_conn_id_failed) {
- devices_->Add(GetTestAddress(1), true);
- devices_->Add(GetTestAddress(0), true);
- devices_->Add(GetTestAddress(4), true);
+ devices_->Add(GetTestAddress(1), DeviceConnectState::CONNECTING_BY_USER);
+ devices_->Add(GetTestAddress(0), DeviceConnectState::CONNECTING_BY_USER);
+ devices_->Add(GetTestAddress(4), DeviceConnectState::CONNECTING_BY_USER);
ASSERT_EQ(nullptr, devices_->FindByConnId(0x0006));
}
@@ -412,8 +413,8 @@
int snk_ase_num_cached = 0,
int src_ase_num_cached = 0) {
int index = group_->Size() + 1;
- auto device =
- (std::make_shared<LeAudioDevice>(GetTestAddress(index), false));
+ auto device = (std::make_shared<LeAudioDevice>(
+ GetTestAddress(index), DeviceConnectState::DISCONNECTED));
devices_.push_back(device);
group_->AddNode(device);
@@ -449,6 +450,7 @@
::le_audio::codec_spec_conf::kLeAudioLocationFrontRight;
device->conn_id_ = index;
+ device->SetConnectionState(DeviceConnectState::CONNECTED);
return device.get();
}
diff --git a/system/bta/le_audio/le_audio_types.cc b/system/bta/le_audio/le_audio_types.cc
index f6d03e1..447e131 100644
--- a/system/bta/le_audio/le_audio_types.cc
+++ b/system/bta/le_audio/le_audio_types.cc
@@ -578,6 +578,17 @@
}
namespace types {
+std::ostream& operator<<(std::ostream& os,
+ const AudioStreamDataPathState& state) {
+ static const char* char_value_[6] = {
+ "IDLE", "CIS_DISCONNECTING", "CIS_ASSIGNED",
+ "CIS_PENDING", "CIS_ESTABLISHED", "DATA_PATH_ESTABLISHED"};
+
+ os << char_value_[static_cast<uint8_t>(state)] << " ("
+ << "0x" << std::setfill('0') << std::setw(2) << static_cast<int>(state)
+ << ")";
+ return os;
+}
std::ostream& operator<<(std::ostream& os, const types::CigState& state) {
static const char* char_value_[5] = {"NONE", "CREATING", "CREATED",
"REMOVING", "RECOVERING"};
diff --git a/system/bta/le_audio/le_audio_types.h b/system/bta/le_audio/le_audio_types.h
index 776ef52..4f915ab 100644
--- a/system/bta/le_audio/le_audio_types.h
+++ b/system/bta/le_audio/le_audio_types.h
@@ -310,7 +310,6 @@
constexpr uint16_t kMaxTransportLatencyMin = 0x0005;
constexpr uint16_t kMaxTransportLatencyMax = 0x0FA0;
-/* Enums */
enum class CigState : uint8_t { NONE, CREATING, CREATED, REMOVING, RECOVERING };
/* ASE states according to BAP defined state machine states */
@@ -600,6 +599,8 @@
std::ostream& operator<<(std::ostream& os, const CigState& state);
std::ostream& operator<<(std::ostream& os, const LeAudioLc3Config& config);
std::ostream& operator<<(std::ostream& os, const LeAudioContextType& context);
+std::ostream& operator<<(std::ostream& os,
+ const AudioStreamDataPathState& state);
} // namespace types
namespace set_configurations {
diff --git a/system/bta/le_audio/state_machine.cc b/system/bta/le_audio/state_machine.cc
index 20ae741..f74fe8c 100644
--- a/system/bta/le_audio/state_machine.cc
+++ b/system/bta/le_audio/state_machine.cc
@@ -919,6 +919,10 @@
}
void SetTargetState(LeAudioDeviceGroup* group, AseState state) {
+ LOG_DEBUG("Watchdog watch started for group=%d transition from %s to %s",
+ group->group_id_, ToString(group->GetTargetState()).c_str(),
+ ToString(state).c_str());
+
group->SetTargetState(state);
/* Group should tie in time to get requested status */
@@ -1491,8 +1495,9 @@
ase = leAudioDevice->GetFirstActiveAse();
ASSERT_LOG(ase, "shouldn't be called without an active ASE");
for (; ase != nullptr; ase = leAudioDevice->GetNextActiveAse(ase)) {
- LOG_INFO(" Configure ase_id %d, cis_id %d, ase state %s", ase->id,
- ase->cis_id, ToString(ase->state).c_str());
+ LOG_DEBUG("device: %s, ase_id: %d, cis_id: %d, ase state: %s",
+ leAudioDevice->address_.ToString().c_str(), ase->id,
+ ase->cis_id, ToString(ase->state).c_str());
conf.ase_id = ase->id;
conf.target_latency = ase->target_latency;
conf.target_phy = group->GetTargetPhy(ase->direction);
@@ -1897,6 +1902,9 @@
ase = leAudioDevice->GetFirstActiveAse();
LOG_ASSERT(ase) << __func__ << " shouldn't be called without an active ASE";
do {
+ LOG_DEBUG("device: %s, ase_id: %d, cis_id: %d, ase state: %s",
+ leAudioDevice->address_.ToString().c_str(), ase->id,
+ ase->cis_id, ToString(ase->state).c_str());
conf.ase_id = ase->id;
conf.metadata = ase->metadata;
confs.push_back(conf);
@@ -1915,6 +1923,9 @@
std::vector<uint8_t> ids;
do {
+ LOG_DEBUG("device: %s, ase_id: %d, cis_id: %d, ase state: %s",
+ leAudioDevice->address_.ToString().c_str(), ase->id,
+ ase->cis_id, ToString(ase->state).c_str());
ids.push_back(ase->id);
} while ((ase = leAudioDevice->GetNextActiveAse(ase)));
@@ -1932,6 +1943,9 @@
std::vector<uint8_t> ids;
do {
+ LOG_DEBUG("device: %s, ase_id: %d, cis_id: %d, ase state: %s",
+ leAudioDevice->address_.ToString().c_str(), ase->id,
+ ase->cis_id, ToString(ase->state).c_str());
ids.push_back(ase->id);
} while ((ase = leAudioDevice->GetNextActiveAse(ase)));
@@ -1949,6 +1963,10 @@
for (struct ase* ase = leAudioDevice->GetFirstActiveAse(); ase != nullptr;
ase = leAudioDevice->GetNextActiveAse(ase)) {
+ LOG_DEBUG("device: %s, ase_id: %d, cis_id: %d, ase state: %s",
+ leAudioDevice->address_.ToString().c_str(), ase->id,
+ ase->cis_id, ToString(ase->state).c_str());
+
/* TODO: Configure first ASE qos according to context type */
struct le_audio::client_parser::ascs::ctp_qos_conf conf;
conf.ase_id = ase->id;
@@ -2006,6 +2024,10 @@
/* Request server to update ASEs with new metadata */
for (struct ase* ase = leAudioDevice->GetFirstActiveAse(); ase != nullptr;
ase = leAudioDevice->GetNextActiveAse(ase)) {
+ LOG_DEBUG("device: %s, ase_id: %d, cis_id: %d, ase state: %s",
+ leAudioDevice->address_.ToString().c_str(), ase->id,
+ ase->cis_id, ToString(ase->state).c_str());
+
struct le_audio::client_parser::ascs::ctp_update_metadata conf;
conf.ase_id = ase->id;
diff --git a/system/bta/le_audio/state_machine_test.cc b/system/bta/le_audio/state_machine_test.cc
index 56cb8cc..e6160cd 100644
--- a/system/bta/le_audio/state_machine_test.cc
+++ b/system/bta/le_audio/state_machine_test.cc
@@ -35,6 +35,7 @@
#include "mock_iso_manager.h"
#include "types/bt_transport.h"
+using ::le_audio::DeviceConnectState;
using ::testing::_;
using ::testing::AnyNumber;
using ::testing::AtLeast;
@@ -443,13 +444,13 @@
::le_audio::AudioSetConfigurationProvider::Cleanup();
}
- std::shared_ptr<LeAudioDevice> PrepareConnectedDevice(uint8_t id,
- bool first_connection,
- uint8_t num_ase_snk,
- uint8_t num_ase_src) {
- auto leAudioDevice =
- std::make_shared<LeAudioDevice>(GetTestAddress(id), first_connection);
+ std::shared_ptr<LeAudioDevice> PrepareConnectedDevice(
+ uint8_t id, DeviceConnectState initial_connect_state, uint8_t num_ase_snk,
+ uint8_t num_ase_src) {
+ auto leAudioDevice = std::make_shared<LeAudioDevice>(GetTestAddress(id),
+ initial_connect_state);
leAudioDevice->conn_id_ = id;
+ leAudioDevice->SetConnectionState(DeviceConnectState::CONNECTED);
uint16_t attr_handle = ATTR_HANDLE_ASCS_POOL_START;
leAudioDevice->snk_audio_locations_hdls_.val_hdl = attr_handle++;
@@ -668,7 +669,8 @@
uint16_t update_context_type,
bool insert_default_pac_records = true) {
// Prepare fake connected device group
- bool first_connections = true;
+ DeviceConnectState initial_connect_state =
+ DeviceConnectState::CONNECTING_BY_USER;
int total_devices = device_cnt;
le_audio::LeAudioDeviceGroup* group = nullptr;
@@ -696,7 +698,7 @@
while (device_cnt) {
auto leAudioDevice = PrepareConnectedDevice(
- device_cnt--, first_connections, num_ase_snk, num_ase_src);
+ device_cnt--, initial_connect_state, num_ase_snk, num_ase_src);
if (insert_default_pac_records) {
uint16_t attr_handle = ATTR_HANDLE_PACS_POOL_START;
diff --git a/system/bta/le_audio/storage_helper_test.cc b/system/bta/le_audio/storage_helper_test.cc
index 5b765b1..fe99f52 100644
--- a/system/bta/le_audio/storage_helper_test.cc
+++ b/system/bta/le_audio/storage_helper_test.cc
@@ -105,7 +105,7 @@
// clang-format on
RawAddress test_address0 = GetTestAddress(0);
- LeAudioDevice leAudioDevice(test_address0, false);
+ LeAudioDevice leAudioDevice(test_address0, DeviceConnectState::DISCONNECTED);
ASSERT_TRUE(DeserializeSinkPacs(&leAudioDevice, validSinkPack));
std::vector<uint8_t> serialize;
ASSERT_TRUE(SerializeSinkPacs(&leAudioDevice, serialize));
@@ -189,7 +189,7 @@
// clang-format on
RawAddress test_address0 = GetTestAddress(0);
- LeAudioDevice leAudioDevice(test_address0, false);
+ LeAudioDevice leAudioDevice(test_address0, DeviceConnectState::DISCONNECTED);
ASSERT_TRUE(DeserializeSourcePacs(&leAudioDevice, validSourcePack));
std::vector<uint8_t> serialize;
ASSERT_TRUE(SerializeSourcePacs(&leAudioDevice, serialize));
@@ -252,7 +252,7 @@
};
// clang-format on
RawAddress test_address0 = GetTestAddress(0);
- LeAudioDevice leAudioDevice(test_address0, false);
+ LeAudioDevice leAudioDevice(test_address0, DeviceConnectState::DISCONNECTED);
ASSERT_TRUE(DeserializeAses(&leAudioDevice, validAses));
std::vector<uint8_t> serialize;
@@ -310,7 +310,7 @@
};
// clang-format on
RawAddress test_address0 = GetTestAddress(0);
- LeAudioDevice leAudioDevice(test_address0, false);
+ LeAudioDevice leAudioDevice(test_address0, DeviceConnectState::DISCONNECTED);
ASSERT_TRUE(DeserializeHandles(&leAudioDevice, validHandles));
std::vector<uint8_t> serialize;
ASSERT_TRUE(SerializeHandles(&leAudioDevice, serialize));
diff --git a/system/bta/vc/vc.cc b/system/bta/vc/vc.cc
index b2b85cb..53816f9 100644
--- a/system/bta/vc/vc.cc
+++ b/system/bta/vc/vc.cc
@@ -753,7 +753,7 @@
devices_control_point_helper(op->devices_, op->opcode_, &(op->arguments_));
}
- void PrepareVolumeControlOperation(std::vector<RawAddress>& devices,
+ void PrepareVolumeControlOperation(std::vector<RawAddress> devices,
int group_id, bool is_autonomous,
uint8_t opcode,
std::vector<uint8_t>& arguments) {
@@ -764,11 +764,22 @@
arguments.size());
if (std::find_if(ongoing_operations_.begin(), ongoing_operations_.end(),
- [opcode, &arguments](const VolumeOperation& op) {
- return (op.opcode_ == opcode) &&
- std::equal(op.arguments_.begin(),
- op.arguments_.end(),
- arguments.begin());
+ [opcode, &devices, &arguments](const VolumeOperation& op) {
+ if (op.opcode_ != opcode) return false;
+ if (!std::equal(op.arguments_.begin(),
+ op.arguments_.end(), arguments.begin()))
+ return false;
+ // Filter out all devices which have the exact operation
+ // already scheduled
+ devices.erase(
+ std::remove_if(devices.begin(), devices.end(),
+ [&op](auto d) {
+ return find(op.devices_.begin(),
+ op.devices_.end(),
+ d) != op.devices_.end();
+ }),
+ devices.end());
+ return devices.empty();
}) == ongoing_operations_.end()) {
ongoing_operations_.emplace_back(latest_operation_id_++, group_id,
is_autonomous, opcode, arguments,
diff --git a/system/bta/vc/vc_test.cc b/system/bta/vc/vc_test.cc
index cac1f92..3043689 100644
--- a/system/bta/vc/vc_test.cc
+++ b/system/bta/vc/vc_test.cc
@@ -1087,6 +1087,23 @@
std::vector<uint8_t> value({0x03, 0x01, 0x02});
GetNotificationEvent(conn_id_1, test_address_1, 0x0021, value);
GetNotificationEvent(conn_id_2, test_address_2, 0x0021, value);
+
+ /* Verify exactly one operation with this exact value is queued for each
+ * device */
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(conn_id_1, 0x0024, _, GATT_WRITE, _, _))
+ .Times(1);
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(conn_id_2, 0x0024, _, GATT_WRITE, _, _))
+ .Times(1);
+ VolumeControl::Get()->SetVolume(test_address_1, 20);
+ VolumeControl::Get()->SetVolume(test_address_2, 20);
+ VolumeControl::Get()->SetVolume(test_address_1, 20);
+ VolumeControl::Get()->SetVolume(test_address_2, 20);
+
+ std::vector<uint8_t> value2({20, 0x00, 0x03});
+ GetNotificationEvent(conn_id_1, test_address_1, 0x0021, value2);
+ GetNotificationEvent(conn_id_2, test_address_2, 0x0021, value2);
}
TEST_F(VolumeControlCsis, test_set_volume_device_not_ready) {
diff --git a/system/btif/Android.bp b/system/btif/Android.bp
index 22d24f3..6299237 100644
--- a/system/btif/Android.bp
+++ b/system/btif/Android.bp
@@ -102,7 +102,6 @@
"co/bta_gatts_co.cc",
// HAL layer
"src/bluetooth.cc",
- "src/bluetooth_data_migration.cc",
// BTIF implementation
"src/btif_a2dp.cc",
"src/btif_a2dp_control.cc",
diff --git a/system/btif/src/bluetooth.cc b/system/btif/src/bluetooth.cc
index f588993..0e170c8 100644
--- a/system/btif/src/bluetooth.cc
+++ b/system/btif/src/bluetooth.cc
@@ -172,29 +172,17 @@
*
****************************************************************************/
-#ifdef OS_ANDROID
-const std::vector<std::string> get_allowed_bt_package_name(void);
-void handle_migration(const std::string& dst,
- const std::vector<std::string>& allowed_bt_package_name);
-#endif
-
static int init(bt_callbacks_t* callbacks, bool start_restricted,
bool is_common_criteria_mode, int config_compare_result,
const char** init_flags, bool is_atv,
const char* user_data_directory) {
+ (void)user_data_directory;
LOG_INFO(
"%s: start restricted = %d ; common criteria mode = %d, config compare "
"result = %d",
__func__, start_restricted, is_common_criteria_mode,
config_compare_result);
-#ifdef OS_ANDROID
- if (user_data_directory != nullptr) {
- handle_migration(std::string(user_data_directory),
- get_allowed_bt_package_name());
- }
-#endif
-
bluetooth::common::InitFlags::Load(init_flags);
if (interface_ready()) return BT_STATUS_DONE;
diff --git a/system/btif/src/bluetooth_data_migration.cc b/system/btif/src/bluetooth_data_migration.cc
deleted file mode 100644
index 45d0bda..0000000
--- a/system/btif/src/bluetooth_data_migration.cc
+++ /dev/null
@@ -1,126 +0,0 @@
-/******************************************************************************
- *
- * Copyright 2022 Google LLC
- *
- * 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.
- *
- ******************************************************************************/
-
-#include <base/logging.h>
-
-#include <filesystem>
-#include <string>
-#include <vector>
-
-namespace fs = std::filesystem;
-
-// The user data should be stored in the subdirectory of |USER_DE_PATH|
-static const std::string USER_DE_PATH = "/data/user_de/0";
-
-// The migration process start only if |MIGRATION_FILE_CHECKER| is found in a
-// previous location
-static const std::string MIGRATION_FILE_CHECKER = "databases/bluetooth_db";
-
-// List of possible package_name for bluetooth to get the data from / to
-static const std::vector<std::string> ALLOWED_BT_PACKAGE_NAME = {
- "com.android.bluetooth", // legacy name
- "com.android.bluetooth.services", // Beta users
- "com.google.android.bluetooth.services", // Droid fooder users
-};
-
-// Accessor to get the default allowed package list to be used in migration
-// OEM can call their own method with their own allowed list
-const std::vector<std::string> get_allowed_bt_package_name(void) {
- return ALLOWED_BT_PACKAGE_NAME;
-}
-
-// Check if |dst| is in |base_dir| subdirectory and check the package name in
-// |dst| is a allowed package name in the |pkg_list|
-//
-// Return an empty string if an issue occurred
-// or the package name contained in |dst| on success
-static std::string parse_destination_package_name(
- const std::string& dst, const std::string& base_dir,
- const std::vector<std::string>& pkg_list) {
- const std::size_t found = dst.rfind("/");
- // |dst| must contain a '/'
- if (found == std::string::npos) {
- LOG(ERROR) << "Destination format not valid " << dst;
- return "";
- }
- // |dst| directory is supposed to be in |base_dir|
- if (found != base_dir.length()) {
- LOG(ERROR) << "Destination location not allowed: " << dst;
- return "";
- }
- // This check prevent a '/' to be at the end of |dst|
- if (found >= dst.length() - 1) {
- LOG(ERROR) << "Destination format not valid " << dst;
- return "";
- }
-
- const std::string dst_package_name = dst.substr(found + 1); // +1 for '/'
-
- if (std::find(pkg_list.begin(), pkg_list.end(), dst_package_name) ==
- pkg_list.end()) {
- LOG(ERROR) << "Destination package_name not valid: " << dst_package_name
- << " Created from " << dst;
- return "";
- }
- LOG(INFO) << "Current Bluetooth package name is: " << dst_package_name;
- return dst_package_name;
-}
-
-// Check for data to migrate from the |allowed_bt_package_name|
-// A migration will be performed if:
-// * |dst| is different than |allowed_bt_package_name|
-// * the following file is found:
-// |USER_DE_PATH|/|allowed_bt_package_name|/|MIGRATION_FILE_CHECKER|
-//
-// After migration occurred, the |MIGRATION_FILE_CHECKER| is deleted to ensure
-// the migration is only performed once
-void handle_migration(const std::string& dst,
- const std::vector<std::string>& allowed_bt_package_name) {
- const std::string dst_package_name = parse_destination_package_name(
- dst, USER_DE_PATH, allowed_bt_package_name);
- if (dst_package_name.empty()) return;
-
- for (const auto& pkg_name : allowed_bt_package_name) {
- std::error_code error;
-
- if (dst_package_name == pkg_name) {
- LOG(INFO) << "Same location skipped: " << dst_package_name;
- continue;
- }
- const fs::path dst_path = dst;
- const fs::path pkg_path = USER_DE_PATH + "/" + pkg_name;
- const fs::path local_migration_file_checker =
- pkg_path.string() + "/" + MIGRATION_FILE_CHECKER;
- if (!fs::exists(local_migration_file_checker, error)) {
- LOG(INFO) << "Not a valid candidate for migration: " << pkg_path;
- continue;
- }
-
- const fs::copy_options copy_flag =
- fs::copy_options::overwrite_existing | fs::copy_options::recursive;
- fs::copy(pkg_path, dst_path, copy_flag, error);
-
- if (error) {
- LOG(ERROR) << "Migration failed: " << error.message();
- } else {
- fs::remove(local_migration_file_checker);
- LOG(INFO) << "Migration completed from " << pkg_path << " to " << dst;
- }
- break; // Copy from one and only one directory
- }
-}
diff --git a/system/btif/src/btif_av.cc b/system/btif/src/btif_av.cc
index 7f613b8..4e132c6 100644
--- a/system/btif/src/btif_av.cc
+++ b/system/btif/src/btif_av.cc
@@ -790,6 +790,10 @@
}
}
+const char* dump_av_sm_event_name(int event) {
+ return dump_av_sm_event_name(static_cast<btif_av_sm_event_t>(event));
+}
+
BtifAvEvent::BtifAvEvent(uint32_t event, const void* p_data, size_t data_length)
: event_(event), data_(nullptr), data_length_(0) {
DeepCopy(event, p_data, data_length);
diff --git a/system/btif/src/btif_dm.cc b/system/btif/src/btif_dm.cc
index c5f4a6b..8ee07ad 100644
--- a/system/btif/src/btif_dm.cc
+++ b/system/btif/src/btif_dm.cc
@@ -109,8 +109,6 @@
const Uuid UUID_BATTERY = Uuid::FromString("180F");
const bool enable_address_consolidate = true; // TODO remove
-#define COD_MASK 0x07FF
-
#define COD_UNCLASSIFIED ((0x1F) << 8)
#define COD_HID_KEYBOARD 0x0540
#define COD_HID_POINTING 0x0580
@@ -123,6 +121,8 @@
#define COD_AV_PORTABLE_AUDIO 0x041C
#define COD_AV_HIFI_AUDIO 0x0428
+#define COD_CLASS_LE_AUDIO (1 << 14)
+
#define BTIF_DM_MAX_SDP_ATTEMPTS_AFTER_PAIRING 2
#ifndef PROPERTY_CLASS_OF_DEVICE
@@ -445,7 +445,7 @@
if (btif_storage_get_remote_device_property(
(RawAddress*)remote_bdaddr, &prop_name) == BT_STATUS_SUCCESS) {
LOG_INFO("%s remote_cod = 0x%08x", __func__, remote_cod);
- return remote_cod & COD_MASK;
+ return remote_cod;
}
return 0;
@@ -463,6 +463,9 @@
return (get_cod(&bd_addr) & COD_HID_MASK) == COD_HID_MAJOR;
}
+bool check_cod_le_audio(const RawAddress& bd_addr) {
+ return (get_cod(&bd_addr) & COD_CLASS_LE_AUDIO) == COD_CLASS_LE_AUDIO;
+}
/*****************************************************************************
*
* Function check_sdp_bl
@@ -657,6 +660,25 @@
properties);
}
+/* If device is LE Audio capable, we prefer LE connection first, this speeds
+ * up LE profile connection, and limits all possible service discovery
+ * ordering issues (first Classic, GATT over SDP, etc) */
+static bool is_device_le_audio_capable(const RawAddress bd_addr) {
+ if (!LeAudioClient::IsLeAudioClientRunning() ||
+ !check_cod_le_audio(bd_addr)) {
+ return false;
+ }
+
+ tBT_DEVICE_TYPE tmp_dev_type;
+ tBLE_ADDR_TYPE addr_type = BLE_ADDR_PUBLIC;
+ BTM_ReadDevInfo(bd_addr, &tmp_dev_type, &addr_type);
+ if (tmp_dev_type & BT_DEVICE_TYPE_BLE) {
+ return true;
+ }
+
+ return false;
+}
+
/*******************************************************************************
*
* Function btif_dm_cb_create_bond
@@ -672,6 +694,11 @@
bool is_hid = check_cod(&bd_addr, COD_HID_POINTING);
bond_state_changed(BT_STATUS_SUCCESS, bd_addr, BT_BOND_STATE_BONDING);
+ if (transport == BT_TRANSPORT_AUTO && is_device_le_audio_capable(bd_addr)) {
+ LOG_INFO("LE Audio && advertising over LE, use LE transport for Bonding");
+ transport = BT_TRANSPORT_LE;
+ }
+
int device_type = 0;
tBLE_ADDR_TYPE addr_type = BLE_ADDR_PUBLIC;
std::string addrstr = bd_addr.ToString();
@@ -1044,8 +1071,7 @@
}
bool is_crosskey = false;
- if (pairing_cb.state == BT_BOND_STATE_BONDING &&
- p_auth_cmpl->bd_addr != pairing_cb.bd_addr) {
+ if (pairing_cb.state == BT_BOND_STATE_BONDING && p_auth_cmpl->is_ctkd) {
LOG_INFO("bonding initiated due to cross key pairing");
is_crosskey = true;
}
@@ -1078,8 +1104,7 @@
invoke_remote_device_properties_cb(BT_STATUS_SUCCESS, bd_addr, 1, &prop);
} else {
/* If bonded due to cross-key, save the static address too*/
- if (pairing_cb.state == BT_BOND_STATE_BONDING &&
- p_auth_cmpl->bd_addr != pairing_cb.bd_addr) {
+ if (is_crosskey) {
BTIF_TRACE_DEBUG(
"%s: bonding initiated due to cross key, adding static address",
__func__);
diff --git a/system/btif/test/btif_core_test.cc b/system/btif/test/btif_core_test.cc
index 82f86dc..2ecdd2f 100644
--- a/system/btif/test/btif_core_test.cc
+++ b/system/btif/test/btif_core_test.cc
@@ -20,9 +20,16 @@
#include <map>
#include "bta/include/bta_ag_api.h"
+#include "bta/include/bta_av_api.h"
+#include "bta/include/bta_hd_api.h"
+#include "bta/include/bta_hf_client_api.h"
+#include "bta/include/bta_hh_api.h"
#include "btcore/include/module.h"
#include "btif/include/btif_api.h"
#include "btif/include/btif_common.h"
+#include "btif/include/btif_util.h"
+#include "include/hardware/bluetooth.h"
+#include "include/hardware/bt_av.h"
#include "types/raw_address.h"
void set_hal_cbacks(bt_callbacks_t* callbacks);
@@ -174,3 +181,456 @@
ASSERT_EQ(std::future_status::ready, future.wait_for(timeout_time));
ASSERT_EQ(val, future.get());
}
+
+extern const char* dump_av_sm_event_name(int event);
+TEST_F(BtifCoreTest, dump_av_sm_event_name) {
+ std::vector<std::pair<int, std::string>> events = {
+ std::make_pair(BTA_AV_ENABLE_EVT, "BTA_AV_ENABLE_EVT"),
+ std::make_pair(BTA_AV_REGISTER_EVT, "BTA_AV_REGISTER_EVT"),
+ std::make_pair(BTA_AV_OPEN_EVT, "BTA_AV_OPEN_EVT"),
+ std::make_pair(BTA_AV_CLOSE_EVT, "BTA_AV_CLOSE_EVT"),
+ std::make_pair(BTA_AV_START_EVT, "BTA_AV_START_EVT"),
+ std::make_pair(BTA_AV_STOP_EVT, "BTA_AV_STOP_EVT"),
+ std::make_pair(BTA_AV_PROTECT_REQ_EVT, "BTA_AV_PROTECT_REQ_EVT"),
+ std::make_pair(BTA_AV_PROTECT_RSP_EVT, "BTA_AV_PROTECT_RSP_EVT"),
+ std::make_pair(BTA_AV_RC_OPEN_EVT, "BTA_AV_RC_OPEN_EVT"),
+ std::make_pair(BTA_AV_RC_CLOSE_EVT, "BTA_AV_RC_CLOSE_EVT"),
+ std::make_pair(BTA_AV_RC_BROWSE_OPEN_EVT, "BTA_AV_RC_BROWSE_OPEN_EVT"),
+ std::make_pair(BTA_AV_RC_BROWSE_CLOSE_EVT, "BTA_AV_RC_BROWSE_CLOSE_EVT"),
+ std::make_pair(BTA_AV_REMOTE_CMD_EVT, "BTA_AV_REMOTE_CMD_EVT"),
+ std::make_pair(BTA_AV_REMOTE_RSP_EVT, "BTA_AV_REMOTE_RSP_EVT"),
+ std::make_pair(BTA_AV_VENDOR_CMD_EVT, "BTA_AV_VENDOR_CMD_EVT"),
+ std::make_pair(BTA_AV_VENDOR_RSP_EVT, "BTA_AV_VENDOR_RSP_EVT"),
+ std::make_pair(BTA_AV_RECONFIG_EVT, "BTA_AV_RECONFIG_EVT"),
+ std::make_pair(BTA_AV_SUSPEND_EVT, "BTA_AV_SUSPEND_EVT"),
+ std::make_pair(BTA_AV_PENDING_EVT, "BTA_AV_PENDING_EVT"),
+ std::make_pair(BTA_AV_META_MSG_EVT, "BTA_AV_META_MSG_EVT"),
+ std::make_pair(BTA_AV_REJECT_EVT, "BTA_AV_REJECT_EVT"),
+ std::make_pair(BTA_AV_RC_FEAT_EVT, "BTA_AV_RC_FEAT_EVT"),
+ std::make_pair(BTA_AV_RC_PSM_EVT, "BTA_AV_RC_PSM_EVT"),
+ std::make_pair(BTA_AV_OFFLOAD_START_RSP_EVT,
+ "BTA_AV_OFFLOAD_START_RSP_EVT"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_av_sm_event_name(event.first));
+ }
+ std::ostringstream oss;
+ oss << "UNKNOWN_EVENT";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_av_sm_event_name(std::numeric_limits<int>::max()));
+}
+
+TEST_F(BtifCoreTest, dump_dm_search_event) {
+ std::vector<std::pair<uint16_t, std::string>> events = {
+ std::make_pair(BTA_DM_INQ_RES_EVT, "BTA_DM_INQ_RES_EVT"),
+ std::make_pair(BTA_DM_INQ_CMPL_EVT, "BTA_DM_INQ_CMPL_EVT"),
+ std::make_pair(BTA_DM_DISC_RES_EVT, "BTA_DM_DISC_RES_EVT"),
+ std::make_pair(BTA_DM_DISC_BLE_RES_EVT, "BTA_DM_DISC_BLE_RES_EVT"),
+ std::make_pair(BTA_DM_DISC_CMPL_EVT, "BTA_DM_DISC_CMPL_EVT"),
+ std::make_pair(BTA_DM_SEARCH_CANCEL_CMPL_EVT,
+ "BTA_DM_SEARCH_CANCEL_CMPL_EVT"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_dm_search_event(event.first));
+ }
+ std::ostringstream oss;
+ oss << "UNKNOWN MSG ID";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_dm_search_event(std::numeric_limits<uint16_t>::max()));
+}
+
+TEST_F(BtifCoreTest, dump_property_type) {
+ std::vector<std::pair<bt_property_type_t, std::string>> types = {
+ std::make_pair(BT_PROPERTY_BDNAME, "BT_PROPERTY_BDNAME"),
+ std::make_pair(BT_PROPERTY_BDADDR, "BT_PROPERTY_BDADDR"),
+ std::make_pair(BT_PROPERTY_UUIDS, "BT_PROPERTY_UUIDS"),
+ std::make_pair(BT_PROPERTY_CLASS_OF_DEVICE,
+ "BT_PROPERTY_CLASS_OF_DEVICE"),
+ std::make_pair(BT_PROPERTY_TYPE_OF_DEVICE, "BT_PROPERTY_TYPE_OF_DEVICE"),
+ std::make_pair(BT_PROPERTY_REMOTE_RSSI, "BT_PROPERTY_REMOTE_RSSI"),
+ std::make_pair(BT_PROPERTY_ADAPTER_DISCOVERABLE_TIMEOUT,
+ "BT_PROPERTY_ADAPTER_DISCOVERABLE_TIMEOUT"),
+ std::make_pair(BT_PROPERTY_ADAPTER_BONDED_DEVICES,
+ "BT_PROPERTY_ADAPTER_BONDED_DEVICES"),
+ std::make_pair(BT_PROPERTY_ADAPTER_SCAN_MODE,
+ "BT_PROPERTY_ADAPTER_SCAN_MODE"),
+ std::make_pair(BT_PROPERTY_REMOTE_FRIENDLY_NAME,
+ "BT_PROPERTY_REMOTE_FRIENDLY_NAME"),
+ };
+ for (const auto& type : types) {
+ ASSERT_STREQ(type.second.c_str(), dump_property_type(type.first));
+ }
+ std::ostringstream oss;
+ oss << "UNKNOWN PROPERTY ID";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_property_type(static_cast<bt_property_type_t>(
+ std::numeric_limits<uint16_t>::max())));
+}
+
+TEST_F(BtifCoreTest, dump_dm_event) {
+ std::vector<std::pair<uint8_t, std::string>> events = {
+ std::make_pair(BTA_DM_PIN_REQ_EVT, "BTA_DM_PIN_REQ_EVT"),
+ std::make_pair(BTA_DM_AUTH_CMPL_EVT, "BTA_DM_AUTH_CMPL_EVT"),
+ std::make_pair(BTA_DM_LINK_UP_EVT, "BTA_DM_LINK_UP_EVT"),
+ std::make_pair(BTA_DM_LINK_DOWN_EVT, "BTA_DM_LINK_DOWN_EVT"),
+ std::make_pair(BTA_DM_BOND_CANCEL_CMPL_EVT,
+ "BTA_DM_BOND_CANCEL_CMPL_EVT"),
+ std::make_pair(BTA_DM_SP_CFM_REQ_EVT, "BTA_DM_SP_CFM_REQ_EVT"),
+ std::make_pair(BTA_DM_SP_KEY_NOTIF_EVT, "BTA_DM_SP_KEY_NOTIF_EVT"),
+ std::make_pair(BTA_DM_BLE_KEY_EVT, "BTA_DM_BLE_KEY_EVT"),
+ std::make_pair(BTA_DM_BLE_SEC_REQ_EVT, "BTA_DM_BLE_SEC_REQ_EVT"),
+ std::make_pair(BTA_DM_BLE_PASSKEY_NOTIF_EVT,
+ "BTA_DM_BLE_PASSKEY_NOTIF_EVT"),
+ std::make_pair(BTA_DM_BLE_PASSKEY_REQ_EVT, "BTA_DM_BLE_PASSKEY_REQ_EVT"),
+ std::make_pair(BTA_DM_BLE_OOB_REQ_EVT, "BTA_DM_BLE_OOB_REQ_EVT"),
+ std::make_pair(BTA_DM_BLE_SC_OOB_REQ_EVT, "BTA_DM_BLE_SC_OOB_REQ_EVT"),
+ std::make_pair(BTA_DM_BLE_LOCAL_IR_EVT, "BTA_DM_BLE_LOCAL_IR_EVT"),
+ std::make_pair(BTA_DM_BLE_LOCAL_ER_EVT, "BTA_DM_BLE_LOCAL_ER_EVT"),
+ std::make_pair(BTA_DM_BLE_AUTH_CMPL_EVT, "BTA_DM_BLE_AUTH_CMPL_EVT"),
+ std::make_pair(BTA_DM_DEV_UNPAIRED_EVT, "BTA_DM_DEV_UNPAIRED_EVT"),
+ std::make_pair(BTA_DM_ENER_INFO_READ, "BTA_DM_ENER_INFO_READ"),
+ std::make_pair(BTA_DM_REPORT_BONDING_EVT, "BTA_DM_REPORT_BONDING_EVT"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_dm_event(event.first));
+ }
+ std::ostringstream oss;
+ oss << "UNKNOWN DM EVENT";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_dm_event(std::numeric_limits<uint8_t>::max()));
+}
+
+TEST_F(BtifCoreTest, dump_hf_event) {
+ std::vector<std::pair<uint8_t, std::string>> events = {
+ std::make_pair(BTA_AG_ENABLE_EVT, "BTA_AG_ENABLE_EVT"),
+ std::make_pair(BTA_AG_REGISTER_EVT, "BTA_AG_REGISTER_EVT"),
+ std::make_pair(BTA_AG_OPEN_EVT, "BTA_AG_OPEN_EVT"),
+ std::make_pair(BTA_AG_CLOSE_EVT, "BTA_AG_CLOSE_EVT"),
+ std::make_pair(BTA_AG_CONN_EVT, "BTA_AG_CONN_EVT"),
+ std::make_pair(BTA_AG_AUDIO_OPEN_EVT, "BTA_AG_AUDIO_OPEN_EVT"),
+ std::make_pair(BTA_AG_AUDIO_CLOSE_EVT, "BTA_AG_AUDIO_CLOSE_EVT"),
+ std::make_pair(BTA_AG_SPK_EVT, "BTA_AG_SPK_EVT"),
+ std::make_pair(BTA_AG_MIC_EVT, "BTA_AG_MIC_EVT"),
+ std::make_pair(BTA_AG_AT_CKPD_EVT, "BTA_AG_AT_CKPD_EVT"),
+ std::make_pair(BTA_AG_DISABLE_EVT, "BTA_AG_DISABLE_EVT"),
+ std::make_pair(BTA_AG_WBS_EVT, "BTA_AG_WBS_EVT"),
+ std::make_pair(BTA_AG_AT_A_EVT, "BTA_AG_AT_A_EVT"),
+ std::make_pair(BTA_AG_AT_D_EVT, "BTA_AG_AT_D_EVT"),
+ std::make_pair(BTA_AG_AT_CHLD_EVT, "BTA_AG_AT_CHLD_EVT"),
+ std::make_pair(BTA_AG_AT_CHUP_EVT, "BTA_AG_AT_CHUP_EVT"),
+ std::make_pair(BTA_AG_AT_CIND_EVT, "BTA_AG_AT_CIND_EVT"),
+ std::make_pair(BTA_AG_AT_VTS_EVT, "BTA_AG_AT_VTS_EVT"),
+ std::make_pair(BTA_AG_AT_BINP_EVT, "BTA_AG_AT_BINP_EVT"),
+ std::make_pair(BTA_AG_AT_BLDN_EVT, "BTA_AG_AT_BLDN_EVT"),
+ std::make_pair(BTA_AG_AT_BVRA_EVT, "BTA_AG_AT_BVRA_EVT"),
+ std::make_pair(BTA_AG_AT_NREC_EVT, "BTA_AG_AT_NREC_EVT"),
+ std::make_pair(BTA_AG_AT_CNUM_EVT, "BTA_AG_AT_CNUM_EVT"),
+ std::make_pair(BTA_AG_AT_BTRH_EVT, "BTA_AG_AT_BTRH_EVT"),
+ std::make_pair(BTA_AG_AT_CLCC_EVT, "BTA_AG_AT_CLCC_EVT"),
+ std::make_pair(BTA_AG_AT_COPS_EVT, "BTA_AG_AT_COPS_EVT"),
+ std::make_pair(BTA_AG_AT_UNAT_EVT, "BTA_AG_AT_UNAT_EVT"),
+ std::make_pair(BTA_AG_AT_CBC_EVT, "BTA_AG_AT_CBC_EVT"),
+ std::make_pair(BTA_AG_AT_BAC_EVT, "BTA_AG_AT_BAC_EVT"),
+ std::make_pair(BTA_AG_AT_BCS_EVT, "BTA_AG_AT_BCS_EVT"),
+ std::make_pair(BTA_AG_AT_BIND_EVT, "BTA_AG_AT_BIND_EVT"),
+ std::make_pair(BTA_AG_AT_BIEV_EVT, "BTA_AG_AT_BIEV_EVT"),
+ std::make_pair(BTA_AG_AT_BIA_EVT, "BTA_AG_AT_BIA_EVT"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_hf_event(event.first));
+ }
+ std::ostringstream oss;
+ oss << "UNKNOWN MSG ID";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_hf_event(std::numeric_limits<uint8_t>::max()));
+}
+
+TEST_F(BtifCoreTest, dump_hf_client_event) {
+ std::vector<std::pair<int, std::string>> events = {
+ std::make_pair(BTA_HF_CLIENT_ENABLE_EVT, "BTA_HF_CLIENT_ENABLE_EVT"),
+ std::make_pair(BTA_HF_CLIENT_REGISTER_EVT, "BTA_HF_CLIENT_REGISTER_EVT"),
+ std::make_pair(BTA_HF_CLIENT_OPEN_EVT, "BTA_HF_CLIENT_OPEN_EVT"),
+ std::make_pair(BTA_HF_CLIENT_CLOSE_EVT, "BTA_HF_CLIENT_CLOSE_EVT"),
+ std::make_pair(BTA_HF_CLIENT_CONN_EVT, "BTA_HF_CLIENT_CONN_EVT"),
+ std::make_pair(BTA_HF_CLIENT_AUDIO_OPEN_EVT,
+ "BTA_HF_CLIENT_AUDIO_OPEN_EVT"),
+ std::make_pair(BTA_HF_CLIENT_AUDIO_MSBC_OPEN_EVT,
+ "BTA_HF_CLIENT_AUDIO_MSBC_OPEN_EVT"),
+ std::make_pair(BTA_HF_CLIENT_AUDIO_CLOSE_EVT,
+ "BTA_HF_CLIENT_AUDIO_CLOSE_EVT"),
+ std::make_pair(BTA_HF_CLIENT_SPK_EVT, "BTA_HF_CLIENT_SPK_EVT"),
+ std::make_pair(BTA_HF_CLIENT_MIC_EVT, "BTA_HF_CLIENT_MIC_EVT"),
+ std::make_pair(BTA_HF_CLIENT_DISABLE_EVT, "BTA_HF_CLIENT_DISABLE_EVT"),
+ std::make_pair(BTA_HF_CLIENT_IND_EVT, "BTA_HF_CLIENT_IND_EVT"),
+ std::make_pair(BTA_HF_CLIENT_VOICE_REC_EVT,
+ "BTA_HF_CLIENT_VOICE_REC_EVT"),
+ std::make_pair(BTA_HF_CLIENT_OPERATOR_NAME_EVT,
+ "BTA_HF_CLIENT_OPERATOR_NAME_EVT"),
+ std::make_pair(BTA_HF_CLIENT_CLIP_EVT, "BTA_HF_CLIENT_CLIP_EVT"),
+ std::make_pair(BTA_HF_CLIENT_CCWA_EVT, "BTA_HF_CLIENT_CCWA_EVT"),
+ std::make_pair(BTA_HF_CLIENT_AT_RESULT_EVT,
+ "BTA_HF_CLIENT_AT_RESULT_EVT"),
+ std::make_pair(BTA_HF_CLIENT_CLCC_EVT, "BTA_HF_CLIENT_CLCC_EVT"),
+ std::make_pair(BTA_HF_CLIENT_CNUM_EVT, "BTA_HF_CLIENT_CNUM_EVT"),
+ std::make_pair(BTA_HF_CLIENT_BTRH_EVT, "BTA_HF_CLIENT_BTRH_EVT"),
+ std::make_pair(BTA_HF_CLIENT_BSIR_EVT, "BTA_HF_CLIENT_BSIR_EVT"),
+ std::make_pair(BTA_HF_CLIENT_BINP_EVT, "BTA_HF_CLIENT_BINP_EVT"),
+ std::make_pair(BTA_HF_CLIENT_RING_INDICATION,
+ "BTA_HF_CLIENT_RING_INDICATION"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_hf_client_event(event.first));
+ }
+ std::ostringstream oss;
+ oss << "UNKNOWN MSG ID";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_hf_client_event(std::numeric_limits<uint16_t>::max()));
+}
+
+TEST_F(BtifCoreTest, dump_hh_event) {
+ std::vector<std::pair<int, std::string>> events = {
+ std::make_pair(BTA_HH_ENABLE_EVT, "BTA_HH_ENABLE_EVT"),
+ std::make_pair(BTA_HH_DISABLE_EVT, "BTA_HH_DISABLE_EVT"),
+ std::make_pair(BTA_HH_OPEN_EVT, "BTA_HH_OPEN_EVT"),
+ std::make_pair(BTA_HH_CLOSE_EVT, "BTA_HH_CLOSE_EVT"),
+ std::make_pair(BTA_HH_GET_DSCP_EVT, "BTA_HH_GET_DSCP_EVT"),
+ std::make_pair(BTA_HH_GET_PROTO_EVT, "BTA_HH_GET_PROTO_EVT"),
+ std::make_pair(BTA_HH_GET_RPT_EVT, "BTA_HH_GET_RPT_EVT"),
+ std::make_pair(BTA_HH_GET_IDLE_EVT, "BTA_HH_GET_IDLE_EVT"),
+ std::make_pair(BTA_HH_SET_PROTO_EVT, "BTA_HH_SET_PROTO_EVT"),
+ std::make_pair(BTA_HH_SET_RPT_EVT, "BTA_HH_SET_RPT_EVT"),
+ std::make_pair(BTA_HH_SET_IDLE_EVT, "BTA_HH_SET_IDLE_EVT"),
+ std::make_pair(BTA_HH_VC_UNPLUG_EVT, "BTA_HH_VC_UNPLUG_EVT"),
+ std::make_pair(BTA_HH_ADD_DEV_EVT, "BTA_HH_ADD_DEV_EVT"),
+ std::make_pair(BTA_HH_RMV_DEV_EVT, "BTA_HH_RMV_DEV_EVT"),
+ std::make_pair(BTA_HH_API_ERR_EVT, "BTA_HH_API_ERR_EVT"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_hh_event(event.first));
+ }
+ std::ostringstream oss;
+ oss << "UNKNOWN MSG ID";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_hh_event(std::numeric_limits<uint16_t>::max()));
+}
+
+TEST_F(BtifCoreTest, dump_hd_event) {
+ std::vector<std::pair<uint16_t, std::string>> events = {
+ std::make_pair(BTA_HD_ENABLE_EVT, "BTA_HD_ENABLE_EVT"),
+ std::make_pair(BTA_HD_DISABLE_EVT, "BTA_HD_DISABLE_EVT"),
+ std::make_pair(BTA_HD_REGISTER_APP_EVT, "BTA_HD_REGISTER_APP_EVT"),
+ std::make_pair(BTA_HD_UNREGISTER_APP_EVT, "BTA_HD_UNREGISTER_APP_EVT"),
+ std::make_pair(BTA_HD_OPEN_EVT, "BTA_HD_OPEN_EVT"),
+ std::make_pair(BTA_HD_CLOSE_EVT, "BTA_HD_CLOSE_EVT"),
+ std::make_pair(BTA_HD_GET_REPORT_EVT, "BTA_HD_GET_REPORT_EVT"),
+ std::make_pair(BTA_HD_SET_REPORT_EVT, "BTA_HD_SET_REPORT_EVT"),
+ std::make_pair(BTA_HD_SET_PROTOCOL_EVT, "BTA_HD_SET_PROTOCOL_EVT"),
+ std::make_pair(BTA_HD_INTR_DATA_EVT, "BTA_HD_INTR_DATA_EVT"),
+ std::make_pair(BTA_HD_VC_UNPLUG_EVT, "BTA_HD_VC_UNPLUG_EVT"),
+ std::make_pair(BTA_HD_CONN_STATE_EVT, "BTA_HD_CONN_STATE_EVT"),
+ std::make_pair(BTA_HD_API_ERR_EVT, "BTA_HD_API_ERR_EVT"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_hd_event(event.first));
+ }
+ std::ostringstream oss;
+ oss << "UNKNOWN MSG ID";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_hd_event(std::numeric_limits<uint16_t>::max()));
+}
+
+TEST_F(BtifCoreTest, dump_thread_evt) {
+ std::vector<std::pair<bt_cb_thread_evt, std::string>> events = {
+ std::make_pair(ASSOCIATE_JVM, "ASSOCIATE_JVM"),
+ std::make_pair(DISASSOCIATE_JVM, "DISASSOCIATE_JVM"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_thread_evt(event.first));
+ }
+ std::ostringstream oss;
+ oss << "unknown thread evt";
+ ASSERT_STREQ(oss.str().c_str(), dump_thread_evt(static_cast<bt_cb_thread_evt>(
+ std::numeric_limits<uint16_t>::max())));
+}
+
+TEST_F(BtifCoreTest, dump_av_conn_state) {
+ std::vector<std::pair<uint16_t, std::string>> events = {
+ std::make_pair(BTAV_CONNECTION_STATE_DISCONNECTED,
+ "BTAV_CONNECTION_STATE_DISCONNECTED"),
+ std::make_pair(BTAV_CONNECTION_STATE_CONNECTING,
+ "BTAV_CONNECTION_STATE_CONNECTING"),
+ std::make_pair(BTAV_CONNECTION_STATE_CONNECTED,
+ "BTAV_CONNECTION_STATE_CONNECTED"),
+ std::make_pair(BTAV_CONNECTION_STATE_DISCONNECTING,
+ "BTAV_CONNECTION_STATE_DISCONNECTING"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_av_conn_state(event.first));
+ }
+ std::ostringstream oss;
+ oss << "UNKNOWN MSG ID";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_av_conn_state(std::numeric_limits<uint16_t>::max()));
+}
+
+TEST_F(BtifCoreTest, dump_av_audio_state) {
+ std::vector<std::pair<uint16_t, std::string>> events = {
+ std::make_pair(BTAV_AUDIO_STATE_REMOTE_SUSPEND,
+ "BTAV_AUDIO_STATE_REMOTE_SUSPEND"),
+ std::make_pair(BTAV_AUDIO_STATE_STOPPED, "BTAV_AUDIO_STATE_STOPPED"),
+ std::make_pair(BTAV_AUDIO_STATE_STARTED, "BTAV_AUDIO_STATE_STARTED"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_av_audio_state(event.first));
+ }
+ std::ostringstream oss;
+ oss << "UNKNOWN MSG ID";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_av_audio_state(std::numeric_limits<uint16_t>::max()));
+}
+
+TEST_F(BtifCoreTest, dump_adapter_scan_mode) {
+ std::vector<std::pair<bt_scan_mode_t, std::string>> events = {
+ std::make_pair(BT_SCAN_MODE_NONE, "BT_SCAN_MODE_NONE"),
+ std::make_pair(BT_SCAN_MODE_CONNECTABLE, "BT_SCAN_MODE_CONNECTABLE"),
+ std::make_pair(BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE,
+ "BT_SCAN_MODE_CONNECTABLE_DISCOVERABLE"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_adapter_scan_mode(event.first));
+ }
+ std::ostringstream oss;
+ oss << "unknown scan mode";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_adapter_scan_mode(static_cast<bt_scan_mode_t>(
+ std::numeric_limits<int>::max())));
+}
+
+TEST_F(BtifCoreTest, dump_bt_status) {
+ std::vector<std::pair<bt_status_t, std::string>> events = {
+ std::make_pair(BT_STATUS_SUCCESS, "BT_STATUS_SUCCESS"),
+ std::make_pair(BT_STATUS_FAIL, "BT_STATUS_FAIL"),
+ std::make_pair(BT_STATUS_NOT_READY, "BT_STATUS_NOT_READY"),
+ std::make_pair(BT_STATUS_NOMEM, "BT_STATUS_NOMEM"),
+ std::make_pair(BT_STATUS_BUSY, "BT_STATUS_BUSY"),
+ std::make_pair(BT_STATUS_UNSUPPORTED, "BT_STATUS_UNSUPPORTED"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_bt_status(event.first));
+ }
+ std::ostringstream oss;
+ oss << "unknown scan mode";
+ ASSERT_STREQ(oss.str().c_str(), dump_bt_status(static_cast<bt_status_t>(
+ std::numeric_limits<int>::max())));
+}
+
+TEST_F(BtifCoreTest, dump_rc_event) {
+ std::vector<std::pair<uint8_t, std::string>> events = {
+ std::make_pair(BTA_AV_RC_OPEN_EVT, "BTA_AV_RC_OPEN_EVT"),
+ std::make_pair(BTA_AV_RC_CLOSE_EVT, "BTA_AV_RC_CLOSE_EVT"),
+ std::make_pair(BTA_AV_RC_BROWSE_OPEN_EVT, "BTA_AV_RC_BROWSE_OPEN_EVT"),
+ std::make_pair(BTA_AV_RC_BROWSE_CLOSE_EVT, "BTA_AV_RC_BROWSE_CLOSE_EVT"),
+ std::make_pair(BTA_AV_REMOTE_CMD_EVT, "BTA_AV_REMOTE_CMD_EVT"),
+ std::make_pair(BTA_AV_REMOTE_RSP_EVT, "BTA_AV_REMOTE_RSP_EVT"),
+ std::make_pair(BTA_AV_VENDOR_CMD_EVT, "BTA_AV_VENDOR_CMD_EVT"),
+ std::make_pair(BTA_AV_VENDOR_RSP_EVT, "BTA_AV_VENDOR_RSP_EVT"),
+ std::make_pair(BTA_AV_META_MSG_EVT, "BTA_AV_META_MSG_EVT"),
+ std::make_pair(BTA_AV_RC_FEAT_EVT, "BTA_AV_RC_FEAT_EVT"),
+ std::make_pair(BTA_AV_RC_PSM_EVT, "BTA_AV_RC_PSM_EVT"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(), dump_rc_event(event.first));
+ }
+ std::ostringstream oss;
+ oss << "UNKNOWN_EVENT";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_rc_event(std::numeric_limits<uint8_t>::max()));
+}
+
+TEST_F(BtifCoreTest, dump_rc_notification_event_id) {
+ std::vector<std::pair<uint8_t, std::string>> events = {
+ std::make_pair(AVRC_EVT_PLAY_STATUS_CHANGE,
+ "AVRC_EVT_PLAY_STATUS_CHANGE"),
+ std::make_pair(AVRC_EVT_TRACK_CHANGE, "AVRC_EVT_TRACK_CHANGE"),
+ std::make_pair(AVRC_EVT_TRACK_REACHED_END, "AVRC_EVT_TRACK_REACHED_END"),
+ std::make_pair(AVRC_EVT_TRACK_REACHED_START,
+ "AVRC_EVT_TRACK_REACHED_START"),
+ std::make_pair(AVRC_EVT_PLAY_POS_CHANGED, "AVRC_EVT_PLAY_POS_CHANGED"),
+ std::make_pair(AVRC_EVT_BATTERY_STATUS_CHANGE,
+ "AVRC_EVT_BATTERY_STATUS_CHANGE"),
+ std::make_pair(AVRC_EVT_SYSTEM_STATUS_CHANGE,
+ "AVRC_EVT_SYSTEM_STATUS_CHANGE"),
+ std::make_pair(AVRC_EVT_APP_SETTING_CHANGE,
+ "AVRC_EVT_APP_SETTING_CHANGE"),
+ std::make_pair(AVRC_EVT_VOLUME_CHANGE, "AVRC_EVT_VOLUME_CHANGE"),
+ std::make_pair(AVRC_EVT_ADDR_PLAYER_CHANGE,
+ "AVRC_EVT_ADDR_PLAYER_CHANGE"),
+ std::make_pair(AVRC_EVT_AVAL_PLAYERS_CHANGE,
+ "AVRC_EVT_AVAL_PLAYERS_CHANGE"),
+ std::make_pair(AVRC_EVT_NOW_PLAYING_CHANGE,
+ "AVRC_EVT_NOW_PLAYING_CHANGE"),
+ std::make_pair(AVRC_EVT_UIDS_CHANGE, "AVRC_EVT_UIDS_CHANGE"),
+ };
+ for (const auto& event : events) {
+ ASSERT_STREQ(event.second.c_str(),
+ dump_rc_notification_event_id(event.first));
+ }
+ std::ostringstream oss;
+ oss << "Unhandled Event ID";
+ ASSERT_STREQ(oss.str().c_str(), dump_rc_notification_event_id(
+ std::numeric_limits<uint8_t>::max()));
+}
+
+TEST_F(BtifCoreTest, dump_rc_pdu) {
+ std::vector<std::pair<uint8_t, std::string>> pdus = {
+ std::make_pair(AVRC_PDU_LIST_PLAYER_APP_ATTR,
+ "AVRC_PDU_LIST_PLAYER_APP_ATTR"),
+ std::make_pair(AVRC_PDU_LIST_PLAYER_APP_VALUES,
+ "AVRC_PDU_LIST_PLAYER_APP_VALUES"),
+ std::make_pair(AVRC_PDU_GET_CUR_PLAYER_APP_VALUE,
+ "AVRC_PDU_GET_CUR_PLAYER_APP_VALUE"),
+ std::make_pair(AVRC_PDU_SET_PLAYER_APP_VALUE,
+ "AVRC_PDU_SET_PLAYER_APP_VALUE"),
+ std::make_pair(AVRC_PDU_GET_PLAYER_APP_ATTR_TEXT,
+ "AVRC_PDU_GET_PLAYER_APP_ATTR_TEXT"),
+ std::make_pair(AVRC_PDU_GET_PLAYER_APP_VALUE_TEXT,
+ "AVRC_PDU_GET_PLAYER_APP_VALUE_TEXT"),
+ std::make_pair(AVRC_PDU_INFORM_DISPLAY_CHARSET,
+ "AVRC_PDU_INFORM_DISPLAY_CHARSET"),
+ std::make_pair(AVRC_PDU_INFORM_BATTERY_STAT_OF_CT,
+ "AVRC_PDU_INFORM_BATTERY_STAT_OF_CT"),
+ std::make_pair(AVRC_PDU_GET_ELEMENT_ATTR, "AVRC_PDU_GET_ELEMENT_ATTR"),
+ std::make_pair(AVRC_PDU_GET_PLAY_STATUS, "AVRC_PDU_GET_PLAY_STATUS"),
+ std::make_pair(AVRC_PDU_REGISTER_NOTIFICATION,
+ "AVRC_PDU_REGISTER_NOTIFICATION"),
+ std::make_pair(AVRC_PDU_REQUEST_CONTINUATION_RSP,
+ "AVRC_PDU_REQUEST_CONTINUATION_RSP"),
+ std::make_pair(AVRC_PDU_ABORT_CONTINUATION_RSP,
+ "AVRC_PDU_ABORT_CONTINUATION_RSP"),
+ std::make_pair(AVRC_PDU_SET_ABSOLUTE_VOLUME,
+ "AVRC_PDU_SET_ABSOLUTE_VOLUME"),
+ std::make_pair(AVRC_PDU_SET_ADDRESSED_PLAYER,
+ "AVRC_PDU_SET_ADDRESSED_PLAYER"),
+ std::make_pair(AVRC_PDU_CHANGE_PATH, "AVRC_PDU_CHANGE_PATH"),
+ std::make_pair(AVRC_PDU_GET_CAPABILITIES, "AVRC_PDU_GET_CAPABILITIES"),
+ std::make_pair(AVRC_PDU_SET_BROWSED_PLAYER,
+ "AVRC_PDU_SET_BROWSED_PLAYER"),
+ std::make_pair(AVRC_PDU_GET_FOLDER_ITEMS, "AVRC_PDU_GET_FOLDER_ITEMS"),
+ std::make_pair(AVRC_PDU_GET_ITEM_ATTRIBUTES,
+ "AVRC_PDU_GET_ITEM_ATTRIBUTES"),
+ std::make_pair(AVRC_PDU_PLAY_ITEM, "AVRC_PDU_PLAY_ITEM"),
+ std::make_pair(AVRC_PDU_SEARCH, "AVRC_PDU_SEARCH"),
+ std::make_pair(AVRC_PDU_ADD_TO_NOW_PLAYING,
+ "AVRC_PDU_ADD_TO_NOW_PLAYING"),
+ std::make_pair(AVRC_PDU_GET_TOTAL_NUM_OF_ITEMS,
+ "AVRC_PDU_GET_TOTAL_NUM_OF_ITEMS"),
+ std::make_pair(AVRC_PDU_GENERAL_REJECT, "AVRC_PDU_GENERAL_REJECT"),
+ };
+ for (const auto& pdu : pdus) {
+ ASSERT_STREQ(pdu.second.c_str(), dump_rc_pdu(pdu.first));
+ }
+ std::ostringstream oss;
+ oss << "Unknown PDU";
+ ASSERT_STREQ(oss.str().c_str(),
+ dump_rc_pdu(std::numeric_limits<uint8_t>::max()));
+}
diff --git a/system/device/include/interop_database.h b/system/device/include/interop_database.h
index d2f0c99..d9dd282 100644
--- a/system/device/include/interop_database.h
+++ b/system/device/include/interop_database.h
@@ -126,6 +126,9 @@
// Toyota Prius - 2015
{{{0xfc, 0xc2, 0xde, 0, 0, 0}}, 3, INTEROP_DISABLE_ROLE_SWITCH},
+ // Toyota Prius - b/231092023
+ {{{0x9c, 0xdf, 0x03, 0, 0, 0}}, 3, INTEROP_DISABLE_ROLE_SWITCH},
+
// OBU II Bluetooth dongle
{{{0x00, 0x04, 0x3e, 0, 0, 0}}, 3, INTEROP_DISABLE_ROLE_SWITCH},
diff --git a/system/gd/common/init_flags.cc b/system/gd/common/init_flags.cc
index c119aa6..853f94c 100644
--- a/system/gd/common/init_flags.cc
+++ b/system/gd/common/init_flags.cc
@@ -30,6 +30,7 @@
bool InitFlags::logging_debug_enabled_for_all = false;
int InitFlags::hci_adapter = 0;
std::unordered_map<std::string, bool> InitFlags::logging_debug_explicit_tag_settings = {};
+bool InitFlags::btm_dm_flush_discovery_queue_on_search_cancel = false;
bool ParseBoolFlag(const std::vector<std::string>& flag_pair, const std::string& flag, bool* variable) {
if (flag != flag_pair[0]) {
@@ -70,6 +71,11 @@
// Parse adapter index (defaults to 0)
ParseIntFlag(flag_pair, "--hci", &hci_adapter);
+ ParseBoolFlag(
+ flag_pair,
+ "INIT_btm_dm_flush_discovery_queue_on_search_cancel",
+ &btm_dm_flush_discovery_queue_on_search_cancel);
+
ParseBoolFlag(flag_pair, "INIT_logging_debug_enabled_for_all", &logging_debug_enabled_for_all);
if ("INIT_logging_debug_enabled_for_tags" == flag_pair[0]) {
auto tags = StringSplit(flag_pair[1], ",");
diff --git a/system/gd/common/init_flags.fbs b/system/gd/common/init_flags.fbs
index aba1b07..ae561ef 100644
--- a/system/gd/common/init_flags.fbs
+++ b/system/gd/common/init_flags.fbs
@@ -12,6 +12,7 @@
gd_controller_enabled:bool (privacy:"Any");
gd_core_enabled:bool (privacy:"Any");
btaa_hci_log_enabled:bool (privacy:"Any");
+ btm_dm_flush_discovery_queue_on_search_cancel:bool (privacy:"Any");
}
root_type InitFlagsData;
diff --git a/system/gd/common/init_flags.h b/system/gd/common/init_flags.h
index 250e4f5..8ee9cc8 100644
--- a/system/gd/common/init_flags.h
+++ b/system/gd/common/init_flags.h
@@ -41,6 +41,10 @@
return logging_debug_enabled_for_all;
}
+ inline static bool IsBtmDmFlushDiscoveryQueueOnSearchCancel() {
+ return btm_dm_flush_discovery_queue_on_search_cancel;
+ }
+
inline static int GetAdapterIndex() {
return hci_adapter;
}
@@ -50,6 +54,7 @@
private:
static void SetAll(bool value);
static bool logging_debug_enabled_for_all;
+ static bool btm_dm_flush_discovery_queue_on_search_cancel;
static int hci_adapter;
// save both log allow list and block list in the map to save hashing time
static std::unordered_map<std::string, bool> logging_debug_explicit_tag_settings;
diff --git a/system/gd/common/init_flags_test.cc b/system/gd/common/init_flags_test.cc
index 76babcc..74186e5 100644
--- a/system/gd/common/init_flags_test.cc
+++ b/system/gd/common/init_flags_test.cc
@@ -22,6 +22,12 @@
using bluetooth::common::InitFlags;
+TEST(InitFlagsTest, test_enable_btm_flush_discovery_queue_on_search_cancel) {
+ const char* input[] = {"INIT_btm_dm_flush_discovery_queue_on_search_cancel=true", nullptr};
+ InitFlags::Load(input);
+ ASSERT_TRUE(InitFlags::IsBtmDmFlushDiscoveryQueueOnSearchCancel());
+}
+
TEST(InitFlagsTest, test_enable_debug_logging_for_all) {
const char* input[] = {"INIT_logging_debug_enabled_for_all=true", nullptr};
InitFlags::Load(input);
diff --git a/system/gd/hci/acl_manager/classic_impl.h b/system/gd/hci/acl_manager/classic_impl.h
index 569ce75..87b18b4 100644
--- a/system/gd/hci/acl_manager/classic_impl.h
+++ b/system/gd/hci/acl_manager/classic_impl.h
@@ -25,6 +25,7 @@
#include "hci/acl_manager/event_checkers.h"
#include "hci/acl_manager/round_robin_scheduler.h"
#include "hci/controller.h"
+#include "os/metrics.h"
#include "security/security_manager_listener.h"
#include "security/security_module.h"
@@ -209,6 +210,14 @@
}
return kIllegalConnectionHandle;
}
+ Address get_address(uint16_t handle) const {
+ std::unique_lock<std::mutex> lock(acl_connections_guard_);
+ auto connection = acl_connections_.find(handle);
+ if (connection == acl_connections_.end()) {
+ return Address::kEmpty;
+ }
+ return connection->second.address_with_type_.GetAddress();
+ }
bool is_classic_link_already_connected(const Address& address) const {
std::unique_lock<std::mutex> lock(acl_connections_guard_);
for (const auto& connection : acl_connections_) {
@@ -411,6 +420,8 @@
static constexpr bool kRemoveConnectionAfterwards = true;
void on_classic_disconnect(uint16_t handle, ErrorCode reason) {
bool event_also_routes_to_other_receivers = connections.crash_on_unknown_handle_;
+ bluetooth::os::LogMetricBluetoothDisconnectionReasonReported(
+ static_cast<uint32_t>(reason), connections.get_address(handle), handle);
connections.crash_on_unknown_handle_ = false;
connections.execute(
handle,
@@ -604,6 +615,8 @@
auto view = ReadRemoteSupportedFeaturesCompleteView::Create(packet);
ASSERT_LOG(view.IsValid(), "Read remote supported features packet invalid");
uint16_t handle = view.GetConnectionHandle();
+ bluetooth::os::LogMetricBluetoothRemoteSupportedFeatures(
+ connections.get_address(handle), 0, view.GetLmpFeatures(), handle);
connections.execute(handle, [=](ConnectionManagementCallbacks* callbacks) {
callbacks->OnReadRemoteSupportedFeaturesComplete(view.GetLmpFeatures());
});
@@ -613,6 +626,8 @@
auto view = ReadRemoteExtendedFeaturesCompleteView::Create(packet);
ASSERT_LOG(view.IsValid(), "Read remote extended features packet invalid");
uint16_t handle = view.GetConnectionHandle();
+ bluetooth::os::LogMetricBluetoothRemoteSupportedFeatures(
+ connections.get_address(handle), view.GetPageNumber(), view.GetExtendedLmpFeatures(), handle);
connections.execute(handle, [=](ConnectionManagementCallbacks* callbacks) {
callbacks->OnReadRemoteExtendedFeaturesComplete(
view.GetPageNumber(), view.GetMaximumPageNumber(), view.GetExtendedLmpFeatures());
diff --git a/system/gd/hci/controller.cc b/system/gd/hci/controller.cc
index 3827f83..4508904 100644
--- a/system/gd/hci/controller.cc
+++ b/system/gd/hci/controller.cc
@@ -23,6 +23,7 @@
#include "common/init_flags.h"
#include "hci/hci_layer.h"
+#include "os/metrics.h"
namespace bluetooth {
namespace hci {
@@ -244,6 +245,12 @@
ASSERT_LOG(status == ErrorCode::SUCCESS, "Status 0x%02hhx, %s", status, ErrorCodeText(status).c_str());
local_version_information_ = complete_view.GetLocalVersionInformation();
+ bluetooth::os::LogMetricBluetoothLocalVersions(
+ local_version_information_.manufacturer_name_,
+ static_cast<uint8_t>(local_version_information_.lmp_version_),
+ local_version_information_.lmp_subversion_,
+ static_cast<uint8_t>(local_version_information_.hci_version_),
+ local_version_information_.hci_revision_);
}
void read_local_supported_commands_complete_handler(CommandCompleteView view) {
@@ -261,7 +268,7 @@
ASSERT_LOG(status == ErrorCode::SUCCESS, "Status 0x%02hhx, %s", status, ErrorCodeText(status).c_str());
uint8_t page_number = complete_view.GetPageNumber();
extended_lmp_features_array_.push_back(complete_view.GetExtendedLmpFeatures());
-
+ bluetooth::os::LogMetricBluetoothLocalSupportedFeatures(page_number, complete_view.GetExtendedLmpFeatures());
// Query all extended features
if (page_number < complete_view.GetMaximumPageNumber()) {
page_number++;
diff --git a/system/gd/hci/hci_packets.pdl b/system/gd/hci/hci_packets.pdl
index abee8a1..c09fd05 100644
--- a/system/gd/hci/hci_packets.pdl
+++ b/system/gd/hci/hci_packets.pdl
@@ -777,6 +777,7 @@
// Vendor specific events
enum VseSubeventCode : 8 {
BLE_THRESHOLD = 0x54,
+ BLE_STCHANGE = 0x55,
BLE_TRACKING = 0x56,
DEBUG_INFO = 0x57,
BQR_EVENT = 0x58,
@@ -6000,6 +6001,17 @@
_body_,
}
+enum VseStateChangeReason : 8 {
+ CONNECTION_RECEIVED = 0x00,
+}
+
+packet LEAdvertiseStateChangeEvent : VendorSpecificEvent (subevent_code = BLE_STCHANGE) {
+ advertising_instance : 8,
+ state_change_reason : VseStateChangeReason,
+ connection_handle : 12,
+ reserved : 4,
+}
+
packet LEAdvertisementTrackingWithInfoEvent : LEAdvertisementTrackingEvent {
tx_power : 8,
rssi : 8,
diff --git a/system/gd/hci/le_advertising_manager.cc b/system/gd/hci/le_advertising_manager.cc
index 5e5a79f..9437a94 100644
--- a/system/gd/hci/le_advertising_manager.cc
+++ b/system/gd/hci/le_advertising_manager.cc
@@ -24,6 +24,7 @@
#include "hci/hci_layer.h"
#include "hci/hci_packets.h"
#include "hci/le_advertising_interface.h"
+#include "hci/vendor_specific_event_manager.h"
#include "module.h"
#include "os/handler.h"
#include "os/log.h"
@@ -103,17 +104,25 @@
advertising_sets_.clear();
}
- void start(os::Handler* handler, hci::HciLayer* hci_layer, hci::Controller* controller,
- hci::AclManager* acl_manager) {
+ void start(
+ os::Handler* handler,
+ hci::HciLayer* hci_layer,
+ hci::Controller* controller,
+ hci::AclManager* acl_manager,
+ hci::VendorSpecificEventManager* vendor_specific_event_manager) {
module_handler_ = handler;
hci_layer_ = hci_layer;
controller_ = controller;
le_maximum_advertising_data_length_ = controller_->GetLeMaximumAdvertisingDataLength();
acl_manager_ = acl_manager;
le_address_manager_ = acl_manager->GetLeAddressManager();
+ num_instances_ = controller_->GetLeNumberOfSupportedAdverisingSets();
+
le_advertising_interface_ =
hci_layer_->GetLeAdvertisingInterface(module_handler_->BindOn(this, &LeAdvertisingManager::impl::handle_event));
- num_instances_ = controller_->GetLeNumberOfSupportedAdverisingSets();
+ vendor_specific_event_manager->RegisterEventHandler(
+ hci::VseSubeventCode::BLE_STCHANGE,
+ handler->BindOn(this, &LeAdvertisingManager::impl::multi_advertising_state_change));
if (controller_->SupportsBleExtendedAdvertising()) {
advertising_api_type_ = AdvertisingApiType::EXTENDED;
@@ -152,6 +161,33 @@
advertising_callbacks_ = advertising_callback;
}
+ void multi_advertising_state_change(hci::VendorSpecificEventView event) {
+ auto view = hci::LEAdvertiseStateChangeEventView::Create(event);
+ ASSERT(view.IsValid());
+
+ auto advertiser_id = view.GetAdvertisingInstance();
+
+ LOG_INFO(
+ "Instance: 0x%x StateChangeReason: 0x%s Handle: 0x%x Address: %s",
+ advertiser_id,
+ VseStateChangeReasonText(view.GetStateChangeReason()).c_str(),
+ view.GetConnectionHandle(),
+ advertising_sets_[view.GetAdvertisingInstance()].current_address.ToString().c_str());
+
+ if (view.GetStateChangeReason() == VseStateChangeReason::CONNECTION_RECEIVED) {
+ acl_manager_->OnAdvertisingSetTerminated(
+ ErrorCode::SUCCESS, view.GetConnectionHandle(), advertising_sets_[advertiser_id].current_address);
+
+ enabled_sets_[advertiser_id].advertising_handle_ = kInvalidHandle;
+
+ if (!advertising_sets_[advertiser_id].directed) {
+ // TODO(250666237) calculate remaining duration and advertising events
+ LOG_INFO("Resuming advertising, since not directed");
+ enable_advertiser(advertiser_id, true, 0, 0);
+ }
+ }
+ }
+
void handle_event(LeMetaEventView event) {
switch (event.GetSubeventCode()) {
case hci::SubeventCode::SCAN_REQUEST_RECEIVED:
@@ -1374,11 +1410,16 @@
list->add<hci::HciLayer>();
list->add<hci::Controller>();
list->add<hci::AclManager>();
+ list->add<hci::VendorSpecificEventManager>();
}
void LeAdvertisingManager::Start() {
- pimpl_->start(GetHandler(), GetDependency<hci::HciLayer>(), GetDependency<hci::Controller>(),
- GetDependency<AclManager>());
+ pimpl_->start(
+ GetHandler(),
+ GetDependency<hci::HciLayer>(),
+ GetDependency<hci::Controller>(),
+ GetDependency<AclManager>(),
+ GetDependency<VendorSpecificEventManager>());
}
void LeAdvertisingManager::Stop() {
diff --git a/system/gd/hci/le_scanning_manager_test.cc b/system/gd/hci/le_scanning_manager_test.cc
index 1d70ef5..8671958 100644
--- a/system/gd/hci/le_scanning_manager_test.cc
+++ b/system/gd/hci/le_scanning_manager_test.cc
@@ -14,34 +14,101 @@
* limitations under the License.
*/
+#include "hci/le_scanning_manager.h"
+
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <chrono>
#include <future>
+#include <list>
#include <map>
+#include <memory>
+#include <mutex>
+#include <queue>
+#include <vector>
#include "common/bind.h"
#include "hci/acl_manager.h"
#include "hci/address.h"
#include "hci/controller.h"
#include "hci/hci_layer.h"
-#include "hci/le_scanning_manager.h"
+#include "hci/uuid.h"
#include "os/thread.h"
#include "packet/raw_builder.h"
using ::testing::_;
using ::testing::Eq;
-namespace bluetooth {
-namespace hci {
-namespace {
+using namespace bluetooth;
+using namespace std::chrono_literals;
using packet::kLittleEndian;
using packet::PacketView;
using packet::RawBuilder;
+namespace {
+
+hci::AdvertisingPacketContentFilterCommand make_filter(const hci::ApcfFilterType& filter_type) {
+ hci::AdvertisingPacketContentFilterCommand filter{};
+ filter.filter_type = filter_type;
+
+ switch (filter_type) {
+ case hci::ApcfFilterType::AD_TYPE:
+ case hci::ApcfFilterType::SERVICE_DATA:
+ filter.ad_type = 0x09;
+ filter.data = {0x12, 0x34, 0x56, 0x78};
+ filter.data_mask = {0xff, 0xff, 0xff, 0xff};
+ break;
+ case hci::ApcfFilterType::BROADCASTER_ADDRESS:
+ filter.address = hci::Address::kEmpty;
+ filter.application_address_type = hci::ApcfApplicationAddressType::RANDOM;
+ break;
+ case hci::ApcfFilterType::SERVICE_UUID:
+ filter.uuid = hci::Uuid::From32Bit(0x12345678);
+ filter.uuid_mask = hci::Uuid::From32Bit(0xffffffff);
+ break;
+ case hci::ApcfFilterType::LOCAL_NAME:
+ filter.name = {0x01, 0x02, 0x03};
+ break;
+ case hci::ApcfFilterType::MANUFACTURER_DATA:
+ filter.company = 0x12;
+ filter.company_mask = 0xff;
+ filter.data = {0x12, 0x34, 0x56, 0x78};
+ filter.data_mask = {0xff, 0xff, 0xff, 0xff};
+ break;
+ default:
+ break;
+ }
+ return filter;
+}
+
+hci::LeAdvertisingResponse make_advertising_report() {
+ hci::LeAdvertisingResponse report{};
+ report.event_type_ = hci::AdvertisingEventType::ADV_DIRECT_IND;
+ report.address_type_ = hci::AddressType::PUBLIC_DEVICE_ADDRESS;
+ hci::Address::FromString("12:34:56:78:9a:bc", report.address_);
+ std::vector<hci::LengthAndData> adv_data{};
+ hci::LengthAndData data_item{};
+ data_item.data_.push_back(static_cast<uint8_t>(hci::GapDataType::FLAGS));
+ data_item.data_.push_back(0x34);
+ adv_data.push_back(data_item);
+ data_item.data_.push_back(static_cast<uint8_t>(hci::GapDataType::COMPLETE_LOCAL_NAME));
+ for (auto octet : {'r', 'a', 'n', 'd', 'o', 'm', ' ', 'd', 'e', 'v', 'i', 'c', 'e'}) {
+ data_item.data_.push_back(octet);
+ }
+ adv_data.push_back(data_item);
+ report.advertising_data_ = adv_data;
+ return report;
+}
+
+} // namespace
+
+namespace bluetooth {
+namespace hci {
+namespace {
+
PacketView<kLittleEndian> GetPacketView(std::unique_ptr<packet::BasePacketBuilder> packet) {
auto bytes = std::make_shared<std::vector<uint8_t>>();
BitInserter i(*bytes);
@@ -98,17 +165,21 @@
}
// Set command future for 'num_command' commands are expected
- void SetCommandFuture(uint16_t num_command) {
+ void SetCommandFuture(uint16_t num_command = 1) {
ASSERT_TRUE(command_promise_ == nullptr) << "Promises, Promises, ... Only one at a time.";
command_count_ = num_command;
command_promise_ = std::make_unique<std::promise<void>>();
- command_future_ = std::make_unique<std::future<void>>(command_promise_->get_future());
+ command_future_ = command_promise_->get_future();
}
CommandView GetCommand() {
// Wait for EnqueueCommand if command_queue_ is empty
- if (command_queue_.empty() && command_future_ != nullptr) {
- command_future_->wait_for(std::chrono::milliseconds(1000));
+ if (command_promise_ != nullptr) {
+ if (command_queue_.empty()) {
+ LOG_ERROR("Waiting for command queue to fill ");
+ command_future_.wait_for(1s);
+ }
+ command_promise_.reset();
}
std::lock_guard<std::mutex> lock(mutex_);
@@ -166,6 +237,7 @@
void CommandCompleteCallback(EventView event) {
CommandCompleteView complete_view = CommandCompleteView::Create(event);
ASSERT_TRUE(complete_view.IsValid());
+ ASSERT_TRUE(!command_complete_callbacks.empty());
std::move(command_complete_callbacks.front()).Invoke(complete_view);
command_complete_callbacks.pop_front();
}
@@ -191,7 +263,14 @@
EventCode::COMMAND_COMPLETE, GetHandler()->BindOn(this, &TestHciLayer::CommandCompleteCallback));
RegisterEventHandler(EventCode::COMMAND_STATUS, GetHandler()->BindOn(this, &TestHciLayer::CommandStatusCallback));
}
- void Stop() override {}
+ void Stop() override {
+ UnregisterEventHandler(EventCode::COMMAND_STATUS);
+ UnregisterEventHandler(EventCode::COMMAND_COMPLETE);
+ }
+
+ size_t CommandQueueSize() const {
+ return command_queue_.size();
+ }
private:
std::map<EventCode, common::ContextualCallback<void(EventView)>> registered_events_;
@@ -200,7 +279,7 @@
std::list<common::ContextualOnceCallback<void(CommandStatusView)>> command_status_callbacks;
std::queue<std::unique_ptr<CommandBuilder>> command_queue_;
std::unique_ptr<std::promise<void>> command_promise_;
- std::unique_ptr<std::future<void>> command_future_;
+ std::future<void> command_future_;
mutable std::mutex mutex_;
uint16_t command_count_ = 0;
CommandView empty_command_view_ =
@@ -252,52 +331,86 @@
void enqueue_command(std::unique_ptr<CommandBuilder> command_packet){};
+ private:
os::Thread* thread_;
os::Handler* handler_;
TestLeAddressManager* test_le_address_manager_;
};
+class MockCallbacks : public bluetooth::hci::ScanningCallback {
+ public:
+ MOCK_METHOD(
+ void,
+ OnScannerRegistered,
+ (const bluetooth::hci::Uuid app_uuid, ScannerId scanner_id, ScanningStatus status),
+ (override));
+ MOCK_METHOD(void, OnSetScannerParameterComplete, (ScannerId scanner_id, ScanningStatus status), (override));
+ MOCK_METHOD(
+ void,
+ OnScanResult,
+ (uint16_t event_type,
+ uint8_t address_type,
+ Address address,
+ uint8_t primary_phy,
+ uint8_t secondary_phy,
+ uint8_t advertising_sid,
+ int8_t tx_power,
+ int8_t rssi,
+ uint16_t periodic_advertising_interval,
+ std::vector<uint8_t> advertising_data),
+ (override));
+ MOCK_METHOD(
+ void,
+ OnTrackAdvFoundLost,
+ (bluetooth::hci::AdvertisingFilterOnFoundOnLostInfo on_found_on_lost_info),
+ (override));
+ MOCK_METHOD(
+ void,
+ OnBatchScanReports,
+ (int client_if, int status, int report_format, int num_records, std::vector<uint8_t> data),
+ (override));
+ MOCK_METHOD(void, OnBatchScanThresholdCrossed, (int client_if), (override));
+ MOCK_METHOD(void, OnTimeout, (), (override));
+ MOCK_METHOD(void, OnFilterEnable, (Enable enable, uint8_t status), (override));
+ MOCK_METHOD(void, OnFilterParamSetup, (uint8_t available_spaces, ApcfAction action, uint8_t status), (override));
+ MOCK_METHOD(
+ void,
+ OnFilterConfigCallback,
+ (ApcfFilterType filter_type, uint8_t available_spaces, ApcfAction action, uint8_t status),
+ (override));
+ MOCK_METHOD(void, OnPeriodicSyncStarted, (int, uint8_t, uint16_t, uint8_t, AddressWithType, uint8_t, uint16_t));
+ MOCK_METHOD(void, OnPeriodicSyncReport, (uint16_t, int8_t, int8_t, uint8_t, std::vector<uint8_t>));
+ MOCK_METHOD(void, OnPeriodicSyncLost, (uint16_t));
+ MOCK_METHOD(void, OnPeriodicSyncTransferred, (int, uint8_t, Address));
+} mock_callbacks_;
+
class LeScanningManagerTest : public ::testing::Test {
protected:
void SetUp() override {
test_hci_layer_ = new TestHciLayer; // Ownership is transferred to registry
test_controller_ = new TestController;
- test_controller_->AddSupported(param_opcode_);
- if (is_filter_support_) {
- test_controller_->AddSupported(OpCode::LE_ADV_FILTER);
- }
- if (is_batch_scan_support_) {
- test_controller_->AddSupported(OpCode::LE_BATCH_SCAN);
- }
test_acl_manager_ = new TestAclManager;
fake_registry_.InjectTestModule(&HciLayer::Factory, test_hci_layer_);
fake_registry_.InjectTestModule(&Controller::Factory, test_controller_);
fake_registry_.InjectTestModule(&AclManager::Factory, test_acl_manager_);
client_handler_ = fake_registry_.GetTestModuleHandler(&HciLayer::Factory);
- ASSERT_NE(client_handler_, nullptr);
- if (is_filter_support_) {
- // Will send APCF read extended features command if APCF supported
- test_hci_layer_->SetCommandFuture(2);
- } else {
- test_hci_layer_->SetCommandFuture(1);
- }
- // configure_scan will be trigger by impl.start() and enqueue set scan parameter command
- fake_registry_.Start<LeScanningManager>(&thread_);
- le_scanning_manager =
- static_cast<LeScanningManager*>(fake_registry_.GetModuleUnderTest(&LeScanningManager::Factory));
- HandleConfiguration();
- le_scanning_manager->RegisterScanningCallback(&mock_callbacks_);
+ ASSERT_TRUE(client_handler_ != nullptr);
}
void TearDown() override {
sync_client_handler();
- fake_registry_.SynchronizeModuleHandler(&LeScanningManager::Factory, std::chrono::milliseconds(20));
+ if (fake_registry_.IsStarted<LeScanningManager>()) {
+ fake_registry_.SynchronizeModuleHandler(&LeScanningManager::Factory, std::chrono::milliseconds(20));
+ }
fake_registry_.StopAll();
}
- virtual void HandleConfiguration() {
- ASSERT_EQ(param_opcode_, test_hci_layer_->GetCommand().GetOpCode());
- test_hci_layer_->IncomingEvent(LeSetScanParametersCompleteBuilder::Create(1, ErrorCode::SUCCESS));
+ void start_le_scanning_manager() {
+ fake_registry_.Start<LeScanningManager>(&thread_);
+ le_scanning_manager =
+ static_cast<LeScanningManager*>(fake_registry_.GetModuleUnderTest(&LeScanningManager::Factory));
+ le_scanning_manager->RegisterScanningCallback(&mock_callbacks_);
+ sync_client_handler();
}
void sync_client_handler() {
@@ -316,193 +429,106 @@
LeScanningManager* le_scanning_manager = nullptr;
os::Handler* client_handler_ = nullptr;
- class MockCallbacks : public bluetooth::hci::ScanningCallback {
- public:
- MOCK_METHOD(
- void,
- OnScannerRegistered,
- (const bluetooth::hci::Uuid app_uuid, ScannerId scanner_id, ScanningStatus status),
- (override));
- MOCK_METHOD(void, OnSetScannerParameterComplete, (ScannerId scanner_id, ScanningStatus status), (override));
- MOCK_METHOD(
- void,
- OnScanResult,
- (uint16_t event_type,
- uint8_t address_type,
- Address address,
- uint8_t primary_phy,
- uint8_t secondary_phy,
- uint8_t advertising_sid,
- int8_t tx_power,
- int8_t rssi,
- uint16_t periodic_advertising_interval,
- std::vector<uint8_t> advertising_data),
- (override));
- MOCK_METHOD(
- void,
- OnTrackAdvFoundLost,
- (bluetooth::hci::AdvertisingFilterOnFoundOnLostInfo on_found_on_lost_info),
- (override));
- MOCK_METHOD(
- void,
- OnBatchScanReports,
- (int client_if, int status, int report_format, int num_records, std::vector<uint8_t> data),
- (override));
- MOCK_METHOD(void, OnBatchScanThresholdCrossed, (int client_if), (override));
- MOCK_METHOD(void, OnTimeout, (), (override));
- MOCK_METHOD(void, OnFilterEnable, (Enable enable, uint8_t status), (override));
- MOCK_METHOD(void, OnFilterParamSetup, (uint8_t available_spaces, ApcfAction action, uint8_t status), (override));
- MOCK_METHOD(
- void,
- OnFilterConfigCallback,
- (ApcfFilterType filter_type, uint8_t available_spaces, ApcfAction action, uint8_t status),
- (override));
- MOCK_METHOD(void, OnPeriodicSyncStarted, (int, uint8_t, uint16_t, uint8_t, AddressWithType, uint8_t, uint16_t));
- MOCK_METHOD(void, OnPeriodicSyncReport, (uint16_t, int8_t, int8_t, uint8_t, std::vector<uint8_t>));
- MOCK_METHOD(void, OnPeriodicSyncLost, (uint16_t));
- MOCK_METHOD(void, OnPeriodicSyncTransferred, (int, uint8_t, Address));
- } mock_callbacks_;
-
- OpCode param_opcode_{OpCode::LE_SET_SCAN_PARAMETERS};
- OpCode enable_opcode_{OpCode::LE_SET_SCAN_ENABLE};
- bool is_filter_support_ = false;
- bool is_batch_scan_support_ = false;
+ MockCallbacks mock_callbacks_;
};
-class LeAndroidHciScanningManagerTest : public LeScanningManagerTest {
+class LeScanningManagerAndroidHciTest : public LeScanningManagerTest {
protected:
void SetUp() override {
- param_opcode_ = OpCode::LE_EXTENDED_SCAN_PARAMS;
- is_filter_support_ = true;
- is_batch_scan_support_ = true;
LeScanningManagerTest::SetUp();
- sync_client_handler();
- }
+ test_controller_->AddSupported(OpCode::LE_EXTENDED_SCAN_PARAMS);
+ test_controller_->AddSupported(OpCode::LE_ADV_FILTER);
+ test_controller_->AddSupported(OpCode::LE_BATCH_SCAN);
+ start_le_scanning_manager();
+ ASSERT_TRUE(fake_registry_.IsStarted(&HciLayer::Factory));
- void HandleConfiguration() override {
+ test_hci_layer_->SetCommandFuture();
ASSERT_EQ(OpCode::LE_ADV_FILTER, test_hci_layer_->GetCommand().GetOpCode());
+ ASSERT_EQ(0UL, test_hci_layer_->CommandQueueSize());
test_hci_layer_->IncomingEvent(LeAdvFilterReadExtendedFeaturesCompleteBuilder::Create(1, ErrorCode::SUCCESS, 0x01));
- sync_client_handler();
- ASSERT_EQ(param_opcode_, test_hci_layer_->GetCommand().GetOpCode());
- test_hci_layer_->IncomingEvent(LeExtendedScanParamsCompleteBuilder::Create(1, ErrorCode::SUCCESS));
+ }
+
+ void TearDown() override {
+ LeScanningManagerTest::TearDown();
}
};
-class LeExtendedScanningManagerTest : public LeScanningManagerTest {
+class LeScanningManagerExtendedTest : public LeScanningManagerTest {
protected:
void SetUp() override {
- param_opcode_ = OpCode::LE_SET_EXTENDED_SCAN_PARAMETERS;
- enable_opcode_ = OpCode::LE_SET_EXTENDED_SCAN_ENABLE;
LeScanningManagerTest::SetUp();
- }
-
- void HandleConfiguration() override {
- ASSERT_EQ(param_opcode_, test_hci_layer_->GetCommand().GetOpCode());
- test_hci_layer_->IncomingEvent(LeSetExtendedScanParametersCompleteBuilder::Create(1, ErrorCode::SUCCESS));
+ test_controller_->AddSupported(OpCode::LE_SET_EXTENDED_SCAN_PARAMETERS);
+ test_controller_->AddSupported(OpCode::LE_SET_EXTENDED_SCAN_ENABLE);
+ start_le_scanning_manager();
}
};
TEST_F(LeScanningManagerTest, startup_teardown) {}
TEST_F(LeScanningManagerTest, start_scan_test) {
- // Enable scan
+ start_le_scanning_manager();
+
test_hci_layer_->SetCommandFuture(2);
+ // Enable scan
le_scanning_manager->Scan(true);
- ASSERT_EQ(param_opcode_, test_hci_layer_->GetCommand().GetOpCode());
+ ASSERT_EQ(OpCode::LE_SET_SCAN_PARAMETERS, test_hci_layer_->GetCommand().GetOpCode());
test_hci_layer_->IncomingEvent(LeSetScanParametersCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS));
- ASSERT_EQ(enable_opcode_, test_hci_layer_->GetCommand().GetOpCode());
+ ASSERT_EQ(OpCode::LE_SET_SCAN_ENABLE, test_hci_layer_->GetCommand().GetOpCode());
test_hci_layer_->IncomingEvent(LeSetScanEnableCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS));
- LeAdvertisingResponse report{};
- report.event_type_ = AdvertisingEventType::ADV_DIRECT_IND;
- report.address_type_ = AddressType::PUBLIC_DEVICE_ADDRESS;
- Address::FromString("12:34:56:78:9a:bc", report.address_);
- std::vector<LengthAndData> adv_data{};
- LengthAndData data_item{};
- data_item.data_.push_back(static_cast<uint8_t>(GapDataType::FLAGS));
- data_item.data_.push_back(0x34);
- adv_data.push_back(data_item);
- data_item.data_.push_back(static_cast<uint8_t>(GapDataType::COMPLETE_LOCAL_NAME));
- for (auto octet : {'r', 'a', 'n', 'd', 'o', 'm', ' ', 'd', 'e', 'v', 'i', 'c', 'e'}) {
- data_item.data_.push_back(octet);
- }
- adv_data.push_back(data_item);
- report.advertising_data_ = adv_data;
-
+ LeAdvertisingResponse report = make_advertising_report();
EXPECT_CALL(mock_callbacks_, OnScanResult);
test_hci_layer_->IncomingLeMetaEvent(LeAdvertisingReportBuilder::Create({report}));
}
TEST_F(LeScanningManagerTest, is_ad_type_filter_supported_false_test) {
+ start_le_scanning_manager();
+ ASSERT_TRUE(fake_registry_.IsStarted(&HciLayer::Factory));
ASSERT_FALSE(le_scanning_manager->IsAdTypeFilterSupported());
}
TEST_F(LeScanningManagerTest, scan_filter_add_ad_type_not_supported_test) {
- test_hci_layer_->SetCommandFuture(1);
+ start_le_scanning_manager();
+ ASSERT_TRUE(fake_registry_.IsStarted(&HciLayer::Factory));
+ test_hci_layer_->SetCommandFuture();
std::vector<AdvertisingPacketContentFilterCommand> filters = {};
- AdvertisingPacketContentFilterCommand filter{};
- filter.filter_type = ApcfFilterType::AD_TYPE;
- filter.ad_type = 0x09;
- filter.data = {0x12, 0x34, 0x56, 0x78};
- filter.data_mask = {0xff, 0xff, 0xff, 0xff};
- filters.push_back(filter);
+ filters.push_back(make_filter(hci::ApcfFilterType::AD_TYPE));
le_scanning_manager->ScanFilterAdd(0x01, filters);
}
-TEST_F(LeAndroidHciScanningManagerTest, startup_teardown) {}
+TEST_F(LeScanningManagerAndroidHciTest, startup_teardown) {}
-TEST_F(LeAndroidHciScanningManagerTest, start_scan_test) {
- // Enable scan
+TEST_F(LeScanningManagerAndroidHciTest, start_scan_test) {
test_hci_layer_->SetCommandFuture(2);
+ // Enable scan
le_scanning_manager->Scan(true);
- ASSERT_EQ(param_opcode_, test_hci_layer_->GetCommand().GetOpCode());
- test_hci_layer_->IncomingEvent(LeSetScanParametersCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS));
- ASSERT_EQ(enable_opcode_, test_hci_layer_->GetCommand().GetOpCode());
- test_hci_layer_->IncomingEvent(LeSetScanEnableCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS));
+ ASSERT_EQ(OpCode::LE_EXTENDED_SCAN_PARAMS, test_hci_layer_->GetCommand().GetOpCode());
- LeAdvertisingResponse report{};
- report.event_type_ = AdvertisingEventType::ADV_DIRECT_IND;
- report.address_type_ = AddressType::PUBLIC_DEVICE_ADDRESS;
- Address::FromString("12:34:56:78:9a:bc", report.address_);
- std::vector<LengthAndData> adv_data{};
- LengthAndData data_item{};
- data_item.data_.push_back(static_cast<uint8_t>(GapDataType::FLAGS));
- data_item.data_.push_back(0x34);
- adv_data.push_back(data_item);
- data_item.data_.push_back(static_cast<uint8_t>(GapDataType::COMPLETE_LOCAL_NAME));
- for (auto octet : {'r', 'a', 'n', 'd', 'o', 'm', ' ', 'd', 'e', 'v', 'i', 'c', 'e'}) {
- data_item.data_.push_back(octet);
- }
- adv_data.push_back(data_item);
- report.advertising_data_ = adv_data;
+ LeAdvertisingResponse report = make_advertising_report();
EXPECT_CALL(mock_callbacks_, OnScanResult);
test_hci_layer_->IncomingLeMetaEvent(LeAdvertisingReportBuilder::Create({report}));
}
-TEST_F(LeAndroidHciScanningManagerTest, is_ad_type_filter_supported_true_test) {
- ASSERT_TRUE(le_scanning_manager->IsAdTypeFilterSupported());
+TEST_F(LeScanningManagerAndroidHciTest, is_ad_type_filter_supported_true_test) {
+ sync_client_handler();
+ client_handler_->Post(common::BindOnce(
+ [](LeScanningManager* le_scanning_manager) { ASSERT_TRUE(le_scanning_manager->IsAdTypeFilterSupported()); },
+ le_scanning_manager));
}
-TEST_F(LeAndroidHciScanningManagerTest, scan_filter_enable_test) {
- test_hci_layer_->SetCommandFuture(1);
+TEST_F(LeScanningManagerAndroidHciTest, scan_filter_enable_test) {
le_scanning_manager->ScanFilterEnable(true);
- auto commandView = test_hci_layer_->GetCommand();
- ASSERT_EQ(OpCode::LE_ADV_FILTER, commandView.GetOpCode());
- auto filter_command_view =
- LeAdvFilterEnableView::Create(LeAdvFilterView::Create(LeScanningCommandView::Create(commandView)));
- ASSERT_TRUE(filter_command_view.IsValid());
- ASSERT_EQ(filter_command_view.GetApcfOpcode(), ApcfOpcode::ENABLE);
EXPECT_CALL(mock_callbacks_, OnFilterEnable);
test_hci_layer_->IncomingEvent(
LeAdvFilterEnableCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS, Enable::ENABLED));
}
-TEST_F(LeAndroidHciScanningManagerTest, scan_filter_parameter_test) {
- test_hci_layer_->SetCommandFuture(1);
+TEST_F(LeScanningManagerAndroidHciTest, scan_filter_parameter_test) {
+ test_hci_layer_->SetCommandFuture();
AdvertisingFilterParameter advertising_filter_parameter{};
advertising_filter_parameter.delivery_mode = DeliveryMode::IMMEDIATE;
le_scanning_manager->ScanFilterParameterSetup(ApcfAction::ADD, 0x01, advertising_filter_parameter);
@@ -518,14 +544,10 @@
LeAdvFilterSetFilteringParametersCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS, ApcfAction::ADD, 0x0a));
}
-TEST_F(LeAndroidHciScanningManagerTest, scan_filter_add_broadcaster_address_test) {
- test_hci_layer_->SetCommandFuture(1);
+TEST_F(LeScanningManagerAndroidHciTest, scan_filter_add_broadcaster_address_test) {
+ test_hci_layer_->SetCommandFuture();
std::vector<AdvertisingPacketContentFilterCommand> filters = {};
- AdvertisingPacketContentFilterCommand filter{};
- filter.filter_type = ApcfFilterType::BROADCASTER_ADDRESS;
- filter.address = Address::kEmpty;
- filter.application_address_type = ApcfApplicationAddressType::RANDOM;
- filters.push_back(filter);
+ filters.push_back(make_filter(ApcfFilterType::BROADCASTER_ADDRESS));
le_scanning_manager->ScanFilterAdd(0x01, filters);
auto commandView = test_hci_layer_->GetCommand();
ASSERT_EQ(OpCode::LE_ADV_FILTER, commandView.GetOpCode());
@@ -539,14 +561,10 @@
LeAdvFilterBroadcasterAddressCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS, ApcfAction::ADD, 0x0a));
}
-TEST_F(LeAndroidHciScanningManagerTest, scan_filter_add_service_uuid_test) {
- test_hci_layer_->SetCommandFuture(1);
+TEST_F(LeScanningManagerAndroidHciTest, scan_filter_add_service_uuid_test) {
+ test_hci_layer_->SetCommandFuture();
std::vector<AdvertisingPacketContentFilterCommand> filters = {};
- AdvertisingPacketContentFilterCommand filter{};
- filter.filter_type = ApcfFilterType::SERVICE_UUID;
- filter.uuid = Uuid::From32Bit(0x12345678);
- filter.uuid_mask = Uuid::From32Bit(0xffffffff);
- filters.push_back(filter);
+ filters.push_back(make_filter(ApcfFilterType::SERVICE_UUID));
le_scanning_manager->ScanFilterAdd(0x01, filters);
auto commandView = test_hci_layer_->GetCommand();
ASSERT_EQ(OpCode::LE_ADV_FILTER, commandView.GetOpCode());
@@ -560,13 +578,10 @@
LeAdvFilterServiceUuidCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS, ApcfAction::ADD, 0x0a));
}
-TEST_F(LeAndroidHciScanningManagerTest, scan_filter_add_local_name_test) {
- test_hci_layer_->SetCommandFuture(1);
+TEST_F(LeScanningManagerAndroidHciTest, scan_filter_add_local_name_test) {
+ test_hci_layer_->SetCommandFuture();
std::vector<AdvertisingPacketContentFilterCommand> filters = {};
- AdvertisingPacketContentFilterCommand filter{};
- filter.filter_type = ApcfFilterType::LOCAL_NAME;
- filter.name = {0x01, 0x02, 0x03};
- filters.push_back(filter);
+ filters.push_back(make_filter(ApcfFilterType::LOCAL_NAME));
le_scanning_manager->ScanFilterAdd(0x01, filters);
auto commandView = test_hci_layer_->GetCommand();
ASSERT_EQ(OpCode::LE_ADV_FILTER, commandView.GetOpCode());
@@ -580,16 +595,10 @@
LeAdvFilterLocalNameCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS, ApcfAction::ADD, 0x0a));
}
-TEST_F(LeAndroidHciScanningManagerTest, scan_filter_add_manufacturer_data_test) {
- test_hci_layer_->SetCommandFuture(1);
+TEST_F(LeScanningManagerAndroidHciTest, scan_filter_add_manufacturer_data_test) {
+ test_hci_layer_->SetCommandFuture();
std::vector<AdvertisingPacketContentFilterCommand> filters = {};
- AdvertisingPacketContentFilterCommand filter{};
- filter.filter_type = ApcfFilterType::MANUFACTURER_DATA;
- filter.company = 0x12;
- filter.company_mask = 0xff;
- filter.data = {0x12, 0x34, 0x56, 0x78};
- filter.data_mask = {0xff, 0xff, 0xff, 0xff};
- filters.push_back(filter);
+ filters.push_back(make_filter(ApcfFilterType::MANUFACTURER_DATA));
le_scanning_manager->ScanFilterAdd(0x01, filters);
auto commandView = test_hci_layer_->GetCommand();
ASSERT_EQ(OpCode::LE_ADV_FILTER, commandView.GetOpCode());
@@ -603,14 +612,10 @@
LeAdvFilterManufacturerDataCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS, ApcfAction::ADD, 0x0a));
}
-TEST_F(LeAndroidHciScanningManagerTest, scan_filter_add_service_data_test) {
- test_hci_layer_->SetCommandFuture(1);
+TEST_F(LeScanningManagerAndroidHciTest, scan_filter_add_service_data_test) {
+ test_hci_layer_->SetCommandFuture();
std::vector<AdvertisingPacketContentFilterCommand> filters = {};
- AdvertisingPacketContentFilterCommand filter{};
- filter.filter_type = ApcfFilterType::SERVICE_DATA;
- filter.data = {0x12, 0x34, 0x56, 0x78};
- filter.data_mask = {0xff, 0xff, 0xff, 0xff};
- filters.push_back(filter);
+ filters.push_back(make_filter(hci::ApcfFilterType::SERVICE_DATA));
le_scanning_manager->ScanFilterAdd(0x01, filters);
auto commandView = test_hci_layer_->GetCommand();
ASSERT_EQ(OpCode::LE_ADV_FILTER, commandView.GetOpCode());
@@ -624,32 +629,26 @@
LeAdvFilterServiceDataCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS, ApcfAction::ADD, 0x0a));
}
-TEST_F(LeAndroidHciScanningManagerTest, scan_filter_add_ad_type_test) {
- test_hci_layer_->SetCommandFuture(1);
+TEST_F(LeScanningManagerAndroidHciTest, scan_filter_add_ad_type_test) {
+ sync_client_handler();
+ client_handler_->Post(common::BindOnce(
+ [](LeScanningManager* le_scanning_manager) { ASSERT_TRUE(le_scanning_manager->IsAdTypeFilterSupported()); },
+ le_scanning_manager));
+
std::vector<AdvertisingPacketContentFilterCommand> filters = {};
- AdvertisingPacketContentFilterCommand filter{};
- filter.filter_type = ApcfFilterType::AD_TYPE;
- filter.ad_type = 0x09;
- filter.data = {0x12, 0x34, 0x56, 0x78};
- filter.data_mask = {0xff, 0xff, 0xff, 0xff};
+ hci::AdvertisingPacketContentFilterCommand filter = make_filter(hci::ApcfFilterType::AD_TYPE);
filters.push_back(filter);
le_scanning_manager->ScanFilterAdd(0x01, filters);
- auto commandView = test_hci_layer_->GetCommand();
- ASSERT_EQ(OpCode::LE_ADV_FILTER, commandView.GetOpCode());
- auto filter_command_view =
- LeAdvFilterADTypeView::Create(LeAdvFilterView::Create(LeScanningCommandView::Create(commandView)));
- ASSERT_TRUE(filter_command_view.IsValid());
- ASSERT_EQ(filter_command_view.GetApcfOpcode(), ApcfOpcode::AD_TYPE);
+ sync_client_handler();
EXPECT_CALL(mock_callbacks_, OnFilterConfigCallback);
test_hci_layer_->IncomingEvent(
LeAdvFilterADTypeCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS, ApcfAction::ADD, 0x0a));
}
-TEST_F(LeAndroidHciScanningManagerTest, read_batch_scan_result) {
- // Enable batch scan feature
- test_hci_layer_->SetCommandFuture(2);
+TEST_F(LeScanningManagerAndroidHciTest, read_batch_scan_result) {
le_scanning_manager->BatchScanConifgStorage(100, 0, 95, 0x00);
+ sync_client_handler();
ASSERT_EQ(OpCode::LE_BATCH_SCAN, test_hci_layer_->GetCommand().GetOpCode());
test_hci_layer_->IncomingEvent(LeBatchScanEnableCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS));
ASSERT_EQ(OpCode::LE_BATCH_SCAN, test_hci_layer_->GetCommand().GetOpCode());
@@ -657,20 +656,20 @@
LeBatchScanSetStorageParametersCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS));
// Enable batch scan
- test_hci_layer_->SetCommandFuture(1);
+ test_hci_layer_->SetCommandFuture();
le_scanning_manager->BatchScanEnable(BatchScanMode::FULL, 2400, 2400, BatchScanDiscardRule::OLDEST);
ASSERT_EQ(OpCode::LE_BATCH_SCAN, test_hci_layer_->GetCommand().GetOpCode());
test_hci_layer_->IncomingEvent(LeBatchScanEnableCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS));
// Read batch scan data
- test_hci_layer_->SetCommandFuture(1);
+ test_hci_layer_->SetCommandFuture();
le_scanning_manager->BatchScanReadReport(0x01, BatchScanMode::FULL);
ASSERT_EQ(OpCode::LE_BATCH_SCAN, test_hci_layer_->GetCommand().GetOpCode());
// We will send read command while num_of_record != 0
std::vector<uint8_t> raw_data = {0x5c, 0x1f, 0xa2, 0xc3, 0x63, 0x5d, 0x01, 0xf5, 0xb3, 0x5e, 0x00, 0x0c, 0x02,
0x01, 0x02, 0x05, 0x09, 0x6d, 0x76, 0x38, 0x76, 0x02, 0x0a, 0xf5, 0x00};
- test_hci_layer_->SetCommandFuture(1);
+ test_hci_layer_->SetCommandFuture();
test_hci_layer_->IncomingEvent(LeBatchScanReadResultParametersCompleteRawBuilder::Create(
uint8_t{1}, ErrorCode::SUCCESS, BatchScanDataRead::FULL_MODE_DATA, 1, raw_data));
ASSERT_EQ(OpCode::LE_BATCH_SCAN, test_hci_layer_->GetCommand().GetOpCode());
@@ -681,15 +680,15 @@
uint8_t{1}, ErrorCode::SUCCESS, BatchScanDataRead::FULL_MODE_DATA, 0, {}));
}
-TEST_F(LeExtendedScanningManagerTest, startup_teardown) {}
+TEST_F(LeScanningManagerExtendedTest, startup_teardown) {}
-TEST_F(LeExtendedScanningManagerTest, start_scan_test) {
+TEST_F(LeScanningManagerExtendedTest, start_scan_test) {
// Enable scan
test_hci_layer_->SetCommandFuture(2);
le_scanning_manager->Scan(true);
- ASSERT_EQ(param_opcode_, test_hci_layer_->GetCommand().GetOpCode());
+ ASSERT_EQ(OpCode::LE_SET_EXTENDED_SCAN_PARAMETERS, test_hci_layer_->GetCommand().GetOpCode());
test_hci_layer_->IncomingEvent(LeSetExtendedScanParametersCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS));
- ASSERT_EQ(enable_opcode_, test_hci_layer_->GetCommand().GetOpCode());
+ ASSERT_EQ(OpCode::LE_SET_EXTENDED_SCAN_ENABLE, test_hci_layer_->GetCommand().GetOpCode());
test_hci_layer_->IncomingEvent(LeSetExtendedScanEnableCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS));
LeExtendedAdvertisingResponse report{};
report.connectable_ = 1;
@@ -714,13 +713,13 @@
test_hci_layer_->IncomingLeMetaEvent(LeExtendedAdvertisingReportBuilder::Create({report}));
}
-TEST_F(LeExtendedScanningManagerTest, drop_insignificant_bytes_test) {
+TEST_F(LeScanningManagerExtendedTest, drop_insignificant_bytes_test) {
// Enable scan
test_hci_layer_->SetCommandFuture(2);
le_scanning_manager->Scan(true);
- ASSERT_EQ(param_opcode_, test_hci_layer_->GetCommand().GetOpCode());
+ ASSERT_EQ(OpCode::LE_SET_EXTENDED_SCAN_PARAMETERS, test_hci_layer_->GetCommand().GetOpCode());
test_hci_layer_->IncomingEvent(LeSetExtendedScanParametersCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS));
- ASSERT_EQ(enable_opcode_, test_hci_layer_->GetCommand().GetOpCode());
+ ASSERT_EQ(OpCode::LE_SET_EXTENDED_SCAN_ENABLE, test_hci_layer_->GetCommand().GetOpCode());
test_hci_layer_->IncomingEvent(LeSetExtendedScanEnableCompleteBuilder::Create(uint8_t{1}, ErrorCode::SUCCESS));
// Prepare advertisement report
diff --git a/system/gd/os/android/metrics.cc b/system/gd/os/android/metrics.cc
index d158ab4..db42fc5a 100644
--- a/system/gd/os/android/metrics.cc
+++ b/system/gd/os/android/metrics.cc
@@ -424,6 +424,76 @@
}
}
+void LogMetricBluetoothLocalSupportedFeatures(uint32_t page_num, uint64_t features) {
+ int ret = stats_write(BLUETOOTH_LOCAL_SUPPORTED_FEATURES_REPORTED, page_num, features);
+ if (ret < 0) {
+ LOG_WARN(
+ "Failed for LogMetricBluetoothLocalSupportedFeatures, "
+ "page_num %d, features %s, error %d",
+ page_num,
+ std::to_string(features).c_str(),
+ ret);
+ }
+}
+
+void LogMetricBluetoothLocalVersions(
+ uint32_t lmp_manufacturer_name,
+ uint8_t lmp_version,
+ uint32_t lmp_subversion,
+ uint8_t hci_version,
+ uint32_t hci_revision) {
+ int ret = stats_write(
+ BLUETOOTH_LOCAL_VERSIONS_REPORTED, lmp_manufacturer_name, lmp_version, lmp_subversion, hci_version, hci_revision);
+ if (ret < 0) {
+ LOG_WARN(
+ "Failed for LogMetricBluetoothLocalVersions, "
+ "lmp_manufacturer_name %d, lmp_version %hhu, lmp_subversion %d, hci_version %hhu, hci_revision %d, error %d",
+ lmp_manufacturer_name,
+ lmp_version,
+ lmp_subversion,
+ hci_version,
+ hci_revision,
+ ret);
+ }
+}
+
+void LogMetricBluetoothDisconnectionReasonReported(
+ uint32_t reason, const Address& address, uint32_t connection_handle) {
+ int metric_id = 0;
+ if (!address.IsEmpty()) {
+ metric_id = MetricIdManager::GetInstance().AllocateId(address);
+ }
+ int ret = stats_write(BLUETOOTH_DISCONNECTION_REASON_REPORTED, reason, metric_id, connection_handle);
+ if (ret < 0) {
+ LOG_WARN(
+ "Failed for LogMetricBluetoothDisconnectionReasonReported, "
+ "reason %d, metric_id %d, connection_handle %d, error %d",
+ reason,
+ metric_id,
+ connection_handle,
+ ret);
+ }
+}
+
+void LogMetricBluetoothRemoteSupportedFeatures(
+ const Address& address, uint32_t page, uint64_t features, uint32_t connection_handle) {
+ int metric_id = 0;
+ if (!address.IsEmpty()) {
+ metric_id = MetricIdManager::GetInstance().AllocateId(address);
+ }
+ int ret = stats_write(BLUETOOTH_REMOTE_SUPPORTED_FEATURES_REPORTED, metric_id, page, features, connection_handle);
+ if (ret < 0) {
+ LOG_WARN(
+ "Failed for LogMetricBluetoothRemoteSupportedFeatures, "
+ "metric_id %d, page %d, features %s, connection_handle %d, error %d",
+ metric_id,
+ page,
+ std::to_string(features).c_str(),
+ connection_handle,
+ ret);
+ }
+}
+
void LogMetricBluetoothCodePathCounterMetrics(int32_t key, int64_t count) {
int ret = stats_write(BLUETOOTH_CODE_PATH_COUNTER, key, count);
if (ret < 0) {
diff --git a/system/gd/os/host/metrics.cc b/system/gd/os/host/metrics.cc
index b175714..955d22e 100644
--- a/system/gd/os/host/metrics.cc
+++ b/system/gd/os/host/metrics.cc
@@ -103,6 +103,21 @@
void LogMetricBluetoothHalCrashReason(
const Address& address, uint32_t error_code, uint32_t vendor_error_code) {}
+void LogMetricBluetoothLocalSupportedFeatures(uint32_t page_num, uint64_t features) {}
+
+void LogMetricBluetoothLocalVersions(
+ uint32_t lmp_manufacturer_name,
+ uint8_t lmp_version,
+ uint32_t lmp_subversion,
+ uint8_t hci_version,
+ uint32_t hci_reversion) {}
+
+void LogMetricBluetoothDisconnectionReasonReported(
+ uint32_t reason, const Address& address, uint32_t connection_handle) {}
+
+void LogMetricBluetoothRemoteSupportedFeatures(
+ const Address& address, uint32_t page, uint64_t features, uint32_t connection_handle) {}
+
void LogMetricBluetoothCodePathCounterMetrics(int32_t key, int64_t count) {}
} // namespace os
} // namespace bluetooth
diff --git a/system/gd/os/linux/metrics.cc b/system/gd/os/linux/metrics.cc
index 37bd673..65fb181 100644
--- a/system/gd/os/linux/metrics.cc
+++ b/system/gd/os/linux/metrics.cc
@@ -103,6 +103,21 @@
void LogMetricBluetoothHalCrashReason(
const Address& address, uint32_t error_code, uint32_t vendor_error_code) {}
+void LogMetricBluetoothLocalSupportedFeatures(uint32_t page_num, uint64_t features) {}
+
+void LogMetricBluetoothLocalVersions(
+ uint32_t lmp_manufacturer_name,
+ uint8_t lmp_version,
+ uint32_t lmp_subversion,
+ uint8_t hci_version,
+ uint32_t hci_revision) {}
+
+void LogMetricBluetoothDisconnectionReasonReported(
+ uint32_t reason, const Address& address, uint32_t connection_handle) {}
+
+void LogMetricBluetoothRemoteSupportedFeatures(
+ const Address& address, uint32_t page, uint64_t features, uint32_t connection_handle) {}
+
void LogMetricBluetoothCodePathCounterMetrics(int32_t key, int64_t count) {}
} // namespace os
diff --git a/system/gd/os/metrics.h b/system/gd/os/metrics.h
index 3b1ae51..339a2cf 100644
--- a/system/gd/os/metrics.h
+++ b/system/gd/os/metrics.h
@@ -265,6 +265,21 @@
uint32_t error_code,
uint32_t vendor_error_code);
+void LogMetricBluetoothLocalSupportedFeatures(uint32_t page_num, uint64_t features);
+
+void LogMetricBluetoothLocalVersions(
+ uint32_t lmp_manufacturer_name,
+ uint8_t lmp_version,
+ uint32_t lmp_subversion,
+ uint8_t hci_version,
+ uint32_t hci_revision);
+
+void LogMetricBluetoothDisconnectionReasonReported(
+ uint32_t reason, const hci::Address& address, uint32_t connection_handle);
+
+void LogMetricBluetoothRemoteSupportedFeatures(
+ const hci::Address& address, uint32_t page, uint64_t features, uint32_t connection_handle);
+
void LogMetricBluetoothCodePathCounterMetrics(int32_t key, int64_t count);
} // namespace os
diff --git a/system/main/shim/btm_api.cc b/system/main/shim/btm_api.cc
index 8b761c0..2833a48 100644
--- a/system/main/shim/btm_api.cc
+++ b/system/main/shim/btm_api.cc
@@ -460,7 +460,7 @@
LinkKey key; // Never want to send the key to the stack
(*bta_callbacks_->p_link_key_callback)(
bluetooth::ToRawAddress(device.GetAddress()), 0, name, key,
- BTM_LKEY_TYPE_COMBINATION);
+ BTM_LKEY_TYPE_COMBINATION, false /* is_ctkd */);
}
if (*bta_callbacks_->p_auth_complete_callback) {
(*bta_callbacks_->p_auth_complete_callback)(
diff --git a/system/main/test/main_shim_dumpsys_test.cc b/system/main/test/main_shim_dumpsys_test.cc
index 318a513..06f89a5 100644
--- a/system/main/test/main_shim_dumpsys_test.cc
+++ b/system/main/test/main_shim_dumpsys_test.cc
@@ -48,7 +48,6 @@
ModuleList modules;
modules.add<shim::Dumpsys>();
- modules.add<storage::StorageModule>();
os::Thread* thread = new os::Thread("thread", os::Thread::Priority::NORMAL);
stack_manager_.StartUp(&modules, thread);
diff --git a/system/stack/Android.bp b/system/stack/Android.bp
index 80244ce..8514ed0 100644
--- a/system/stack/Android.bp
+++ b/system/stack/Android.bp
@@ -890,14 +890,9 @@
"libprotobuf-cpp-lite",
],
sanitize: {
- address: true,
- all_undefined: true,
- cfi: true,
- integer_overflow: true,
- scs: true,
- diag: {
- undefined : true
- },
+ hwaddress: true,
+ integer_overflow: true,
+ scs: true,
},
}
diff --git a/system/stack/a2dp/a2dp_codec_config.cc b/system/stack/a2dp/a2dp_codec_config.cc
index 34503c8..27a42f9 100644
--- a/system/stack/a2dp/a2dp_codec_config.cc
+++ b/system/stack/a2dp/a2dp_codec_config.cc
@@ -559,6 +559,9 @@
LOG_INFO("%s", __func__);
std::lock_guard<std::recursive_mutex> lock(codec_mutex_);
+ bool opus_enabled =
+ osi_property_get_bool("persist.bluetooth.opus.enabled", false);
+
for (int i = BTAV_A2DP_CODEC_INDEX_MIN; i < BTAV_A2DP_CODEC_INDEX_MAX; i++) {
btav_a2dp_codec_index_t codec_index =
static_cast<btav_a2dp_codec_index_t>(i);
@@ -571,6 +574,13 @@
codec_priority = cp_iter->second;
}
+ // If OPUS is not supported it is disabled
+ if (codec_index == BTAV_A2DP_CODEC_INDEX_SOURCE_OPUS && !opus_enabled) {
+ codec_priority = BTAV_A2DP_CODEC_PRIORITY_DISABLED;
+ LOG_INFO("%s: OPUS codec disabled, updated priority to %d", __func__,
+ codec_priority);
+ }
+
A2dpCodecConfig* codec_config =
A2dpCodecConfig::createCodec(codec_index, codec_priority);
if (codec_config == nullptr) continue;
diff --git a/system/stack/acl/btm_acl.cc b/system/stack/acl/btm_acl.cc
index e242c53..6e94539 100644
--- a/system/stack/acl/btm_acl.cc
+++ b/system/stack/acl/btm_acl.cc
@@ -630,6 +630,21 @@
return;
}
+ /* if we are trying to drop encryption on an encrypted connection, drop the
+ * connection */
+ if (p->is_encrypted && !encr_enable) {
+ android_errorWriteLog(0x534e4554, "251436534");
+ LOG(ERROR) << __func__
+ << " attempting to decrypt encrypted connection, disconnecting. "
+ "handle: "
+ << loghex(handle);
+
+ acl_disconnect_from_handle(handle, HCI_ERR_HOST_REJECT_SECURITY,
+ "stack::btu::btu_hcif::read_drop_encryption "
+ "Connection Already Encrypted");
+ return;
+ }
+
p->is_encrypted = encr_enable;
/* Process Role Switch if active */
diff --git a/system/stack/btm/btm_sco.h b/system/stack/btm/btm_sco.h
index 8e85f1d..a598f22 100644
--- a/system/stack/btm/btm_sco.h
+++ b/system/stack/btm/btm_sco.h
@@ -54,6 +54,13 @@
/* Define the structures needed by sco
*/
+#ifndef CASE_RETURN_TEXT
+#define CASE_RETURN_TEXT(code) \
+ case code: \
+ return #code
+#endif
+
+/* Define the structures needed by sco */
typedef enum : uint16_t {
SCO_ST_UNUSED = 0,
SCO_ST_LISTENING = 1,
@@ -68,27 +75,23 @@
inline std::string sco_state_text(const tSCO_STATE& state) {
switch (state) {
- case SCO_ST_UNUSED:
- return std::string("unused");
- case SCO_ST_LISTENING:
- return std::string("listening");
- case SCO_ST_W4_CONN_RSP:
- return std::string("connect_response");
- case SCO_ST_CONNECTING:
- return std::string("connecting");
- case SCO_ST_CONNECTED:
- return std::string("connected");
- case SCO_ST_DISCONNECTING:
- return std::string("disconnecting");
- case SCO_ST_PEND_UNPARK:
- return std::string("pending_unpark");
- case SCO_ST_PEND_ROLECHANGE:
- return std::string("pending_role_change");
- case SCO_ST_PEND_MODECHANGE:
- return std::string("pending_mode_change");
+ CASE_RETURN_TEXT(SCO_ST_UNUSED);
+ CASE_RETURN_TEXT(SCO_ST_LISTENING);
+ CASE_RETURN_TEXT(SCO_ST_W4_CONN_RSP);
+ CASE_RETURN_TEXT(SCO_ST_CONNECTING);
+ CASE_RETURN_TEXT(SCO_ST_CONNECTED);
+ CASE_RETURN_TEXT(SCO_ST_DISCONNECTING);
+ CASE_RETURN_TEXT(SCO_ST_PEND_UNPARK);
+ CASE_RETURN_TEXT(SCO_ST_PEND_ROLECHANGE);
+ CASE_RETURN_TEXT(SCO_ST_PEND_MODECHANGE);
+ default:
+ return std::string("unknown_sco_state: ") +
+ std::to_string(static_cast<uint16_t>(state));
}
}
+#undef CASE_RETURN_TEXT
+
/* Define the structure that contains (e)SCO data */
typedef struct {
tBTM_ESCO_CBACK* p_esco_cback; /* Callback for eSCO events */
diff --git a/system/stack/btm/btm_sec.cc b/system/stack/btm/btm_sec.cc
index ea51aa8..6c7309d 100644
--- a/system/stack/btm/btm_sec.cc
+++ b/system/stack/btm/btm_sec.cc
@@ -3941,9 +3941,9 @@
if (btm_cb.api.p_link_key_callback) {
BTM_TRACE_DEBUG("%s() Save LTK derived LK (key_type = %d)", __func__,
p_dev_rec->link_key_type);
- (*btm_cb.api.p_link_key_callback)(p_bda, p_dev_rec->dev_class,
- p_dev_rec->sec_bd_name, link_key,
- p_dev_rec->link_key_type);
+ (*btm_cb.api.p_link_key_callback)(
+ p_bda, p_dev_rec->dev_class, p_dev_rec->sec_bd_name, link_key,
+ p_dev_rec->link_key_type, true /* is_ctkd */);
}
} else {
if ((p_dev_rec->link_key_type == BTM_LKEY_TYPE_UNAUTH_COMB_P_256) ||
@@ -3991,9 +3991,9 @@
" (key_type = %d)",
p_dev_rec->link_key_type);
} else {
- (*btm_cb.api.p_link_key_callback)(p_bda, p_dev_rec->dev_class,
- p_dev_rec->sec_bd_name, link_key,
- p_dev_rec->link_key_type);
+ (*btm_cb.api.p_link_key_callback)(
+ p_bda, p_dev_rec->dev_class, p_dev_rec->sec_bd_name, link_key,
+ p_dev_rec->link_key_type, false /* is_ctkd */);
}
}
}
@@ -4579,7 +4579,7 @@
if (btm_cb.api.p_link_key_callback)
(*btm_cb.api.p_link_key_callback)(
p_dev_rec->bd_addr, p_dev_rec->dev_class, p_dev_rec->sec_bd_name,
- p_dev_rec->link_key, p_dev_rec->link_key_type);
+ p_dev_rec->link_key, p_dev_rec->link_key_type, false);
}
/*******************************************************************************
@@ -4745,7 +4745,8 @@
if (btm_status == BTM_SUCCESS && is_le_transport) {
/* Link is encrypted, start EATT */
- bluetooth::eatt::EattExtension::GetInstance()->Connect(p_dev_rec->bd_addr);
+ bluetooth::eatt::EattExtension::GetInstance()->Connect(
+ p_dev_rec->ble.pseudo_addr);
}
}
diff --git a/system/stack/eatt/eatt.h b/system/stack/eatt/eatt.h
index 40542b0..a6cfbea 100644
--- a/system/stack/eatt/eatt.h
+++ b/system/stack/eatt/eatt.h
@@ -17,7 +17,7 @@
#pragma once
-#include <queue>
+#include <deque>
#include "stack/gatt/gatt_int.h"
#include "types/raw_address.h"
@@ -54,7 +54,7 @@
/* indication confirmation timer */
alarm_t* ind_confirmation_timer_;
/* GATT client command queue */
- std::queue<tGATT_CMD_Q> cl_cmd_q_;
+ std::deque<tGATT_CMD_Q> cl_cmd_q_;
EattChannel(RawAddress& bda, uint16_t cid, uint16_t tx_mtu, uint16_t rx_mtu)
: bda_(bda),
@@ -79,7 +79,7 @@
void EattChannelSetState(EattChannelState state) {
if (state_ == EattChannelState::EATT_CHANNEL_PENDING) {
if (state == EattChannelState::EATT_CHANNEL_OPENED) {
- cl_cmd_q_ = std::queue<tGATT_CMD_Q>();
+ cl_cmd_q_ = std::deque<tGATT_CMD_Q>();
memset(&server_outstanding_cmd_, 0, sizeof(tGATT_SR_CMD));
char name[64];
sprintf(name, "eatt_ind_ack_timer_%s_cid_0x%04x",
diff --git a/system/stack/eatt/eatt_impl.h b/system/stack/eatt/eatt_impl.h
index f8a3770..8293bb6 100644
--- a/system/stack/eatt/eatt_impl.h
+++ b/system/stack/eatt/eatt_impl.h
@@ -111,6 +111,13 @@
}
void remove_channel_by_cid(eatt_device* eatt_dev, uint16_t lcid) {
+ auto channel = eatt_dev->eatt_channels[lcid];
+ if (!channel->cl_cmd_q_.empty()) {
+ LOG_WARN("Channel %c, for device %s is not empty on disconnection.", lcid,
+ channel->bda_.ToString().c_str());
+ channel->cl_cmd_q_.clear();
+ }
+
eatt_dev->eatt_channels.erase(lcid);
if (eatt_dev->eatt_channels.size() == 0) eatt_dev->eatt_tcb_ = NULL;
diff --git a/system/stack/gatt/att_protocol.cc b/system/stack/gatt/att_protocol.cc
index 3683d40..ef4a3a6 100644
--- a/system/stack/gatt/att_protocol.cc
+++ b/system/stack/gatt/att_protocol.cc
@@ -436,7 +436,8 @@
if (gatt_tcb_is_cid_busy(tcb, p_clcb->cid) &&
cmd_code != GATT_HANDLE_VALUE_CONF) {
gatt_cmd_enq(tcb, p_clcb, true, cmd_code, p_cmd);
- LOG_DEBUG("Enqueued ATT command");
+ LOG_DEBUG("Enqueued ATT command %p conn_id=0x%04x, cid=%d", p_clcb,
+ p_clcb->conn_id, p_clcb->cid);
return GATT_CMD_STARTED;
}
@@ -445,7 +446,9 @@
p_clcb->cid, tcb.eatt, bt_transport_text(tcb.transport).c_str());
tGATT_STATUS att_ret = attp_send_msg_to_l2cap(tcb, p_clcb->cid, p_cmd);
if (att_ret != GATT_CONGESTED && att_ret != GATT_SUCCESS) {
- LOG_WARN("Unable to send ATT command to l2cap layer");
+ LOG_WARN(
+ "Unable to send ATT command to l2cap layer %p conn_id=0x%04x, cid=%d",
+ p_clcb, p_clcb->conn_id, p_clcb->cid);
return GATT_INTERNAL_ERROR;
}
@@ -453,7 +456,8 @@
return att_ret;
}
- LOG_DEBUG("Starting ATT response timer");
+ LOG_DEBUG("Starting ATT response timer %p conn_id=0x%04x, cid=%d", p_clcb,
+ p_clcb->conn_id, p_clcb->cid);
gatt_start_rsp_timer(p_clcb);
gatt_cmd_enq(tcb, p_clcb, false, cmd_code, NULL);
return att_ret;
diff --git a/system/stack/gatt/gatt_api.cc b/system/stack/gatt/gatt_api.cc
index b956924..0e3c23e 100644
--- a/system/stack/gatt/gatt_api.cc
+++ b/system/stack/gatt/gatt_api.cc
@@ -702,11 +702,6 @@
return GATT_ERROR;
}
- if (gatt_is_clcb_allocated(conn_id)) {
- LOG_WARN("Connection is already used conn_id:%hu", conn_id);
- return GATT_BUSY;
- }
-
tGATT_CLCB* p_clcb = gatt_clcb_alloc(conn_id);
if (!p_clcb) {
LOG_WARN("Unable to allocate connection link control block");
@@ -765,11 +760,6 @@
return GATT_ILLEGAL_PARAMETER;
}
- if (gatt_is_clcb_allocated(conn_id)) {
- LOG(ERROR) << __func__ << "GATT_BUSY conn_id = " << +conn_id;
- return GATT_BUSY;
- }
-
tGATT_CLCB* p_clcb = gatt_clcb_alloc(conn_id);
if (!p_clcb) {
LOG(WARNING) << __func__ << " No resources conn_id=" << loghex(conn_id)
@@ -835,11 +825,6 @@
return GATT_ILLEGAL_PARAMETER;
}
- if (gatt_is_clcb_allocated(conn_id)) {
- LOG(ERROR) << "GATT_BUSY conn_id=" << loghex(conn_id);
- return GATT_BUSY;
- }
-
tGATT_CLCB* p_clcb = gatt_clcb_alloc(conn_id);
if (!p_clcb) return GATT_NO_RESOURCES;
@@ -942,11 +927,6 @@
return GATT_ILLEGAL_PARAMETER;
}
- if (gatt_is_clcb_allocated(conn_id)) {
- LOG(ERROR) << "GATT_BUSY conn_id=" << loghex(conn_id);
- return GATT_BUSY;
- }
-
tGATT_CLCB* p_clcb = gatt_clcb_alloc(conn_id);
if (!p_clcb) return GATT_NO_RESOURCES;
@@ -995,11 +975,6 @@
return GATT_ILLEGAL_PARAMETER;
}
- if (gatt_is_clcb_allocated(conn_id)) {
- LOG(ERROR) << " GATT_BUSY conn_id=" << loghex(conn_id);
- return GATT_BUSY;
- }
-
tGATT_CLCB* p_clcb = gatt_clcb_alloc(conn_id);
if (!p_clcb) return GATT_NO_RESOURCES;
@@ -1185,7 +1160,7 @@
/* When an application deregisters, check remove the link associated with the
* app */
tGATT_TCB* p_tcb;
- int i, j;
+ int i;
for (i = 0, p_tcb = gatt_cb.tcb; i < GATT_MAX_PHY_CHANNEL; i++, p_tcb++) {
if (!p_tcb->in_use) continue;
@@ -1193,13 +1168,15 @@
gatt_update_app_use_link_flag(gatt_if, p_tcb, false, true);
}
- tGATT_CLCB* p_clcb;
- for (j = 0, p_clcb = &gatt_cb.clcb[j]; j < GATT_CL_MAX_LCB; j++, p_clcb++) {
- if (p_clcb->in_use && (p_clcb->p_reg->gatt_if == gatt_if) &&
- (p_clcb->p_tcb->tcb_idx == p_tcb->tcb_idx)) {
- alarm_cancel(p_clcb->gatt_rsp_timer_ent);
- gatt_clcb_dealloc(p_clcb);
- break;
+ for (auto clcb_it = gatt_cb.clcb_queue.begin();
+ clcb_it != gatt_cb.clcb_queue.end();) {
+ if ((clcb_it->p_reg->gatt_if == gatt_if) &&
+ (clcb_it->p_tcb->tcb_idx == p_tcb->tcb_idx)) {
+ alarm_cancel(clcb_it->gatt_rsp_timer_ent);
+ gatt_clcb_invalidate(p_tcb, &(*clcb_it));
+ clcb_it = gatt_cb.clcb_queue.erase(clcb_it);
+ } else {
+ clcb_it++;
}
}
}
diff --git a/system/stack/gatt/gatt_cl.cc b/system/stack/gatt/gatt_cl.cc
index 215ea2a..9778466 100644
--- a/system/stack/gatt/gatt_cl.cc
+++ b/system/stack/gatt/gatt_cl.cc
@@ -1130,11 +1130,12 @@
/** Find next command in queue and sent to server */
bool gatt_cl_send_next_cmd_inq(tGATT_TCB& tcb) {
- std::queue<tGATT_CMD_Q>* cl_cmd_q;
+ std::deque<tGATT_CMD_Q>* cl_cmd_q = nullptr;
- while (!tcb.cl_cmd_q.empty() ||
- EattExtension::GetInstance()->IsOutstandingMsgInSendQueue(tcb.peer_bda)) {
- if (!tcb.cl_cmd_q.empty()) {
+ while (
+ gatt_is_outstanding_msg_in_att_send_queue(tcb) ||
+ EattExtension::GetInstance()->IsOutstandingMsgInSendQueue(tcb.peer_bda)) {
+ if (gatt_is_outstanding_msg_in_att_send_queue(tcb)) {
cl_cmd_q = &tcb.cl_cmd_q;
} else {
EattChannel* channel =
@@ -1153,7 +1154,7 @@
if (att_ret != GATT_SUCCESS && att_ret != GATT_CONGESTED) {
LOG(ERROR) << __func__ << ": L2CAP sent error";
- cl_cmd_q->pop();
+ cl_cmd_q->pop_front();
continue;
}
@@ -1185,7 +1186,7 @@
void gatt_client_handle_server_rsp(tGATT_TCB& tcb, uint16_t cid,
uint8_t op_code, uint16_t len,
uint8_t* p_data) {
- VLOG(1) << __func__ << " opcode: " << loghex(op_code);
+ VLOG(1) << __func__ << " opcode: " << loghex(op_code) << " cid" << +cid;
uint16_t payload_size = gatt_tcb_get_payload_size_rx(tcb, cid);
@@ -1204,20 +1205,26 @@
uint8_t cmd_code = 0;
tGATT_CLCB* p_clcb = gatt_cmd_dequeue(tcb, cid, &cmd_code);
- uint8_t rsp_code = gatt_cmd_to_rsp_code(cmd_code);
- if (!p_clcb || (rsp_code != op_code && op_code != GATT_RSP_ERROR)) {
- LOG(WARNING) << StringPrintf(
- "ATT - Ignore wrong response. Receives (%02x) Request(%02x) Ignored",
- op_code, rsp_code);
+ if (!p_clcb) {
+ LOG_WARN("ATT - clcb already not in use, ignoring response");
+ gatt_cl_send_next_cmd_inq(tcb);
return;
}
- if (!p_clcb->in_use) {
- LOG(WARNING) << "ATT - clcb already not in use, ignoring response";
+ uint8_t rsp_code = gatt_cmd_to_rsp_code(cmd_code);
+ if (!p_clcb) {
+ LOG_WARN("ATT - clcb already not in use, ignoring response");
gatt_cl_send_next_cmd_inq(tcb);
return;
}
+ if (rsp_code != op_code && op_code != GATT_RSP_ERROR) {
+ LOG(WARNING) << StringPrintf(
+ "ATT - Ignore wrong response. Receives (%02x) Request(%02x) Ignored",
+ op_code, rsp_code);
+ return;
+ }
+
gatt_stop_rsp_timer(p_clcb);
p_clcb->retry_count = 0;
diff --git a/system/stack/gatt/gatt_int.h b/system/stack/gatt/gatt_int.h
index 1e9e7e1..5e6e23f 100644
--- a/system/stack/gatt/gatt_int.h
+++ b/system/stack/gatt/gatt_int.h
@@ -23,6 +23,7 @@
#include <base/strings/stringprintf.h>
#include <string.h>
+#include <deque>
#include <list>
#include <queue>
#include <unordered_set>
@@ -330,7 +331,7 @@
uint8_t prep_cnt[GATT_MAX_APPS];
uint8_t ind_count;
- std::queue<tGATT_CMD_Q> cl_cmd_q;
+ std::deque<tGATT_CMD_Q> cl_cmd_q;
alarm_t* ind_ack_timer; /* local app confirm to indication timer */
// TODO(hylo): support byte array data
@@ -369,7 +370,6 @@
tGATT_STATUS status; /* operation status */
bool first_read_blob_after_read;
tGATT_READ_INC_UUID128 read_uuid128;
- bool in_use;
alarm_t* gatt_rsp_timer_ent; /* peer response timer */
uint8_t retry_count;
uint16_t read_req_current_mtu; /* This is the MTU value that the read was
@@ -416,7 +416,14 @@
fixed_queue_t* srv_chg_clt_q; /* service change clients queue */
tGATT_REG cl_rcb[GATT_MAX_APPS];
- tGATT_CLCB clcb[GATT_CL_MAX_LCB]; /* connection link control block*/
+
+ /* list of connection link control blocks.
+ * Since clcbs are also keep in the channels (ATT and EATT) queues while
+ * processing, we want to make sure that references to elements are not
+ * invalidated when elements are added or removed from the list. This is why
+ * std::list is used.
+ */
+ std::list<tGATT_CLCB> clcb_queue;
#if (GATT_CONFORMANCE_TESTING == TRUE)
bool enable_err_rsp;
@@ -586,6 +593,7 @@
extern uint16_t gatt_tcb_get_att_cid(tGATT_TCB& tcb, bool eatt_support);
extern uint16_t gatt_tcb_get_payload_size_tx(tGATT_TCB& tcb, uint16_t cid);
extern uint16_t gatt_tcb_get_payload_size_rx(tGATT_TCB& tcb, uint16_t cid);
+extern void gatt_clcb_invalidate(tGATT_TCB* p_tcb, const tGATT_CLCB* p_clcb);
extern void gatt_clcb_dealloc(tGATT_CLCB* p_clcb);
extern void gatt_sr_copy_prep_cnt_to_cback_cnt(tGATT_TCB& p_tcb);
@@ -637,6 +645,7 @@
uint8_t* p_data);
extern void gatt_send_queue_write_cancel(tGATT_TCB& tcb, tGATT_CLCB* p_clcb,
tGATT_EXEC_FLAG flag);
+extern bool gatt_is_outstanding_msg_in_att_send_queue(const tGATT_TCB& tcb);
/* gatt_auth.cc */
extern bool gatt_security_check_start(tGATT_CLCB* p_clcb);
diff --git a/system/stack/gatt/gatt_utils.cc b/system/stack/gatt/gatt_utils.cc
index 64b2f7a..8fa2406 100644
--- a/system/stack/gatt/gatt_utils.cc
+++ b/system/stack/gatt/gatt_utils.cc
@@ -27,6 +27,7 @@
#include <base/strings/stringprintf.h>
#include <cstdint>
+#include <deque>
#include "bt_target.h" // Must be first to define build configuration
#include "osi/include/allocator.h"
@@ -948,38 +949,6 @@
/*******************************************************************************
*
- * Function gatt_is_clcb_allocated
- *
- * Description The function check clcb for conn_id is allocated or not
- *
- * Returns True already allocated
- *
- ******************************************************************************/
-
-bool gatt_is_clcb_allocated(uint16_t conn_id) {
- uint8_t i = 0;
- uint8_t num_of_allocated = 0;
- tGATT_IF gatt_if = GATT_GET_GATT_IF(conn_id);
- uint8_t tcb_idx = GATT_GET_TCB_IDX(conn_id);
- tGATT_TCB* p_tcb = gatt_get_tcb_by_idx(tcb_idx);
- tGATT_REG* p_reg = gatt_get_regcb(gatt_if);
- int possible_plcb = 1;
-
- if (p_reg->eatt_support) possible_plcb += p_tcb->eatt;
-
- /* With eatt number of active clcbs can me up to 1 + number of eatt channels
- */
- for (i = 0; i < GATT_CL_MAX_LCB; i++) {
- if (gatt_cb.clcb[i].in_use && (gatt_cb.clcb[i].conn_id == conn_id)) {
- if (++num_of_allocated == possible_plcb) return true;
- }
- }
-
- return false;
-}
-
-/*******************************************************************************
- *
* Function gatt_tcb_is_cid_busy
*
* Description The function check if channel with given cid is busy
@@ -1008,29 +977,30 @@
*
******************************************************************************/
tGATT_CLCB* gatt_clcb_alloc(uint16_t conn_id) {
- uint8_t i = 0;
- tGATT_CLCB* p_clcb = NULL;
+ tGATT_CLCB clcb;
tGATT_IF gatt_if = GATT_GET_GATT_IF(conn_id);
uint8_t tcb_idx = GATT_GET_TCB_IDX(conn_id);
tGATT_TCB* p_tcb = gatt_get_tcb_by_idx(tcb_idx);
tGATT_REG* p_reg = gatt_get_regcb(gatt_if);
- for (i = 0; i < GATT_CL_MAX_LCB; i++) {
- if (!gatt_cb.clcb[i].in_use) {
- p_clcb = &gatt_cb.clcb[i];
+ clcb.conn_id = conn_id;
+ clcb.p_reg = p_reg;
+ clcb.p_tcb = p_tcb;
+ /* Use eatt only when clients wants that */
+ clcb.cid = gatt_tcb_get_att_cid(*p_tcb, p_reg->eatt_support);
- p_clcb->in_use = true;
- p_clcb->conn_id = conn_id;
- p_clcb->p_reg = p_reg;
- p_clcb->p_tcb = p_tcb;
+ gatt_cb.clcb_queue.emplace_back(clcb);
+ auto p_clcb = &(gatt_cb.clcb_queue.back());
- /* Use eatt only when clients wants that */
- p_clcb->cid = gatt_tcb_get_att_cid(*p_tcb, p_reg->eatt_support);
-
- break;
- }
+ if (gatt_cb.clcb_queue.size() > GATT_CL_MAX_LCB) {
+ /* GATT_CL_MAX_LCB is here from the historical reasons. We believe this
+ * limitation is not needed. In addition, number of clcb should not be
+ * bigger than that and also if it is bigger, we believe it should not
+ * cause the problem. This WARN is just to monitor number of CLCB and will
+ * help in debugging in case we are wrong */
+ LOG_WARN("Number of CLCB: %zu > %d", gatt_cb.clcb_queue.size(),
+ GATT_CL_MAX_LCB);
}
-
return p_clcb;
}
@@ -1166,14 +1136,64 @@
*
******************************************************************************/
void gatt_clcb_dealloc(tGATT_CLCB* p_clcb) {
- if (p_clcb && p_clcb->in_use) {
+ if (p_clcb) {
alarm_free(p_clcb->gatt_rsp_timer_ent);
- memset(p_clcb, 0, sizeof(tGATT_CLCB));
+ for (auto clcb_it = gatt_cb.clcb_queue.begin();
+ clcb_it != gatt_cb.clcb_queue.end(); clcb_it++) {
+ if (&(*clcb_it) == p_clcb) {
+ gatt_cb.clcb_queue.erase(clcb_it);
+ return;
+ }
+ }
}
}
/*******************************************************************************
*
+ * Function gatt_clcb_invalidate
+ *
+ * Description The function invalidates already scheduled p_clcb.
+ *
+ * Returns None
+ *
+ ******************************************************************************/
+void gatt_clcb_invalidate(tGATT_TCB* p_tcb, const tGATT_CLCB* p_clcb) {
+ std::deque<tGATT_CMD_Q>* cl_cmd_q_p;
+ uint16_t cid = p_clcb->cid;
+
+ if (cid == p_tcb->att_lcid) {
+ cl_cmd_q_p = &p_tcb->cl_cmd_q;
+ } else {
+ EattChannel* channel = EattExtension::GetInstance()->FindEattChannelByCid(
+ p_tcb->peer_bda, cid);
+ if (channel == nullptr) {
+ return;
+ }
+ cl_cmd_q_p = &channel->cl_cmd_q_;
+ }
+
+ if (cl_cmd_q_p->empty()) {
+ return;
+ }
+
+ auto iter = std::find_if(cl_cmd_q_p->begin(), cl_cmd_q_p->end(),
+ [p_clcb](auto& el) { return el.p_clcb == p_clcb; });
+
+ if (iter == cl_cmd_q_p->end()) {
+ return;
+ }
+
+ if (iter->to_send) {
+ /* If command was not send, just remove the entire element */
+ cl_cmd_q_p->erase(iter);
+ } else {
+ /* If command has been sent, just invalidate p_clcb pointer for proper
+ * response handling */
+ iter->p_clcb = NULL;
+ }
+}
+/*******************************************************************************
+ *
* Function gatt_find_tcb_by_cid
*
* Description The function searches for an empty entry
@@ -1208,10 +1228,10 @@
*
******************************************************************************/
uint8_t gatt_num_clcb_by_bd_addr(const RawAddress& bda) {
- uint8_t i, num = 0;
+ uint8_t num = 0;
- for (i = 0; i < GATT_CL_MAX_LCB; i++) {
- if (gatt_cb.clcb[i].in_use && gatt_cb.clcb[i].p_tcb->peer_bda == bda) num++;
+ for (auto const& clcb : gatt_cb.clcb_queue) {
+ if (clcb.p_tcb->peer_bda == bda) num++;
}
return num;
}
@@ -1449,18 +1469,18 @@
cmd.cid = p_clcb->cid;
if (p_clcb->cid == tcb.att_lcid) {
- tcb.cl_cmd_q.push(cmd);
+ tcb.cl_cmd_q.push_back(cmd);
} else {
EattChannel* channel =
EattExtension::GetInstance()->FindEattChannelByCid(tcb.peer_bda, cmd.cid);
CHECK(channel);
- channel->cl_cmd_q_.push(cmd);
+ channel->cl_cmd_q_.push_back(cmd);
}
}
/** dequeue the command in the client CCB command queue */
tGATT_CLCB* gatt_cmd_dequeue(tGATT_TCB& tcb, uint16_t cid, uint8_t* p_op_code) {
- std::queue<tGATT_CMD_Q>* cl_cmd_q_p;
+ std::deque<tGATT_CMD_Q>* cl_cmd_q_p;
if (cid == tcb.att_lcid) {
cl_cmd_q_p = &tcb.cl_cmd_q;
@@ -1476,8 +1496,16 @@
tGATT_CMD_Q cmd = cl_cmd_q_p->front();
tGATT_CLCB* p_clcb = cmd.p_clcb;
*p_op_code = cmd.op_code;
- p_clcb->cid = cid;
- cl_cmd_q_p->pop();
+
+ /* Note: If GATT client deregistered while the ATT request was on the way to
+ * peer, device p_clcb will be null.
+ */
+ if (p_clcb && p_clcb->cid != cid) {
+ LOG_WARN(" CID does not match (%d!=%d), conn_id=0x%04x", p_clcb->cid, cid,
+ p_clcb->conn_id);
+ }
+
+ cl_cmd_q_p->pop_front();
return p_clcb;
}
@@ -1498,6 +1526,18 @@
/*******************************************************************************
*
+ * Function gatt_is_outstanding_msg_in_att_send_queue
+ *
+ * Description checks if there is message on the ATT fixed channel to send
+ *
+ * Returns true: on success; false otherwise
+ *
+ ******************************************************************************/
+bool gatt_is_outstanding_msg_in_att_send_queue(const tGATT_TCB& tcb) {
+ return (!tcb.cl_cmd_q.empty() && (tcb.cl_cmd_q.front()).to_send);
+}
+/*******************************************************************************
+ *
* Function gatt_end_operation
*
* Description This function ends a discovery, send callback and finalize
@@ -1585,20 +1625,32 @@
}
gatt_set_ch_state(p_tcb, GATT_CH_CLOSE);
- for (uint8_t i = 0; i < GATT_CL_MAX_LCB; i++) {
- tGATT_CLCB* p_clcb = &gatt_cb.clcb[i];
- if (!p_clcb->in_use || p_clcb->p_tcb != p_tcb) continue;
- gatt_stop_rsp_timer(p_clcb);
- VLOG(1) << "found p_clcb conn_id=" << +p_clcb->conn_id;
- if (p_clcb->operation == GATTC_OPTYPE_NONE) {
- gatt_clcb_dealloc(p_clcb);
+ /* Notify EATT about disconnection. */
+ EattExtension::GetInstance()->Disconnect(p_tcb->peer_bda);
+
+ for (auto clcb_it = gatt_cb.clcb_queue.begin();
+ clcb_it != gatt_cb.clcb_queue.end();) {
+ if (clcb_it->p_tcb != p_tcb) {
+ ++clcb_it;
continue;
}
+ gatt_stop_rsp_timer(&(*clcb_it));
+ VLOG(1) << "found p_clcb conn_id=" << +clcb_it->conn_id;
+ if (clcb_it->operation == GATTC_OPTYPE_NONE) {
+ clcb_it = gatt_cb.clcb_queue.erase(clcb_it);
+ continue;
+ }
+
+ tGATT_CLCB* p_clcb = &(*clcb_it);
+ ++clcb_it;
gatt_end_operation(p_clcb, GATT_ERROR, NULL);
}
+ /* Remove the outstanding ATT commnads if any */
+ p_tcb->cl_cmd_q.clear();
+
alarm_free(p_tcb->ind_ack_timer);
p_tcb->ind_ack_timer = NULL;
alarm_free(p_tcb->conf_timer);
diff --git a/system/stack/include/security_client_callbacks.h b/system/stack/include/security_client_callbacks.h
index de59ff1..d6070cd 100644
--- a/system/stack/include/security_client_callbacks.h
+++ b/system/stack/include/security_client_callbacks.h
@@ -52,7 +52,8 @@
typedef uint8_t(tBTM_LINK_KEY_CALLBACK)(const RawAddress& bd_addr,
DEV_CLASS dev_class,
tBTM_BD_NAME bd_name,
- const LinkKey& key, uint8_t key_type);
+ const LinkKey& key, uint8_t key_type,
+ bool is_ctkd);
/* Remote Name Resolved. Parameters are
* BD Address of remote
diff --git a/system/stack/smp/smp_l2c.cc b/system/stack/smp/smp_l2c.cc
index 3aaa479..7debcea 100644
--- a/system/stack/smp/smp_l2c.cc
+++ b/system/stack/smp/smp_l2c.cc
@@ -24,6 +24,7 @@
#define LOG_TAG "bluetooth"
+#include <base/logging.h>
#include <string.h>
#include "bt_target.h"
@@ -35,11 +36,10 @@
#include "osi/include/log.h"
#include "osi/include/osi.h" // UNUSED_ATTR
#include "smp_int.h"
+#include "stack/btm/btm_dev.h"
#include "stack/include/bt_hdr.h"
#include "types/raw_address.h"
-#include <base/logging.h>
-
static void smp_connect_callback(uint16_t channel, const RawAddress& bd_addr,
bool connected, uint16_t reason,
tBT_TRANSPORT transport);
@@ -249,6 +249,24 @@
if (bd_addr != p_cb->pairing_bda) return;
+ /* Check if we already finished SMP pairing over LE, and are waiting to
+ * check if other side returns some errors. Connection/disconnection on
+ * Classic transport shouldn't impact that.
+ */
+ tBTM_SEC_DEV_REC* p_dev_rec = btm_find_dev(p_cb->pairing_bda);
+ if (smp_get_state() == SMP_STATE_BOND_PENDING &&
+ (p_dev_rec && p_dev_rec->is_link_key_known()) &&
+ alarm_is_scheduled(p_cb->delayed_auth_timer_ent)) {
+ /* If we were to not return here, we would reset SMP control block, and
+ * delayed_auth_timer_ent would never be executed. Even though we stored all
+ * keys, stack would consider device as not bonded. It would reappear after
+ * stack restart, when we re-read record from storage. Service discovery
+ * would stay broken.
+ */
+ LOG_INFO("Classic event after CTKD on LE transport");
+ return;
+ }
+
if (connected) {
if (!p_cb->connect_initialized) {
p_cb->connect_initialized = true;
diff --git a/system/stack/test/btm/stack_btm_test.cc b/system/stack/test/btm/stack_btm_test.cc
index b9285e6..ecd14fe 100644
--- a/system/stack/test/btm/stack_btm_test.cc
+++ b/system/stack/test/btm/stack_btm_test.cc
@@ -22,6 +22,7 @@
#include <iomanip>
#include <iostream>
#include <map>
+#include <sstream>
#include <vector>
#include "btif/include/btif_hh.h"
@@ -397,3 +398,26 @@
wipe_secrets_and_remove(device_record);
}
+
+TEST_F(StackBtmTest, sco_state_text) {
+ std::vector<std::pair<tSCO_STATE, std::string>> states = {
+ std::make_pair(SCO_ST_UNUSED, "SCO_ST_UNUSED"),
+ std::make_pair(SCO_ST_LISTENING, "SCO_ST_LISTENING"),
+ std::make_pair(SCO_ST_W4_CONN_RSP, "SCO_ST_W4_CONN_RSP"),
+ std::make_pair(SCO_ST_CONNECTING, "SCO_ST_CONNECTING"),
+ std::make_pair(SCO_ST_CONNECTED, "SCO_ST_CONNECTED"),
+ std::make_pair(SCO_ST_DISCONNECTING, "SCO_ST_DISCONNECTING"),
+ std::make_pair(SCO_ST_PEND_UNPARK, "SCO_ST_PEND_UNPARK"),
+ std::make_pair(SCO_ST_PEND_ROLECHANGE, "SCO_ST_PEND_ROLECHANGE"),
+ std::make_pair(SCO_ST_PEND_MODECHANGE, "SCO_ST_PEND_MODECHANGE"),
+ };
+ for (const auto& state : states) {
+ ASSERT_STREQ(state.second.c_str(), sco_state_text(state.first).c_str());
+ }
+ std::ostringstream oss;
+ oss << "unknown_sco_state: " << std::numeric_limits<std::uint16_t>::max();
+ ASSERT_STREQ(oss.str().c_str(),
+ sco_state_text(static_cast<tSCO_STATE>(
+ std::numeric_limits<std::uint16_t>::max()))
+ .c_str());
+}
diff --git a/system/test/common/init_flags.cc b/system/test/common/init_flags.cc
index 9b8585b..38f1382 100644
--- a/system/test/common/init_flags.cc
+++ b/system/test/common/init_flags.cc
@@ -9,6 +9,7 @@
namespace common {
bool InitFlags::logging_debug_enabled_for_all = false;
+bool InitFlags::btm_dm_flush_discovery_queue_on_search_cancel = false;
std::unordered_map<std::string, bool>
InitFlags::logging_debug_explicit_tag_settings = {};
void InitFlags::Load(const char** flags) {}