summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--android/app/src/com/android/bluetooth/btservice/AdapterService.java69
-rw-r--r--android/app/src/com/android/bluetooth/gatt/ScanManager.java57
-rw-r--r--android/app/src/com/android/bluetooth/hap/HapClientService.java12
-rw-r--r--android/app/src/com/android/bluetooth/tbs/TbsGeneric.java407
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/hap/HapClientTest.java26
-rw-r--r--android/leaudio/.gitignore13
-rw-r--r--android/leaudio/.idea/codeStyles/Project.xml116
-rw-r--r--android/leaudio/.idea/compiler.xml6
-rw-r--r--android/leaudio/.idea/encodings.xml4
-rw-r--r--android/leaudio/.idea/google-java-format.xml7
-rw-r--r--android/leaudio/.idea/gradle.xml23
-rw-r--r--android/leaudio/.idea/inspectionProfiles/Project_Default.xml9
-rw-r--r--android/leaudio/.idea/jarRepositories.xml25
-rw-r--r--android/leaudio/.idea/misc.xml9
-rw-r--r--android/leaudio/.idea/uiDesigner.xml124
-rw-r--r--android/leaudio/.idea/vcs.xml7
-rw-r--r--android/leaudio/.project28
-rw-r--r--android/leaudio/.settings/org.eclipse.buildship.core.prefs13
-rw-r--r--android/leaudio/Android.bp30
-rw-r--r--android/leaudio/CleanSpec.mk51
-rw-r--r--android/leaudio/app/.classpath6
-rw-r--r--android/leaudio/app/.gitignore1
-rw-r--r--android/leaudio/app/.project34
-rw-r--r--android/leaudio/app/.settings/org.eclipse.buildship.core.prefs2
-rw-r--r--android/leaudio/app/build.gradle36
-rw-r--r--android/leaudio/app/proguard-rules.pro21
-rw-r--r--android/leaudio/app/src/androidTest/java/com/android/bluetooth/leaudio/ExampleInstrumentedTest.java27
-rw-r--r--android/leaudio/app/src/main/AndroidManifest.xml41
-rw-r--r--android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java1040
-rw-r--r--android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastItemsAdapter.java134
-rw-r--r--android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterActivity.java181
-rw-r--r--android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterViewModel.java92
-rw-r--r--android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioDeviceStateWrapper.java116
-rw-r--r--android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java1623
-rw-r--r--android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioViewModel.java118
-rw-r--r--android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/MainActivity.java574
-rw-r--r--android/leaudio/app/src/main/res/drawable-anydpi/ic_bluetooth_connected_black.xml11
-rw-r--r--android/leaudio/app/src/main/res/drawable-anydpi/ic_bluetooth_dots_black.xml11
-rw-r--r--android/leaudio/app/src/main/res/drawable-hdpi/ic_bluetooth_connected_black.pngbin0 -> 280 bytes
-rw-r--r--android/leaudio/app/src/main/res/drawable-hdpi/ic_bluetooth_dots_black.pngbin0 -> 266 bytes
-rw-r--r--android/leaudio/app/src/main/res/drawable-mdpi/ic_bluetooth_connected_black.pngbin0 -> 195 bytes
-rw-r--r--android/leaudio/app/src/main/res/drawable-mdpi/ic_bluetooth_dots_black.pngbin0 -> 196 bytes
-rw-r--r--android/leaudio/app/src/main/res/drawable-v24/ic_launcher_foreground.xml34
-rw-r--r--android/leaudio/app/src/main/res/drawable-xhdpi/ic_bluetooth_connected_black.pngbin0 -> 317 bytes
-rw-r--r--android/leaudio/app/src/main/res/drawable-xhdpi/ic_bluetooth_dots_black.pngbin0 -> 303 bytes
-rw-r--r--android/leaudio/app/src/main/res/drawable-xxhdpi/ic_bluetooth_connected_black.pngbin0 -> 444 bytes
-rw-r--r--android/leaudio/app/src/main/res/drawable-xxhdpi/ic_bluetooth_dots_black.pngbin0 -> 405 bytes
-rw-r--r--android/leaudio/app/src/main/res/drawable/dotted_line.xml14
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_add_white_24dp.xml5
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_arrow_drop_down_black_24dp.xml9
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_arrow_drop_up_black_24dp.xml9
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_bluetooth_searching_black_24dp.xml9
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_cast_black_24dp.xml9
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_cast_connected_black_24dp.xml9
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_download_black_24dp.xml9
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_launcher_background.xml170
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_refresh.xml10
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_refresh_black_24dp.xml9
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_upload_black_24dp.xml9
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_vpn_key_black_24dp.xml9
-rw-r--r--android/leaudio/app/src/main/res/drawable/ic_warning_black_24dp.xml9
-rw-r--r--android/leaudio/app/src/main/res/layout/activity_main.xml35
-rw-r--r--android/leaudio/app/src/main/res/layout/bass_add_source_dialog.xml53
-rw-r--r--android/leaudio/app/src/main/res/layout/bass_layout.xml60
-rw-r--r--android/leaudio/app/src/main/res/layout/broadcast_item.xml43
-rw-r--r--android/leaudio/app/src/main/res/layout/broadcaster_activity.xml52
-rw-r--r--android/leaudio/app/src/main/res/layout/broadcaster_add_broadcast_dialog.xml54
-rw-r--r--android/leaudio/app/src/main/res/layout/content_main.xml20
-rw-r--r--android/leaudio/app/src/main/res/layout/hap_layout.xml172
-rw-r--r--android/leaudio/app/src/main/res/layout/header_layout.xml72
-rw-r--r--android/leaudio/app/src/main/res/layout/le_audio_device_fragment.xml67
-rw-r--r--android/leaudio/app/src/main/res/layout/le_audio_layout.xml226
-rw-r--r--android/leaudio/app/src/main/res/layout/vc_inputs_layout.xml289
-rw-r--r--android/leaudio/app/src/main/res/layout/vc_layout.xml59
-rw-r--r--android/leaudio/app/src/main/res/layout/vc_outputs_layout.xml183
-rw-r--r--android/leaudio/app/src/main/res/menu/menu_main.xml9
-rw-r--r--android/leaudio/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml5
-rw-r--r--android/leaudio/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml5
-rw-r--r--android/leaudio/app/src/main/res/mipmap-hdpi/ic_launcher.pngbin0 -> 2963 bytes
-rw-r--r--android/leaudio/app/src/main/res/mipmap-hdpi/ic_launcher_round.pngbin0 -> 4905 bytes
-rw-r--r--android/leaudio/app/src/main/res/mipmap-mdpi/ic_launcher.pngbin0 -> 2060 bytes
-rw-r--r--android/leaudio/app/src/main/res/mipmap-mdpi/ic_launcher_round.pngbin0 -> 2783 bytes
-rw-r--r--android/leaudio/app/src/main/res/mipmap-xhdpi/ic_launcher.pngbin0 -> 4490 bytes
-rw-r--r--android/leaudio/app/src/main/res/mipmap-xhdpi/ic_launcher_round.pngbin0 -> 6895 bytes
-rw-r--r--android/leaudio/app/src/main/res/mipmap-xxhdpi/ic_launcher.pngbin0 -> 6387 bytes
-rw-r--r--android/leaudio/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngbin0 -> 10413 bytes
-rw-r--r--android/leaudio/app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngbin0 -> 9128 bytes
-rw-r--r--android/leaudio/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngbin0 -> 15132 bytes
-rw-r--r--android/leaudio/app/src/main/res/values/colors.xml6
-rw-r--r--android/leaudio/app/src/main/res/values/dimens.xml3
-rw-r--r--android/leaudio/app/src/main/res/values/strings.xml152
-rw-r--r--android/leaudio/app/src/main/res/values/styles.xml20
-rw-r--r--android/leaudio/app/src/test/java/pl/codecoup/ehima/leaudio/ExampleUnitTest.java17
-rw-r--r--android/leaudio/build.gradle27
-rw-r--r--android/leaudio/gradle.properties22
-rw-r--r--android/leaudio/gradle/wrapper/gradle-wrapper.jarbin0 -> 54329 bytes
-rw-r--r--android/leaudio/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xandroid/leaudio/gradlew172
-rw-r--r--android/leaudio/gradlew.bat84
-rw-r--r--android/leaudio/settings.gradle1
-rw-r--r--framework/java/android/bluetooth/BluetoothAdapter.java26
-rw-r--r--framework/java/android/bluetooth/BluetoothDevice.java9
-rw-r--r--framework/java/android/bluetooth/BluetoothHapClient.java23
-rw-r--r--system/audio_hal_interface/aidl/client_interface_aidl.cc7
-rw-r--r--system/binder/android/bluetooth/IBluetoothHapClientCallback.aidl1
-rw-r--r--system/blueberry/tests/gd_sl4a/hci/le_advanced_scanning_test.py678
-rw-r--r--system/bta/Android.bp1
-rw-r--r--system/bta/dm/bta_dm_pm.cc4
-rw-r--r--system/bta/gatt/bta_gatts_queue.cc125
-rw-r--r--system/bta/include/bta_gatt_server_queue.h54
-rw-r--r--system/bta/le_audio/client.cc2
-rw-r--r--system/btif/src/btif_storage.cc10
-rw-r--r--system/gd/rust/linux/client/src/command_handler.rs12
-rw-r--r--system/gd/rust/linux/client/src/dbus_iface.rs27
-rw-r--r--system/gd/rust/linux/service/src/iface_bluetooth.rs25
-rw-r--r--system/gd/rust/linux/stack/src/bluetooth.rs44
-rw-r--r--system/gd/rust/topshim/src/btif.rs1
-rw-r--r--system/stack/include/btm_api_types.h76
-rw-r--r--system/stack/test/btm/stack_btm_test.cc2
-rw-r--r--tools/rootcanal/model/controller/dual_mode_controller.cc4
120 files changed, 8111 insertions, 361 deletions
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
index cc8e9b962a..1a5e7a5a73 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
@@ -112,6 +112,7 @@ import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.bluetooth.btservice.storage.MetadataDatabase;
import com.android.bluetooth.csip.CsipSetCoordinatorService;
import com.android.bluetooth.gatt.GattService;
+import com.android.bluetooth.gatt.ScanManager;
import com.android.bluetooth.hap.HapClientService;
import com.android.bluetooth.hearingaid.HearingAidService;
import com.android.bluetooth.hfp.HeadsetService;
@@ -5114,6 +5115,18 @@ public class AdapterService extends Service {
@GuardedBy("mDeviceConfigLock")
private int mScanUpgradeDurationMillis =
DeviceConfigListener.DEFAULT_SCAN_UPGRADE_DURATION_MILLIS;
+ @GuardedBy("mDeviceConfigLock")
+ private int mScreenOffLowPowerWindowMillis =
+ ScanManager.SCAN_MODE_SCREEN_OFF_LOW_POWER_WINDOW_MS;
+ @GuardedBy("mDeviceConfigLock")
+ private int mScreenOffLowPowerIntervalMillis =
+ ScanManager.SCAN_MODE_SCREEN_OFF_LOW_POWER_INTERVAL_MS;
+ @GuardedBy("mDeviceConfigLock")
+ private int mScreenOffBalancedWindowMillis =
+ ScanManager.SCAN_MODE_SCREEN_OFF_BALANCED_WINDOW_MS;
+ @GuardedBy("mDeviceConfigLock")
+ private int mScreenOffBalancedIntervalMillis =
+ ScanManager.SCAN_MODE_SCREEN_OFF_BALANCED_INTERVAL_MS;
public @NonNull Predicate<String> getLocationDenylistName() {
synchronized (mDeviceConfigLock) {
@@ -5172,6 +5185,42 @@ public class AdapterService extends Service {
}
}
+ /**
+ * Returns SCREEN_OFF_BALANCED scan window in millis.
+ */
+ public int getScreenOffBalancedWindowMillis() {
+ synchronized (mDeviceConfigLock) {
+ return mScreenOffBalancedWindowMillis;
+ }
+ }
+
+ /**
+ * Returns SCREEN_OFF_BALANCED scan interval in millis.
+ */
+ public int getScreenOffBalancedIntervalMillis() {
+ synchronized (mDeviceConfigLock) {
+ return mScreenOffBalancedIntervalMillis;
+ }
+ }
+
+ /**
+ * Returns SCREEN_OFF low power scan window in millis.
+ */
+ public int getScreenOffLowPowerWindowMillis() {
+ synchronized (mDeviceConfigLock) {
+ return mScreenOffLowPowerWindowMillis;
+ }
+ }
+
+ /**
+ * Returns SCREEN_OFF low power scan interval in millis.
+ */
+ public int getScreenOffLowPowerIntervalMillis() {
+ synchronized (mDeviceConfigLock) {
+ return mScreenOffLowPowerIntervalMillis;
+ }
+ }
+
private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();
private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
@@ -5189,6 +5238,14 @@ public class AdapterService extends Service {
"scan_timeout_millis";
private static final String SCAN_UPGRADE_DURATION_MILLIS =
"scan_upgrade_duration_millis";
+ private static final String SCREEN_OFF_LOW_POWER_WINDOW_MILLIS =
+ "screen_off_low_power_window_millis";
+ private static final String SCREEN_OFF_LOW_POWER_INTERVAL_MILLIS =
+ "screen_off_low_power_interval_millis";
+ private static final String SCREEN_OFF_BALANCED_WINDOW_MILLIS =
+ "screen_off_balanced_window_millis";
+ private static final String SCREEN_OFF_BALANCED_INTERVAL_MILLIS =
+ "screen_off_balanced_interval_millis";
/**
* Default denylist which matches Eddystone and iBeacon payloads.
@@ -5227,6 +5284,18 @@ public class AdapterService extends Service {
DEFAULT_SCAN_TIMEOUT_MILLIS);
mScanUpgradeDurationMillis = properties.getInt(SCAN_UPGRADE_DURATION_MILLIS,
DEFAULT_SCAN_UPGRADE_DURATION_MILLIS);
+ mScreenOffLowPowerWindowMillis = properties.getInt(
+ SCREEN_OFF_LOW_POWER_WINDOW_MILLIS,
+ ScanManager.SCAN_MODE_SCREEN_OFF_LOW_POWER_WINDOW_MS);
+ mScreenOffLowPowerIntervalMillis = properties.getInt(
+ SCREEN_OFF_LOW_POWER_INTERVAL_MILLIS,
+ ScanManager.SCAN_MODE_SCREEN_OFF_LOW_POWER_INTERVAL_MS);
+ mScreenOffBalancedWindowMillis = properties.getInt(
+ SCREEN_OFF_BALANCED_WINDOW_MILLIS,
+ ScanManager.SCAN_MODE_SCREEN_OFF_BALANCED_WINDOW_MS);
+ mScreenOffBalancedIntervalMillis = properties.getInt(
+ SCREEN_OFF_BALANCED_INTERVAL_MILLIS,
+ ScanManager.SCAN_MODE_SCREEN_OFF_BALANCED_INTERVAL_MS);
}
}
}
diff --git a/android/app/src/com/android/bluetooth/gatt/ScanManager.java b/android/app/src/com/android/bluetooth/gatt/ScanManager.java
index 6494a6f448..84033cd980 100644
--- a/android/app/src/com/android/bluetooth/gatt/ScanManager.java
+++ b/android/app/src/com/android/bluetooth/gatt/ScanManager.java
@@ -67,6 +67,20 @@ public class ScanManager {
private static final boolean DBG = GattServiceConfig.DBG;
private static final String TAG = GattServiceConfig.TAG_PREFIX + "ScanManager";
+ /**
+ * Scan params corresponding to regular scan setting
+ */
+ private static final int SCAN_MODE_LOW_POWER_WINDOW_MS = 140;
+ private static final int SCAN_MODE_LOW_POWER_INTERVAL_MS = 1400;
+ private static final int SCAN_MODE_BALANCED_WINDOW_MS = 183;
+ private static final int SCAN_MODE_BALANCED_INTERVAL_MS = 730;
+ private static final int SCAN_MODE_LOW_LATENCY_WINDOW_MS = 100;
+ private static final int SCAN_MODE_LOW_LATENCY_INTERVAL_MS = 100;
+ public static final int SCAN_MODE_SCREEN_OFF_LOW_POWER_WINDOW_MS = 512;
+ public static final int SCAN_MODE_SCREEN_OFF_LOW_POWER_INTERVAL_MS = 10240;
+ public static final int SCAN_MODE_SCREEN_OFF_BALANCED_WINDOW_MS = 183;
+ public static final int SCAN_MODE_SCREEN_OFF_BALANCED_INTERVAL_MS = 730;
+
// Result type defined in bt stack. Need to be accessed by GattService.
static final int SCAN_RESULT_TYPE_TRUNCATED = 1;
static final int SCAN_RESULT_TYPE_FULL = 2;
@@ -695,19 +709,6 @@ public class ScanManager {
private static final int DISCARD_OLDEST_WHEN_BUFFER_FULL = 0;
- /**
- * Scan params corresponding to regular scan setting
- */
- private static final int SCAN_MODE_LOW_POWER_WINDOW_MS = 35;
- private static final int SCAN_MODE_LOW_POWER_INTERVAL_MS = 350;
- private static final int SCAN_MODE_BALANCED_WINDOW_MS = 35;
- private static final int SCAN_MODE_BALANCED_INTERVAL_MS = 175;
- private static final int SCAN_MODE_LOW_LATENCY_WINDOW_MS = 4096;
- private static final int SCAN_MODE_LOW_LATENCY_INTERVAL_MS = 4096;
- private static final int SCAN_MODE_SCREEN_OFF_WINDOW_MS = 512;
- private static final int SCAN_MODE_SCREEN_OFF_INTERVAL_MS = 10240;
- private static final int SCAN_MODE_SCREEN_OFF_BALANCED_WINDOW_MS = 128;
- private static final int SCAN_MODE_SCREEN_OFF_BALANCED_INTERVAL_MS = 640;
/**
* Onfound/onlost for scan settings
@@ -964,20 +965,34 @@ public class ScanManager {
// infrequently anyway. To avoid redefining paramete sets, map to the low duty cycle
// parameter set as follows.
private int getBatchScanWindowMillis(int scanMode) {
+ ContentResolver resolver = mService.getContentResolver();
switch (scanMode) {
case ScanSettings.SCAN_MODE_LOW_LATENCY:
- return SCAN_MODE_BALANCED_WINDOW_MS;
+ return Settings.Global.getInt(
+ resolver,
+ Settings.Global.BLE_SCAN_BALANCED_WINDOW_MS,
+ SCAN_MODE_BALANCED_WINDOW_MS);
default:
- return SCAN_MODE_LOW_POWER_WINDOW_MS;
+ return Settings.Global.getInt(
+ resolver,
+ Settings.Global.BLE_SCAN_LOW_POWER_WINDOW_MS,
+ SCAN_MODE_LOW_POWER_WINDOW_MS);
}
}
private int getBatchScanIntervalMillis(int scanMode) {
+ ContentResolver resolver = mService.getContentResolver();
switch (scanMode) {
case ScanSettings.SCAN_MODE_LOW_LATENCY:
- return SCAN_MODE_BALANCED_INTERVAL_MS;
+ return Settings.Global.getInt(
+ resolver,
+ Settings.Global.BLE_SCAN_BALANCED_INTERVAL_MS,
+ SCAN_MODE_BALANCED_INTERVAL_MS);
default:
- return SCAN_MODE_LOW_POWER_INTERVAL_MS;
+ return Settings.Global.getInt(
+ resolver,
+ Settings.Global.BLE_SCAN_LOW_POWER_INTERVAL_MS,
+ SCAN_MODE_LOW_POWER_INTERVAL_MS);
}
}
@@ -1343,9 +1358,9 @@ public class ScanManager {
Settings.Global.BLE_SCAN_LOW_POWER_WINDOW_MS,
SCAN_MODE_LOW_POWER_WINDOW_MS);
case ScanSettings.SCAN_MODE_SCREEN_OFF:
- return SCAN_MODE_SCREEN_OFF_WINDOW_MS;
+ return mAdapterService.getScreenOffLowPowerWindowMillis();
case ScanSettings.SCAN_MODE_SCREEN_OFF_BALANCED:
- return SCAN_MODE_SCREEN_OFF_BALANCED_WINDOW_MS;
+ return mAdapterService.getScreenOffBalancedWindowMillis();
default:
return Settings.Global.getInt(
resolver,
@@ -1380,9 +1395,9 @@ public class ScanManager {
Settings.Global.BLE_SCAN_LOW_POWER_INTERVAL_MS,
SCAN_MODE_LOW_POWER_INTERVAL_MS);
case ScanSettings.SCAN_MODE_SCREEN_OFF:
- return SCAN_MODE_SCREEN_OFF_INTERVAL_MS;
+ return mAdapterService.getScreenOffLowPowerIntervalMillis();
case ScanSettings.SCAN_MODE_SCREEN_OFF_BALANCED:
- return SCAN_MODE_SCREEN_OFF_BALANCED_INTERVAL_MS;
+ return mAdapterService.getScreenOffBalancedIntervalMillis();
default:
return Settings.Global.getInt(
resolver,
diff --git a/android/app/src/com/android/bluetooth/hap/HapClientService.java b/android/app/src/com/android/bluetooth/hap/HapClientService.java
index b6d4c8b2f9..166f3702c3 100644
--- a/android/app/src/com/android/bluetooth/hap/HapClientService.java
+++ b/android/app/src/com/android/bluetooth/hap/HapClientService.java
@@ -772,17 +772,7 @@ public class HapClientService extends ProfileService {
}
private void notifyFeaturesAvailable(BluetoothDevice device, int features) {
- if (mCallbacks != null) {
- int n = mCallbacks.beginBroadcast();
- for (int i = 0; i < n; i++) {
- try {
- mCallbacks.getBroadcastItem(i).onHapFeaturesAvailable(device, features);
- } catch (RemoteException e) {
- continue;
- }
- }
- mCallbacks.finishBroadcast();
- }
+ Log.d(TAG, "HAP device: " + device + ", features: " + String.format("0x%04X", features));
}
private void notifyActivePresetChanged(BluetoothDevice device, int presetIndex,
diff --git a/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java b/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java
index cc974cf5f8..704f55a18a 100644
--- a/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java
+++ b/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java
@@ -107,6 +107,7 @@ public class TbsGeneric {
}
}
+ private boolean mIsInitialized = false;
private TbsGatt mTbsGatt = null;
private List<Bearer> mBearerList = new ArrayList<>();
private int mLastIndexAssigned = TbsCall.INDEX_UNASSIGNED;
@@ -120,50 +121,59 @@ public class TbsGeneric {
private final class Receiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
- int ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1);
+ synchronized (TbsGeneric.this) {
+ if (!mIsInitialized) {
+ Log.w(TAG, "onReceive called while not initialized.");
+ return;
+ }
+
+ final String action = intent.getAction();
+ if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
+ int ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1);
- if (ringerMode < 0 || ringerMode == mStoredRingerMode) return;
+ if (ringerMode < 0 || ringerMode == mStoredRingerMode) return;
- mStoredRingerMode = ringerMode;
+ mStoredRingerMode = ringerMode;
- if (isSilentModeEnabled()) {
- mTbsGatt.setSilentModeFlag();
- } else {
- mTbsGatt.clearSilentModeFlag();
+ if (isSilentModeEnabled()) {
+ mTbsGatt.setSilentModeFlag();
+ } else {
+ mTbsGatt.clearSilentModeFlag();
+ }
}
}
}
};
- public boolean init(TbsGatt tbsGatt) {
+ public synchronized boolean init(TbsGatt tbsGatt) {
if (DBG) {
Log.d(TAG, "init");
}
-
mTbsGatt = tbsGatt;
int ccid = ContentControlIdKeeper.acquireCcid();
if (!isCcidValid(ccid)) {
+ Log.e(TAG, " CCID is not valid");
+ cleanup();
return false;
}
if (!mTbsGatt.init(ccid, UCI, mUriSchemes, true, true, DEFAULT_PROVIDER_NAME,
DEFAULT_BEARER_TECHNOLOGY, mTbsGattCallback)) {
Log.e(TAG, " TbsGatt init failed");
+ cleanup();
return false;
}
- AudioManager mAudioManager = mTbsGatt.getContext()
- .getSystemService(AudioManager.class);
- if (mAudioManager == null) {
+ AudioManager audioManager = mTbsGatt.getContext().getSystemService(AudioManager.class);
+ if (audioManager == null) {
Log.w(TAG, " AudioManager is not available");
- return true;
+ cleanup();
+ return false;
}
// read initial value of ringer mode
- mStoredRingerMode = mAudioManager.getRingerMode();
+ mStoredRingerMode = audioManager.getRingerMode();
if (isSilentModeEnabled()) {
mTbsGatt.setSilentModeFlag();
@@ -174,10 +184,12 @@ public class TbsGeneric {
mReceiver = new Receiver();
mTbsGatt.getContext().registerReceiver(mReceiver,
new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION));
+
+ mIsInitialized = true;
return true;
}
- public void cleanup() {
+ public synchronized void cleanup() {
if (DBG) {
Log.d(TAG, "cleanup");
}
@@ -189,72 +201,68 @@ public class TbsGeneric {
mTbsGatt.cleanup();
mTbsGatt = null;
}
+
+ mIsInitialized = false;
}
- private boolean isSilentModeEnabled() {
+ private synchronized boolean isSilentModeEnabled() {
return mStoredRingerMode != AudioManager.RINGER_MODE_NORMAL;
}
- private Bearer getBearerByToken(String token) {
- synchronized (mBearerList) {
- for (Bearer bearer : mBearerList) {
- if (bearer.token.equals(token)) {
- return bearer;
- }
+ private synchronized Bearer getBearerByToken(String token) {
+ for (Bearer bearer : mBearerList) {
+ if (bearer.token.equals(token)) {
+ return bearer;
}
}
-
return null;
}
- private Bearer getBearerByCcid(int ccid) {
- synchronized (mBearerList) {
- for (Bearer bearer : mBearerList) {
- if (bearer.ccid == ccid) {
- return bearer;
- }
+ private synchronized Bearer getBearerByCcid(int ccid) {
+ for (Bearer bearer : mBearerList) {
+ if (bearer.ccid == ccid) {
+ return bearer;
}
}
-
return null;
}
- private Bearer getBearerSupportingUri(String uri) {
- synchronized (mBearerList) {
- for (Bearer bearer : mBearerList) {
- for (String s : bearer.uriSchemes) {
- if (uri.startsWith(s + ":")) {
- return bearer;
- }
+ private synchronized Bearer getBearerSupportingUri(String uri) {
+ for (Bearer bearer : mBearerList) {
+ for (String s : bearer.uriSchemes) {
+ if (uri.startsWith(s + ":")) {
+ return bearer;
}
}
}
-
return null;
}
- private Map.Entry<UUID, Bearer> getCallIdByIndex(int callIndex) {
- synchronized (mBearerList) {
- for (Bearer bearer : mBearerList) {
- for (Map.Entry<UUID, Integer> callIdToIndex : bearer.callIdIndexMap.entrySet()) {
- if (callIndex == callIdToIndex.getValue()) {
- return Map.entry(callIdToIndex.getKey(), bearer);
- }
+ private synchronized Map.Entry<UUID, Bearer> getCallIdByIndex(int callIndex) {
+ for (Bearer bearer : mBearerList) {
+ for (Map.Entry<UUID, Integer> callIdToIndex : bearer.callIdIndexMap.entrySet()) {
+ if (callIndex == callIdToIndex.getValue()) {
+ return Map.entry(callIdToIndex.getKey(), bearer);
}
}
}
-
return null;
}
- public boolean addBearer(String token, IBluetoothLeCallControlCallback callback, String uci,
- List<String> uriSchemes, int capabilities, String providerName, int technology) {
+ public synchronized boolean addBearer(String token, IBluetoothLeCallControlCallback callback,
+ String uci, List<String> uriSchemes, int capabilities, String providerName,
+ int technology) {
if (DBG) {
Log.d(TAG,
"addBearer: token=" + token + " uci=" + uci + " uriSchemes=" + uriSchemes
+ " capabilities=" + capabilities + " providerName=" + providerName
+ " technology=" + technology);
}
+ if (!mIsInitialized) {
+ Log.w(TAG, "addBearer called while not initialized.");
+ return false;
+ }
+
if (getBearerByToken(token) != null) {
Log.w(TAG, "addBearer: token=" + token + " registered already");
return false;
@@ -264,9 +272,7 @@ public class TbsGeneric {
Bearer bearer = new Bearer(token, callback, uci, uriSchemes, capabilities, providerName,
technology, ContentControlIdKeeper.acquireCcid());
if (isCcidValid(bearer.ccid)) {
- synchronized (mBearerList) {
- mBearerList.add(bearer);
- }
+ mBearerList.add(bearer);
updateUriSchemesSupported();
if (mForegroundBearer == null) {
@@ -288,10 +294,16 @@ public class TbsGeneric {
return isCcidValid(bearer.ccid);
}
- public void removeBearer(String token) {
+ public synchronized void removeBearer(String token) {
if (DBG) {
Log.d(TAG, "removeBearer: token=" + token);
}
+
+ if (!mIsInitialized) {
+ Log.w(TAG, "removeBearer called while not initialized.");
+ return;
+ }
+
Bearer bearer = getBearerByToken(token);
if (bearer == null) {
return;
@@ -317,7 +329,7 @@ public class TbsGeneric {
}
}
- private void checkRequestComplete(Bearer bearer, UUID callId, TbsCall tbsCall) {
+ private synchronized void checkRequestComplete(Bearer bearer, UUID callId, TbsCall tbsCall) {
// check if there's any pending request related to this call
Map.Entry<Integer, Request> requestEntry = null;
if (bearer.requestMap.size() > 0) {
@@ -401,7 +413,7 @@ public class TbsGeneric {
bearer.requestMap.remove(requestId);
}
- private int getTbsResult(int result, int requestedOpcode) {
+ private synchronized int getTbsResult(int result, int requestedOpcode) {
if (result == BluetoothLeCallControl.RESULT_ERROR_UNKNOWN_CALL_ID) {
return TbsGatt.CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX;
}
@@ -414,11 +426,17 @@ public class TbsGeneric {
return TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
}
- public void requestResult(int ccid, int requestId, int result) {
+ public synchronized void requestResult(int ccid, int requestId, int result) {
if (DBG) {
Log.d(TAG, "requestResult: ccid=" + ccid + " requestId=" + requestId + " result="
+ result);
}
+
+ if (!mIsInitialized) {
+ Log.w(TAG, "requestResult called while not initialized.");
+ return;
+ }
+
Bearer bearer = getBearerByCcid(ccid);
if (bearer == null) {
Log.i(TAG, " Bearer for ccid " + ccid + " does not exist");
@@ -442,10 +460,16 @@ public class TbsGeneric {
request.callIndex, tbsResult);
}
- public void callAdded(int ccid, BluetoothLeCall call) {
+ public synchronized void callAdded(int ccid, BluetoothLeCall call) {
if (DBG) {
Log.d(TAG, "callAdded: ccid=" + ccid + " call=" + call);
}
+
+ if (!mIsInitialized) {
+ Log.w(TAG, "callAdded called while not initialized.");
+ return;
+ }
+
Bearer bearer = getBearerByCcid(ccid);
if (bearer == null) {
Log.e(TAG, "callAdded: unknown ccid=" + ccid);
@@ -485,10 +509,16 @@ public class TbsGeneric {
}
}
- public void callRemoved(int ccid, UUID callId, int reason) {
+ public synchronized void callRemoved(int ccid, UUID callId, int reason) {
if (DBG) {
Log.d(TAG, "callRemoved: ccid=" + ccid + "reason=" + reason);
}
+
+ if (!mIsInitialized) {
+ Log.w(TAG, "callRemoved called while not initialized.");
+ return;
+ }
+
Bearer bearer = getBearerByCcid(ccid);
if (bearer == null) {
Log.e(TAG, "callRemoved: unknown ccid=" + ccid);
@@ -524,10 +554,16 @@ public class TbsGeneric {
}
}
- public void callStateChanged(int ccid, UUID callId, int state) {
+ public synchronized void callStateChanged(int ccid, UUID callId, int state) {
if (DBG) {
Log.d(TAG, "callStateChanged: ccid=" + ccid + " callId=" + callId + " state=" + state);
}
+
+ if (!mIsInitialized) {
+ Log.w(TAG, "callStateChanged called while not initialized.");
+ return;
+ }
+
Bearer bearer = getBearerByCcid(ccid);
if (bearer == null) {
Log.e(TAG, "callStateChanged: unknown ccid=" + ccid);
@@ -557,10 +593,16 @@ public class TbsGeneric {
}
}
- public void currentCallsList(int ccid, List<BluetoothLeCall> calls) {
+ public synchronized void currentCallsList(int ccid, List<BluetoothLeCall> calls) {
if (DBG) {
Log.d(TAG, "currentCallsList: ccid=" + ccid + " callsNum=" + calls.size());
}
+
+ if (!mIsInitialized) {
+ Log.w(TAG, "currentCallsList called while not initialized.");
+ return;
+ }
+
Bearer bearer = getBearerByCcid(ccid);
if (bearer == null) {
Log.e(TAG, "currentCallsList: unknown ccid=" + ccid);
@@ -607,11 +649,17 @@ public class TbsGeneric {
}
}
- public void networkStateChanged(int ccid, String providerName, int technology) {
+ public synchronized void networkStateChanged(int ccid, String providerName, int technology) {
if (DBG) {
Log.d(TAG, "networkStateChanged: ccid=" + ccid + " providerName=" + providerName
+ " technology=" + technology);
}
+
+ if (!mIsInitialized) {
+ Log.w(TAG, "networkStateChanged called while not initialized.");
+ return;
+ }
+
Bearer bearer = getBearerByCcid(ccid);
if (bearer == null) {
return;
@@ -638,7 +686,7 @@ public class TbsGeneric {
}
}
- private int processOriginateCall(BluetoothDevice device, String uri) {
+ private synchronized int processOriginateCall(BluetoothDevice device, String uri) {
if (uri.startsWith("tel")) {
/*
* FIXME: For now, process telephone call originate request here, as
@@ -681,143 +729,155 @@ public class TbsGeneric {
@Override
public void onServiceAdded(boolean success) {
- if (DBG) {
- Log.d(TAG, "onServiceAdded: success=" + success);
+ synchronized (TbsGeneric.this) {
+ if (DBG) {
+ Log.d(TAG, "onServiceAdded: success=" + success);
+ }
}
}
@Override
public void onCallControlPointRequest(BluetoothDevice device, int opcode, byte[] args) {
- if (DBG) {
- Log.d(TAG, "onCallControlPointRequest: device=" + device + " opcode=" + opcode
- + "argsLen=" + args.length);
- }
- int result;
-
- switch (opcode) {
- case TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT:
- case TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE:
- case TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD:
- case TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE: {
- if (args.length == 0) {
- result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
- break;
- }
+ synchronized (TbsGeneric.this) {
+ if (DBG) {
+ Log.d(TAG, "onCallControlPointRequest: device=" + device + " opcode="
+ + opcode + " argsLen=" + args.length);
+ }
- int callIndex = args[0];
- Map.Entry<UUID, Bearer> entry = getCallIdByIndex(callIndex);
- if (entry == null) {
- result = TbsGatt.CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX;
- break;
- }
+ if (!mIsInitialized) {
+ Log.w(TAG, "onCallControlPointRequest called while not initialized.");
+ return;
+ }
- TbsCall call = mCurrentCallsList.get(callIndex);
- if (!isCallStateTransitionValid(call.getState(), opcode)) {
- result = TbsGatt.CALL_CONTROL_POINT_RESULT_STATE_MISMATCH;
- break;
- }
+ int result;
- Bearer bearer = entry.getValue();
- UUID callId = entry.getKey();
- int requestId = mLastRequestIdAssigned + 1;
- Request request = new Request(device, callId, opcode, callIndex);
- try {
- if (opcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT) {
- bearer.callback.onAcceptCall(requestId, new ParcelUuid(callId));
- } else if (opcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE) {
- bearer.callback.onTerminateCall(requestId, new ParcelUuid(callId));
- } else if (opcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD) {
- if ((bearer.capabilities & BluetoothLeCallControl.CAPABILITY_HOLD_CALL) == 0) {
- result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED;
- break;
- }
- bearer.callback.onHoldCall(requestId, new ParcelUuid(callId));
- } else {
- if ((bearer.capabilities & BluetoothLeCallControl.CAPABILITY_HOLD_CALL) == 0) {
- result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED;
- break;
- }
- bearer.callback.onUnholdCall(requestId, new ParcelUuid(callId));
+ switch (opcode) {
+ case TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT:
+ case TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE:
+ case TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD:
+ case TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE: {
+ if (args.length == 0) {
+ result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+ break;
}
- } catch (RemoteException e) {
- e.printStackTrace();
- result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
- break;
- }
- bearer.requestMap.put(requestId, request);
- mLastRequestIdAssigned = requestId;
+ int callIndex = args[0];
+ Map.Entry<UUID, Bearer> entry = getCallIdByIndex(callIndex);
+ if (entry == null) {
+ result = TbsGatt.CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX;
+ break;
+ }
- result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
- break;
- }
+ TbsCall call = mCurrentCallsList.get(callIndex);
+ if (!isCallStateTransitionValid(call.getState(), opcode)) {
+ result = TbsGatt.CALL_CONTROL_POINT_RESULT_STATE_MISMATCH;
+ break;
+ }
- case TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE: {
- result = processOriginateCall(device, new String(args));
- break;
- }
+ Bearer bearer = entry.getValue();
+ UUID callId = entry.getKey();
+ int requestId = mLastRequestIdAssigned + 1;
+ Request request = new Request(device, callId, opcode, callIndex);
+ try {
+ if (opcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT) {
+ bearer.callback.onAcceptCall(requestId, new ParcelUuid(callId));
+ } else if (opcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE) {
+ bearer.callback.onTerminateCall(requestId, new ParcelUuid(callId));
+ } else if (opcode == TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD) {
+ if ((bearer.capabilities
+ & BluetoothLeCallControl.CAPABILITY_HOLD_CALL) == 0) {
+ result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED;
+ break;
+ }
+ bearer.callback.onHoldCall(requestId, new ParcelUuid(callId));
+ } else {
+ if ((bearer.capabilities
+ & BluetoothLeCallControl.CAPABILITY_HOLD_CALL) == 0) {
+ result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED;
+ break;
+ }
+ bearer.callback.onUnholdCall(requestId, new ParcelUuid(callId));
+ }
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+ break;
+ }
+
+ bearer.requestMap.put(requestId, request);
+ mLastRequestIdAssigned = requestId;
- case TbsGatt.CALL_CONTROL_POINT_OPCODE_JOIN: {
- // at least 2 call indices are required
- if (args.length < 2) {
- result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+ result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
break;
}
- Map.Entry<UUID, Bearer> firstEntry = null;
- List<ParcelUuid> parcelUuids = new ArrayList<>();
- for (int callIndex : args) {
- Map.Entry<UUID, Bearer> entry = getCallIdByIndex(callIndex);
- if (entry == null) {
- result = TbsGatt.CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX;
+ case TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE: {
+ result = processOriginateCall(device, new String(args));
+ break;
+ }
+
+ case TbsGatt.CALL_CONTROL_POINT_OPCODE_JOIN: {
+ // at least 2 call indices are required
+ if (args.length < 2) {
+ result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
break;
}
- // state transition is valid, because a call in any state can requested to
- // join
+ Map.Entry<UUID, Bearer> firstEntry = null;
+ List<ParcelUuid> parcelUuids = new ArrayList<>();
+ for (int callIndex : args) {
+ Map.Entry<UUID, Bearer> entry = getCallIdByIndex(callIndex);
+ if (entry == null) {
+ result = TbsGatt.CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX;
+ break;
+ }
+
+ // state transition is valid, because a call in any state
+ // can requested to join
+
+ if (firstEntry == null) {
+ firstEntry = entry;
+ }
+
+ if (firstEntry.getValue() != entry.getValue()) {
+ Log.w(TAG, "Cannot join calls from different bearers!");
+ result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+ break;
+ }
- if (firstEntry == null) {
- firstEntry = entry;
+ parcelUuids.add(new ParcelUuid(entry.getKey()));
}
- if (firstEntry.getValue() != entry.getValue()) {
- Log.w(TAG, "Cannot join calls from different bearers!");
+ Bearer bearer = firstEntry.getValue();
+ Request request = new Request(device, parcelUuids, opcode, args[0]);
+ int requestId = mLastRequestIdAssigned + 1;
+ try {
+ bearer.callback.onJoinCalls(requestId, parcelUuids);
+ } catch (RemoteException e) {
+ e.printStackTrace();
result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
break;
}
- parcelUuids.add(new ParcelUuid(entry.getKey()));
- }
+ bearer.requestMap.put(requestId, request);
+ mLastIndexAssigned = requestId;
- Bearer bearer = firstEntry.getValue();
- Request request = new Request(device, parcelUuids, opcode, args[0]);
- int requestId = mLastRequestIdAssigned + 1;
- try {
- bearer.callback.onJoinCalls(requestId, parcelUuids);
- } catch (RemoteException e) {
- e.printStackTrace();
- result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE;
+ result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
break;
}
- bearer.requestMap.put(requestId, request);
- mLastIndexAssigned = requestId;
-
- result = TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS;
- break;
+ default:
+ result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED;
+ break;
}
- default:
- result = TbsGatt.CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED;
- break;
- }
+ if (result == TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS) {
+ // return here and wait for the request completition from application
+ return;
+ }
- if (result == TbsGatt.CALL_CONTROL_POINT_RESULT_SUCCESS) {
- // return here and wait for the request completition from application
- return;
+ mTbsGatt.setCallControlPointResult(device, opcode, 0, result);
}
-
- mTbsGatt.setCallControlPointResult(device, opcode, 0, result);
}
};
@@ -829,7 +889,7 @@ public class TbsGeneric {
return callIndex != TbsCall.INDEX_UNASSIGNED;
}
- private Integer getFreeCallIndex() {
+ private synchronized Integer getFreeCallIndex() {
int callIndex = mLastIndexAssigned;
for (int i = TbsCall.INDEX_MIN; i <= TbsCall.INDEX_MAX; i++) {
callIndex = (callIndex + 1) % TbsCall.INDEX_MAX;
@@ -849,7 +909,8 @@ public class TbsGeneric {
return null;
}
- private Map.Entry<Integer, TbsCall> getCallByStates(LinkedHashSet<Integer> states) {
+ private synchronized Map.Entry<Integer, TbsCall> getCallByStates(
+ LinkedHashSet<Integer> states) {
for (Map.Entry<Integer, TbsCall> entry : mCurrentCallsList.entrySet()) {
if (states.contains(entry.getValue().getState())) {
return entry;
@@ -859,7 +920,7 @@ public class TbsGeneric {
return null;
}
- private Map.Entry<Integer, TbsCall> getForegroundCall() {
+ private synchronized Map.Entry<Integer, TbsCall> getForegroundCall() {
LinkedHashSet<Integer> states = new LinkedHashSet<Integer>();
Map.Entry<Integer, TbsCall> foregroundCall;
@@ -891,7 +952,7 @@ public class TbsGeneric {
return null;
}
- private Bearer findNewForegroundBearer() {
+ private synchronized Bearer findNewForegroundBearer() {
if (mBearerList.size() == 0) {
return null;
}
@@ -910,7 +971,7 @@ public class TbsGeneric {
return mBearerList.get(mBearerList.size() - 1);
}
- private void setForegroundBearer(Bearer bearer) {
+ private synchronized void setForegroundBearer(Bearer bearer) {
if (DBG) {
Log.d(TAG, "setForegroundBearer: bearer=" + bearer);
}
@@ -934,7 +995,7 @@ public class TbsGeneric {
mForegroundBearer = bearer;
}
- private void notifyCclc() {
+ private synchronized void notifyCclc() {
if (DBG) {
Log.d(TAG, "notifyCclc");
}
@@ -942,7 +1003,7 @@ public class TbsGeneric {
mTbsGatt.setBearerListCurrentCalls(mCurrentCallsList);
}
- private void updateUriSchemesSupported() {
+ private synchronized void updateUriSchemesSupported() {
List<String> newUriSchemes = new ArrayList<>();
for (Bearer bearer : mBearerList) {
newUriSchemes.addAll(bearer.uriSchemes);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientTest.java b/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientTest.java
index 6b31044644..53b06cc765 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientTest.java
@@ -678,25 +678,6 @@ public class HapClientTest {
* Test that native callback generates proper callback call.
*/
@Test
- public void testStackEventOnFeaturesUpdate() {
- doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
- .getRemoteUuids(any(BluetoothDevice.class));
-
- mNativeInterface.onDeviceAvailable(getByteAddress(mDevice), 0x00);
- mNativeInterface.onFeaturesUpdate(getByteAddress(mDevice), 0x03);
-
- try {
- verify(mCallback, after(TIMEOUT_MS).times(1)).onHapFeaturesAvailable(eq(mDevice),
- eq(0x03));
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
- * Test that native callback generates proper callback call.
- */
- @Test
public void testStackEventOnPresetSelected() {
doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
.getRemoteUuids(any(BluetoothDevice.class));
@@ -953,13 +934,6 @@ public class HapClientTest {
evt.valueInt1 = 0x01; // features
mService.messageFromNative(evt);
- try {
- verify(mCallback, after(TIMEOUT_MS).times(1)).onHapFeaturesAvailable(eq(device),
- eq(evt.valueInt1));
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
-
// Inject some initial presets
List<BluetoothHapPresetInfo> presets =
new ArrayList<BluetoothHapPresetInfo>(Arrays.asList(
diff --git a/android/leaudio/.gitignore b/android/leaudio/.gitignore
new file mode 100644
index 0000000000..2b75303ac5
--- /dev/null
+++ b/android/leaudio/.gitignore
@@ -0,0 +1,13 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/android/leaudio/.idea/codeStyles/Project.xml b/android/leaudio/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000000..681f41ae2a
--- /dev/null
+++ b/android/leaudio/.idea/codeStyles/Project.xml
@@ -0,0 +1,116 @@
+<component name="ProjectCodeStyleConfiguration">
+ <code_scheme name="Project" version="173">
+ <codeStyleSettings language="XML">
+ <indentOptions>
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
+ </indentOptions>
+ <arrangement>
+ <rules>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>xmlns:android</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>xmlns:.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:id</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:name</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>name</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>style</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>ANDROID_ATTRIBUTE_ORDER</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>.*</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ </rules>
+ </arrangement>
+ </codeStyleSettings>
+ </code_scheme>
+</component> \ No newline at end of file
diff --git a/android/leaudio/.idea/compiler.xml b/android/leaudio/.idea/compiler.xml
new file mode 100644
index 0000000000..fb7f4a8a46
--- /dev/null
+++ b/android/leaudio/.idea/compiler.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="CompilerConfiguration">
+ <bytecodeTargetLevel target="11" />
+ </component>
+</project> \ No newline at end of file
diff --git a/android/leaudio/.idea/encodings.xml b/android/leaudio/.idea/encodings.xml
new file mode 100644
index 0000000000..15a15b218a
--- /dev/null
+++ b/android/leaudio/.idea/encodings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="Encoding" addBOMForNewFiles="with NO BOM" />
+</project> \ No newline at end of file
diff --git a/android/leaudio/.idea/google-java-format.xml b/android/leaudio/.idea/google-java-format.xml
new file mode 100644
index 0000000000..05946a82a5
--- /dev/null
+++ b/android/leaudio/.idea/google-java-format.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="GoogleJavaFormatSettings">
+ <option name="enabled" value="true" />
+ <option name="style" value="AOSP" />
+ </component>
+</project> \ No newline at end of file
diff --git a/android/leaudio/.idea/gradle.xml b/android/leaudio/.idea/gradle.xml
new file mode 100644
index 0000000000..51239da0b6
--- /dev/null
+++ b/android/leaudio/.idea/gradle.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="GradleMigrationSettings" migrationVersion="1" />
+ <component name="GradleSettings">
+ <option name="linkedExternalProjectsSettings">
+ <GradleProjectSettings>
+ <option name="delegatedBuild" value="false" />
+ <option name="testRunner" value="PLATFORM" />
+ <option name="disableWrapperSourceDistributionNotification" value="true" />
+ <option name="distributionType" value="DEFAULT_WRAPPED" />
+ <option name="externalProjectPath" value="$PROJECT_DIR$" />
+ <option name="modules">
+ <set>
+ <option value="$PROJECT_DIR$" />
+ <option value="$PROJECT_DIR$/app" />
+ </set>
+ </option>
+ <option name="resolveModulePerSourceSet" value="false" />
+ <option name="useQualifiedModuleNames" value="true" />
+ </GradleProjectSettings>
+ </option>
+ </component>
+</project> \ No newline at end of file
diff --git a/android/leaudio/.idea/inspectionProfiles/Project_Default.xml b/android/leaudio/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000000..aa998686ec
--- /dev/null
+++ b/android/leaudio/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,9 @@
+<component name="InspectionProjectProfileManager">
+ <profile version="1.0">
+ <option name="myName" value="Project Default" />
+ <inspection_tool class="AndroidLintMinSdkTooLow" enabled="false" level="WARNING" enabled_by_default="false" />
+ <inspection_tool class="AndroidLintNewApi" enabled="false" level="ERROR" enabled_by_default="false" />
+ <inspection_tool class="AndroidLintRequiresFeature" enabled="false" level="WARNING" enabled_by_default="false" />
+ <inspection_tool class="AndroidLintUsesMinSdkAttributes" enabled="false" level="WARNING" enabled_by_default="false" />
+ </profile>
+</component> \ No newline at end of file
diff --git a/android/leaudio/.idea/jarRepositories.xml b/android/leaudio/.idea/jarRepositories.xml
new file mode 100644
index 0000000000..a5f05cd8c8
--- /dev/null
+++ b/android/leaudio/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="RemoteRepositoriesConfiguration">
+ <remote-repository>
+ <option name="id" value="central" />
+ <option name="name" value="Maven Central repository" />
+ <option name="url" value="https://repo1.maven.org/maven2" />
+ </remote-repository>
+ <remote-repository>
+ <option name="id" value="jboss.community" />
+ <option name="name" value="JBoss Community repository" />
+ <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
+ </remote-repository>
+ <remote-repository>
+ <option name="id" value="BintrayJCenter" />
+ <option name="name" value="BintrayJCenter" />
+ <option name="url" value="https://jcenter.bintray.com/" />
+ </remote-repository>
+ <remote-repository>
+ <option name="id" value="Google" />
+ <option name="name" value="Google" />
+ <option name="url" value="https://dl.google.com/dl/android/maven2/" />
+ </remote-repository>
+ </component>
+</project> \ No newline at end of file
diff --git a/android/leaudio/.idea/misc.xml b/android/leaudio/.idea/misc.xml
new file mode 100644
index 0000000000..6199cc2a4d
--- /dev/null
+++ b/android/leaudio/.idea/misc.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
+ <output url="file://$PROJECT_DIR$/build/classes" />
+ </component>
+ <component name="ProjectType">
+ <option name="id" value="Android" />
+ </component>
+</project> \ No newline at end of file
diff --git a/android/leaudio/.idea/uiDesigner.xml b/android/leaudio/.idea/uiDesigner.xml
new file mode 100644
index 0000000000..e96534fb27
--- /dev/null
+++ b/android/leaudio/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="Palette2">
+ <group name="Swing">
+ <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
+ </item>
+ <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
+ </item>
+ <item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
+ </item>
+ <item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
+ <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
+ </item>
+ <item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
+ <initial-values>
+ <property name="text" value="Button" />
+ </initial-values>
+ </item>
+ <item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
+ <initial-values>
+ <property name="text" value="RadioButton" />
+ </initial-values>
+ </item>
+ <item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
+ <initial-values>
+ <property name="text" value="CheckBox" />
+ </initial-values>
+ </item>
+ <item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
+ <initial-values>
+ <property name="text" value="Label" />
+ </initial-values>
+ </item>
+ <item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
+ <preferred-size width="150" height="-1" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
+ <preferred-size width="150" height="-1" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
+ <preferred-size width="150" height="-1" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+ <preferred-size width="150" height="50" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+ <preferred-size width="150" height="50" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+ <preferred-size width="150" height="50" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
+ </item>
+ <item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+ <preferred-size width="150" height="50" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
+ <preferred-size width="150" height="50" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+ <preferred-size width="150" height="50" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
+ <preferred-size width="200" height="200" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
+ <preferred-size width="200" height="200" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
+ </item>
+ <item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
+ </item>
+ <item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
+ </item>
+ <item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
+ </item>
+ <item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
+ <preferred-size width="-1" height="20" />
+ </default-constraints>
+ </item>
+ <item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
+ <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
+ </item>
+ <item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
+ <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
+ </item>
+ </group>
+ </component>
+</project> \ No newline at end of file
diff --git a/android/leaudio/.idea/vcs.xml b/android/leaudio/.idea/vcs.xml
new file mode 100644
index 0000000000..fdf1fc87c0
--- /dev/null
+++ b/android/leaudio/.idea/vcs.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
+ </component>
+</project> \ No newline at end of file
diff --git a/android/leaudio/.project b/android/leaudio/.project
new file mode 100644
index 0000000000..16544d5b05
--- /dev/null
+++ b/android/leaudio/.project
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>LeAudio</name>
+ <comment>Project LeAudio created by Buildship.</comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
+ </natures>
+ <filteredResources>
+ <filter>
+ <id>1645602707759</id>
+ <name></name>
+ <type>30</type>
+ <matcher>
+ <id>org.eclipse.core.resources.regexFilterMatcher</id>
+ <arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
+ </matcher>
+ </filter>
+ </filteredResources>
+</projectDescription>
diff --git a/android/leaudio/.settings/org.eclipse.buildship.core.prefs b/android/leaudio/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000000..2b6d83b978
--- /dev/null
+++ b/android/leaudio/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,13 @@
+arguments=
+auto.sync=false
+build.scans.enabled=false
+connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
+connection.project.dir=
+eclipse.preferences.version=1
+gradle.user.home=
+java.home=/usr/lib/jvm/java-11-openjdk-amd64
+jvm.arguments=
+offline.mode=false
+override.workspace.settings=true
+show.console.view=true
+show.executions.view=true
diff --git a/android/leaudio/Android.bp b/android/leaudio/Android.bp
new file mode 100644
index 0000000000..1fb8450d3c
--- /dev/null
+++ b/android/leaudio/Android.bp
@@ -0,0 +1,30 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "LeAudioTestApp",
+ certificate: "platform",
+ platform_apis: true,
+
+ srcs: ["app/src/main/**/*.java"],
+ resource_dirs: ["app/src/main/res"],
+ manifest: "app/src/main/AndroidManifest.xml",
+
+ static_libs: [
+ "androidx.appcompat_appcompat",
+ "androidx-constraintlayout_constraintlayout",
+ "com.google.android.material_material",
+ "androidx.legacy_legacy-support-v4",
+ "androidx.lifecycle_lifecycle-extensions",
+ ],
+
+ privileged: true,
+
+ required: ["libbluetooth"],
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.bluetooth.updatable",
+ ],
+
+}
diff --git a/android/leaudio/CleanSpec.mk b/android/leaudio/CleanSpec.mk
new file mode 100644
index 0000000000..9462230a44
--- /dev/null
+++ b/android/leaudio/CleanSpec.mk
@@ -0,0 +1,51 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# If you don't need to do a full clean build but would like to touch
+# a file or delete some intermediate files, add a clean step to the end
+# of the list. These steps will only be run once, if they haven't been
+# run before.
+#
+# E.g.:
+# $(call add-clean-step, touch -c external/sqlite/sqlite3.h)
+# $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates)
+#
+# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with
+# files that are missing or have been moved.
+#
+# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory.
+# Use $(OUT_DIR) to refer to the "out" directory.
+#
+# If you need to re-do something that's already mentioned, just copy
+# the command and add it to the bottom of the list. E.g., if a change
+# that you made last week required touching a file and a change you
+# made today requires touching the same file, just copy the old
+# touch step and add it to the end of the list.
+#
+# ************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
+# ************************************************
+
+# For example:
+#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/AndroidTests_intermediates)
+#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/core_intermediates)
+#$(call add-clean-step, find $(OUT_DIR) -type f -name "IGTalkSession*" -print0 | xargs -0 rm -f)
+#$(call add-clean-step, rm -rf $(PRODUCT_OUT)/data/*)
+
+$(call add-clean-step, rm -rf $(PRODUCT_OUT)/system/app/LeAudio)
+
+# ************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
+# ************************************************
diff --git a/android/leaudio/app/.classpath b/android/leaudio/app/.classpath
new file mode 100644
index 0000000000..4a04201ca2
--- /dev/null
+++ b/android/leaudio/app/.classpath
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
+ <classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
+ <classpathentry kind="output" path="bin/default"/>
+</classpath>
diff --git a/android/leaudio/app/.gitignore b/android/leaudio/app/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/android/leaudio/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/android/leaudio/app/.project b/android/leaudio/app/.project
new file mode 100644
index 0000000000..4cb8db990d
--- /dev/null
+++ b/android/leaudio/app/.project
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>app</name>
+ <comment>Project app created by Buildship.</comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
+ </natures>
+ <filteredResources>
+ <filter>
+ <id>1645602707778</id>
+ <name></name>
+ <type>30</type>
+ <matcher>
+ <id>org.eclipse.core.resources.regexFilterMatcher</id>
+ <arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
+ </matcher>
+ </filter>
+ </filteredResources>
+</projectDescription>
diff --git a/android/leaudio/app/.settings/org.eclipse.buildship.core.prefs b/android/leaudio/app/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000000..b1886adb46
--- /dev/null
+++ b/android/leaudio/app/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,2 @@
+connection.project.dir=..
+eclipse.preferences.version=1
diff --git a/android/leaudio/app/build.gradle b/android/leaudio/app/build.gradle
new file mode 100644
index 0000000000..f6f894d84e
--- /dev/null
+++ b/android/leaudio/app/build.gradle
@@ -0,0 +1,36 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion = 'android-S'
+ defaultConfig {
+ applicationId "com.android.bluetooth.leaudio"
+ minSdkVersion "S"
+ targetSdkVersion "S"
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility = '1.8'
+ targetCompatibility = '1.8'
+ }
+ buildToolsVersion = '29.0.3'
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
+ implementation 'com.google.android.material:material:1.2.1'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test:runner:1.3.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+}
diff --git a/android/leaudio/app/proguard-rules.pro b/android/leaudio/app/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/android/leaudio/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/android/leaudio/app/src/androidTest/java/com/android/bluetooth/leaudio/ExampleInstrumentedTest.java b/android/leaudio/app/src/androidTest/java/com/android/bluetooth/leaudio/ExampleInstrumentedTest.java
new file mode 100644
index 0000000000..9011eb4687
--- /dev/null
+++ b/android/leaudio/app/src/androidTest/java/com/android/bluetooth/leaudio/ExampleInstrumentedTest.java
@@ -0,0 +1,27 @@
+package com.android.bluetooth.leaudio;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("com.android.bluetooth.leaudio", appContext.getPackageName());
+ }
+}
diff --git a/android/leaudio/app/src/main/AndroidManifest.xml b/android/leaudio/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..996dcee9ff
--- /dev/null
+++ b/android/leaudio/app/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.bluetooth.leaudio">
+
+ <uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
+
+ <uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+ <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+ <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
+ <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme">
+ <activity
+ android:name="com.android.bluetooth.leaudio.MainActivity"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme.NoActionBar"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name="com.android.bluetooth.leaudio.BroadcasterActivity"
+ android:excludeFromRecents="true"
+ android:theme="@style/AppTheme.NoActionBar">
+ </activity>
+ </application>
+
+</manifest>
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java
new file mode 100644
index 0000000000..7f2dc173e5
--- /dev/null
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BluetoothProxy.java
@@ -0,0 +1,1040 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.leaudio;
+
+import android.app.Application;
+import android.bluetooth.*;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import androidx.core.util.Pair;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+import com.android.bluetooth.leaudio.R;
+
+public class BluetoothProxy {
+ private static BluetoothProxy INSTANCE;
+ private final Application application;
+ private final BluetoothAdapter bluetoothAdapter;
+ private BluetoothLeAudio bluetoothLeAudio = null;
+ private BluetoothLeBroadcast mBluetoothLeBroadcast = null;
+ private BluetoothCsipSetCoordinator bluetoothCsis = null;
+ private BluetoothVolumeControl bluetoothVolumeControl = null;
+ private BluetoothHapClient bluetoothHapClient = null;
+ private BluetoothProfile.ServiceListener profileListener = null;
+ private BluetoothHapClient.Callback hapCallback = null;
+ private final IntentFilter adapterIntentFilter;
+ private IntentFilter intentFilter;
+ private final ExecutorService mExecutor;
+
+ private final Map<Integer, UUID> mGroupLocks = new HashMap<>();
+
+ private final MutableLiveData<Boolean> enabledBluetoothMutable;
+ private final BroadcastReceiver adapterIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
+ int toState =
+ intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
+ if (toState == BluetoothAdapter.STATE_ON) {
+ enabledBluetoothMutable.setValue(true);
+ } else if (toState == BluetoothAdapter.STATE_OFF) {
+ enabledBluetoothMutable.setValue(false);
+ }
+ }
+ }
+ };
+ private final MutableLiveData<List<LeAudioDeviceStateWrapper>> allLeAudioDevicesMutable;
+ private final BroadcastReceiver leAudioIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+
+ if (allLeAudioDevicesMutable.getValue() != null) {
+ if (device != null) {
+ Optional<LeAudioDeviceStateWrapper> valid_device_opt = allLeAudioDevicesMutable
+ .getValue().stream()
+ .filter(state -> state.device.getAddress().equals(device.getAddress()))
+ .findAny();
+
+ if (valid_device_opt.isPresent()) {
+ LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
+ LeAudioDeviceStateWrapper.LeAudioData svc_data = valid_device.leAudioData;
+ int group_id;
+
+ // Handle Le Audio actions
+ switch (action) {
+ case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED: {
+ final int toState =
+ intent.getIntExtra(BluetoothLeAudio.EXTRA_STATE, -1);
+ if (toState == BluetoothLeAudio.STATE_CONNECTED
+ || toState == BluetoothLeAudio.STATE_DISCONNECTED)
+ svc_data.isConnectedMutable
+ .postValue(toState == BluetoothLeAudio.STATE_CONNECTED);
+
+ group_id = bluetoothLeAudio.getGroupId(device);
+ svc_data.nodeStatusMutable.setValue(
+ new Pair<>(group_id, BluetoothLeAudio.GROUP_NODE_ADDED));
+ svc_data.groupStatusMutable
+ .setValue(new Pair<>(group_id, new Pair<>(-1, -1)));
+ break;
+ }
+ case BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_NODE_STATUS_CHANGED:
+ group_id =
+ intent.getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_ID,
+ BluetoothLeAudio.GROUP_ID_INVALID);
+ final int node_status = intent.getIntExtra(
+ BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_NODE_STATUS, -1);
+ svc_data.nodeStatusMutable
+ .setValue(new Pair<>(group_id, node_status));
+ svc_data.groupStatusMutable
+ .setValue(new Pair<>(group_id, new Pair<>(-1, -1)));
+ break;
+ }
+ }
+ } else {
+ final int group_id =
+ intent.getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_ID,
+ BluetoothLeAudio.GROUP_ID_INVALID);
+ final int group_status =
+ intent.getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_STATUS, -1);
+
+ List<LeAudioDeviceStateWrapper> valid_devices = null;
+
+ if (group_id != BluetoothLeAudio.GROUP_ID_INVALID)
+ valid_devices = allLeAudioDevicesMutable.getValue().stream().filter(
+ state -> state.leAudioData.nodeStatusMutable.getValue() != null
+ && state.leAudioData.nodeStatusMutable.getValue().first
+ .equals(group_id))
+ .collect(Collectors.toList());
+
+ if (valid_devices != null) {
+ switch (action) {
+ case BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_STATUS_CHANGED:
+ for (LeAudioDeviceStateWrapper dev : valid_devices) {
+ dev.leAudioData.groupStatusMutable.setValue(
+ new Pair<>(group_id, new Pair<>(group_status, 0)));
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ };
+
+ private final BroadcastReceiver hapClientIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+
+ if (allLeAudioDevicesMutable.getValue() != null) {
+ if (device != null) {
+ Optional<LeAudioDeviceStateWrapper> valid_device_opt = allLeAudioDevicesMutable
+ .getValue().stream()
+ .filter(state -> state.device.getAddress().equals(device.getAddress()))
+ .findAny();
+
+ if (valid_device_opt.isPresent()) {
+ LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
+ LeAudioDeviceStateWrapper.HapData svc_data = valid_device.hapData;
+
+ switch (action) {
+ case BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED: {
+ final int toState =
+ intent.getIntExtra(BluetoothHapClient.EXTRA_STATE, -1);
+ svc_data.hapStateMutable.postValue(toState);
+ break;
+ }
+ // Hidden API
+ case "android.bluetooth.action.HAP_DEVICE_AVAILABLE": {
+ final int features = intent
+ .getIntExtra("android.bluetooth.extra.HAP_FEATURES", -1);
+ svc_data.hapFeaturesMutable.postValue(features);
+ break;
+ }
+ default:
+ // Do nothing
+ break;
+ }
+ }
+ }
+ }
+ }
+ };
+
+ private final BroadcastReceiver volumeControlIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+
+ if (allLeAudioDevicesMutable.getValue() != null) {
+ if (device != null) {
+ Optional<LeAudioDeviceStateWrapper> valid_device_opt = allLeAudioDevicesMutable
+ .getValue().stream()
+ .filter(state -> state.device.getAddress().equals(device.getAddress()))
+ .findAny();
+
+ if (valid_device_opt.isPresent()) {
+ LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
+ LeAudioDeviceStateWrapper.VolumeControlData svc_data =
+ valid_device.volumeControlData;
+
+ switch (action) {
+ case BluetoothVolumeControl.ACTION_CONNECTION_STATE_CHANGED:
+ final int toState =
+ intent.getIntExtra(BluetoothVolumeControl.EXTRA_STATE, -1);
+ if (toState == BluetoothVolumeControl.STATE_CONNECTED
+ || toState == BluetoothVolumeControl.STATE_DISCONNECTED)
+ svc_data.isConnectedMutable.postValue(
+ toState == BluetoothVolumeControl.STATE_CONNECTED);
+ break;
+ }
+ }
+ }
+ }
+ }
+ };
+ private final MutableLiveData<BluetoothLeBroadcastMetadata> mBroadcastUpdateMutableLive;
+ private final MutableLiveData<Pair<Integer /* reason */, Integer /* broadcastId */>> mBroadcastPlaybackStartedMutableLive;
+ private final MutableLiveData<Pair<Integer /* reason */, Integer /* broadcastId */>> mBroadcastPlaybackStoppedMutableLive;
+ private final MutableLiveData<Integer /* broadcastId */> mBroadcastAddedMutableLive;
+ private final MutableLiveData<Pair<Integer /* reason */, Integer /* broadcastId */>> mBroadcastRemovedMutableLive;
+ private final MutableLiveData<String> mBroadcastStatusMutableLive;
+ private final BluetoothLeBroadcast.Callback mBroadcasterCallback =
+ new BluetoothLeBroadcast.Callback() {
+ @Override
+ public void onBroadcastStarted(int reason, int broadcastId) {
+ if ((reason != BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST)
+ && (reason != BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST)) {
+ mBroadcastStatusMutableLive.postValue("Unable to create broadcast: "
+ + broadcastId + ", reason: " + reason);
+ }
+
+ mBroadcastAddedMutableLive.postValue(broadcastId);
+ }
+
+ @Override
+ public void onBroadcastStartFailed(int reason) {
+ mBroadcastStatusMutableLive
+ .postValue("Unable to START broadcast due to reason: " + reason);
+ }
+
+ @Override
+ public void onBroadcastStopped(int reason, int broadcastId) {
+ mBroadcastRemovedMutableLive.postValue(new Pair<>(reason, broadcastId));
+ }
+
+ @Override
+ public void onBroadcastStopFailed(int reason) {
+ mBroadcastStatusMutableLive
+ .postValue("Unable to STOP broadcast due to reason: " + reason);
+ }
+
+ @Override
+ public void onPlaybackStarted(int reason, int broadcastId) {
+ mBroadcastPlaybackStartedMutableLive.postValue(new Pair<>(reason, broadcastId));
+ }
+
+ @Override
+ public void onPlaybackStopped(int reason, int broadcastId) {
+ mBroadcastPlaybackStoppedMutableLive.postValue(new Pair<>(reason, broadcastId));
+ }
+
+ @Override
+ public void onBroadcastUpdated(int reason, int broadcastId) {
+ mBroadcastStatusMutableLive.postValue("Broadcast " + broadcastId
+ + "has been updated due to reason: " + reason);
+ }
+
+ @Override
+ public void onBroadcastUpdateFailed(int reason, int broadcastId) {
+ mBroadcastStatusMutableLive.postValue("Unable to UPDATE broadcast "
+ + broadcastId + " due to reason: " + reason);
+ }
+
+ @Override
+ public void onBroadcastMetadataChanged(int broadcastId,
+ BluetoothLeBroadcastMetadata metadata) {
+ mBroadcastUpdateMutableLive.postValue(metadata);
+ }
+ };
+
+ private BluetoothProxy(Application application) {
+ this.application = application;
+ bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ enabledBluetoothMutable = new MutableLiveData<>();
+ allLeAudioDevicesMutable = new MutableLiveData<>();
+
+ mBroadcastUpdateMutableLive = new MutableLiveData<>();
+ mBroadcastStatusMutableLive = new MutableLiveData<>();
+
+ mBroadcastPlaybackStartedMutableLive = new MutableLiveData<>();
+ mBroadcastPlaybackStoppedMutableLive = new MutableLiveData<>();
+ mBroadcastAddedMutableLive = new MutableLiveData();
+ mBroadcastRemovedMutableLive = new MutableLiveData<>();
+
+ MutableLiveData<String> mBroadcastStatusMutableLive;
+
+ mExecutor = Executors.newSingleThreadExecutor();
+
+ adapterIntentFilter = new IntentFilter();
+ adapterIntentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+ application.registerReceiver(adapterIntentReceiver, adapterIntentFilter);
+ }
+
+ // Lazy constructing Singleton acquire method
+ public static BluetoothProxy getBluetoothProxy(Application application) {
+ if (INSTANCE == null) {
+ INSTANCE = new BluetoothProxy(application);
+ }
+ return (INSTANCE);
+ }
+
+ public void initProfiles() {
+ if (profileListener != null) return;
+
+ hapCallback = new BluetoothHapClient.Callback() {
+ @Override
+ public void onPresetSelected(BluetoothDevice device, int presetIndex, int statusCode) {
+ Optional<LeAudioDeviceStateWrapper> valid_device_opt = allLeAudioDevicesMutable
+ .getValue().stream()
+ .filter(state -> state.device.getAddress().equals(device.getAddress()))
+ .findAny();
+
+ if (!valid_device_opt.isPresent())
+ return;
+
+ LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
+ LeAudioDeviceStateWrapper.HapData svc_data = valid_device.hapData;
+
+ svc_data.hapActivePresetIndexMutable.postValue(presetIndex);
+
+ svc_data.hapStatusMutable
+ .postValue("Preset changed to " + presetIndex + ", reason: " + statusCode);
+ }
+
+ @Override
+ public void onPresetSelectionFailed(BluetoothDevice device, int statusCode) {
+ Optional<LeAudioDeviceStateWrapper> valid_device_opt = allLeAudioDevicesMutable
+ .getValue().stream()
+ .filter(state -> state.device.getAddress().equals(device.getAddress()))
+ .findAny();
+
+ if (!valid_device_opt.isPresent())
+ return;
+
+ LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
+ LeAudioDeviceStateWrapper.HapData svc_data = valid_device.hapData;
+
+ svc_data.hapStatusMutable
+ .postValue("Select preset failed with status " + statusCode);
+ }
+
+ @Override
+ public void onPresetSelectionForGroupFailed(int hapGroupId, int statusCode) {
+ List<LeAudioDeviceStateWrapper> valid_devices = null;
+ if (hapGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID)
+ valid_devices = allLeAudioDevicesMutable.getValue().stream()
+ .filter(state -> state.leAudioData.nodeStatusMutable.getValue() != null
+ && state.leAudioData.nodeStatusMutable.getValue().first
+ .equals(hapGroupId))
+ .collect(Collectors.toList());
+
+ if (valid_devices != null) {
+ for (LeAudioDeviceStateWrapper device : valid_devices) {
+ device.hapData.hapStatusMutable.postValue("Select preset for group "
+ + hapGroupId + " failed with status " + statusCode);
+ }
+ }
+ }
+
+ @Override
+ public void onPresetInfoChanged(BluetoothDevice device,
+ List<BluetoothHapPresetInfo> presetInfoList, int statusCode) {
+ Optional<LeAudioDeviceStateWrapper> valid_device_opt = allLeAudioDevicesMutable
+ .getValue().stream()
+ .filter(state -> state.device.getAddress().equals(device.getAddress()))
+ .findAny();
+
+ if (!valid_device_opt.isPresent())
+ return;
+
+ LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
+ LeAudioDeviceStateWrapper.HapData svc_data = valid_device.hapData;
+
+ svc_data.hapStatusMutable
+ .postValue("Preset list changed due to status " + statusCode);
+ svc_data.hapPresetsMutable.postValue(presetInfoList);
+ }
+
+ @Override
+ public void onSetPresetNameFailed(BluetoothDevice device, int status) {
+ Optional<LeAudioDeviceStateWrapper> valid_device_opt = allLeAudioDevicesMutable
+ .getValue().stream()
+ .filter(state -> state.device.getAddress().equals(device.getAddress()))
+ .findAny();
+
+ if (!valid_device_opt.isPresent())
+ return;
+
+ LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
+ LeAudioDeviceStateWrapper.HapData svc_data = valid_device.hapData;
+
+ svc_data.hapStatusMutable.postValue("Name set error: " + status);
+ }
+
+ @Override
+ public void onSetPresetNameForGroupFailed(int hapGroupId, int status) {
+ List<LeAudioDeviceStateWrapper> valid_devices = null;
+ if (hapGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID)
+ valid_devices = allLeAudioDevicesMutable.getValue().stream()
+ .filter(state -> state.leAudioData.nodeStatusMutable.getValue() != null
+ && state.leAudioData.nodeStatusMutable.getValue().first
+ .equals(hapGroupId))
+ .collect(Collectors.toList());
+
+ if (valid_devices != null) {
+ for (LeAudioDeviceStateWrapper device : valid_devices) {
+ device.hapData.hapStatusMutable
+ .postValue("Group Name set error: " + status);
+ }
+ }
+ }
+ };
+
+ profileListener = new BluetoothProfile.ServiceListener() {
+ @Override
+ public void onServiceConnected(int i, BluetoothProfile bluetoothProfile) {
+ switch (i) {
+ case BluetoothProfile.CSIP_SET_COORDINATOR:
+ bluetoothCsis = (BluetoothCsipSetCoordinator) bluetoothProfile;
+ break;
+ case BluetoothProfile.LE_AUDIO:
+ bluetoothLeAudio = (BluetoothLeAudio) bluetoothProfile;
+ break;
+ case BluetoothProfile.VOLUME_CONTROL:
+ bluetoothVolumeControl = (BluetoothVolumeControl) bluetoothProfile;
+ break;
+ case BluetoothProfile.HAP_CLIENT:
+ bluetoothHapClient = (BluetoothHapClient) bluetoothProfile;
+ bluetoothHapClient.registerCallback(mExecutor, hapCallback);
+ break;
+ case BluetoothProfile.LE_AUDIO_BROADCAST:
+ mBluetoothLeBroadcast = (BluetoothLeBroadcast) bluetoothProfile;
+ mBluetoothLeBroadcast.registerCallback(mExecutor, mBroadcasterCallback);
+ break;
+ }
+ queryLeAudioDevices();
+ }
+
+ @Override
+ public void onServiceDisconnected(int i) {}
+ };
+
+ initCsisProxy();
+ initLeAudioProxy();
+ initVolumeControlProxy();
+ initHapProxy();
+ initLeAudioBroadcastProxy();
+ }
+
+ public void cleanupProfiles() {
+ if (profileListener == null) return;
+
+ cleanupCsisProxy();
+ cleanupLeAudioProxy();
+ cleanupVolumeControlProxy();
+ cleanupHapProxy();
+ cleanupLeAudioBroadcastProxy();
+
+ profileListener = null;
+ }
+
+ private void initCsisProxy() {
+ if (bluetoothCsis == null) {
+ bluetoothAdapter.getProfileProxy(this.application, profileListener,
+ BluetoothProfile.CSIP_SET_COORDINATOR);
+ }
+ }
+
+ private void cleanupCsisProxy() {
+ if (bluetoothCsis != null) {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.LE_AUDIO, bluetoothCsis);
+ }
+ }
+
+ private void initLeAudioProxy() {
+ if (bluetoothLeAudio == null) {
+ bluetoothAdapter.getProfileProxy(this.application, profileListener,
+ BluetoothProfile.LE_AUDIO);
+ }
+
+ intentFilter = new IntentFilter();
+ intentFilter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
+ intentFilter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_NODE_STATUS_CHANGED);
+ intentFilter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_STATUS_CHANGED);
+ application.registerReceiver(leAudioIntentReceiver, intentFilter);
+ }
+
+ private void cleanupLeAudioProxy() {
+ if (bluetoothLeAudio != null) {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.LE_AUDIO, bluetoothLeAudio);
+ application.unregisterReceiver(leAudioIntentReceiver);
+ }
+ }
+
+ private void initVolumeControlProxy() {
+ bluetoothAdapter.getProfileProxy(this.application, profileListener,
+ BluetoothProfile.VOLUME_CONTROL);
+
+ intentFilter = new IntentFilter();
+ intentFilter.addAction(BluetoothVolumeControl.ACTION_CONNECTION_STATE_CHANGED);
+ application.registerReceiver(volumeControlIntentReceiver, intentFilter);
+ }
+
+ private void cleanupVolumeControlProxy() {
+ if (bluetoothVolumeControl != null) {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.VOLUME_CONTROL,
+ bluetoothVolumeControl);
+ application.unregisterReceiver(volumeControlIntentReceiver);
+ }
+ }
+
+ private void initHapProxy() {
+ bluetoothAdapter.getProfileProxy(this.application, profileListener,
+ BluetoothProfile.HAP_CLIENT);
+
+ intentFilter = new IntentFilter();
+ intentFilter.addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
+ intentFilter.addAction("android.bluetooth.action.HAP_DEVICE_AVAILABLE");
+ application.registerReceiver(hapClientIntentReceiver, intentFilter);
+ }
+
+ private void cleanupHapProxy() {
+ if (bluetoothHapClient != null) {
+ bluetoothHapClient.unregisterCallback(hapCallback);
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.HAP_CLIENT, bluetoothHapClient);
+ application.unregisterReceiver(hapClientIntentReceiver);
+ }
+ }
+
+ private Boolean checkForEnabledBluetooth() {
+ Boolean current_state = bluetoothAdapter.isEnabled();
+
+ // Force the update since event may not come if bt was already enabled
+ if (enabledBluetoothMutable.getValue() != current_state)
+ enabledBluetoothMutable.setValue(current_state);
+
+ return current_state;
+ }
+
+ public void queryLeAudioDevices() {
+ if (checkForEnabledBluetooth()) {
+ // Consider those with the ASC service as valid devices
+ List<LeAudioDeviceStateWrapper> validDevices = new ArrayList<>();
+ for (BluetoothDevice dev : bluetoothAdapter.getBondedDevices()) {
+ LeAudioDeviceStateWrapper state_wrapper = new LeAudioDeviceStateWrapper(dev);
+ Boolean valid_device = false;
+
+ if (Arrays.asList(dev.getUuids() != null ? dev.getUuids() : new ParcelUuid[0])
+ .contains(ParcelUuid
+ .fromString(application.getString(R.string.svc_uuid_le_audio)))) {
+ if (state_wrapper.leAudioData == null)
+ state_wrapper.leAudioData = new LeAudioDeviceStateWrapper.LeAudioData();
+ valid_device = true;
+
+ if (bluetoothLeAudio != null) {
+ state_wrapper.leAudioData.isConnectedMutable.postValue(bluetoothLeAudio
+ .getConnectionState(dev) == BluetoothLeAudio.STATE_CONNECTED);
+ int group_id = bluetoothLeAudio.getGroupId(dev);
+ state_wrapper.leAudioData.nodeStatusMutable
+ .setValue(new Pair<>(group_id, BluetoothLeAudio.GROUP_NODE_ADDED));
+ state_wrapper.leAudioData.groupStatusMutable
+ .setValue(new Pair<>(group_id, new Pair<>(-1, -1)));
+ }
+ }
+
+ if (Arrays.asList(dev.getUuids() != null ? dev.getUuids() : new ParcelUuid[0])
+ .contains(ParcelUuid.fromString(
+ application.getString(R.string.svc_uuid_volume_control)))) {
+ if (state_wrapper.volumeControlData == null)
+ state_wrapper.volumeControlData =
+ new LeAudioDeviceStateWrapper.VolumeControlData();
+ valid_device = true;
+
+ if (bluetoothVolumeControl != null) {
+ state_wrapper.volumeControlData.isConnectedMutable
+ .postValue(bluetoothVolumeControl.getConnectionState(
+ dev) == BluetoothVolumeControl.STATE_CONNECTED);
+ // FIXME: We don't have the api to get the volume and mute states? :(
+ }
+ }
+
+ if (Arrays.asList(dev.getUuids() != null ? dev.getUuids() : new ParcelUuid[0])
+ .contains(ParcelUuid
+ .fromString(application.getString(R.string.svc_uuid_has)))) {
+ if (state_wrapper.hapData == null)
+ state_wrapper.hapData = new LeAudioDeviceStateWrapper.HapData();
+ valid_device = true;
+
+ if (bluetoothHapClient != null) {
+ state_wrapper.hapData.hapStateMutable
+ .postValue(bluetoothHapClient.getConnectionState(dev));
+ boolean is_connected = bluetoothHapClient
+ .getConnectionState(dev) == BluetoothHapClient.STATE_CONNECTED;
+ if (is_connected) {
+ // Use hidden API
+ try {
+ Method getFeaturesMethod = BluetoothHapClient.class
+ .getDeclaredMethod("getFeatures", BluetoothDevice.class);
+ getFeaturesMethod.setAccessible(true);
+ state_wrapper.hapData.hapFeaturesMutable
+ .postValue((Integer) getFeaturesMethod
+ .invoke(bluetoothHapClient, dev));
+ } catch (NoSuchMethodException | IllegalAccessException
+ | InvocationTargetException e) {
+ state_wrapper.hapData.hapStatusMutable
+ .postValue("Hidden API for getFeatures not accessible.");
+ }
+
+ state_wrapper.hapData.hapPresetsMutable
+ .postValue(bluetoothHapClient.getAllPresetInfo(dev));
+ try {
+ Method getActivePresetIndexMethod =
+ BluetoothHapClient.class.getDeclaredMethod(
+ "getActivePresetIndex", BluetoothDevice.class);
+ getActivePresetIndexMethod.setAccessible(true);
+ state_wrapper.hapData.hapActivePresetIndexMutable
+ .postValue((Integer) getActivePresetIndexMethod
+ .invoke(bluetoothHapClient, dev));
+ } catch (NoSuchMethodException | IllegalAccessException
+ | InvocationTargetException e) {
+ state_wrapper.hapData.hapStatusMutable
+ .postValue("Hidden API for getFeatures not accessible.");
+ }
+ }
+ }
+ }
+
+ if (valid_device) validDevices.add(state_wrapper);
+ }
+
+ // Async update
+ allLeAudioDevicesMutable.postValue(validDevices);
+ }
+ }
+
+ public void connectLeAudio(BluetoothDevice device, boolean connect) {
+ if (bluetoothLeAudio != null) {
+ if (connect) {
+ try {
+ Method connectMethod = BluetoothLeAudio.class.getDeclaredMethod("connect",
+ BluetoothDevice.class);
+ connectMethod.setAccessible(true);
+ connectMethod.invoke(bluetoothLeAudio, device);
+ } catch (NoSuchMethodException | IllegalAccessException
+ | InvocationTargetException e) {
+ // Do nothing
+ }
+ } else {
+ try {
+ Method disconnectMethod = BluetoothLeAudio.class.getDeclaredMethod("disconnect",
+ BluetoothDevice.class);
+ disconnectMethod.setAccessible(true);
+ disconnectMethod.invoke(bluetoothLeAudio, device);
+ } catch (NoSuchMethodException | IllegalAccessException
+ | InvocationTargetException e) {
+ // Do nothing
+ }
+ }
+ }
+ }
+
+ public void streamAction(Integer group_id, int action, Integer content_type) {
+ if (bluetoothLeAudio != null) {
+ switch (action) {
+ case 0:
+ // No longer available, not needed
+ // bluetoothLeAudio.groupStream(group_id, content_type);
+ break;
+ case 1:
+ // No longer available, not needed
+ // bluetoothLeAudio.groupSuspend(group_id);
+ break;
+ case 2:
+ // No longer available, not needed
+ // bluetoothLeAudio.groupStop(group_id);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ public void groupSet(BluetoothDevice device, Integer group_id) {
+ if (bluetoothLeAudio == null) return;
+
+ try {
+ Method groupAddNodeMethod = BluetoothLeAudio.class.getDeclaredMethod("groupAddNode",
+ Integer.class, BluetoothDevice.class);
+ groupAddNodeMethod.setAccessible(true);
+ groupAddNodeMethod.invoke(bluetoothLeAudio, group_id, device);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ // Do nothing
+ }
+ }
+
+ public void groupUnset(BluetoothDevice device, Integer group_id) {
+ if (bluetoothLeAudio == null) return;
+
+ try {
+ Method groupRemoveNodeMethod = BluetoothLeAudio.class
+ .getDeclaredMethod("groupRemoveNode", Integer.class, BluetoothDevice.class);
+ groupRemoveNodeMethod.setAccessible(true);
+ groupRemoveNodeMethod.invoke(bluetoothLeAudio, group_id, device);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ // Do nothing
+ }
+ }
+
+ public void groupSetLock(Integer group_id, boolean lock) {
+ Log.d("Lock", "lock: " + lock);
+ if (lock) {
+ if (mGroupLocks.containsKey(group_id)) return;
+
+ UUID uuid = bluetoothCsis.lockGroup(group_id, mExecutor,
+ (int group, int op_status, boolean is_locked) -> {
+ Log.d("LockCb", "lock: " + is_locked + " status: " + op_status);
+ if (((op_status == BluetoothStatusCodes.SUCCESS)
+ || (op_status == BluetoothStatusCodes.ERROR_CSIP_LOCKED_GROUP_MEMBER_LOST))
+ && (group != BluetoothLeAudio.GROUP_ID_INVALID)) {
+ allLeAudioDevicesMutable.getValue().forEach((dev_wrapper) -> {
+ if (dev_wrapper.leAudioData.nodeStatusMutable.getValue() != null
+ && dev_wrapper.leAudioData.nodeStatusMutable
+ .getValue().first.equals(group_id)) {
+ dev_wrapper.leAudioData.groupLockStateMutable.postValue(
+ new Pair<Integer, Boolean>(group, is_locked));
+ }
+ });
+ } else {
+ // TODO: Set error status so it could be notified/toasted to the
+ // user
+ }
+
+ if (!is_locked)
+ mGroupLocks.remove(group_id);
+ });
+ // Store the lock key
+ mGroupLocks.put(group_id, uuid);
+ } else {
+ if (!mGroupLocks.containsKey(group_id)) return;
+
+ // Use the stored lock key
+ bluetoothCsis.unlockGroup(mGroupLocks.get(group_id));
+ mGroupLocks.remove(group_id);
+ }
+ }
+
+ public void setVolume(BluetoothDevice device, int volume) {
+ if (bluetoothLeAudio != null) {
+ bluetoothLeAudio.setVolume(volume);
+ }
+ }
+
+ public LiveData<Boolean> getBluetoothEnabled() {
+ return enabledBluetoothMutable;
+ }
+
+ public LiveData<List<LeAudioDeviceStateWrapper>> getAllLeAudioDevices() {
+ return allLeAudioDevicesMutable;
+ }
+
+ public void connectHap(BluetoothDevice device, boolean connect) {
+ if (bluetoothHapClient != null) {
+ if (connect) {
+ bluetoothHapClient.setConnectionPolicy(device,
+ BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ } else {
+ bluetoothHapClient.setConnectionPolicy(device,
+ BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+ }
+ }
+ }
+
+ public boolean hapReadPresetInfo(BluetoothDevice device, int preset_index) {
+ if (bluetoothHapClient == null)
+ return false;
+
+ BluetoothHapPresetInfo new_preset = null;
+
+ // Use hidden API
+ try {
+ Method getPresetInfoMethod = BluetoothHapClient.class.getDeclaredMethod("getPresetInfo",
+ BluetoothDevice.class, Integer.class);
+ getPresetInfoMethod.setAccessible(true);
+
+ new_preset = (BluetoothHapPresetInfo) getPresetInfoMethod.invoke(bluetoothHapClient,
+ device, preset_index);
+ if (new_preset == null)
+ return false;
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ // Do nothing'
+ return false;
+ }
+
+ Optional<LeAudioDeviceStateWrapper> valid_device_opt = allLeAudioDevicesMutable.getValue()
+ .stream().filter(state -> state.device.getAddress().equals(device.getAddress()))
+ .findAny();
+
+ if (!valid_device_opt.isPresent())
+ return false;
+
+ LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
+ LeAudioDeviceStateWrapper.HapData svc_data = valid_device.hapData;
+
+ List current_presets = svc_data.hapPresetsMutable.getValue();
+ if (current_presets == null)
+ current_presets = new ArrayList<BluetoothHapPresetInfo>();
+
+ // Remove old one and add back the new one
+ ListIterator<BluetoothHapPresetInfo> iter = current_presets.listIterator();
+ while (iter.hasNext()) {
+ if (iter.next().getIndex() == new_preset.getIndex()) {
+ iter.remove();
+ }
+ }
+ current_presets.add(new_preset);
+
+ svc_data.hapPresetsMutable.postValue(current_presets);
+ return true;
+ }
+
+ public boolean hapSetActivePreset(BluetoothDevice device, int preset_index) {
+ if (bluetoothHapClient == null)
+ return false;
+
+ bluetoothHapClient.selectPreset(device, preset_index);
+ return true;
+ }
+
+ public boolean hapChangePresetName(BluetoothDevice device, int preset_index, String name) {
+ if (bluetoothHapClient == null)
+ return false;
+
+ bluetoothHapClient.setPresetName(device, preset_index, name);
+ return true;
+ }
+
+ public boolean hapPreviousDevicePreset(BluetoothDevice device) {
+ if (bluetoothHapClient == null)
+ return false;
+
+ // Use hidden API
+ try {
+ Method switchToPreviousPresetMethod = BluetoothHapClient.class
+ .getDeclaredMethod("switchToPreviousPreset", BluetoothDevice.class);
+ switchToPreviousPresetMethod.setAccessible(true);
+
+ switchToPreviousPresetMethod.invoke(bluetoothHapClient, device);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ // Do nothing
+ }
+ return true;
+ }
+
+ public boolean hapNextDevicePreset(BluetoothDevice device) {
+ if (bluetoothHapClient == null)
+ return false;
+
+ // Use hidden API
+ try {
+ Method switchToNextPresetMethod = BluetoothHapClient.class
+ .getDeclaredMethod("switchToNextPreset", BluetoothDevice.class);
+ switchToNextPresetMethod.setAccessible(true);
+
+ switchToNextPresetMethod.invoke(bluetoothHapClient, device);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ // Do nothing
+ }
+ return true;
+ }
+
+ public boolean hapPreviousGroupPreset(int group_id) {
+ if (bluetoothHapClient == null)
+ return false;
+
+ // Use hidden API
+ try {
+ Method switchToPreviousPresetForGroupMethod = BluetoothHapClient.class
+ .getDeclaredMethod("switchToPreviousPresetForGroup", Integer.class);
+ switchToPreviousPresetForGroupMethod.setAccessible(true);
+
+ switchToPreviousPresetForGroupMethod.invoke(bluetoothHapClient, group_id);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ // Do nothing
+ }
+ return true;
+ }
+
+ public boolean hapNextGroupPreset(int group_id) {
+ if (bluetoothHapClient == null)
+ return false;
+
+ // Use hidden API
+ try {
+ Method switchToNextPresetForGroupMethod = BluetoothHapClient.class
+ .getDeclaredMethod("switchToNextPresetForGroup", Integer.class);
+ switchToNextPresetForGroupMethod.setAccessible(true);
+
+ switchToNextPresetForGroupMethod.invoke(bluetoothHapClient, group_id);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ // Do nothing
+ }
+ return true;
+ }
+
+ public int hapGetHapGroup(BluetoothDevice device) {
+ if (bluetoothHapClient == null)
+ return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+
+ // Use hidden API
+ try {
+ Method getHapGroupMethod = BluetoothHapClient.class.getDeclaredMethod("getHapGroup",
+ BluetoothDevice.class);
+ getHapGroupMethod.setAccessible(true);
+
+ return (Integer) getHapGroupMethod.invoke(bluetoothHapClient, device);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ // Do nothing
+ }
+ return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+ }
+
+ private void initLeAudioBroadcastProxy() {
+ if (mBluetoothLeBroadcast == null) {
+ bluetoothAdapter.getProfileProxy(this.application, profileListener,
+ BluetoothProfile.LE_AUDIO_BROADCAST);
+ }
+ }
+
+ private void cleanupLeAudioBroadcastProxy() {
+ if (mBluetoothLeBroadcast != null) {
+ bluetoothAdapter.closeProfileProxy(BluetoothProfile.LE_AUDIO_BROADCAST,
+ mBluetoothLeBroadcast);
+ }
+ }
+
+ public LiveData<BluetoothLeBroadcastMetadata> getBroadcastUpdateMetadataLive() {
+ return mBroadcastUpdateMutableLive;
+ }
+
+ public LiveData<Pair<Integer /* reason */, Integer /* broadcastId */>> getBroadcastPlaybackStartedMutableLive() {
+ return mBroadcastPlaybackStartedMutableLive;
+ }
+
+ public LiveData<Pair<Integer /* reason */, Integer /* broadcastId */>> getBroadcastPlaybackStoppedMutableLive() {
+ return mBroadcastPlaybackStoppedMutableLive;
+ }
+
+ public LiveData<Integer /* broadcastId */> getBroadcastAddedMutableLive() {
+ return mBroadcastAddedMutableLive;
+ }
+
+ public LiveData<Pair<Integer /* reason */, Integer /* broadcastId */>> getBroadcastRemovedMutableLive() {
+ return mBroadcastRemovedMutableLive;
+ }
+
+ public LiveData<String> getBroadcastStatusMutableLive() {
+ return mBroadcastStatusMutableLive;
+ }
+
+ public boolean startBroadcast(String programInfo, byte[] code) {
+ if (mBluetoothLeBroadcast == null)
+ return false;
+
+ BluetoothLeAudioContentMetadata.Builder contentBuilder =
+ new BluetoothLeAudioContentMetadata.Builder();
+ contentBuilder.setProgramInfo(programInfo);
+ mBluetoothLeBroadcast.startBroadcast(contentBuilder.build(), code);
+ return true;
+ }
+
+ public boolean stopBroadcast(int broadcastId) {
+ if (mBluetoothLeBroadcast == null) return false;
+ mBluetoothLeBroadcast.stopBroadcast(broadcastId);
+ return true;
+ }
+
+ public List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() {
+ if (mBluetoothLeBroadcast == null) return Collections.emptyList();
+ return mBluetoothLeBroadcast.getAllBroadcastMetadata();
+ }
+
+ public boolean updateBroadcast(int broadcastId, String programInfo) {
+ if (mBluetoothLeBroadcast == null) return false;
+
+ BluetoothLeAudioContentMetadata.Builder contentBuilder =
+ new BluetoothLeAudioContentMetadata.Builder();
+ contentBuilder.setProgramInfo(programInfo);
+
+ mBluetoothLeBroadcast.updateBroadcast(broadcastId, contentBuilder.build());
+ return true;
+ }
+
+ public int getMaximumNumberOfBroadcast() {
+ if (mBluetoothLeBroadcast == null) return 0;
+ return mBluetoothLeBroadcast.getMaximumNumberOfBroadcasts();
+ }
+
+ public boolean isPlaying(int broadcastId) {
+ if (mBluetoothLeBroadcast == null) return false;
+ return mBluetoothLeBroadcast.isPlaying(broadcastId);
+ }
+
+ public boolean isLeAudioBroadcastSourceSupported() {
+ return (bluetoothAdapter
+ .isLeAudioBroadcastSourceSupported() == BluetoothStatusCodes.FEATURE_SUPPORTED);
+ }
+}
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastItemsAdapter.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastItemsAdapter.java
new file mode 100644
index 0000000000..215d71b7da
--- /dev/null
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcastItemsAdapter.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.leaudio;
+
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.cardview.widget.CardView;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.android.bluetooth.leaudio.R;
+
+public class BroadcastItemsAdapter
+ extends RecyclerView.Adapter<BroadcastItemsAdapter.BroadcastItemHolder> {
+ private List<BluetoothLeBroadcastMetadata> mBroadcastMetadata = new ArrayList<>();
+ private final Map<Integer /* broadcastId */, Boolean /* isPlaying */> mBroadcastPlayback =
+ new HashMap<>();
+ private OnItemClickListener mOnItemClickListener;
+
+ @NonNull
+ @Override
+ public BroadcastItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View item_view = LayoutInflater.from(parent.getContext()).inflate(R.layout.broadcast_item,
+ parent, false);
+ return new BroadcastItemHolder(item_view, mOnItemClickListener);
+ }
+
+ public void setOnItemClickListener(OnItemClickListener listener) {
+ this.mOnItemClickListener = listener;
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull BroadcastItemHolder holder, int position) {
+ Integer broadcastId = (Integer) mBroadcastPlayback.keySet().toArray()[position];
+ Boolean isPlaying = mBroadcastPlayback.get(broadcastId);
+
+ // Set card color based on the playback state
+ if (isPlaying) {
+ holder.background
+ .setCardBackgroundColor(ColorStateList.valueOf(Color.parseColor("#92b141")));
+ holder.mTextViewBroadcastId.setText("ID: " + broadcastId + " ▶️");
+ } else {
+ holder.background.setCardBackgroundColor(ColorStateList.valueOf(Color.WHITE));
+ holder.mTextViewBroadcastId.setText("ID: " + broadcastId + " ⏸");
+ }
+
+ // TODO: Add additional informations to the card
+ // BluetoothLeBroadcastMetadata current_item = mBroadcastMetadata.get(position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mBroadcastPlayback.size();
+ }
+
+ public void updateBroadcastsMetadata(List<BluetoothLeBroadcastMetadata> broadcasts) {
+ mBroadcastMetadata = broadcasts;
+ notifyDataSetChanged();
+ }
+
+ public void updateBroadcastMetadata(BluetoothLeBroadcastMetadata broadcast) {
+ mBroadcastMetadata.removeIf(bc -> (bc.getBroadcastId() == broadcast.getBroadcastId()));
+ mBroadcastMetadata.add(broadcast);
+ notifyDataSetChanged();
+ }
+
+ public void addBroadcasts(Integer broadcastId) {
+ if (!mBroadcastPlayback.containsKey(broadcastId))
+ mBroadcastPlayback.put(broadcastId, false);
+ }
+
+ public void removeBroadcast(Integer broadcastId) {
+ mBroadcastMetadata.removeIf(bc -> (broadcastId.equals(bc.getBroadcastId())));
+ mBroadcastPlayback.remove(broadcastId);
+ notifyDataSetChanged();
+ }
+
+ public void updateBroadcastPlayback(Integer broadcastId, boolean isPlaying) {
+ mBroadcastPlayback.put(broadcastId, isPlaying);
+ notifyDataSetChanged();
+ }
+
+ public interface OnItemClickListener {
+ void onItemClick(Integer broadcastId);
+ }
+
+ class BroadcastItemHolder extends RecyclerView.ViewHolder {
+ private final TextView mTextViewBroadcastId;
+ private final CardView background;
+
+ public BroadcastItemHolder(@NonNull View itemView, OnItemClickListener listener) {
+ super(itemView);
+
+ mTextViewBroadcastId = itemView.findViewById(R.id.broadcast_id_text);
+ background = itemView.findViewById(R.id.broadcast_item_card_view);
+
+ itemView.setOnClickListener(v -> {
+ if (listener == null) return;
+
+ int position = getAdapterPosition();
+ if (position != RecyclerView.NO_POSITION) {
+ Integer broadcastId = (Integer) mBroadcastPlayback.keySet().toArray()[position];
+ listener.onItemClick(broadcastId);
+ }
+ });
+ }
+ }
+}
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterActivity.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterActivity.java
new file mode 100644
index 0000000000..1e397927b2
--- /dev/null
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterActivity.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.leaudio;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import com.android.bluetooth.leaudio.R;
+
+public class BroadcasterActivity extends AppCompatActivity {
+ private BroadcasterViewModel mViewModel;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.broadcaster_activity);
+
+ FloatingActionButton fab = findViewById(R.id.broadcast_fab);
+ fab.setOnClickListener(view -> {
+ if (mViewModel.getBroadcastCount() < mViewModel.getMaximumNumberOfBroadcast()) {
+ // Start Dialog with the broadcast input details
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+ LayoutInflater inflater = getLayoutInflater();
+ alert.setTitle("Add the Broadcast:");
+
+ View alertView = inflater.inflate(R.layout.broadcaster_add_broadcast_dialog, null);
+ final EditText code_input_text = alertView.findViewById(R.id.broadcast_code_input);
+ EditText metadata_input_text = alertView.findViewById(R.id.broadcast_meta_input);
+
+ alert.setView(alertView).setNegativeButton("Cancel", (dialog, which) -> {
+ // Do nothing
+ }).setPositiveButton("Start", (dialog, which) -> {
+ if (mViewModel.startBroadcast(metadata_input_text.getText().toString(),
+ code_input_text.getText() == null
+ || code_input_text.getText().length() == 0 ? null
+ : code_input_text.getText().toString().getBytes()))
+ Toast.makeText(BroadcasterActivity.this, "Broadcast was created.",
+ Toast.LENGTH_SHORT).show();
+ });
+
+ alert.show();
+ } else {
+ Toast.makeText(BroadcasterActivity.this,
+ "Maximum number of broadcasts reached: " + Integer
+ .valueOf(mViewModel.getMaximumNumberOfBroadcast()).toString(),
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ RecyclerView recyclerView = findViewById(R.id.broadcaster_recycle_view);
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ recyclerView.setHasFixedSize(true);
+
+ final BroadcastItemsAdapter itemsAdapter = new BroadcastItemsAdapter();
+ itemsAdapter.setOnItemClickListener(broadcastId -> {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+ alert.setTitle("Broadcast actions:");
+
+ alert.setNeutralButton("Stop", (dialog, which) -> {
+ mViewModel.stopBroadcast(broadcastId);
+ });
+ alert.setPositiveButton("Modify", (dialog, which) -> {
+ // Open activity for progam info
+ AlertDialog.Builder modifyAlert = new AlertDialog.Builder(this);
+ modifyAlert.setTitle("Modify the Broadcast:");
+
+ LayoutInflater inflater = getLayoutInflater();
+ View alertView = inflater.inflate(R.layout.broadcaster_add_broadcast_dialog, null);
+ EditText metadata_input_text = alertView.findViewById(R.id.broadcast_meta_input);
+
+ // The Code cannot be changed, so just hide it
+ final EditText code_input_text = alertView.findViewById(R.id.broadcast_code_input);
+ code_input_text.setVisibility(View.GONE);
+
+ modifyAlert.setView(alertView)
+ .setNegativeButton("Cancel", (modifyDialog, modifyWhich) -> {
+ // Do nothing
+ }).setPositiveButton("Update", (modifyDialog, modifyWhich) -> {
+ if (mViewModel.updateBroadcast(broadcastId,
+ metadata_input_text.getText().toString()))
+ Toast.makeText(BroadcasterActivity.this, "Broadcast was updated.",
+ Toast.LENGTH_SHORT).show();
+ });
+
+ modifyAlert.show();
+ });
+
+ alert.show();
+ Log.d("CC", "Num broadcasts: " + mViewModel.getBroadcastCount());
+ });
+ recyclerView.setAdapter(itemsAdapter);
+
+ // Get the initial state
+ mViewModel = ViewModelProviders.of(this).get(BroadcasterViewModel.class);
+ itemsAdapter.updateBroadcastsMetadata(mViewModel.getAllBroadcastMetadata());
+
+ // Put a watch on updates
+ mViewModel.getBroadcastUpdateMetadataLive().observe(this, audioBroadcast -> {
+ itemsAdapter.updateBroadcastMetadata(audioBroadcast);
+
+ Toast.makeText(BroadcasterActivity.this,
+ "Updated broadcast " + audioBroadcast.getBroadcastId(), Toast.LENGTH_SHORT)
+ .show();
+ });
+
+ // Put a watch on any error reports
+ mViewModel.getBroadcastStatusMutableLive().observe(this, msg -> {
+ Toast.makeText(BroadcasterActivity.this, msg, Toast.LENGTH_SHORT).show();
+ });
+
+ // Put a watch on broadcast playback states
+ mViewModel.getBroadcastPlaybackStartedMutableLive().observe(this, reasonAndBidPair -> {
+ Toast.makeText(BroadcasterActivity.this, "Playing broadcast " + reasonAndBidPair.second
+ + ", reason " + reasonAndBidPair.first, Toast.LENGTH_SHORT).show();
+
+ itemsAdapter.updateBroadcastPlayback(reasonAndBidPair.second, true);
+ });
+
+ mViewModel.getBroadcastPlaybackStoppedMutableLive().observe(this, reasonAndBidPair -> {
+ Toast.makeText(BroadcasterActivity.this, "Paused broadcast " + reasonAndBidPair.second
+ + ", reason " + reasonAndBidPair.first, Toast.LENGTH_SHORT).show();
+
+ itemsAdapter.updateBroadcastPlayback(reasonAndBidPair.second, false);
+ });
+
+ mViewModel.getBroadcastAddedMutableLive().observe(this, broadcastId -> {
+ itemsAdapter.addBroadcasts(broadcastId);
+
+ Toast.makeText(BroadcasterActivity.this,
+ "Broadcast was added broadcastId: " + broadcastId, Toast.LENGTH_SHORT).show();
+ });
+
+ // Put a watch on broadcast removal
+ mViewModel.getBroadcastRemovedMutableLive().observe(this, reasonAndBidPair -> {
+ itemsAdapter.removeBroadcast(reasonAndBidPair.second);
+
+ Toast.makeText(
+ BroadcasterActivity.this, "Broadcast was removed " + " broadcastId: "
+ + reasonAndBidPair.second + ", reason: " + reasonAndBidPair.first,
+ Toast.LENGTH_SHORT).show();
+ });
+
+ // Prevent destruction when loses focus
+ this.setFinishOnTouchOutside(false);
+ }
+
+ @Override
+ public void onBackPressed() {
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ startActivity(intent);
+ }
+}
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterViewModel.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterViewModel.java
new file mode 100644
index 0000000000..94d4948930
--- /dev/null
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/BroadcasterViewModel.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.leaudio;
+
+import android.app.Application;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Pair;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import java.util.List;
+
+public class BroadcasterViewModel extends AndroidViewModel {
+ private final BluetoothProxy mBluetooth;
+ private final Application mApplication;
+
+ public BroadcasterViewModel(@NonNull Application application) {
+ super(application);
+ mApplication = application;
+
+ mBluetooth = BluetoothProxy.getBluetoothProxy(application);
+ mBluetooth.initProfiles();
+ }
+
+ public boolean startBroadcast(String programInfo, byte[] code) {
+ return mBluetooth.startBroadcast(programInfo, code);
+ }
+
+ public boolean stopBroadcast(int broadcastId) {
+ return mBluetooth.stopBroadcast(broadcastId);
+ }
+
+ public boolean updateBroadcast(int broadcastId, String programInfo) {
+ return mBluetooth.updateBroadcast(broadcastId, programInfo);
+ }
+
+ public int getMaximumNumberOfBroadcast() {
+ return mBluetooth.getMaximumNumberOfBroadcast();
+ }
+
+ public List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() {
+ return mBluetooth.getAllBroadcastMetadata();
+ }
+
+ public int getBroadcastCount() {
+ return mBluetooth.getAllBroadcastMetadata().size();
+ }
+
+ public LiveData<BluetoothLeBroadcastMetadata> getBroadcastUpdateMetadataLive() {
+ return mBluetooth.getBroadcastUpdateMetadataLive();
+ }
+
+ public LiveData<Pair<Integer /* reason */, Integer /* broadcastId */>> getBroadcastPlaybackStartedMutableLive() {
+ return mBluetooth.getBroadcastPlaybackStartedMutableLive();
+ }
+
+ public LiveData<Pair<Integer /* reason */, Integer /* broadcastId */>> getBroadcastPlaybackStoppedMutableLive() {
+ return mBluetooth.getBroadcastPlaybackStoppedMutableLive();
+ }
+
+ public LiveData<Integer /* broadcastId */> getBroadcastAddedMutableLive() {
+ return mBluetooth.getBroadcastAddedMutableLive();
+ }
+
+ public LiveData<Pair<Integer /* reason */, Integer /* broadcastId */>> getBroadcastRemovedMutableLive() {
+ return mBluetooth.getBroadcastRemovedMutableLive();
+ }
+
+ public LiveData<String> getBroadcastStatusMutableLive() {
+ return mBluetooth.getBroadcastStatusMutableLive();
+ }
+
+ @Override
+ public void onCleared() {}
+}
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioDeviceStateWrapper.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioDeviceStateWrapper.java
new file mode 100644
index 0000000000..e94885684d
--- /dev/null
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioDeviceStateWrapper.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.leaudio;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapPresetInfo;
+
+import androidx.core.util.Pair;
+import androidx.lifecycle.MutableLiveData;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class LeAudioDeviceStateWrapper {
+ public BluetoothDevice device;
+ public LeAudioData leAudioData = null;
+ public VolumeControlData volumeControlData = null;
+ public BassData bassData = null;
+ public HapData hapData = null;
+
+ public LeAudioDeviceStateWrapper(BluetoothDevice device) {
+ this.device = device;
+ }
+
+ public static class LeAudioData {
+ public MutableLiveData<Boolean> isConnectedMutable = new MutableLiveData<>();
+ public MutableLiveData<Pair<Integer, Integer>> nodeStatusMutable =
+ new MutableLiveData<>();
+ public MutableLiveData<Pair<Integer, Pair<Integer, Integer>>> groupStatusMutable =
+ new MutableLiveData<>();
+ public MutableLiveData<Pair<Integer, Boolean>> groupLockStateMutable =
+ new MutableLiveData<>();
+ public MutableLiveData<Integer> microphoneStateMutable = new MutableLiveData<>();
+
+ public Object viewsData = null;
+ }
+
+ public static class HapData {
+ public MutableLiveData<Integer> hapStateMutable = new MutableLiveData<>();
+ public MutableLiveData<String> hapStatusMutable = new MutableLiveData<>();
+ public MutableLiveData<Integer> hapFeaturesMutable = new MutableLiveData<>();
+ public MutableLiveData<Integer> hapActivePresetIndexMutable =
+ new MutableLiveData<>();
+ public MutableLiveData<List<BluetoothHapPresetInfo>> hapPresetsMutable =
+ new MutableLiveData<>();
+
+ public Object viewsData = null;
+ }
+
+ public static class VolumeControlData {
+ public MutableLiveData<Boolean> isConnectedMutable = new MutableLiveData<>(false);
+ public MutableLiveData<Integer> numInputsMutable = new MutableLiveData<>(0);
+ public MutableLiveData<Integer> numOffsetsMutable = new MutableLiveData<>(0);
+ public MutableLiveData<Integer> volumeStateMutable = new MutableLiveData<>(0);
+ public MutableLiveData<Boolean> mutedStateMutable = new MutableLiveData<>(false);
+
+ public MutableLiveData<Map<Integer, String>> inputDescriptionsMutable =
+ new MutableLiveData<>(new TreeMap<>());
+ public MutableLiveData<Map<Integer, Integer>> inputStateGainMutable =
+ new MutableLiveData<>(new TreeMap<>());
+ public MutableLiveData<Map<Integer, Integer>> inputStateGainModeMutable =
+ new MutableLiveData<>(new TreeMap<>());
+ public MutableLiveData<Map<Integer, Integer>> inputStateGainUnitMutable =
+ new MutableLiveData<>(new TreeMap<>());
+ public MutableLiveData<Map<Integer, Integer>> inputStateGainMinMutable =
+ new MutableLiveData<>(new TreeMap<>());
+ public MutableLiveData<Map<Integer, Integer>> inputStateGainMaxMutable =
+ new MutableLiveData<>(new TreeMap<>());
+ public MutableLiveData<Map<Integer, Boolean>> inputStateMuteMutable =
+ new MutableLiveData<>(new TreeMap<>());
+ public MutableLiveData<Map<Integer, Integer>> inputStatusMutable =
+ new MutableLiveData<>(new TreeMap<>());
+ public MutableLiveData<Map<Integer, Integer>> inputTypeMutable =
+ new MutableLiveData<>(new TreeMap<>());
+
+ public MutableLiveData<Map<Integer, Integer>> outputVolumeOffsetMutable =
+ new MutableLiveData<>(new TreeMap<>());
+ public MutableLiveData<Map<Integer, Integer>> outputLocationMutable =
+ new MutableLiveData<>(new TreeMap<>());
+ public MutableLiveData<Map<Integer, String>> outputDescriptionMutable =
+ new MutableLiveData<>(new TreeMap<>());
+
+ public Object viewsData = null;
+ }
+
+ public static class ReceiverState {
+ int receiver_id;
+ int state;
+ }
+
+ public static class BassData {
+ public MutableLiveData<Boolean> isValidBassDevice = new MutableLiveData<>();
+ public MutableLiveData<Boolean> isConnectedMutable = new MutableLiveData<>();
+ public MutableLiveData<HashMap<Integer, ReceiverState>> receiverStatesMutable =
+ new MutableLiveData<>();
+
+ public Object viewsData = null;
+ }
+}
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java
new file mode 100644
index 0000000000..3aa9e9d607
--- /dev/null
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioRecycleViewAdapter.java
@@ -0,0 +1,1623 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.leaudio;
+
+import android.animation.ObjectAnimator;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothLeAudio;
+import android.content.res.Configuration;
+import android.os.ParcelUuid;
+import android.text.InputType;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.*;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.MutableLiveData;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import com.android.bluetooth.leaudio.R;
+
+public class LeAudioRecycleViewAdapter
+ extends RecyclerView.Adapter<LeAudioRecycleViewAdapter.ViewHolder> {
+ private final AppCompatActivity parent;
+ private OnItemClickListener clickListener;
+ private OnLeAudioInteractionListener leAudioInteractionListener;
+ private OnVolumeControlInteractionListener volumeControlInteractionListener;
+ private OnBassInteractionListener bassInteractionListener;
+ private OnHapInteractionListener hapInteractionListener;
+ private final ArrayList<LeAudioDeviceStateWrapper> devices;
+
+ public LeAudioRecycleViewAdapter(AppCompatActivity context) {
+ this.parent = context;
+ devices = new ArrayList<>();
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.le_audio_device_fragment,
+ parent, false);
+ return new ViewHolder(v);
+ }
+
+ // As we scroll this methods rebinds devices below to our ViewHolders which are reused when
+ // they go off the screen. This is also called when notifyItemChanged(position) is called
+ // without the payloads.
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper = devices.get(position);
+
+ if (leAudioDeviceStateWrapper != null) {
+ holder.deviceName.setText(parent.getString(R.string.notes_icon) + " "
+ + leAudioDeviceStateWrapper.device.getName() + " ["
+ + leAudioDeviceStateWrapper.device.getAddress() + "]");
+
+ if (leAudioDeviceStateWrapper.device.getUuids() != null) {
+ holder.itemView.findViewById(R.id.le_audio_switch)
+ .setEnabled(Arrays.asList(leAudioDeviceStateWrapper.device.getUuids())
+ .contains(ParcelUuid
+ .fromString(parent.getString(R.string.svc_uuid_le_audio))));
+
+ holder.itemView.findViewById(R.id.vc_switch)
+ .setEnabled(Arrays.asList(leAudioDeviceStateWrapper.device.getUuids())
+ .contains(ParcelUuid.fromString(
+ parent.getString(R.string.svc_uuid_volume_control))));
+
+ holder.itemView.findViewById(R.id.hap_switch).setEnabled(
+ Arrays.asList(leAudioDeviceStateWrapper.device.getUuids()).contains(
+ ParcelUuid.fromString(parent.getString(R.string.svc_uuid_has))));
+ }
+ }
+
+ // Set state observables
+ setLeAudioStateObservers(holder, leAudioDeviceStateWrapper);
+ setVolumeControlStateObservers(holder, leAudioDeviceStateWrapper);
+ setVolumeControlUiStateObservers(holder, leAudioDeviceStateWrapper);
+ setBassStateObservers(holder, leAudioDeviceStateWrapper);
+ setHasStateObservers(holder, leAudioDeviceStateWrapper);
+ }
+
+ private void setLeAudioStateObservers(@NonNull ViewHolder holder,
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+ LeAudioDeviceStateWrapper.LeAudioData le_audio_svc_data =
+ leAudioDeviceStateWrapper.leAudioData;
+ if (le_audio_svc_data != null) {
+ if (le_audio_svc_data.isConnectedMutable.hasObservers())
+ le_audio_svc_data.isConnectedMutable.removeObservers(this.parent);
+ le_audio_svc_data.isConnectedMutable.observe(this.parent, is_connected -> {
+ // FIXME: How to prevent the callback from firing when we set this by code
+ if (is_connected != holder.leAudioConnectionSwitch.isChecked()) {
+ holder.leAudioConnectionSwitch.setActivated(false);
+ holder.leAudioConnectionSwitch.setChecked(is_connected);
+ holder.leAudioConnectionSwitch.setActivated(true);
+ }
+
+ if (holder.itemView.findViewById(R.id.le_audio_layout)
+ .getVisibility() != (is_connected ? View.VISIBLE : View.GONE))
+ holder.itemView.findViewById(R.id.le_audio_layout)
+ .setVisibility(is_connected ? View.VISIBLE : View.GONE);
+ });
+
+ holder.itemView.findViewById(R.id.le_audio_layout)
+ .setVisibility(le_audio_svc_data.isConnectedMutable.getValue() != null
+ && le_audio_svc_data.isConnectedMutable.getValue() ? View.VISIBLE
+ : View.GONE);
+
+ if (le_audio_svc_data.nodeStatusMutable.hasObservers())
+ le_audio_svc_data.nodeStatusMutable.removeObservers(this.parent);
+ le_audio_svc_data.nodeStatusMutable.observe(this.parent, group_id_node_status_pair -> {
+ final Integer status = group_id_node_status_pair.second;
+ final Integer group_id = group_id_node_status_pair.first;
+
+ if (status == BluetoothLeAudio.GROUP_NODE_REMOVED)
+ holder.leAudioGroupIdText
+ .setText(((Integer) BluetoothLeAudio.GROUP_ID_INVALID).toString());
+ else
+ holder.leAudioGroupIdText.setText(group_id.toString());
+ });
+
+ if (le_audio_svc_data.groupStatusMutable.hasObservers())
+ le_audio_svc_data.groupStatusMutable.removeObservers(this.parent);
+ le_audio_svc_data.groupStatusMutable.observe(this.parent, group_id_node_status_pair -> {
+ final Integer group_id = group_id_node_status_pair.first;
+ final Integer status = group_id_node_status_pair.second.first;
+ final Integer flags = group_id_node_status_pair.second.second;
+
+ // If our group.. actually we shouldn't get this event if it's nor ours,
+ // right?
+ if (holder.leAudioGroupIdText.getText().equals(group_id.toString())) {
+ holder.leAudioGroupStatusText.setText(status >= 0
+ ? this.parent.getResources()
+ .getStringArray(R.array.group_statuses)[status]
+ : this.parent.getResources().getString(R.string.unknown));
+ holder.leAudioGroupFlagsText.setText(flags > 0 ? flags.toString()
+ : this.parent.getResources().getString(R.string.none));
+ }
+ });
+
+ if (le_audio_svc_data.groupLockStateMutable.hasObservers())
+ le_audio_svc_data.groupLockStateMutable.removeObservers(this.parent);
+ le_audio_svc_data.groupLockStateMutable.observe(this.parent,
+ group_id_node_status_pair -> {
+ final Integer group_id = group_id_node_status_pair.first;
+ final Boolean locked = group_id_node_status_pair.second;
+
+ // If our group.. actually we shouldn't get this event if it's nor ours,
+ // right?
+ if (holder.leAudioGroupIdText.getText().equals(group_id.toString())) {
+ holder.leAudioSetLockStateText.setText(this.parent.getResources()
+ .getString(locked ? R.string.group_locked
+ : R.string.group_unlocked));
+ }
+ });
+
+ if (le_audio_svc_data.microphoneStateMutable.hasObservers())
+ le_audio_svc_data.microphoneStateMutable.removeObservers(this.parent);
+ le_audio_svc_data.microphoneStateMutable.observe(this.parent, microphone_state -> {
+ holder.leAudioGroupMicrophoneState.setText(this.parent.getResources()
+ .getStringArray(R.array.mic_states)[microphone_state]);
+ holder.leAudioGroupMicrophoneSwitch.setActivated(false);
+ });
+ }
+ }
+
+ private void setHasStateObservers(@NonNull ViewHolder holder,
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+ LeAudioDeviceStateWrapper.HapData hap_svc_data = leAudioDeviceStateWrapper.hapData;
+ if (hap_svc_data != null) {
+ if (hap_svc_data.hapStateMutable.hasObservers())
+ hap_svc_data.hapStateMutable.removeObservers(this.parent);
+ hap_svc_data.hapStateMutable.observe(this.parent, hap_state -> {
+ holder.leAudioHapState.setText(this.parent.getResources()
+ .getStringArray(R.array.profile_states)[hap_state]);
+
+ boolean is_connected = (hap_state == BluetoothHapClient.STATE_CONNECTED);
+ if (is_connected != holder.hapConnectionSwitch.isChecked()) {
+ holder.hapConnectionSwitch.setActivated(false);
+ holder.hapConnectionSwitch.setChecked(is_connected);
+ holder.hapConnectionSwitch.setActivated(true);
+ }
+
+ if (holder.itemView.findViewById(R.id.hap_layout)
+ .getVisibility() != (is_connected ? View.VISIBLE : View.GONE))
+ holder.itemView.findViewById(R.id.hap_layout)
+ .setVisibility(is_connected ? View.VISIBLE : View.GONE);
+ });
+
+ if (hap_svc_data.hapFeaturesMutable.hasObservers())
+ hap_svc_data.hapFeaturesMutable.removeObservers(this.parent);
+
+ hap_svc_data.hapFeaturesMutable.observe(this.parent, features -> {
+ try {
+ // Get hidden feature bits
+ Field field =
+ BluetoothHapClient.class.getDeclaredField("FEATURE_TYPE_MONAURAL");
+ field.setAccessible(true);
+ Integer FEATURE_TYPE_MONAURAL = (Integer) field.get(null);
+
+ field = BluetoothHapClient.class.getDeclaredField("FEATURE_TYPE_BANDED");
+ field.setAccessible(true);
+ Integer FEATURE_TYPE_BANDED = (Integer) field.get(null);
+
+ field = BluetoothHapClient.class
+ .getDeclaredField("FEATURE_SYNCHRONIZATED_PRESETS");
+ field.setAccessible(true);
+ Integer FEATURE_SYNCHRONIZATED_PRESETS = (Integer) field.get(null);
+
+ field = BluetoothHapClient.class
+ .getDeclaredField("FEATURE_INDEPENDENT_PRESETS");
+ field.setAccessible(true);
+ Integer FEATURE_INDEPENDENT_PRESETS = (Integer) field.get(null);
+
+ field = BluetoothHapClient.class.getDeclaredField("FEATURE_DYNAMIC_PRESETS");
+ field.setAccessible(true);
+ Integer FEATURE_DYNAMIC_PRESETS = (Integer) field.get(null);
+
+ field = BluetoothHapClient.class.getDeclaredField("FEATURE_WRITABLE_PRESETS");
+ field.setAccessible(true);
+ Integer FEATURE_WRITABLE_PRESETS = (Integer) field.get(null);
+
+ int hearing_aid_type_idx = (features & FEATURE_TYPE_MONAURAL) != 0 ? 0
+ : ((features & FEATURE_TYPE_BANDED) != 0 ? 1 : 2);
+ String hearing_aid_type = this.parent.getResources()
+ .getStringArray(R.array.hearing_aid_types)[hearing_aid_type_idx];
+ String preset_synchronization_support = this.parent.getResources()
+ .getStringArray(R.array.preset_synchronization_support)[(features
+ & FEATURE_SYNCHRONIZATED_PRESETS) != 0 ? 1 : 0];
+ String independent_presets = this.parent.getResources()
+ .getStringArray(R.array.independent_presets)[(features
+ & FEATURE_INDEPENDENT_PRESETS) != 0 ? 1 : 0];
+ String dynamic_presets = this.parent.getResources().getStringArray(
+ R.array.dynamic_presets)[(features & FEATURE_DYNAMIC_PRESETS) != 0 ? 1
+ : 0];
+ String writable_presets_support = this.parent.getResources()
+ .getStringArray(R.array.writable_presets_support)[(features
+ & FEATURE_WRITABLE_PRESETS) != 0 ? 1 : 0];
+ holder.leAudioHapFeatures.setText(hearing_aid_type + " / "
+ + preset_synchronization_support + " / " + independent_presets + " / "
+ + dynamic_presets + " / " + writable_presets_support);
+
+ } catch (IllegalAccessException | NoSuchFieldException e) {
+ // Do nothing
+ holder.leAudioHapFeatures.setText("Hidden API for feature fields unavailable.");
+ }
+ });
+
+ if (hap_svc_data.hapPresetsMutable.hasActiveObservers())
+ hap_svc_data.hapPresetsMutable.removeObservers(this.parent);
+ hap_svc_data.hapPresetsMutable.observe(this.parent, hapPresetsList -> {
+ List<String> all_ids = hapPresetsList.stream()
+ .map(info -> "" + info.getIndex() + " " + info.getName()
+ + (info.isWritable() ? " [wr" : " [")
+ + (info.isAvailable() ? "a]" : "]"))
+ .collect(Collectors.toList());
+
+ ArrayAdapter<Integer> adapter = new ArrayAdapter(this.parent,
+ android.R.layout.simple_spinner_item, all_ids);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ holder.leAudioHapPresetsSpinner.setAdapter(adapter);
+
+ if (hap_svc_data.viewsData != null) {
+ Integer select_pos =
+ ((ViewHolderHapPersistentData) hap_svc_data.viewsData).selectedPresetPositionMutable
+ .getValue();
+ if (select_pos != null)
+ holder.leAudioHapPresetsSpinner.setSelection(select_pos);
+ }
+ });
+
+ if (hap_svc_data.hapActivePresetIndexMutable.hasObservers())
+ hap_svc_data.hapActivePresetIndexMutable.removeObservers(this.parent);
+ hap_svc_data.hapActivePresetIndexMutable.observe(this.parent, active_preset_index -> {
+ holder.leAudioHapActivePresetIndex.setText(String.valueOf(active_preset_index));
+ });
+
+ if (hap_svc_data.hapActivePresetIndexMutable.hasObservers())
+ hap_svc_data.hapActivePresetIndexMutable.removeObservers(this.parent);
+ hap_svc_data.hapActivePresetIndexMutable.observe(this.parent, active_preset_index -> {
+ holder.leAudioHapActivePresetIndex.setText(String.valueOf(active_preset_index));
+ });
+ } else {
+ holder.itemView.findViewById(R.id.hap_layout).setVisibility(View.GONE);
+ }
+ }
+
+ private void setVolumeControlStateObservers(@NonNull ViewHolder holder,
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+ LeAudioDeviceStateWrapper.VolumeControlData vc_svc_data =
+ leAudioDeviceStateWrapper.volumeControlData;
+ if (vc_svc_data != null) {
+ if (vc_svc_data.isConnectedMutable.hasObservers())
+ vc_svc_data.isConnectedMutable.removeObservers(this.parent);
+ vc_svc_data.isConnectedMutable.observe(this.parent, is_connected -> {
+ // FIXME: How to prevent the callback from firing when we set this by code
+ if (is_connected != holder.vcConnectionSwitch.isChecked()) {
+ holder.vcConnectionSwitch.setActivated(false);
+ holder.vcConnectionSwitch.setChecked(is_connected);
+ holder.vcConnectionSwitch.setActivated(true);
+ }
+
+ if (holder.itemView.findViewById(R.id.vc_layout)
+ .getVisibility() != (is_connected ? View.VISIBLE : View.GONE))
+ holder.itemView.findViewById(R.id.vc_layout)
+ .setVisibility(is_connected ? View.VISIBLE : View.GONE);
+ });
+
+ holder.itemView.findViewById(R.id.vc_layout)
+ .setVisibility(vc_svc_data.isConnectedMutable.getValue() != null
+ && vc_svc_data.isConnectedMutable.getValue() ? View.VISIBLE
+ : View.GONE);
+
+ if (vc_svc_data.volumeStateMutable.hasObservers())
+ vc_svc_data.volumeStateMutable.removeObservers(this.parent);
+ vc_svc_data.volumeStateMutable.observe(this.parent, state -> {
+ holder.volumeSeekBar.setProgress(state);
+ });
+
+ if (vc_svc_data.mutedStateMutable.hasObservers())
+ vc_svc_data.mutedStateMutable.removeObservers(this.parent);
+ vc_svc_data.mutedStateMutable.observe(this.parent, state -> {
+ holder.muteSwitch.setActivated(false);
+ holder.muteSwitch.setChecked(state);
+ holder.muteSwitch.setActivated(true);
+ });
+
+ if (vc_svc_data.numInputsMutable.hasObservers())
+ vc_svc_data.numInputsMutable.removeObservers(this.parent);
+ vc_svc_data.numInputsMutable.observe(this.parent, num_inputs -> {
+ List<Integer> range = new ArrayList<>();
+ if (num_inputs != 0)
+ range = IntStream.rangeClosed(1, num_inputs).boxed()
+ .collect(Collectors.toList());
+ ArrayAdapter<Integer> adapter =
+ new ArrayAdapter(this.parent, android.R.layout.simple_spinner_item, range);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ holder.inputIdxSpinner.setAdapter(adapter);
+ });
+
+ if (vc_svc_data.viewsData != null) {
+ Integer select_pos =
+ ((ViewHolderVcPersistentData) vc_svc_data.viewsData).selectedInputPosition;
+ if (select_pos != null)
+ holder.inputIdxSpinner.setSelection(select_pos);
+ }
+
+ if (vc_svc_data.inputDescriptionsMutable.hasObservers())
+ vc_svc_data.inputDescriptionsMutable.removeObservers(this.parent);
+ vc_svc_data.inputDescriptionsMutable.observe(this.parent, integerStringMap -> {
+ if (holder.inputIdxSpinner.getSelectedItem() != null) {
+ Integer input_id =
+ Integer.valueOf(holder.inputIdxSpinner.getSelectedItem().toString());
+ holder.inputDescriptionText
+ .setText(integerStringMap.getOrDefault(input_id, ""));
+ }
+ });
+
+ if (vc_svc_data.inputStateGainMutable.hasObservers())
+ vc_svc_data.inputStateGainMutable.removeObservers(this.parent);
+ vc_svc_data.inputStateGainMutable.observe(this.parent, integerIntegerMap -> {
+ if (holder.inputIdxSpinner.getSelectedItem() != null) {
+ Integer input_id =
+ Integer.valueOf(holder.inputIdxSpinner.getSelectedItem().toString());
+ holder.inputGainSeekBar
+ .setProgress(integerIntegerMap.getOrDefault(input_id, 0));
+ }
+ });
+
+ if (vc_svc_data.inputStateGainModeMutable.hasObservers())
+ vc_svc_data.inputStateGainModeMutable.removeObservers(this.parent);
+ vc_svc_data.inputStateGainModeMutable.observe(this.parent, integerIntegerMap -> {
+ if (holder.inputIdxSpinner.getSelectedItem() != null) {
+ Integer input_id =
+ Integer.valueOf(holder.inputIdxSpinner.getSelectedItem().toString());
+ holder.inputGainModeText.setText(this.parent.getResources().getStringArray(
+ R.array.gain_modes)[integerIntegerMap.getOrDefault(input_id, 1)]);
+ }
+ });
+
+ if (vc_svc_data.inputStateGainUnitMutable.hasObservers())
+ vc_svc_data.inputStateGainUnitMutable.removeObservers(this.parent);
+ vc_svc_data.inputStateGainUnitMutable.observe(this.parent, integerIntegerMap -> {
+ if (holder.inputIdxSpinner.getSelectedItem() != null) {
+ // TODO: Use string map with units instead of plain numbers
+ Integer input_id =
+ Integer.valueOf(holder.inputIdxSpinner.getSelectedItem().toString());
+ holder.inputGainPropsUnitText
+ .setText(integerIntegerMap.getOrDefault(input_id, 0).toString());
+ }
+ });
+
+ if (vc_svc_data.inputStateGainMinMutable.hasObservers())
+ vc_svc_data.inputStateGainMinMutable.removeObservers(this.parent);
+ vc_svc_data.inputStateGainMinMutable.observe(this.parent, integerIntegerMap -> {
+ if (holder.inputIdxSpinner.getSelectedItem() != null) {
+ Integer input_id =
+ Integer.valueOf(holder.inputIdxSpinner.getSelectedItem().toString());
+ holder.inputGainPropsMinText
+ .setText(integerIntegerMap.getOrDefault(input_id, 0).toString());
+ holder.inputGainSeekBar.setMin(integerIntegerMap.getOrDefault(input_id, -255));
+ }
+ });
+
+ if (vc_svc_data.inputStateGainMaxMutable.hasObservers())
+ vc_svc_data.inputStateGainMaxMutable.removeObservers(this.parent);
+ vc_svc_data.inputStateGainMaxMutable.observe(this.parent, integerIntegerMap -> {
+ if (holder.inputIdxSpinner.getSelectedItem() != null) {
+ Integer input_id =
+ Integer.valueOf(holder.inputIdxSpinner.getSelectedItem().toString());
+ holder.inputGainPropsMaxText
+ .setText(integerIntegerMap.getOrDefault(input_id, 0).toString());
+ holder.inputGainSeekBar.setMax(integerIntegerMap.getOrDefault(input_id, 255));
+ }
+ });
+
+ if (vc_svc_data.inputStateMuteMutable.hasObservers())
+ vc_svc_data.inputStateMuteMutable.removeObservers(this.parent);
+ vc_svc_data.inputStateMuteMutable.observe(this.parent, integerIntegerMap -> {
+ if (holder.inputIdxSpinner.getSelectedItem() != null) {
+ Integer input_id =
+ Integer.valueOf(holder.inputIdxSpinner.getSelectedItem().toString());
+ holder.inputMuteSwitch.setActivated(false);
+ holder.inputMuteSwitch
+ .setChecked(integerIntegerMap.getOrDefault(input_id, false));
+ holder.inputMuteSwitch.setActivated(true);
+ }
+ });
+
+ if (vc_svc_data.inputStatusMutable.hasObservers())
+ vc_svc_data.inputStatusMutable.removeObservers(this.parent);
+ vc_svc_data.inputStatusMutable.observe(this.parent, integerIntegerMap -> {
+ if (holder.inputIdxSpinner.getSelectedItem() != null) {
+ Integer input_id =
+ Integer.valueOf(holder.inputIdxSpinner.getSelectedItem().toString());
+ // TODO: Use string map with units instead of plain numbers
+ holder.inputStatusText
+ .setText(integerIntegerMap.getOrDefault(input_id, -1).toString());
+ }
+ });
+
+ if (vc_svc_data.inputTypeMutable.hasObservers())
+ vc_svc_data.inputTypeMutable.removeObservers(this.parent);
+ vc_svc_data.inputTypeMutable.observe(this.parent, integerIntegerMap -> {
+ if (holder.inputIdxSpinner.getSelectedItem() != null) {
+ Integer input_id =
+ Integer.valueOf(holder.inputIdxSpinner.getSelectedItem().toString());
+ // TODO: Use string map with units instead of plain numbers
+ holder.inputTypeText
+ .setText(integerIntegerMap.getOrDefault(input_id, -1).toString());
+ }
+ });
+
+ vc_svc_data.numOffsetsMutable.observe(this.parent, num_offsets -> {
+ List<Integer> range = new ArrayList<>();
+ if (num_offsets != 0)
+ range = IntStream.rangeClosed(1, num_offsets).boxed()
+ .collect(Collectors.toList());
+ ArrayAdapter<Integer> adapter =
+ new ArrayAdapter(this.parent, android.R.layout.simple_spinner_item, range);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ holder.outputIdxSpinner.setAdapter(adapter);
+ });
+
+ if (vc_svc_data.viewsData != null) {
+ Integer select_pos =
+ ((ViewHolderVcPersistentData) vc_svc_data.viewsData).selectedOutputPosition;
+ if (select_pos != null)
+ holder.outputIdxSpinner.setSelection(select_pos);
+ }
+
+ if (vc_svc_data.outputVolumeOffsetMutable.hasObservers())
+ vc_svc_data.outputVolumeOffsetMutable.removeObservers(this.parent);
+ vc_svc_data.outputVolumeOffsetMutable.observe(this.parent, integerIntegerMap -> {
+ if (holder.outputIdxSpinner.getSelectedItem() != null) {
+ Integer output_id =
+ Integer.valueOf(holder.outputIdxSpinner.getSelectedItem().toString());
+ holder.outputGainOffsetSeekBar
+ .setProgress(integerIntegerMap.getOrDefault(output_id, 0));
+ }
+ });
+
+ if (vc_svc_data.outputLocationMutable.hasObservers())
+ vc_svc_data.outputLocationMutable.removeObservers(this.parent);
+ vc_svc_data.outputLocationMutable.observe(this.parent, integerIntegerMap -> {
+ if (holder.outputIdxSpinner.getSelectedItem() != null) {
+ Integer output_id =
+ Integer.valueOf(holder.outputIdxSpinner.getSelectedItem().toString());
+ holder.outputLocationText.setText(this.parent.getResources().getStringArray(
+ R.array.audio_locations)[integerIntegerMap.getOrDefault(output_id, 0)]);
+ }
+ });
+
+ if (vc_svc_data.outputDescriptionMutable.hasObservers())
+ vc_svc_data.outputDescriptionMutable.removeObservers(this.parent);
+ vc_svc_data.outputDescriptionMutable.observe(this.parent, integerStringMap -> {
+ if (holder.outputIdxSpinner.getSelectedItem() != null) {
+ Integer output_id =
+ Integer.valueOf(holder.outputIdxSpinner.getSelectedItem().toString());
+ holder.outputDescriptionText
+ .setText(integerStringMap.getOrDefault(output_id, "no description"));
+ }
+ });
+ } else {
+ holder.itemView.findViewById(R.id.vc_layout).setVisibility(View.GONE);
+ }
+ }
+
+ private void setVolumeControlUiStateObservers(@NonNull ViewHolder holder,
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+ if (leAudioDeviceStateWrapper.volumeControlData == null)
+ return;
+
+ ViewHolderVcPersistentData vData =
+ (ViewHolderVcPersistentData) leAudioDeviceStateWrapper.volumeControlData.viewsData;
+ if (vData == null)
+ return;
+
+ if (vData.isInputsCollapsedMutable.hasObservers())
+ vData.isInputsCollapsedMutable.removeObservers(this.parent);
+ vData.isInputsCollapsedMutable.observe(this.parent, aBoolean -> {
+ Float rbegin = aBoolean ? 0.0f : 180.0f;
+ Float rend = aBoolean ? 180.0f : 0.0f;
+
+ ObjectAnimator.ofFloat(holder.inputFoldableIcon, "rotation", rbegin, rend)
+ .setDuration(300).start();
+ holder.inputFoldable.setVisibility(aBoolean ? View.GONE : View.VISIBLE);
+ });
+ vData.isInputsCollapsedMutable.setValue(holder.inputFoldable.getVisibility() == View.GONE);
+
+ if (vData.isOutputsCollapsedMutable.hasObservers())
+ vData.isOutputsCollapsedMutable.removeObservers(this.parent);
+ vData.isOutputsCollapsedMutable.observe(this.parent, aBoolean -> {
+ Float rbegin = aBoolean ? 0.0f : 180.0f;
+ Float rend = aBoolean ? 180.0f : 0.0f;
+
+ ObjectAnimator.ofFloat(holder.outputFoldableIcon, "rotation", rbegin, rend)
+ .setDuration(300).start();
+ holder.outputFoldable.setVisibility(aBoolean ? View.GONE : View.VISIBLE);
+ });
+ vData.isOutputsCollapsedMutable
+ .setValue(holder.outputFoldable.getVisibility() == View.GONE);
+ }
+
+ private void setBassStateObservers(@NonNull ViewHolder holder,
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+ LeAudioDeviceStateWrapper.BassData bass_svc_data = leAudioDeviceStateWrapper.bassData;
+ if (bass_svc_data != null) {
+ if (bass_svc_data.isConnectedMutable.hasObservers())
+ bass_svc_data.isConnectedMutable.removeObservers(this.parent);
+ bass_svc_data.isConnectedMutable.observe(this.parent, is_connected -> {
+ // FIXME: How to prevent the callback from firing when we set this by code
+ if (is_connected != holder.bassConnectionSwitch.isChecked()) {
+ holder.bassConnectionSwitch.setActivated(false);
+ holder.bassConnectionSwitch.setChecked(is_connected);
+ holder.bassConnectionSwitch.setActivated(true);
+ }
+
+ if (is_connected) {
+ }
+
+ if (holder.itemView.findViewById(R.id.bass_layout)
+ .getVisibility() != (is_connected ? View.VISIBLE : View.GONE))
+ holder.itemView.findViewById(R.id.bass_layout)
+ .setVisibility(is_connected ? View.VISIBLE : View.GONE);
+ });
+
+ holder.itemView.findViewById(R.id.bass_layout)
+ .setVisibility(bass_svc_data.isConnectedMutable.getValue() != null
+ && bass_svc_data.isConnectedMutable.getValue() ? View.VISIBLE
+ : View.GONE);
+
+ if (bass_svc_data.receiverStatesMutable.hasActiveObservers())
+ bass_svc_data.receiverStatesMutable.removeObservers(this.parent);
+ bass_svc_data.receiverStatesMutable.observe(this.parent,
+ integerReceiverStateHashMap -> {
+ List<Integer> all_ids = integerReceiverStateHashMap.entrySet().stream()
+ .map(Map.Entry::getKey).collect(Collectors.toList());
+
+ ArrayAdapter<Integer> adapter = new ArrayAdapter(this.parent,
+ android.R.layout.simple_spinner_item, all_ids);
+ adapter.setDropDownViewResource(
+ android.R.layout.simple_spinner_dropdown_item);
+ holder.bassReceiverIdSpinner.setAdapter(adapter);
+
+ if (bass_svc_data.viewsData != null) {
+ Integer select_pos =
+ ((ViewHolderBassPersistentData) bass_svc_data.viewsData).selectedReceiverPositionMutable
+ .getValue();
+ if (select_pos != null)
+ holder.bassReceiverIdSpinner.setSelection(select_pos);
+ }
+ });
+ } else {
+ holder.itemView.findViewById(R.id.bass_layout).setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return devices.get(position).device.getAddress().hashCode();
+ }
+
+ @Override
+ public int getItemCount() {
+ return devices != null ? devices.size() : 0;
+ }
+
+ // Listeners registration routines
+ // -------------------------------
+ public void setOnItemClickListener(@Nullable OnItemClickListener listener) {
+ this.clickListener = listener;
+ }
+
+ public void setOnLeAudioInteractionListener(@Nullable OnLeAudioInteractionListener listener) {
+ this.leAudioInteractionListener = listener;
+ }
+
+ public void setOnVolumeControlInteractionListener(
+ @Nullable OnVolumeControlInteractionListener listener) {
+ this.volumeControlInteractionListener = listener;
+ }
+
+ public void setOnBassInteractionListener(@Nullable OnBassInteractionListener listener) {
+ this.bassInteractionListener = listener;
+ }
+
+ public void setOnHapInteractionListener(@Nullable OnHapInteractionListener listener) {
+ this.hapInteractionListener = listener;
+ }
+
+ // Device list update routine
+ // -----------------------------
+ public void updateLeAudioDeviceList(@Nullable List<LeAudioDeviceStateWrapper> devices) {
+ this.devices.clear();
+ this.devices.addAll(devices);
+
+ // FIXME: Is this the right way of doing it?
+ for (LeAudioDeviceStateWrapper dev_state : this.devices) {
+ if (dev_state.volumeControlData != null)
+ if (dev_state.volumeControlData.viewsData == null)
+ dev_state.volumeControlData.viewsData = new ViewHolderVcPersistentData();
+ if (dev_state.bassData != null)
+ if (dev_state.bassData.viewsData == null)
+ dev_state.bassData.viewsData = new ViewHolderBassPersistentData();
+ if (dev_state.leAudioData != null)
+ if (dev_state.leAudioData.viewsData == null)
+ dev_state.leAudioData.viewsData = new ViewHolderHapPersistentData();
+ }
+
+ notifyDataSetChanged();
+ }
+
+ public interface OnItemClickListener {
+ void onItemClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
+ }
+
+ public interface OnLeAudioInteractionListener {
+ void onConnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
+
+ void onDisconnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
+
+ void onStreamActionClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ Integer group_id, Integer content_type, Integer action);
+
+ void onGroupSetClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ Integer group_id);
+
+ void onGroupUnsetClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ Integer group_id);
+
+ void onGroupDestroyClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ Integer group_id);
+
+ void onGroupSetLockClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ Integer group_id, boolean lock);
+
+ void onMicrophoneMuteChanged(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ boolean mute, boolean is_from_user);
+ }
+
+ public interface OnVolumeControlInteractionListener {
+ void onConnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
+
+ void onDisconnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
+
+ void onVolumeChanged(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int value,
+ boolean is_from_user);
+
+ void onCheckedChanged(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ boolean is_checked);
+
+ void onInputGetStateButtonClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int input_id);
+
+ void onInputGainValueChanged(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int input_id, int value);
+
+ void onInputMuteSwitched(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int input_id,
+ boolean is_muted);
+
+ void onInputSetGainModeButtonClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int input_id, boolean is_auto);
+
+ void onInputGetGainPropsButtonClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int input_id);
+
+ void onInputGetTypeButtonClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int input_id);
+
+ void onInputGetStatusButton(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int input_id);
+
+ void onInputGetDescriptionButtonClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int input_id);
+
+ void onInputSetDescriptionButtonClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int input_id, String description);
+
+ void onOutputGetGainButtonClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int output_id);
+
+ void onOutputGainOffsetGainValueChanged(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int output_id, int value);
+
+ void onOutputGetLocationButtonClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int output_id);
+
+ void onOutputSetLocationButtonClicked(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int output_id, int location);
+
+ void onOutputGetDescriptionButtonClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int output_id);
+
+ void onOutputSetDescriptionButton(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int output_id, String description);
+ }
+
+ public interface OnBassInteractionListener {
+ void onConnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
+
+ void onDisconnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
+
+ void onReceiverSelected(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int receiver_id);
+
+ void onStopSyncReq(BluetoothDevice device, int receiver_id);
+
+ void onRemoveSourceReq(BluetoothDevice device, int receiver_id);
+
+ void onStopObserving();
+ }
+
+ public interface OnHapInteractionListener {
+ void onConnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
+
+ void onDisconnectClick(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper);
+
+ void onChangePresetNameClicked(BluetoothDevice device, int preset_index, String name);
+
+ void onReadPresetInfoClicked(BluetoothDevice device, int preset_index);
+
+ void onSetActivePresetClicked(BluetoothDevice device, int preset_index);
+
+ void onNextDevicePresetClicked(BluetoothDevice device);
+
+ void onPreviousDevicePresetClicked(BluetoothDevice device);
+
+ void onNextGroupPresetClicked(BluetoothDevice device);
+
+ void onPreviousGroupPresetClicked(BluetoothDevice device);
+ }
+
+ public class ViewHolder extends RecyclerView.ViewHolder {
+ private final TextView deviceName;
+
+ // Le Audio View stuff
+ private Switch leAudioConnectionSwitch;
+ private Button leAudioStartStreamButton;
+ private Button leAudioStopStreamButton;
+ private Button leAudioSuspendStreamButton;
+ private Button leAudioGroupSetButton;
+ private Button leAudioGroupUnsetButton;
+ private Button leAudioGroupDestroyButton;
+ private TextView leAudioGroupIdText;
+ private TextView leAudioGroupStatusText;
+ private TextView leAudioGroupFlagsText;
+
+ // Iso Set stuff
+ private Button leAudioSetLockButton;
+ private Button leAudioSetUnlockButton;
+ private TextView leAudioSetLockStateText;
+
+ // LeAudio Microphone stuff
+ private Switch leAudioGroupMicrophoneSwitch;
+ private TextView leAudioGroupMicrophoneState;
+
+ // LeAudio HAP stuff
+ private Switch hapConnectionSwitch;
+ private TextView leAudioHapState;
+ private TextView leAudioHapFeatures;
+ private TextView leAudioHapActivePresetIndex;
+ private Spinner leAudioHapPresetsSpinner;
+ private Button leAudioHapChangePresetNameButton;
+ private Button leAudioHapSetActivePresetButton;
+ private Button leAudioHapReadPresetInfoButton;
+ private Button leAudioHapNextDevicePresetButton;
+ private Button leAudioHapPreviousDevicePresetButton;
+ private Button leAudioHapNextGroupPresetButton;
+ private Button leAudioHapPreviousGroupPresetButton;
+
+ // VC View stuff
+ private Switch vcConnectionSwitch;
+ private SeekBar volumeSeekBar;
+ private Switch muteSwitch;
+ // VC Ext Input stuff
+ private ImageButton inputFoldableIcon;
+ private View inputFoldable;
+ private Spinner inputIdxSpinner;
+ private ImageButton inputGetStateButton;
+ private SeekBar inputGainSeekBar;
+ private Switch inputMuteSwitch;
+ private ImageButton inputSetGainModeButton;
+ private ImageButton inputGetGainPropsButton;
+ private ImageButton inputGetTypeButton;
+ private ImageButton inputGetStatusButton;
+ private ImageButton inputGetDescriptionButton;
+ private ImageButton inputSetDescriptionButton;
+ private TextView inputGainModeText;
+ private TextView inputGainPropsUnitText;
+ private TextView inputGainPropsMinText;
+ private TextView inputGainPropsMaxText;
+ private TextView inputTypeText;
+ private TextView inputStatusText;
+ private TextView inputDescriptionText;
+ // VC Ext Output stuff
+ private ImageButton outputFoldableIcon;
+ private View outputFoldable;
+ private Spinner outputIdxSpinner;
+ private ImageButton outpuGetGainButton;
+ private SeekBar outputGainOffsetSeekBar;
+ private ImageButton outputGetLocationButton;
+ private ImageButton outputSetLocationButton;
+ private ImageButton outputGetDescriptionButton;
+ private ImageButton outputSetDescriptionButton;
+ private TextView outputLocationText;
+ private TextView outputDescriptionText;
+
+ // BASS View stuff
+ private Switch bassConnectionSwitch;
+ private Spinner bassReceiverIdSpinner;
+ private TextView bassReceiverStateText;
+
+ public ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ deviceName = itemView.findViewById(R.id.device_name);
+
+ SetupLeAudioView(itemView);
+ setupVcView(itemView);
+ setupHapView(itemView);
+
+ // Notify viewmodel via parent's click listener
+ itemView.setOnClickListener(view -> {
+ Integer position = getAdapterPosition();
+ if (clickListener != null && position != RecyclerView.NO_POSITION) {
+ clickListener.onItemClick(devices.get(position));
+ }
+ });
+ }
+
+ private void setupHapView(@NonNull View itemView) {
+ hapConnectionSwitch = itemView.findViewById(R.id.hap_switch);
+ hapConnectionSwitch.setActivated(true);
+
+ hapConnectionSwitch.setOnCheckedChangeListener((compoundButton, b) -> {
+ if (!compoundButton.isActivated())
+ return;
+
+ if (bassInteractionListener != null) {
+ if (b)
+ hapInteractionListener
+ .onConnectClick(devices.get(ViewHolder.this.getAdapterPosition()));
+ else
+ hapInteractionListener.onDisconnectClick(
+ devices.get(ViewHolder.this.getAdapterPosition()));
+ }
+ });
+
+ leAudioHapState = itemView.findViewById(R.id.hap_profile_state_text);
+ leAudioHapFeatures = itemView.findViewById(R.id.hap_profile_features_text);
+ leAudioHapActivePresetIndex =
+ itemView.findViewById(R.id.hap_profile_active_preset_index_text);
+ leAudioHapPresetsSpinner = itemView.findViewById(R.id.hap_presets_spinner);
+ leAudioHapChangePresetNameButton =
+ itemView.findViewById(R.id.hap_change_preset_name_button);
+ leAudioHapSetActivePresetButton =
+ itemView.findViewById(R.id.hap_set_active_preset_button);
+ leAudioHapReadPresetInfoButton =
+ itemView.findViewById(R.id.hap_read_preset_info_button);
+ leAudioHapNextDevicePresetButton =
+ itemView.findViewById(R.id.hap_next_device_preset_button);
+ leAudioHapPreviousDevicePresetButton =
+ itemView.findViewById(R.id.hap_previous_device_preset_button);
+ leAudioHapNextGroupPresetButton =
+ itemView.findViewById(R.id.hap_next_group_preset_button);
+ leAudioHapPreviousGroupPresetButton =
+ itemView.findViewById(R.id.hap_previous_group_preset_button);
+
+ leAudioHapPresetsSpinner
+ .setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> adapterView, View view,
+ int position, long l) {
+ LeAudioDeviceStateWrapper device =
+ devices.get(ViewHolder.this.getAdapterPosition());
+ ((ViewHolderHapPersistentData) device.leAudioData.viewsData).selectedPresetPositionMutable
+ .setValue(position);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> adapterView) {
+ // Nothing to do here
+ }
+ });
+
+ leAudioHapChangePresetNameButton.setOnClickListener(view -> {
+ if (hapInteractionListener != null) {
+ if (leAudioHapPresetsSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known preset, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext());
+ alert.setTitle("Set a name");
+ final EditText input = new EditText(itemView.getContext());
+ alert.setView(input);
+ alert.setPositiveButton("Ok", (dialog, whichButton) -> {
+ Integer index = Integer.valueOf(leAudioHapPresetsSpinner.getSelectedItem()
+ .toString().split("\\s")[0]);
+ hapInteractionListener.onChangePresetNameClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()).device, index,
+ input.getText().toString());
+ });
+ alert.setNegativeButton("Cancel", (dialog, whichButton) -> {
+ // Do nothing
+ });
+ alert.show();
+ }
+ });
+
+ leAudioHapSetActivePresetButton.setOnClickListener(view -> {
+ if (hapInteractionListener != null) {
+ if (leAudioHapPresetsSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known preset, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Integer index = Integer.valueOf(
+ leAudioHapPresetsSpinner.getSelectedItem().toString().split("\\s")[0]);
+ hapInteractionListener.onSetActivePresetClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()).device, index);
+ }
+ });
+
+ leAudioHapReadPresetInfoButton.setOnClickListener(view -> {
+ if (hapInteractionListener != null) {
+ if (leAudioHapPresetsSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known preset, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Integer index = Integer.valueOf(
+ leAudioHapPresetsSpinner.getSelectedItem().toString().split("\\s")[0]);
+ hapInteractionListener.onReadPresetInfoClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()).device, index);
+ }
+ });
+
+ leAudioHapNextDevicePresetButton.setOnClickListener(view -> {
+ if (hapInteractionListener != null) {
+ hapInteractionListener.onNextDevicePresetClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()).device);
+ }
+ });
+
+ leAudioHapPreviousDevicePresetButton.setOnClickListener(view -> {
+ if (hapInteractionListener != null) {
+ hapInteractionListener.onPreviousDevicePresetClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()).device);
+ }
+ });
+
+ leAudioHapNextGroupPresetButton.setOnClickListener(view -> {
+ if (hapInteractionListener != null) {
+ hapInteractionListener.onNextGroupPresetClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()).device);
+ }
+ });
+
+ leAudioHapPreviousGroupPresetButton.setOnClickListener(view -> {
+ if (hapInteractionListener != null) {
+ hapInteractionListener.onPreviousGroupPresetClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()).device);
+ }
+ });
+ }
+
+ private void SetupLeAudioView(@NonNull View itemView) {
+ leAudioConnectionSwitch = itemView.findViewById(R.id.le_audio_switch);
+ leAudioStartStreamButton = itemView.findViewById(R.id.start_stream_button);
+ leAudioStopStreamButton = itemView.findViewById(R.id.stop_stream_button);
+ leAudioSuspendStreamButton = itemView.findViewById(R.id.suspend_stream_button);
+ leAudioGroupSetButton = itemView.findViewById(R.id.group_set_button);
+ leAudioGroupUnsetButton = itemView.findViewById(R.id.group_unset_button);
+ leAudioGroupDestroyButton = itemView.findViewById(R.id.group_destroy_button);
+ leAudioGroupIdText = itemView.findViewById(R.id.group_id_text);
+ leAudioGroupStatusText = itemView.findViewById(R.id.group_status_text);
+ leAudioGroupFlagsText = itemView.findViewById(R.id.group_flags_text);
+ leAudioSetLockButton = itemView.findViewById(R.id.set_lock_button);
+ leAudioSetUnlockButton = itemView.findViewById(R.id.set_unlock_button);
+ leAudioSetLockStateText = itemView.findViewById(R.id.lock_state_text);
+ leAudioGroupMicrophoneSwitch = itemView.findViewById(R.id.group_mic_mute_state_switch);
+ leAudioGroupMicrophoneState = itemView.findViewById(R.id.group_mic_mute_state_text);
+
+ leAudioConnectionSwitch.setOnCheckedChangeListener((compoundButton, b) -> {
+ if (!compoundButton.isActivated())
+ return;
+
+ if (leAudioInteractionListener != null) {
+ if (b)
+ leAudioInteractionListener
+ .onConnectClick(devices.get(ViewHolder.this.getAdapterPosition()));
+ else
+ leAudioInteractionListener.onDisconnectClick(
+ devices.get(ViewHolder.this.getAdapterPosition()));
+ }
+ });
+
+ leAudioStartStreamButton.setOnClickListener(view -> {
+ AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext());
+ alert.setTitle("Pick a content type");
+ NumberPicker input = new NumberPicker(itemView.getContext());
+ input.setMinValue(1);
+ input.setMaxValue(
+ itemView.getResources().getStringArray(R.array.content_types).length - 1);
+ input.setDisplayedValues(
+ itemView.getResources().getStringArray(R.array.content_types));
+ alert.setView(input);
+ alert.setPositiveButton("Ok", (dialog, whichButton) -> {
+ final Integer group_id = Integer
+ .parseInt(ViewHolder.this.leAudioGroupIdText.getText().toString());
+ if (leAudioInteractionListener != null && group_id != null)
+ leAudioInteractionListener.onStreamActionClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), group_id,
+ 1 << (input.getValue() - 1), 0);
+ });
+ alert.setNegativeButton("Cancel", (dialog, whichButton) -> {
+ // Do nothing
+ });
+ alert.show();
+ });
+
+ leAudioSuspendStreamButton.setOnClickListener(view -> {
+ final Integer group_id =
+ Integer.parseInt(ViewHolder.this.leAudioGroupIdText.getText().toString());
+ if (leAudioInteractionListener != null && group_id != null)
+ leAudioInteractionListener.onStreamActionClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), group_id, 0, 1);
+ });
+
+ leAudioStopStreamButton.setOnClickListener(view -> {
+ final Integer group_id =
+ Integer.parseInt(ViewHolder.this.leAudioGroupIdText.getText().toString());
+ if (leAudioInteractionListener != null && group_id != null)
+ leAudioInteractionListener.onStreamActionClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), group_id, 0, 2);
+ });
+
+ leAudioGroupSetButton.setOnClickListener(view -> {
+ AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext());
+ alert.setTitle("Pick a group ID");
+ final EditText input = new EditText(itemView.getContext());
+ input.setInputType(InputType.TYPE_CLASS_NUMBER);
+ input.setRawInputType(Configuration.KEYBOARD_12KEY);
+ alert.setView(input);
+ alert.setPositiveButton("Ok", (dialog, whichButton) -> {
+ final Integer group_id = Integer.valueOf(input.getText().toString());
+ leAudioInteractionListener.onGroupSetClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), group_id);
+ });
+ alert.setNegativeButton("Cancel", (dialog, whichButton) -> {
+ // Do nothing
+ });
+ alert.show();
+ });
+
+ leAudioGroupUnsetButton.setOnClickListener(view -> {
+ final Integer group_id = Integer.parseInt(
+ ViewHolder.this.leAudioGroupIdText.getText().toString().equals("Unknown")
+ ? "0"
+ : ViewHolder.this.leAudioGroupIdText.getText().toString());
+ if (leAudioInteractionListener != null)
+ leAudioInteractionListener.onGroupUnsetClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), group_id);
+ });
+
+ leAudioGroupDestroyButton.setOnClickListener(view -> {
+ final Integer group_id =
+ Integer.parseInt(ViewHolder.this.leAudioGroupIdText.getText().toString());
+ if (leAudioInteractionListener != null)
+ leAudioInteractionListener.onGroupDestroyClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), group_id);
+ });
+
+ leAudioSetLockButton.setOnClickListener(view -> {
+ final Integer group_id =
+ Integer.parseInt(ViewHolder.this.leAudioGroupIdText.getText().toString());
+ if (leAudioInteractionListener != null)
+ leAudioInteractionListener.onGroupSetLockClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), group_id, true);
+ });
+
+ leAudioSetUnlockButton.setOnClickListener(view -> {
+ final Integer group_id =
+ Integer.parseInt(ViewHolder.this.leAudioGroupIdText.getText().toString());
+ if (leAudioInteractionListener != null)
+ leAudioInteractionListener.onGroupSetLockClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), group_id, false);
+ });
+
+ leAudioGroupMicrophoneSwitch.setOnCheckedChangeListener((compoundButton, b) -> {
+ if (!compoundButton.isActivated())
+ return;
+
+ if (leAudioInteractionListener != null)
+ leAudioInteractionListener.onMicrophoneMuteChanged(
+ devices.get(ViewHolder.this.getAdapterPosition()), b, true);
+ });
+ }
+
+ private void setupVcView(@NonNull View itemView) {
+ vcConnectionSwitch = itemView.findViewById(R.id.vc_switch);
+ vcConnectionSwitch.setActivated(true);
+ volumeSeekBar = itemView.findViewById(R.id.volume_seek_bar);
+ muteSwitch = itemView.findViewById(R.id.mute_switch);
+ muteSwitch.setActivated(true);
+ inputFoldableIcon = itemView.findViewById(R.id.vc_input_foldable_icon);
+ inputFoldable = itemView.findViewById(R.id.ext_input_foldable);
+ inputIdxSpinner = itemView.findViewById(R.id.num_inputs_spinner);
+ inputGetStateButton = itemView.findViewById(R.id.inputGetStateButton);
+ inputGainSeekBar = itemView.findViewById(R.id.inputGainSeekBar);
+ inputMuteSwitch = itemView.findViewById(R.id.inputMuteSwitch);
+ inputMuteSwitch.setActivated(true);
+ inputSetGainModeButton = itemView.findViewById(R.id.inputSetGainModeButton);
+ inputGetGainPropsButton = itemView.findViewById(R.id.inputGetGainPropsButton);
+ inputGetTypeButton = itemView.findViewById(R.id.inputGetTypeButton);
+ inputGetStatusButton = itemView.findViewById(R.id.inputGetStatusButton);
+ inputGetDescriptionButton = itemView.findViewById(R.id.inputGetDescriptionButton);
+ inputSetDescriptionButton = itemView.findViewById(R.id.inputSetDescriptionButton);
+ inputGainModeText = itemView.findViewById(R.id.inputGainModeText);
+ inputGainPropsUnitText = itemView.findViewById(R.id.inputGainPropsUnitText);
+ inputGainPropsMinText = itemView.findViewById(R.id.inputGainPropsMinText);
+ inputGainPropsMaxText = itemView.findViewById(R.id.inputGainPropsMaxText);
+ inputTypeText = itemView.findViewById(R.id.inputTypeText);
+ inputStatusText = itemView.findViewById(R.id.inputStatusText);
+ inputDescriptionText = itemView.findViewById(R.id.inputDescriptionText);
+
+ outputFoldableIcon = itemView.findViewById(R.id.vc_output_foldable_icon);
+ outputFoldable = itemView.findViewById(R.id.ext_output_foldable);
+ outputIdxSpinner = itemView.findViewById(R.id.num_outputs_spinner);
+ outpuGetGainButton = itemView.findViewById(R.id.outputGetGainButton);
+ outputGainOffsetSeekBar = itemView.findViewById(R.id.outputGainSeekBar);
+ outputGetLocationButton = itemView.findViewById(R.id.outputGetLocationButton);
+ outputSetLocationButton = itemView.findViewById(R.id.outputSetLocationButton);
+ outputGetDescriptionButton = itemView.findViewById(R.id.outputGetDescriptionButton);
+ outputSetDescriptionButton = itemView.findViewById(R.id.outputSetDescriptionButton);
+ outputLocationText = itemView.findViewById(R.id.outputLocationText);
+ outputDescriptionText = itemView.findViewById(R.id.outputDescriptionText);
+
+ vcConnectionSwitch.setOnCheckedChangeListener((compoundButton, b) -> {
+ if (!compoundButton.isActivated())
+ return;
+
+ if (volumeControlInteractionListener != null) {
+ if (b)
+ volumeControlInteractionListener
+ .onConnectClick(devices.get(ViewHolder.this.getAdapterPosition()));
+ else
+ volumeControlInteractionListener.onDisconnectClick(
+ devices.get(ViewHolder.this.getAdapterPosition()));
+ }
+ });
+
+ volumeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
+ // Nothing to do here
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // Nothing to do here
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // Set value only on release
+ if (volumeControlInteractionListener != null)
+ volumeControlInteractionListener.onVolumeChanged(
+ devices.get(ViewHolder.this.getAdapterPosition()),
+ seekBar.getProgress(), true);
+ }
+ });
+
+ muteSwitch.setOnCheckedChangeListener((compoundButton, b) -> {
+ if (!compoundButton.isActivated())
+ return;
+
+ if (volumeControlInteractionListener != null)
+ volumeControlInteractionListener
+ .onCheckedChanged(devices.get(ViewHolder.this.getAdapterPosition()), b);
+ });
+
+ inputFoldableIcon.setOnClickListener(view -> {
+ ViewHolderVcPersistentData vData = (ViewHolderVcPersistentData) devices
+ .get(ViewHolder.this.getAdapterPosition()).volumeControlData.viewsData;
+ if (vData != null)
+ vData.isInputsCollapsedMutable
+ .setValue(!vData.isInputsCollapsedMutable.getValue());
+ });
+
+ inputIdxSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> adapterView, View view, int position,
+ long l) {
+ Integer index = ViewHolder.this.getAdapterPosition();
+ ((ViewHolderVcPersistentData) devices
+ .get(index).volumeControlData.viewsData).selectedInputPosition =
+ position;
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> adapterView) {
+ // Nothing to do here
+ }
+ });
+
+ outputFoldableIcon.setOnClickListener(view -> {
+ ViewHolderVcPersistentData vData = (ViewHolderVcPersistentData) devices
+ .get(ViewHolder.this.getAdapterPosition()).volumeControlData.viewsData;
+ vData.isOutputsCollapsedMutable
+ .setValue(!vData.isOutputsCollapsedMutable.getValue());
+ });
+
+ outputIdxSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> adapterView, View view, int position,
+ long l) {
+ Integer index = ViewHolder.this.getAdapterPosition();
+ ((ViewHolderVcPersistentData) devices
+ .get(index).volumeControlData.viewsData).selectedOutputPosition =
+ position;
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> adapterView) {
+ // Nothing to do here
+ }
+ });
+
+ inputGetStateButton.setOnClickListener(view -> {
+ if (volumeControlInteractionListener != null) {
+ if (inputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known ext. input, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Integer input_id =
+ Integer.valueOf(inputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onInputGetStateButtonClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), input_id);
+ }
+ });
+
+ inputGainSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int i, boolean is_user_set) {
+ // Nothing to do here
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // Nothing to do here
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (volumeControlInteractionListener != null) {
+ if (inputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(seekBar.getContext(),
+ "No known ext. input, please reconnect.", Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+ Integer input_id =
+ Integer.valueOf(inputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onInputGainValueChanged(
+ devices.get(ViewHolder.this.getAdapterPosition()), input_id,
+ seekBar.getProgress());
+ }
+ }
+ });
+
+ inputMuteSwitch.setOnCheckedChangeListener((compoundButton, b) -> {
+ if (!compoundButton.isActivated())
+ return;
+
+ if (volumeControlInteractionListener != null) {
+ if (inputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(compoundButton.getContext(),
+ "No known ext. input, please reconnect.", Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+ Integer input_id =
+ Integer.valueOf(inputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onInputMuteSwitched(
+ devices.get(ViewHolder.this.getAdapterPosition()), input_id, b);
+ }
+ });
+
+ inputSetGainModeButton.setOnClickListener(view -> {
+ if (volumeControlInteractionListener != null) {
+ if (inputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known ext. input, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext());
+ alert.setTitle("Select Gain mode");
+ NumberPicker input = new NumberPicker(itemView.getContext());
+ input.setMinValue(0);
+ input.setMaxValue(2);
+ input.setDisplayedValues(
+ itemView.getResources().getStringArray(R.array.gain_modes));
+ alert.setView(input);
+ alert.setPositiveButton("Ok", (dialog, whichButton) -> {
+ Integer input_id =
+ Integer.valueOf(inputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onInputSetGainModeButtonClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), input_id,
+ input.getValue() == 2);
+ });
+ alert.setNegativeButton("Cancel", (dialog, whichButton) -> {
+ // Do nothing
+ });
+ alert.show();
+ }
+ });
+
+ inputGetGainPropsButton.setOnClickListener(view -> {
+ if (volumeControlInteractionListener != null) {
+ if (inputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known ext. input, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Integer input_id =
+ Integer.valueOf(inputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onInputGetGainPropsButtonClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), input_id);
+ }
+ });
+
+ inputGetTypeButton.setOnClickListener(view -> {
+ if (volumeControlInteractionListener != null) {
+ if (inputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known ext. input, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Integer input_id =
+ Integer.valueOf(inputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onInputGetTypeButtonClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), input_id);
+ }
+ });
+
+ inputGetStatusButton.setOnClickListener(view -> {
+ if (volumeControlInteractionListener != null) {
+ if (inputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known ext. input, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Integer input_id =
+ Integer.valueOf(inputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onInputGetStatusButton(
+ devices.get(ViewHolder.this.getAdapterPosition()), input_id);
+ }
+ });
+
+ inputGetDescriptionButton.setOnClickListener(view -> {
+ if (volumeControlInteractionListener != null) {
+ if (inputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known ext. input, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Integer input_id =
+ Integer.valueOf(inputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onInputGetDescriptionButtonClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), input_id);
+ }
+ });
+
+ inputSetDescriptionButton.setOnClickListener(view -> {
+ if (volumeControlInteractionListener != null) {
+ if (inputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known ext. input, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext());
+ alert.setTitle("Set a description");
+ final EditText input = new EditText(itemView.getContext());
+ alert.setView(input);
+ alert.setPositiveButton("Ok", (dialog, whichButton) -> {
+ Integer input_id =
+ Integer.valueOf(inputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onInputSetDescriptionButtonClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), input_id,
+ input.getText().toString());
+ });
+ alert.setNegativeButton("Cancel", (dialog, whichButton) -> {
+ // Do nothing
+ });
+ alert.show();
+ }
+ });
+
+ outpuGetGainButton.setOnClickListener(view -> {
+ if (outputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known ext. output, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Integer output_id = Integer.valueOf(outputIdxSpinner.getSelectedItem().toString());
+ if (volumeControlInteractionListener != null)
+ volumeControlInteractionListener.onOutputGetGainButtonClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), output_id);
+ });
+
+ outputGainOffsetSeekBar
+ .setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int value,
+ boolean is_from_user) {
+ // Do nothing here
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // Do nothing here
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (outputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(seekBar.getContext(),
+ "No known ext. output, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Integer output_id =
+ Integer.valueOf(outputIdxSpinner.getSelectedItem().toString());
+ if (volumeControlInteractionListener != null)
+ volumeControlInteractionListener.onOutputGainOffsetGainValueChanged(
+ devices.get(ViewHolder.this.getAdapterPosition()),
+ output_id, seekBar.getProgress());
+ }
+ });
+
+ outputGetLocationButton.setOnClickListener(view -> {
+ if (volumeControlInteractionListener != null) {
+ if (outputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known ext. output, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Integer output_id =
+ Integer.valueOf(outputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onOutputGetLocationButtonClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), output_id);
+ }
+ });
+
+ outputSetLocationButton.setOnClickListener(view -> {
+ if (volumeControlInteractionListener != null) {
+ if (outputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known ext. output, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext());
+ alert.setTitle("Pick an Audio Location");
+ NumberPicker input = new NumberPicker(itemView.getContext());
+ input.setMinValue(0);
+ input.setMaxValue(
+ itemView.getResources().getStringArray(R.array.audio_locations).length
+ - 1);
+ input.setDisplayedValues(
+ itemView.getResources().getStringArray(R.array.audio_locations));
+ alert.setView(input);
+ alert.setPositiveButton("Ok", (dialog, whichButton) -> {
+ Integer output_id =
+ Integer.valueOf(outputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onOutputSetLocationButtonClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), output_id,
+ input.getValue());
+ });
+ alert.setNegativeButton("Cancel", (dialog, whichButton) -> {
+ // Do nothing
+ });
+ alert.show();
+ }
+ });
+
+ outputGetDescriptionButton.setOnClickListener(view -> {
+ if (volumeControlInteractionListener != null) {
+ if (outputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known ext. output, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Integer output_id =
+ Integer.valueOf(outputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onOutputGetDescriptionButtonClicked(
+ devices.get(ViewHolder.this.getAdapterPosition()), output_id);
+ }
+ });
+
+ outputSetDescriptionButton.setOnClickListener(view -> {
+ if (volumeControlInteractionListener != null) {
+ if (outputIdxSpinner.getSelectedItem() == null) {
+ Toast.makeText(view.getContext(), "No known ext. output, please reconnect.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(itemView.getContext());
+ alert.setTitle("Set a description");
+ final EditText input = new EditText(itemView.getContext());
+ alert.setView(input);
+ alert.setPositiveButton("Ok", (dialog, whichButton) -> {
+ Integer output_id =
+ Integer.valueOf(outputIdxSpinner.getSelectedItem().toString());
+ volumeControlInteractionListener.onOutputSetDescriptionButton(
+ devices.get(ViewHolder.this.getAdapterPosition()), output_id,
+ input.getText().toString());
+ });
+ alert.setNegativeButton("Cancel", (dialog, whichButton) -> {
+ // Do nothing
+ });
+ alert.show();
+ }
+ });
+ }
+ }
+
+ private class ViewHolderVcPersistentData {
+ Integer selectedInputPosition;
+ Integer selectedOutputPosition;
+
+ MutableLiveData<Boolean> isInputsCollapsedMutable = new MutableLiveData<>();
+ MutableLiveData<Boolean> isOutputsCollapsedMutable = new MutableLiveData<>();
+ }
+
+ private class ViewHolderBassPersistentData {
+ MutableLiveData<Integer> selectedReceiverPositionMutable = new MutableLiveData<>();
+ }
+
+ private class ViewHolderHapPersistentData {
+ MutableLiveData<Integer> selectedPresetPositionMutable = new MutableLiveData<>();
+ }
+}
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioViewModel.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioViewModel.java
new file mode 100644
index 0000000000..3d6bf34fc8
--- /dev/null
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/LeAudioViewModel.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.leaudio;
+
+import android.app.Application;
+import android.bluetooth.BluetoothDevice;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import java.util.List;
+
+public class LeAudioViewModel extends AndroidViewModel {
+ private final BluetoothProxy bluetoothProxy;
+
+ public LeAudioViewModel(@NonNull Application application) {
+ super(application);
+ bluetoothProxy = BluetoothProxy.getBluetoothProxy(application);
+ bluetoothProxy.initProfiles();
+ }
+
+ @Override
+ public void onCleared() {
+ bluetoothProxy.cleanupProfiles();
+ }
+
+ public void queryDevices() {
+ bluetoothProxy.queryLeAudioDevices();
+ }
+
+ public void connectLeAudio(BluetoothDevice device, boolean connect) {
+ bluetoothProxy.connectLeAudio(device, connect);
+ }
+
+ public void streamAction(Integer group_id, int action, Integer content_type) {
+ bluetoothProxy.streamAction(group_id, action, content_type);
+ }
+
+ public void groupSet(BluetoothDevice device, Integer group_id) {
+ bluetoothProxy.groupSet(device, group_id);
+ }
+
+ public void groupUnset(BluetoothDevice device, Integer group_id) {
+ bluetoothProxy.groupUnset(device, group_id);
+ }
+
+ public void groupSetLock(Integer group_id, boolean lock) {
+ bluetoothProxy.groupSetLock(group_id, lock);
+ }
+
+ public void setVolume(BluetoothDevice device, int volume) {
+ bluetoothProxy.setVolume(device, volume);
+ }
+
+ public void connectHap(BluetoothDevice device, boolean connect) {
+ bluetoothProxy.connectHap(device, connect);
+ }
+
+ public void hapReadPresetInfo(BluetoothDevice device, int preset_index) {
+ bluetoothProxy.hapReadPresetInfo(device, preset_index);
+ }
+
+ public void hapSetActivePreset(BluetoothDevice device, int preset_index) {
+ bluetoothProxy.hapSetActivePreset(device, preset_index);
+ }
+
+ public void hapChangePresetName(BluetoothDevice device, int preset_index, String name) {
+ bluetoothProxy.hapChangePresetName(device, preset_index, name);
+ }
+
+ public void hapPreviousDevicePreset(BluetoothDevice device) {
+ bluetoothProxy.hapPreviousDevicePreset(device);
+ }
+
+ public void hapNextDevicePreset(BluetoothDevice device) {
+ bluetoothProxy.hapNextDevicePreset(device);
+ }
+
+ public boolean hapPreviousGroupPreset(int group_id) {
+ return bluetoothProxy.hapPreviousGroupPreset(group_id);
+ }
+
+ public boolean hapNextGroupPreset(int group_id) {
+ return bluetoothProxy.hapNextGroupPreset(group_id);
+ }
+
+ public int hapGetHapGroup(BluetoothDevice device) {
+ return bluetoothProxy.hapGetHapGroup(device);
+ }
+
+ public LiveData<Boolean> getBluetoothEnabledLive() {
+ return bluetoothProxy.getBluetoothEnabled();
+ }
+
+ public LiveData<List<LeAudioDeviceStateWrapper>> getAllLeAudioDevicesLive() {
+ return bluetoothProxy.getAllLeAudioDevices();
+ }
+
+ public boolean isLeAudioBroadcastSourceSupported() {
+ return bluetoothProxy.isLeAudioBroadcastSourceSupported();
+ }
+}
diff --git a/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/MainActivity.java b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/MainActivity.java
new file mode 100644
index 0000000000..33410073ee
--- /dev/null
+++ b/android/leaudio/app/src/main/java/com/android/bluetooth/leaudio/MainActivity.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.leaudio;
+
+import android.Manifest;
+import android.animation.ObjectAnimator;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import java.util.List;
+import java.util.Objects;
+
+import com.android.bluetooth.leaudio.R;
+
+public class MainActivity extends AppCompatActivity {
+ private static final String[] REQUIRED_PERMISSIONS = new String[] {
+ Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_PRIVILEGED,
+ Manifest.permission.BLUETOOTH_ADVERTISE, Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+ Manifest.permission.ACCESS_FINE_LOCATION,};
+ LeAudioRecycleViewAdapter recyclerViewAdapter;
+ private LeAudioViewModel leAudioViewModel;
+
+ /** Returns true if any of the required permissions is missing. */
+ private boolean isPermissionMissing() {
+ for (String permission : REQUIRED_PERMISSIONS) {
+ if (ContextCompat.checkSelfPermission(this,
+ permission) != PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void initialize() {
+ setContentView(R.layout.activity_main);
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ // Setup each component
+ setupLeAudioViewModel();
+ setupRecyclerViewAdapter();
+ setupViewModelObservers();
+
+ // The 'refresh devices' button
+ FloatingActionButton fab = findViewById(R.id.fab);
+ fab.setOnClickListener(view -> {
+ leAudioViewModel.queryDevices();
+ ObjectAnimator.ofFloat(fab, "rotation", 0f, 360f).setDuration(500).start();
+ });
+ }
+
+ /** Request permission if missing. */
+ private void setupPermissions() {
+ if (isPermissionMissing()) {
+ ActivityResultLauncher<String[]> permissionLauncher = registerForActivityResult(
+ new ActivityResultContracts.RequestMultiplePermissions(), result -> {
+ for (String permission : REQUIRED_PERMISSIONS) {
+ if (!Objects.requireNonNull(result.get(permission))) {
+ Toast.makeText(getApplicationContext(),
+ "LeAudio test apk permission denied.", Toast.LENGTH_SHORT)
+ .show();
+ finish();
+ return;
+ }
+ }
+ initialize();
+ });
+
+ permissionLauncher.launch(REQUIRED_PERMISSIONS);
+ } else {
+ initialize();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setupPermissions();
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ cleanupLeAudioViewModel();
+ cleanupRecyclerViewAdapter();
+ cleanupViewModelObservers();
+
+ FloatingActionButton fab = findViewById(R.id.fab);
+ fab.setOnClickListener(null);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ Intent intent = null;
+
+ switch (item.getItemId()) {
+ case R.id.action_broadcast:
+ if (leAudioViewModel.getBluetoothEnabledLive().getValue() == null
+ || !leAudioViewModel.getBluetoothEnabledLive().getValue()) {
+ Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ startActivityForResult(enableBtIntent, 1);
+ } else if (leAudioViewModel.isLeAudioBroadcastSourceSupported()) {
+ intent = new Intent(MainActivity.this, BroadcasterActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ startActivityForResult(intent, 0);
+ } else {
+ Toast.makeText(MainActivity.this, "Broadcast Source is not supported.",
+ Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ default:
+ // If we got here, the user's action was not recognized.
+ // Invoke the superclass to handle it.onCreate
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ super.onActivityResult(requestCode, resultCode, intent);
+
+ // check if the request code is same as what was passed in request
+ if (requestCode == 0xc0de) {
+ if (intent != null) {
+ String message = intent.getStringExtra("MESSAGE");
+ Toast.makeText(MainActivity.this, message + "(" + resultCode + ")",
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ finishActivity(0);
+ super.onBackPressed();
+ }
+
+ private void setupLeAudioViewModel() {
+ leAudioViewModel = ViewModelProviders.of(this).get(LeAudioViewModel.class);
+
+ // Observe bluetooth adapter state
+ leAudioViewModel.getBluetoothEnabledLive().observe(this, is_enabled -> {
+ if (is_enabled) {
+ List<LeAudioDeviceStateWrapper> deviceList =
+ leAudioViewModel.getAllLeAudioDevicesLive().getValue();
+ if (deviceList == null || deviceList.size() == 0)
+ leAudioViewModel.queryDevices();
+ } else {
+ Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ startActivityForResult(enableBtIntent, 1);
+ }
+
+ Toast.makeText(MainActivity.this,
+ "Bluetooth is " + (is_enabled ? "enabled" : "disabled"), Toast.LENGTH_SHORT)
+ .show();
+ });
+ }
+
+ private void cleanupLeAudioViewModel() {
+ leAudioViewModel.getBluetoothEnabledLive().removeObservers(this);
+ }
+
+ void setupRecyclerViewAdapter() {
+ recyclerViewAdapter = new LeAudioRecycleViewAdapter(this);
+
+ // Set listeners
+ setupViewsListItemClickListener();
+ setupViewsProfileUiEventListeners();
+
+ // Generic stuff
+ RecyclerView recyclerView = findViewById(R.id.recycler_view);
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ recyclerView.setAdapter(recyclerViewAdapter);
+ recyclerView.setHasFixedSize(true);
+ }
+
+ void cleanupRecyclerViewAdapter() {
+ cleanupViewsListItemClickListener();
+ cleanupViewsProfileUiEventListeners();
+ }
+
+ private void setupViewsListItemClickListener() {
+ recyclerViewAdapter.setOnItemClickListener(device -> {
+ // Not used anymore
+ });
+ }
+
+ private void cleanupViewsListItemClickListener() {
+ recyclerViewAdapter.setOnItemClickListener(null);
+ }
+
+ private void setupViewsProfileUiEventListeners() {
+ recyclerViewAdapter.setOnLeAudioInteractionListener(
+ new LeAudioRecycleViewAdapter.OnLeAudioInteractionListener() {
+ @Override
+ public void onConnectClick(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+ Toast.makeText(MainActivity.this,
+ "Connecting Le Audio to "
+ + leAudioDeviceStateWrapper.device.toString(),
+ Toast.LENGTH_SHORT).show();
+ leAudioViewModel.connectLeAudio(leAudioDeviceStateWrapper.device, true);
+ }
+
+ @Override
+ public void onDisconnectClick(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+ Toast.makeText(MainActivity.this,
+ "Disconnecting Le Audio from "
+ + leAudioDeviceStateWrapper.device.toString(),
+ Toast.LENGTH_SHORT).show();
+ leAudioViewModel.connectLeAudio(leAudioDeviceStateWrapper.device, false);
+ }
+
+ @Override
+ public void onStreamActionClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, Integer group_id,
+ Integer content_type, Integer action) {
+ leAudioViewModel.streamAction(group_id, action, content_type);
+ }
+
+ @Override
+ public void onGroupSetClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, Integer group_id) {
+ leAudioViewModel.groupSet(leAudioDeviceStateWrapper.device, group_id);
+ }
+
+ @Override
+ public void onGroupUnsetClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, Integer group_id) {
+ leAudioViewModel.groupUnset(leAudioDeviceStateWrapper.device, group_id);
+ }
+
+ @Override
+ public void onGroupDestroyClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, Integer group_id) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onGroupSetLockClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, Integer group_id,
+ boolean lock) {
+ leAudioViewModel.groupSetLock(group_id, lock);
+ }
+
+ @Override
+ public void onMicrophoneMuteChanged(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, boolean mute,
+ boolean is_from_user) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+ });
+
+ recyclerViewAdapter.setOnVolumeControlInteractionListener(
+ new LeAudioRecycleViewAdapter.OnVolumeControlInteractionListener() {
+ @Override
+ public void onConnectClick(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onDisconnectClick(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onVolumeChanged(LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ int volume, boolean is_from_user) {
+ if (is_from_user) {
+ leAudioViewModel.setVolume(leAudioDeviceStateWrapper.device, volume);
+ }
+ }
+
+ @Override
+ public void onCheckedChanged(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper,
+ boolean is_checked) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onInputGetStateButtonClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int input_id) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onInputGainValueChanged(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int input_id,
+ int value) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onInputMuteSwitched(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int input_id,
+ boolean is_muted) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onInputSetGainModeButtonClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int input_id,
+ boolean is_auto) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onInputGetGainPropsButtonClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int input_id) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onInputGetTypeButtonClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int input_id) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onInputGetStatusButton(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int input_id) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onInputGetDescriptionButtonClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int input_id) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onInputSetDescriptionButtonClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int input_id,
+ String description) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onOutputGetGainButtonClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int output_id) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onOutputGainOffsetGainValueChanged(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int output_id,
+ int value) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onOutputGetLocationButtonClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int output_id) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onOutputSetLocationButtonClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int output_id,
+ int location) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onOutputGetDescriptionButtonClicked(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int output_id) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onOutputSetDescriptionButton(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper, int output_id,
+ String description) {
+ // Not available anymore
+ Toast.makeText(MainActivity.this,
+ "Operation not supported on this API version", Toast.LENGTH_SHORT)
+ .show();
+ }
+ });
+
+ recyclerViewAdapter.setOnHapInteractionListener(
+ new LeAudioRecycleViewAdapter.OnHapInteractionListener() {
+ @Override
+ public void onConnectClick(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+ Toast.makeText(MainActivity.this,
+ "Connecting HAP to " + leAudioDeviceStateWrapper.device.toString(),
+ Toast.LENGTH_SHORT).show();
+ leAudioViewModel.connectHap(leAudioDeviceStateWrapper.device, true);
+ }
+
+ @Override
+ public void onDisconnectClick(
+ LeAudioDeviceStateWrapper leAudioDeviceStateWrapper) {
+ Toast.makeText(MainActivity.this,
+ "Disconnecting HAP from "
+ + leAudioDeviceStateWrapper.device.toString(),
+ Toast.LENGTH_SHORT).show();
+ leAudioViewModel.connectHap(leAudioDeviceStateWrapper.device, false);
+ }
+
+ @Override
+ public void onReadPresetInfoClicked(BluetoothDevice device, int preset_index) {
+ leAudioViewModel.hapReadPresetInfo(device, preset_index);
+ }
+
+ @Override
+ public void onSetActivePresetClicked(BluetoothDevice device, int preset_index) {
+ leAudioViewModel.hapSetActivePreset(device, preset_index);
+ }
+
+ @Override
+ public void onChangePresetNameClicked(BluetoothDevice device, int preset_index,
+ String name) {
+ leAudioViewModel.hapChangePresetName(device, preset_index, name);
+ }
+
+ @Override
+ public void onPreviousDevicePresetClicked(BluetoothDevice device) {
+ leAudioViewModel.hapPreviousDevicePreset(device);
+ }
+
+ @Override
+ public void onNextDevicePresetClicked(BluetoothDevice device) {
+ leAudioViewModel.hapNextDevicePreset(device);
+ }
+
+ @Override
+ public void onPreviousGroupPresetClicked(BluetoothDevice device) {
+ final int group_id = leAudioViewModel.hapGetHapGroup(device);
+ final boolean sent = leAudioViewModel.hapPreviousGroupPreset(group_id);
+ if (!sent)
+ Toast.makeText(MainActivity.this,
+ "Group " + group_id + " operation failed", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onNextGroupPresetClicked(BluetoothDevice device) {
+ final int group_id = leAudioViewModel.hapGetHapGroup(device);
+ final boolean sent = leAudioViewModel.hapNextGroupPreset(group_id);
+ if (!sent)
+ Toast.makeText(MainActivity.this,
+ "Group " + group_id + " operation failed", Toast.LENGTH_SHORT)
+ .show();
+ }
+ });
+ }
+
+ private void cleanupViewsProfileUiEventListeners() {
+ recyclerViewAdapter.setOnLeAudioInteractionListener(null);
+ recyclerViewAdapter.setOnVolumeControlInteractionListener(null);
+ recyclerViewAdapter.setOnHapInteractionListener(null);
+ }
+
+ // This sets the initial values and set up the observers
+ private void setupViewModelObservers() {
+ List<LeAudioDeviceStateWrapper> devices =
+ leAudioViewModel.getAllLeAudioDevicesLive().getValue();
+
+ if (devices != null)
+ recyclerViewAdapter.updateLeAudioDeviceList(devices);
+ leAudioViewModel.getAllLeAudioDevicesLive().observe(this, bluetoothDevices -> {
+ recyclerViewAdapter.updateLeAudioDeviceList(bluetoothDevices);
+ });
+ }
+
+ private void cleanupViewModelObservers() {
+ leAudioViewModel.getAllLeAudioDevicesLive().removeObservers(this);
+ }
+}
diff --git a/android/leaudio/app/src/main/res/drawable-anydpi/ic_bluetooth_connected_black.xml b/android/leaudio/app/src/main/res/drawable-anydpi/ic_bluetooth_connected_black.xml
new file mode 100644
index 0000000000..125b5a716f
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable-anydpi/ic_bluetooth_connected_black.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="#000000"
+ android:alpha="0.8">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M7,12l-2,-2 -2,2 2,2 2,-2zM17.71,7.71L12,2h-1v7.59L6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 11,14.41L11,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM13,5.83l1.88,1.88L13,9.59L13,5.83zM14.88,16.29L13,18.17v-3.76l1.88,1.88zM19,10l-2,2 2,2 2,-2 -2,-2z"/>
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable-anydpi/ic_bluetooth_dots_black.xml b/android/leaudio/app/src/main/res/drawable-anydpi/ic_bluetooth_dots_black.xml
new file mode 100644
index 0000000000..0ace30c32c
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable-anydpi/ic_bluetooth_dots_black.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="#000000"
+ android:alpha="0.8">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M11,24h2v-2h-2v2zM7,24h2v-2L7,22v2zM15,24h2v-2h-2v2zM17.71,5.71L12,0h-1v7.59L6.41,3 5,4.41 10.59,10 5,15.59 6.41,17 11,12.41L11,20h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM13,3.83l1.88,1.88L13,7.59L13,3.83zM14.88,14.29L13,16.17v-3.76l1.88,1.88z"/>
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable-hdpi/ic_bluetooth_connected_black.png b/android/leaudio/app/src/main/res/drawable-hdpi/ic_bluetooth_connected_black.png
new file mode 100644
index 0000000000..dd7f34037e
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable-hdpi/ic_bluetooth_connected_black.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/drawable-hdpi/ic_bluetooth_dots_black.png b/android/leaudio/app/src/main/res/drawable-hdpi/ic_bluetooth_dots_black.png
new file mode 100644
index 0000000000..f1d77881f2
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable-hdpi/ic_bluetooth_dots_black.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/drawable-mdpi/ic_bluetooth_connected_black.png b/android/leaudio/app/src/main/res/drawable-mdpi/ic_bluetooth_connected_black.png
new file mode 100644
index 0000000000..93dcb60154
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable-mdpi/ic_bluetooth_connected_black.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/drawable-mdpi/ic_bluetooth_dots_black.png b/android/leaudio/app/src/main/res/drawable-mdpi/ic_bluetooth_dots_black.png
new file mode 100644
index 0000000000..2594d78b56
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable-mdpi/ic_bluetooth_dots_black.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/leaudio/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..1f6bb29060
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillType="evenOdd"
+ android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="78.5885"
+ android:endY="90.9159"
+ android:startX="48.7653"
+ android:startY="61.0927"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable-xhdpi/ic_bluetooth_connected_black.png b/android/leaudio/app/src/main/res/drawable-xhdpi/ic_bluetooth_connected_black.png
new file mode 100644
index 0000000000..d414bc6268
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable-xhdpi/ic_bluetooth_connected_black.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/drawable-xhdpi/ic_bluetooth_dots_black.png b/android/leaudio/app/src/main/res/drawable-xhdpi/ic_bluetooth_dots_black.png
new file mode 100644
index 0000000000..696668fe18
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable-xhdpi/ic_bluetooth_dots_black.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/drawable-xxhdpi/ic_bluetooth_connected_black.png b/android/leaudio/app/src/main/res/drawable-xxhdpi/ic_bluetooth_connected_black.png
new file mode 100644
index 0000000000..7869a8d9d7
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable-xxhdpi/ic_bluetooth_connected_black.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/drawable-xxhdpi/ic_bluetooth_dots_black.png b/android/leaudio/app/src/main/res/drawable-xxhdpi/ic_bluetooth_dots_black.png
new file mode 100644
index 0000000000..eb0cd35875
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable-xxhdpi/ic_bluetooth_dots_black.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/drawable/dotted_line.xml b/android/leaudio/app/src/main/res/drawable/dotted_line.xml
new file mode 100644
index 0000000000..672d561d23
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/dotted_line.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="line" >
+
+ <solid android:color="#fdfdfd" >
+ </solid>
+
+ <stroke
+ android:dashGap="5dp"
+ android:dashWidth="10dp"
+ android:width="2dp"
+ android:color="#EEEEEE">
+ </stroke>
+</shape> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/drawable/ic_add_white_24dp.xml b/android/leaudio/app/src/main/res/drawable/ic_add_white_24dp.xml
new file mode 100644
index 0000000000..e3979cd7f2
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_add_white_24dp.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+ android:viewportHeight="24.0" android:viewportWidth="24.0"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable/ic_arrow_drop_down_black_24dp.xml b/android/leaudio/app/src/main/res/drawable/ic_arrow_drop_down_black_24dp.xml
new file mode 100644
index 0000000000..62b27ef0b9
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_arrow_drop_down_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M7,10l5,5 5,-5z"/>
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable/ic_arrow_drop_up_black_24dp.xml b/android/leaudio/app/src/main/res/drawable/ic_arrow_drop_up_black_24dp.xml
new file mode 100644
index 0000000000..b1442ce159
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_arrow_drop_up_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M7,14l5,-5 5,5z"/>
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable/ic_bluetooth_searching_black_24dp.xml b/android/leaudio/app/src/main/res/drawable/ic_bluetooth_searching_black_24dp.xml
new file mode 100644
index 0000000000..ece1684c66
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_bluetooth_searching_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M14.24,12.01l2.32,2.32c0.28,-0.72 0.44,-1.51 0.44,-2.33 0,-0.82 -0.16,-1.59 -0.43,-2.31l-2.33,2.32zM19.53,6.71l-1.26,1.26c0.63,1.21 0.98,2.57 0.98,4.02s-0.36,2.82 -0.98,4.02l1.2,1.2c0.97,-1.54 1.54,-3.36 1.54,-5.31 -0.01,-1.89 -0.55,-3.67 -1.48,-5.19zM15.71,7.71L10,2L9,2v7.59L4.41,5 3,6.41 8.59,12 3,17.59 4.41,19 9,14.41L9,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM11,5.83l1.88,1.88L11,9.59L11,5.83zM12.88,16.29L11,18.17v-3.76l1.88,1.88z"/>
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable/ic_cast_black_24dp.xml b/android/leaudio/app/src/main/res/drawable/ic_cast_black_24dp.xml
new file mode 100644
index 0000000000..4ffbdc4212
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_cast_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v3h2L3,5h18v14h-7v2h7c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM1,18v3h3c0,-1.66 -1.34,-3 -3,-3zM1,14v2c2.76,0 5,2.24 5,5h2c0,-3.87 -3.13,-7 -7,-7zM1,10v2c4.97,0 9,4.03 9,9h2c0,-6.08 -4.93,-11 -11,-11z" />
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable/ic_cast_connected_black_24dp.xml b/android/leaudio/app/src/main/res/drawable/ic_cast_connected_black_24dp.xml
new file mode 100644
index 0000000000..486e05ece4
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_cast_connected_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M1,18v3h3c0,-1.66 -1.34,-3 -3,-3zM1,14v2c2.76,0 5,2.24 5,5h2c0,-3.87 -3.13,-7 -7,-7zM19,7L5,7v1.63c3.96,1.28 7.09,4.41 8.37,8.37L19,17L19,7zM1,10v2c4.97,0 9,4.03 9,9h2c0,-6.08 -4.93,-11 -11,-11zM21,3L3,3c-1.1,0 -2,0.9 -2,2v3h2L3,5h18v14h-7v2h7c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2z" />
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable/ic_download_black_24dp.xml b/android/leaudio/app/src/main/res/drawable/ic_download_black_24dp.xml
new file mode 100644
index 0000000000..370bba93dd
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_download_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable/ic_launcher_background.xml b/android/leaudio/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000000..0d025f9bf6
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#008577"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable/ic_refresh.xml b/android/leaudio/app/src/main/res/drawable/ic_refresh.xml
new file mode 100644
index 0000000000..4ca5e73a70
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_refresh.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="#FFFFFF"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable/ic_refresh_black_24dp.xml b/android/leaudio/app/src/main/res/drawable/ic_refresh_black_24dp.xml
new file mode 100644
index 0000000000..8229a9a64c
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_refresh_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable/ic_upload_black_24dp.xml b/android/leaudio/app/src/main/res/drawable/ic_upload_black_24dp.xml
new file mode 100644
index 0000000000..b4f6dce06a
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_upload_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z" />
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable/ic_vpn_key_black_24dp.xml b/android/leaudio/app/src/main/res/drawable/ic_vpn_key_black_24dp.xml
new file mode 100644
index 0000000000..2eddd16f65
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_vpn_key_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
+</vector>
diff --git a/android/leaudio/app/src/main/res/drawable/ic_warning_black_24dp.xml b/android/leaudio/app/src/main/res/drawable/ic_warning_black_24dp.xml
new file mode 100644
index 0000000000..b3a9e036b4
--- /dev/null
+++ b/android/leaudio/app/src/main/res/drawable/ic_warning_black_24dp.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
+</vector>
diff --git a/android/leaudio/app/src/main/res/layout/activity_main.xml b/android/leaudio/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000000..a08c5749e0
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="com.android.bluetooth.leaudio.MainActivity">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:theme="@style/AppTheme.AppBarOverlay">
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ app:popupTheme="@style/AppTheme.PopupOverlay" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <include layout="@layout/content_main" />
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/fab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="@dimen/fab_margin"
+ android:backgroundTint="#009688"
+ app:backgroundTint="#00584C"
+ app:srcCompat="@drawable/ic_refresh" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/layout/bass_add_source_dialog.xml b/android/leaudio/app/src/main/res/layout/bass_add_source_dialog.xml
new file mode 100644
index 0000000000..e044bac793
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/bass_add_source_dialog.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/bass_add_source_dialog"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="8dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView24"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:text="BisSync:" />
+
+ <Spinner
+ android:id="@+id/bis_sync_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView25"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Metadata:" />
+
+ <TextView
+ android:id="@+id/bass_metadata"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:text="" />
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/layout/bass_layout.xml b/android/leaudio/app/src/main/res/layout/bass_layout.xml
new file mode 100644
index 0000000000..862f076e67
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/bass_layout.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/bass_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="5"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ tools:showIn="@layout/le_audio_device_fragment">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="5"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView5"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:layout_weight="0"
+ android:gravity="center_vertical"
+ android:text="Receiver:" />
+
+ <Spinner
+ android:id="@+id/num_receiver_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:minWidth="50dp" />
+
+ <TextView
+ android:id="@+id/textView6"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:layout_weight="0"
+ android:gravity="center_vertical"
+ android:text="State:" />
+
+ <TextView
+ android:id="@+id/receiver_state_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="10dp"
+ android:layout_weight="2"
+ android:text="IDLE" />
+
+ <ImageButton
+ android:id="@+id/broadcast_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:srcCompat="@drawable/ic_cast_black_24dp" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/layout/broadcast_item.xml b/android/leaudio/app/src/main/res/layout/broadcast_item.xml
new file mode 100644
index 0000000000..afbdc84fd1
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/broadcast_item.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/broadcast_item_card_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:padding="4dp"
+ android:paddingHorizontal="4dp">
+
+ <TextView
+ android:id="@+id/textView8"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Broadcast" />
+
+ <TextView
+ android:id="@+id/broadcast_id_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="brodcast_id" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:padding="4dp">
+ </LinearLayout>
+ </LinearLayout>
+
+</androidx.cardview.widget.CardView> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/layout/broadcaster_activity.xml b/android/leaudio/app/src/main/res/layout/broadcaster_activity.xml
new file mode 100644
index 0000000000..993e5aa36a
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/broadcaster_activity.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context="com.android.bluetooth.leaudio.BroadcasterActivity" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center|center_vertical"
+ android:orientation="vertical">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/broadcaster_recycle_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:listitem="@layout/broadcast_item" />
+
+ <ImageView
+ android:id="@+id/imageView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:srcCompat="@drawable/ic_warning_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center|center_vertical"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView14"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Under construction" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/broadcast_fab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="@dimen/fab_margin"
+ android:backgroundTint="#009688"
+ app:backgroundTint="#00584C"
+ app:srcCompat="@drawable/ic_add_white_24dp" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/android/leaudio/app/src/main/res/layout/broadcaster_add_broadcast_dialog.xml b/android/leaudio/app/src/main/res/layout/broadcaster_add_broadcast_dialog.xml
new file mode 100644
index 0000000000..5bc50b5d1c
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/broadcaster_add_broadcast_dialog.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/broadcaster_add_broadcast_dialog"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="8dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView22"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Broadcast code:" />
+
+ <EditText
+ android:id="@+id/broadcast_code_input"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ems="10"
+ android:gravity="start|top"
+ android:maxLength="16"
+ android:inputType="textMultiLine|textFilter" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView18"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Program Info:" />
+
+ <EditText
+ android:id="@+id/broadcast_meta_input"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:ems="10"
+ android:gravity="start|top"
+ android:inputType="text|textAutoCorrect" />
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/layout/content_main.xml b/android/leaudio/app/src/main/res/layout/content_main.xml
new file mode 100644
index 0000000000..f89f297157
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/content_main.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ tools:context="com.android.bluetooth.leaudio.MainActivity"
+ tools:showIn="@layout/activity_main">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ android:paddingBottom="90dp"
+ android:scrollbars="vertical"
+ tools:listitem="@layout/le_audio_device_fragment" />
+
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/layout/hap_layout.xml b/android/leaudio/app/src/main/res/layout/hap_layout.xml
new file mode 100644
index 0000000000..741e3c2e2e
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/hap_layout.xml
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/hap_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ tools:showIn="@layout/le_audio_device_fragment">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/hap_profile_status_text_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="HAP state:" />
+
+ <TextView
+ android:id="@+id/hap_profile_state_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:text="@string/UNAVAILABLE" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/hap_profile_features_text_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:singleLine="true"
+ android:text="Features:" />
+
+ <TextView
+ android:id="@+id/hap_profile_features_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:text="@string/UNAVAILABLE" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/hap_profile_active_preset_index_text_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Active preset index:" />
+
+ <TextView
+ android:id="@+id/hap_profile_active_preset_index_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:text="@string/UNAVAILABLE" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/hap_profile_preset_spinner_list_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:text="Preset:" />
+
+ <Spinner
+ android:id="@+id/hap_presets_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:minWidth="50dp" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/hap_change_preset_name_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Change name" />
+
+ <Button
+ android:id="@+id/hap_set_active_preset_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Set active" />
+
+ <Button
+ android:id="@+id/hap_read_preset_info_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Read preset info" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/hap_previous_device_preset_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Previous preset" />
+
+ <Button
+ android:id="@+id/hap_next_device_preset_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Next preset" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/hap_previous_group_preset_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Precious group preset" />
+
+ <Button
+ android:id="@+id/hap_next_group_preset_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Next group preset" />
+
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/layout/header_layout.xml b/android/leaudio/app/src/main/res/layout/header_layout.xml
new file mode 100644
index 0000000000..027803339c
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/header_layout.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/generic_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="5"
+ android:orientation="vertical"
+ tools:showIn="@layout/le_audio_device_fragment">
+
+ <TextView
+ android:id="@+id/device_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="5dp"
+ android:text="device_name"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+
+ <Switch
+ android:id="@+id/vc_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingHorizontal="5dp"
+ android:text="Vc:" />
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="3" />
+
+ <Switch
+ android:id="@+id/le_audio_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingHorizontal="5dp"
+ android:text="Le Audio:" />
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="3" />
+
+ <Switch
+ android:id="@+id/bass_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingHorizontal="5dp"
+ android:text="Bass:" />
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="3" />
+
+ <Switch
+ android:id="@+id/hap_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingHorizontal="5dp"
+ android:text="Hap:" />
+ </LinearLayout>
+</LinearLayout>
diff --git a/android/leaudio/app/src/main/res/layout/le_audio_device_fragment.xml b/android/leaudio/app/src/main/res/layout/le_audio_device_fragment.xml
new file mode 100644
index 0000000000..7252540b06
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/le_audio_device_fragment.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/le_audio_device_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp">
+
+ <LinearLayout
+ android:id="@+id/root_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingLeft="10dp"
+ android:paddingTop="3dp"
+ android:paddingRight="10dp"
+ android:paddingBottom="3dp">
+
+ <include layout="@layout/header_layout" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="4dp"
+ android:layout_marginVertical="2dp"
+ android:background="@drawable/dotted_line"
+ android:layerType="software"
+ android:orientation="vertical" />
+
+ <include layout="@layout/vc_layout" />
+
+ <View
+ android:id="@+id/view"
+ android:layout_width="match_parent"
+ android:layout_height="4dp"
+ android:layout_marginVertical="2dp"
+ android:background="@drawable/dotted_line"
+ android:layerType="software"
+ android:orientation="vertical" />
+
+ <include layout="@layout/le_audio_layout" />
+
+ <View
+ android:id="@+id/view2"
+ android:layout_width="match_parent"
+ android:layout_height="4dp"
+ android:layout_marginVertical="2dp"
+ android:background="@drawable/dotted_line"
+ android:layerType="software"
+ android:orientation="vertical" />
+
+ <include layout="@layout/bass_layout" />
+
+ <View
+ android:id="@+id/view3"
+ android:layout_width="match_parent"
+ android:layout_height="4dp"
+ android:layout_marginVertical="2dp"
+ android:background="@drawable/dotted_line"
+ android:layerType="software"
+ android:orientation="vertical" />
+
+ <include
+ layout="@layout/hap_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+</androidx.cardview.widget.CardView> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/layout/le_audio_layout.xml b/android/leaudio/app/src/main/res/layout/le_audio_layout.xml
new file mode 100644
index 0000000000..1c6cb17f48
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/le_audio_layout.xml
@@ -0,0 +1,226 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/le_audio_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ tools:showIn="@layout/le_audio_device_fragment">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="2"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Current group: " />
+
+ <TextView
+ android:id="@+id/group_id_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:text="@string/unknown" />
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <TextView
+ android:id="@+id/textView21"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:text="Status:" />
+
+ <TextView
+ android:id="@+id/group_status_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:text="@string/unknown" />
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <TextView
+ android:id="@+id/textView4"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:text="Flags:" />
+
+ <TextView
+ android:id="@+id/group_flags_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="0"
+ android:text="@string/none" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/group_set_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:text="Set" />
+
+ <Button
+ android:id="@+id/group_unset_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:text="Unset" />
+
+ <Button
+ android:id="@+id/group_destroy_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:text="Destroy" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView2"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Group control" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/set_lock_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Lock" />
+
+ <Button
+ android:id="@+id/set_unlock_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Unlock" />
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <TextView
+ android:id="@+id/lock_status_lbl_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Lock status:" />
+
+ <TextView
+ android:id="@+id/lock_state_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:text="@string/unset" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/start_stream_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Start" />
+
+ <Button
+ android:id="@+id/stop_stream_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Stop" />
+
+ <Button
+ android:id="@+id/suspend_stream_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Suspend" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/group_mic_mute_state_text_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Microphone:" />
+
+ <TextView
+ android:id="@+id/group_mic_mute_state_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="2"
+ android:text="@string/UNAVAILABLE" />
+
+ <Switch
+ android:id="@+id/group_mic_mute_state_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="0"
+ android:text="Mute" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/android/leaudio/app/src/main/res/layout/vc_inputs_layout.xml b/android/leaudio/app/src/main/res/layout/vc_inputs_layout.xml
new file mode 100644
index 0000000000..e3a5418d40
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/vc_inputs_layout.xml
@@ -0,0 +1,289 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/vc_inputs_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ tools:showIn="@layout/vc_layout">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="5dp"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/vc_input_foldable_icon"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="5dp"
+ android:layout_marginRight="10dp"
+ android:layout_weight="0"
+ android:tint="#E91E63"
+ app:srcCompat="@drawable/ic_arrow_drop_up_black_24dp" />
+
+ <TextView
+ android:id="@+id/textView9"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="5dp"
+ android:text="Input:" />
+
+ <Spinner
+ android:id="@+id/num_inputs_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0" />
+
+ <ImageButton
+ android:id="@+id/inputGetStateButton"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="0"
+ android:maxWidth="0dp"
+ android:maxHeight="0dp"
+ android:minWidth="0dp"
+ android:minHeight="0dp"
+ app:srcCompat="@drawable/ic_download_black_24dp" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="5"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/textView19"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:paddingHorizontal="20dp"
+ android:text="Gain:" />
+
+ <SeekBar
+ android:id="@+id/inputGainSeekBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:max="255"
+ android:min="-255" />
+ </LinearLayout>
+
+ <Switch
+ android:id="@+id/inputMuteSwitch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="0"
+ android:text="Mute" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/ext_input_foldable"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical"
+ android:visibility="visible">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:gravity="bottom"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/inputSetGainModeButton"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="0"
+ android:maxWidth="0dp"
+ android:maxHeight="0dp"
+ android:minWidth="0dp"
+ android:minHeight="0dp"
+ app:srcCompat="@drawable/ic_upload_black_24dp" />
+
+ <TextView
+ android:id="@+id/textView20"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0"
+ android:text="Gain Mode:" />
+
+ <TextView
+ android:id="@+id/inputGainModeText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="_mode_" />
+
+ <ImageButton
+ android:id="@+id/inputGetGainPropsButton"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dp"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0"
+ android:maxWidth="0dp"
+ android:maxHeight="0dp"
+ android:minWidth="0dp"
+ android:minHeight="0dp"
+ app:srcCompat="@drawable/ic_download_black_24dp" />
+
+ <TextView
+ android:id="@+id/textView17"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0"
+ android:text="Props:" />
+
+ <TextView
+ android:id="@+id/inputGainPropsUnitText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="0"
+ android:text="unit" />
+
+ <TextView
+ android:id="@+id/inputGainPropsMinText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="0"
+ android:text="min" />
+
+ <TextView
+ android:id="@+id/inputGainPropsMaxText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="0"
+ android:text="max" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:gravity="bottom"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/inputGetTypeButton"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="0"
+ app:srcCompat="@drawable/ic_download_black_24dp" />
+
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0"
+ android:text="Type:" />
+
+ <TextView
+ android:id="@+id/inputTypeText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="1"
+ android:text="_type_" />
+
+ <ImageButton
+ android:id="@+id/inputGetStatusButton"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="10dp"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0"
+ app:srcCompat="@drawable/ic_download_black_24dp" />
+
+ <TextView
+ android:id="@+id/textView13"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0"
+ android:text="Status:" />
+
+ <TextView
+ android:id="@+id/inputStatusText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="1"
+ android:text="_status_" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:gravity="bottom"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/inputGetDescriptionButton"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0"
+ android:maxWidth="0dp"
+ android:maxHeight="0dp"
+ android:minWidth="0dp"
+ android:minHeight="0dp"
+ app:srcCompat="@drawable/ic_download_black_24dp" />
+
+ <ImageButton
+ android:id="@+id/inputSetDescriptionButton"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="0"
+ app:srcCompat="@drawable/ic_upload_black_24dp" />
+
+ <TextView
+ android:id="@+id/textView15"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0"
+ android:text="Description:" />
+
+ <TextView
+ android:id="@+id/inputDescriptionText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="1"
+ android:text="_description_" />
+
+ </LinearLayout>
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/layout/vc_layout.xml b/android/leaudio/app/src/main/res/layout/vc_layout.xml
new file mode 100644
index 0000000000..6e87ffa87b
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/vc_layout.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/vc_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="5"
+ android:orientation="vertical"
+ tools:showIn="@layout/le_audio_device_fragment">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ tools:context=".VcDeviceFragment">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="153dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="1"
+ android:paddingHorizontal="20dp"
+ android:text="Volume" />
+
+ <SeekBar
+ android:id="@+id/volume_seek_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="1"
+ android:max="255"
+ android:min="0" />
+ </LinearLayout>
+
+ <Switch
+ android:id="@+id/mute_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:layout_weight="0"
+ android:text="Mute" />
+
+ </LinearLayout>
+
+ <include
+ layout="@layout/vc_inputs_layout"
+ android:visibility="visible" />
+
+ <include
+ layout="@layout/vc_outputs_layout"
+ android:visibility="visible" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/layout/vc_outputs_layout.xml b/android/leaudio/app/src/main/res/layout/vc_outputs_layout.xml
new file mode 100644
index 0000000000..521c07acac
--- /dev/null
+++ b/android/leaudio/app/src/main/res/layout/vc_outputs_layout.xml
@@ -0,0 +1,183 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/vc_outputs_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ tools:showIn="@layout/vc_layout">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="5dp"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/vc_output_foldable_icon"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="5dp"
+ android:layout_marginRight="10dp"
+ android:layout_weight="0"
+ android:tint="#E91E63"
+ app:srcCompat="@drawable/ic_arrow_drop_up_black_24dp" />
+
+ <TextView
+ android:id="@+id/textView9"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="5dp"
+ android:text="Output:" />
+
+ <Spinner
+ android:id="@+id/num_outputs_spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0" />
+
+ <ImageButton
+ android:id="@+id/outputGetGainButton"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="5dp"
+ android:layout_weight="0"
+ android:maxWidth="0dp"
+ android:maxHeight="0dp"
+ android:minWidth="0dp"
+ android:minHeight="0dp"
+ app:srcCompat="@drawable/ic_download_black_24dp" />
+
+ <TextView
+ android:id="@+id/textView19"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="0"
+ android:text="Offset:" />
+
+ <SeekBar
+ android:id="@+id/outputGainSeekBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="5"
+ android:max="255"
+ android:min="-255" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/ext_output_foldable"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical"
+ android:visibility="visible">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:gravity="bottom"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/outputGetLocationButton"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0"
+ android:maxWidth="0dp"
+ android:maxHeight="0dp"
+ android:minWidth="0dp"
+ android:minHeight="0dp"
+ app:srcCompat="@drawable/ic_download_black_24dp" />
+
+ <ImageButton
+ android:id="@+id/outputSetLocationButton"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="0"
+ app:srcCompat="@drawable/ic_upload_black_24dp" />
+
+ <TextView
+ android:id="@+id/textView15"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0"
+ android:text="Location:" />
+
+ <TextView
+ android:id="@+id/outputLocationText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="1"
+ android:text="_location_" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:gravity="bottom"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/outputGetDescriptionButton"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0"
+ android:maxWidth="0dp"
+ android:maxHeight="0dp"
+ android:minWidth="0dp"
+ android:minHeight="0dp"
+ app:srcCompat="@drawable/ic_download_black_24dp" />
+
+ <ImageButton
+ android:id="@+id/outputSetDescriptionButton"
+ style="@style/Widget.AppCompat.CompoundButton.RadioButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="0"
+ app:srcCompat="@drawable/ic_upload_black_24dp" />
+
+ <TextView
+ android:id="@+id/textView115"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="5dp"
+ android:layout_weight="0"
+ android:text="Description:" />
+
+ <TextView
+ android:id="@+id/outputDescriptionText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="5dp"
+ android:layout_weight="1"
+ android:text="_description_" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/menu/menu_main.xml b/android/leaudio/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000000..a0eb44b5bd
--- /dev/null
+++ b/android/leaudio/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,9 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/action_scan"
+ android:title="scan"/>
+ <item
+ android:id="@+id/action_broadcast"
+ android:title="@string/action_broadcast"/>
+</menu>
diff --git a/android/leaudio/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/leaudio/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..eca70cfe52
--- /dev/null
+++ b/android/leaudio/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/leaudio/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000..eca70cfe52
--- /dev/null
+++ b/android/leaudio/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon> \ No newline at end of file
diff --git a/android/leaudio/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/leaudio/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..898f3ed59a
--- /dev/null
+++ b/android/leaudio/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/leaudio/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..dffca3601e
--- /dev/null
+++ b/android/leaudio/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/leaudio/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..64ba76f75e
--- /dev/null
+++ b/android/leaudio/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/leaudio/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..dae5e08234
--- /dev/null
+++ b/android/leaudio/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/leaudio/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..e5ed46597e
--- /dev/null
+++ b/android/leaudio/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/leaudio/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..14ed0af350
--- /dev/null
+++ b/android/leaudio/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/leaudio/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..b0907cac3b
--- /dev/null
+++ b/android/leaudio/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/leaudio/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..d8ae031549
--- /dev/null
+++ b/android/leaudio/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/leaudio/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..2c18de9e66
--- /dev/null
+++ b/android/leaudio/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/leaudio/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..beed3cdd2c
--- /dev/null
+++ b/android/leaudio/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/android/leaudio/app/src/main/res/values/colors.xml b/android/leaudio/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..69b22338c6
--- /dev/null
+++ b/android/leaudio/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="colorPrimary">#008577</color>
+ <color name="colorPrimaryDark">#00574B</color>
+ <color name="colorAccent">#D81B60</color>
+</resources>
diff --git a/android/leaudio/app/src/main/res/values/dimens.xml b/android/leaudio/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..59a0b0c4f5
--- /dev/null
+++ b/android/leaudio/app/src/main/res/values/dimens.xml
@@ -0,0 +1,3 @@
+<resources>
+ <dimen name="fab_margin">16dp</dimen>
+</resources>
diff --git a/android/leaudio/app/src/main/res/values/strings.xml b/android/leaudio/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..45d0add8da
--- /dev/null
+++ b/android/leaudio/app/src/main/res/values/strings.xml
@@ -0,0 +1,152 @@
+<resources>
+ <string name="app_name">LeAudio</string>
+ <string name="tbs_activity_name">TelephoneBearerService</string>
+ <string name="svc_uuid_le_audio">0000184E-0000-1000-8000-00805F9B34FB</string>
+ <string name="svc_uuid_volume_control">00001844-0000-1000-8000-00805F9B34FB</string>
+ <string name="svc_uuid_broadcast_audio">0000184F-0000-1000-8000-00805F9B34FB</string>
+ <string name="svc_uuid_coordinated_set_identification">00001846-0000-1000-8000-00805F9B34FB</string>
+ <string name="svc_uuid_has">00001854-0000-1000-8000-00805F9B34FB</string>
+ <string name="action_disconnect">DISCONNECT</string>
+ <string name="action_connect">CONNECT</string>
+ <string name="unknown">Unknown</string>
+ <string name="unset">UNSET</string>
+ <string name="none">None</string>
+ <string name="UNAVAILABLE">UNAVAILABLE</string>
+ <string name="ENABLED">ENABLED</string>
+ <string name="MUTED">MUTED</string>
+ <string name="DISABLED">DISABLED</string>
+ <string-array name="mic_states">
+ <item>@string/UNAVAILABLE</item>
+ <item>@string/ENABLED</item>
+ <item>@string/MUTED</item>
+ <item>@string/DISABLED</item>
+ </string-array>
+ <string-array name="group_statuses">
+ <item>IDLE</item>
+ <item>STREAMING</item>
+ <item>SUSPENDED</item>
+ <item>RECONFIGURED</item>
+ <item>DESTROYED</item>
+ </string-array>
+ <string-array name="content_types">
+ <item>Unspecified</item>
+ <item>Conversational</item>
+ <item>Media</item>
+ <item>Instructional</item>
+ <item>AttentionSeeking</item>
+ <item>ImmediateAlert</item>
+ <item>ManMachine</item>
+ <item>EmergencyAlert</item>
+ <item>Ringtone</item>
+ <item>TV</item>
+ <item>Live</item>
+ </string-array>
+ <string-array name="audio_locations">
+ <item>Front Left</item>
+ <item>Front Right</item>
+ <item>Front Center</item>
+ <item>Low Frequency Effects 1</item>
+ <item>Back Left</item>
+ <item>Back Right</item>
+ <item>Front Left of Center</item>
+ <item>Front Right of Center</item>
+ <item>Back Center</item>
+ <item>Low Frequency Effects 2</item>
+ <item>Side Left</item>
+ <item>Side Right</item>
+ <item>Top Front Left</item>
+ <item>Top Front Right</item>
+ <item>Top Front Center</item>
+ <item>Top Center</item>
+ <item>Top Back Left</item>
+ <item>Top Back Right</item>
+ <item>Top Side Left</item>
+ <item>Top Side Right</item>
+ <item>Top Back Center</item>
+ <item>Bottom Front Center</item>
+ <item>Bottom Front Left</item>
+ <item>Bottom Front Right</item>
+ <item>Front Left Wide</item>
+ <item>Front Right Wide</item>
+ <item>Left Surround</item>
+ <item>Right Surround</item>
+ <item>UNKNOWN</item>
+ </string-array>
+ <string-array name="gain_modes">
+ <item>Unsupported</item>
+ <item>Manual</item>
+ <item>Auto</item>
+ </string-array>
+ <string name="group_locked">LOCKED</string>
+ <string name="group_unlocked">UNLOCKED</string>
+ <string-array name="tbs_call_states">
+ <item>Incoming</item>
+ <item>Dialing</item>
+ <item>Alerting</item>
+ <item>Active</item>
+ <item>LocallyHeld</item>
+ <item>RemotelyHeld</item>
+ <item>LocallyRemotelyHeld</item>
+ </string-array>
+ <string-array name="tbs_provider_name">
+ <item>AT&amp;T</item>
+ <item>T-Mobile</item>
+ <item>Verizon</item>
+ </string-array>
+ <string-array name="tbs_technology">
+ <item>UNKNOWN</item>
+ <item>3G</item>
+ <item>4G</item>
+ <item>LTE</item>
+ <item>Wi-Fi</item>
+ <item>5G</item>
+ <item>GSM</item>
+ <item>CDMA</item>
+ <item>2G</item>
+ <item>WCDMA</item>
+ <item>IP</item>
+ </string-array>
+ <string-array name="profile_states">
+ <item>DISCONNECTED</item>
+ <item>CONNECTING</item>
+ <item>CONNECTED</item>
+ <item>DISCONNECTING</item>
+ </string-array>
+ <string-array name="hearing_aid_types">
+ <item>Monaural</item>
+ <item>Banded</item>
+ <item>Binaural</item>
+ </string-array>
+ <string-array name="preset_synchronization_support">
+ <item>Not synchronized</item>
+ <item>Synchronized</item>
+ </string-array>
+ <string-array name="independent_presets">
+ <item>The same presets</item>
+ <item>Independent presets</item>
+ </string-array>
+ <string-array name="dynamic_presets">
+ <item>Not dynamic</item>
+ <item>Dynamic</item>
+ </string-array>
+ <string-array name="writable_presets_support">
+ <item>Not writable</item>
+ <item>Writable</item>
+ </string-array>
+ <string name="notes_icon">♫</string>
+ <string name="advertising">Advertising</string>
+ <string name="callhold">CallHold</string>
+ <string name="calljoin">CallJoin</string>
+ <string name="signalstrengthreporting">SignalStrengthReporting</string>
+ <string name="features">Features:</string>
+ <string name="toggle">Toggle:</string>
+ <string name="telephone_bearer_service">Telephone Bearer Service</string>
+ <string name="action_broadcast">Broadcast Audio</string>
+ <string name="add_outgoing_incoming_call">Add outgoing/incoming call:</string>
+ <string name="current_calls">Current calls:</string>
+ <string name="uuid">UUID:</string>
+ <string name="uri">URI:</string>
+ <string name="provider_name">Provider Name:</string>
+ <string name="signal_strength">Signal Strength</string>
+ <string name="not_provided_string"><![CDATA[<not provided>]]></string>
+</resources>
diff --git a/android/leaudio/app/src/main/res/values/styles.xml b/android/leaudio/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..545b9c6d2c
--- /dev/null
+++ b/android/leaudio/app/src/main/res/values/styles.xml
@@ -0,0 +1,20 @@
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ </style>
+
+ <style name="AppTheme.NoActionBar">
+ <item name="windowActionBar">false</item>
+ <item name="windowNoTitle">true</item>
+ </style>
+
+ <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
+
+ <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
+
+</resources>
diff --git a/android/leaudio/app/src/test/java/pl/codecoup/ehima/leaudio/ExampleUnitTest.java b/android/leaudio/app/src/test/java/pl/codecoup/ehima/leaudio/ExampleUnitTest.java
new file mode 100644
index 0000000000..29e90f3641
--- /dev/null
+++ b/android/leaudio/app/src/test/java/pl/codecoup/ehima/leaudio/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.android.bluetooth.leaudio;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+} \ No newline at end of file
diff --git a/android/leaudio/build.gradle b/android/leaudio/build.gradle
new file mode 100644
index 0000000000..97a46252c3
--- /dev/null
+++ b/android/leaudio/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.1.2'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/android/leaudio/gradle.properties b/android/leaudio/gradle.properties
new file mode 100644
index 0000000000..d2bc5db835
--- /dev/null
+++ b/android/leaudio/gradle.properties
@@ -0,0 +1,22 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Disable apk install issue
+android.injected.testOnly=false
+
diff --git a/android/leaudio/gradle/wrapper/gradle-wrapper.jar b/android/leaudio/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..f6b961fd5a
--- /dev/null
+++ b/android/leaudio/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/android/leaudio/gradle/wrapper/gradle-wrapper.properties b/android/leaudio/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..bd98b5dbe3
--- /dev/null
+++ b/android/leaudio/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Oct 15 09:37:54 CEST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
diff --git a/android/leaudio/gradlew b/android/leaudio/gradlew
new file mode 100755
index 0000000000..cccdd3d517
--- /dev/null
+++ b/android/leaudio/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/android/leaudio/gradlew.bat b/android/leaudio/gradlew.bat
new file mode 100644
index 0000000000..e95643d6a2
--- /dev/null
+++ b/android/leaudio/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android/leaudio/settings.gradle b/android/leaudio/settings.gradle
new file mode 100644
index 0000000000..e7b4def49c
--- /dev/null
+++ b/android/leaudio/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/framework/java/android/bluetooth/BluetoothAdapter.java b/framework/java/android/bluetooth/BluetoothAdapter.java
index 01bc1bade2..de3f570d96 100644
--- a/framework/java/android/bluetooth/BluetoothAdapter.java
+++ b/framework/java/android/bluetooth/BluetoothAdapter.java
@@ -190,6 +190,16 @@ public final class BluetoothAdapter {
STATE_BLE_TURNING_OFF
})
@Retention(RetentionPolicy.SOURCE)
+ public @interface InternalAdapterState {}
+
+ /** @hide */
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_OFF,
+ STATE_TURNING_ON,
+ STATE_ON,
+ STATE_TURNING_OFF,
+ })
+ @Retention(RetentionPolicy.SOURCE)
public @interface AdapterState {}
/**
@@ -270,14 +280,14 @@ public final class BluetoothAdapter {
public @interface RfcommListenerResult {}
/**
- * Human-readable string helper for AdapterState
+ * Human-readable string helper for AdapterState and InternalAdapterState
*
* @hide
*/
@SystemApi
@RequiresNoPermission
@NonNull
- public static String nameForState(@AdapterState int state) {
+ public static String nameForState(@InternalAdapterState int state) {
switch (state) {
case STATE_OFF:
return "OFF";
@@ -1151,9 +1161,8 @@ public final class BluetoothAdapter {
new IpcDataCache.QueryHandler<>() {
@RequiresLegacyBluetoothPermission
@RequiresNoPermission
- @AdapterState
@Override
- public Integer apply(Void query) {
+ public @InternalAdapterState Integer apply(Void query) {
int state = BluetoothAdapter.STATE_OFF;
mServiceLock.readLock().lock();
try {
@@ -1189,8 +1198,7 @@ public final class BluetoothAdapter {
* Fetch the current bluetooth state. If the service is down, return
* OFF.
*/
- @AdapterState
- private int getStateInternal() {
+ private @InternalAdapterState int getStateInternal() {
return mBluetoothGetStateCache.query(null);
}
@@ -1206,8 +1214,7 @@ public final class BluetoothAdapter {
*/
@RequiresLegacyBluetoothPermission
@RequiresNoPermission
- @AdapterState
- public int getState() {
+ public @AdapterState int getState() {
int state = getStateInternal();
// Consider all internal states as OFF
@@ -1243,10 +1250,9 @@ public final class BluetoothAdapter {
*/
@RequiresLegacyBluetoothPermission
@RequiresNoPermission
- @AdapterState
@UnsupportedAppUsage(publicAlternatives = "Use {@link #getState()} instead to determine "
+ "whether you can use BLE & BT classic.")
- public int getLeState() {
+ public @InternalAdapterState int getLeState() {
int state = getStateInternal();
if (VDBG) {
diff --git a/framework/java/android/bluetooth/BluetoothDevice.java b/framework/java/android/bluetooth/BluetoothDevice.java
index 90aa3000e6..978c8ed4ac 100644
--- a/framework/java/android/bluetooth/BluetoothDevice.java
+++ b/framework/java/android/bluetooth/BluetoothDevice.java
@@ -1323,7 +1323,14 @@ public final class BluetoothDevice implements Parcelable, Attributable {
mAttributionSource = attributionSource;
}
- /** {@hide} */
+ /**
+ * Method should never be used anywhere. Only exception is from {@link Intent}
+ * Used to set the device current attribution source
+ *
+ * @param attributionSource The associated {@link AttributionSource} for this device in this
+ * process
+ * @hide
+ */
@SystemApi
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
public void prepareToEnterProcess(@NonNull AttributionSource attributionSource) {
diff --git a/framework/java/android/bluetooth/BluetoothHapClient.java b/framework/java/android/bluetooth/BluetoothHapClient.java
index 4d1bd6ac1a..986c4adf14 100644
--- a/framework/java/android/bluetooth/BluetoothHapClient.java
+++ b/framework/java/android/bluetooth/BluetoothHapClient.java
@@ -141,17 +141,6 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable
@Status int statusCode);
/**
- * Invoked to inform about HA device's feature set.
- *
- * @param device remote device
- * @param hapFeatures the feature set integer with feature bits set. The inidividual bits
- * are defined by Bluetooth SIG in Hearing Access Service specification.
- *
- * @hide
- */
- void onHapFeaturesAvailable(@NonNull BluetoothDevice device, @Feature int hapFeatures);
-
- /**
* Invoked to inform about the failed preset rename attempt.
*
* @param device remote device
@@ -224,18 +213,6 @@ public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable
}
@Override
- public void onHapFeaturesAvailable(@NonNull BluetoothDevice device,
- @Feature int hapFeatures) {
- Attributable.setAttributionSource(device, mAttributionSource);
- for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry:
- mCallbackExecutorMap.entrySet()) {
- BluetoothHapClient.Callback callback = callbackExecutorEntry.getKey();
- Executor executor = callbackExecutorEntry.getValue();
- executor.execute(() -> callback.onHapFeaturesAvailable(device, hapFeatures));
- }
- }
-
- @Override
public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int status) {
Attributable.setAttributionSource(device, mAttributionSource);
for (Map.Entry<BluetoothHapClient.Callback, Executor> callbackExecutorEntry:
diff --git a/system/audio_hal_interface/aidl/client_interface_aidl.cc b/system/audio_hal_interface/aidl/client_interface_aidl.cc
index 2c1de92dd5..3e3706645e 100644
--- a/system/audio_hal_interface/aidl/client_interface_aidl.cc
+++ b/system/audio_hal_interface/aidl/client_interface_aidl.cc
@@ -241,9 +241,10 @@ bool BluetoothAudioClientInterface::SetLowLatencyModeAllowed(bool allowed) {
auto aidl_retval = provider_->setLowLatencyModeAllowed(allowed);
if (!aidl_retval.isOk()) {
- LOG(ERROR) << __func__ << ": BluetoothAudioHal failure: "
- << aidl_retval.getDescription();
- return false;
+ LOG(WARNING) << __func__ << ": BluetoothAudioHal is not ready: "
+ << aidl_retval.getDescription()
+ << ". is_low_latency_allowed_ is saved "
+ <<"and it will be sent to BluetoothAudioHal at StartSession.";
}
return true;
}
diff --git a/system/binder/android/bluetooth/IBluetoothHapClientCallback.aidl b/system/binder/android/bluetooth/IBluetoothHapClientCallback.aidl
index 274c802ee9..2480b305e9 100644
--- a/system/binder/android/bluetooth/IBluetoothHapClientCallback.aidl
+++ b/system/binder/android/bluetooth/IBluetoothHapClientCallback.aidl
@@ -33,7 +33,6 @@ oneway interface IBluetoothHapClientCallback {
void onPresetInfoChanged(in BluetoothDevice device,
in List<BluetoothHapPresetInfo> presetInfoList,
in int statusCode);
- void onHapFeaturesAvailable(in BluetoothDevice device, in int hapFeatures);
void onSetPresetNameFailed(in BluetoothDevice device, in int status);
void onSetPresetNameForGroupFailed(in int hapGroupId, in int status);
}
diff --git a/system/blueberry/tests/gd_sl4a/hci/le_advanced_scanning_test.py b/system/blueberry/tests/gd_sl4a/hci/le_advanced_scanning_test.py
index ed22e1a8e5..b2abdc4219 100644
--- a/system/blueberry/tests/gd_sl4a/hci/le_advanced_scanning_test.py
+++ b/system/blueberry/tests/gd_sl4a/hci/le_advanced_scanning_test.py
@@ -32,7 +32,7 @@ class LeAdvancedScanningTest(GdSl4aBaseTestClass):
def setup_class(self):
super().setup_class(cert_module='HCI_INTERFACES')
- self.default_timeout = 10 # seconds
+ self.default_timeout = 60 # seconds
def setup_test(self):
super().setup_test()
@@ -48,6 +48,17 @@ class LeAdvancedScanningTest(GdSl4aBaseTestClass):
type=common.RANDOM_DEVICE_ADDRESS))
self.cert.hci_le_initiator_address.SetPrivacyPolicyForInitiatorAddress(private_policy)
+ def set_cert_privacy_policy_with_random_address_but_advertise_resolvable(self, irk):
+ random_address_bytes = "DD:34:02:05:5C:EE".encode()
+ private_policy = le_initiator_address_facade.PrivacyPolicy(
+ address_policy=le_initiator_address_facade.AddressPolicy.USE_RESOLVABLE_ADDRESS,
+ address_with_type=common.BluetoothAddressWithType(
+ address=common.BluetoothAddress(address=random_address_bytes), type=common.RANDOM_DEVICE_ADDRESS),
+ rotation_irk=irk)
+ self.cert.hci_le_initiator_address.SetPrivacyPolicyForInitiatorAddress(private_policy)
+ # Bluetooth MAC address must be upper case
+ return random_address_bytes.decode('utf-8').upper()
+
def set_cert_privacy_policy_with_public_address(self):
public_address_bytes = self.cert.hci_controller.GetMacAddress(empty_proto.Empty()).address
private_policy = le_initiator_address_facade.PrivacyPolicy(
@@ -58,6 +69,246 @@ class LeAdvancedScanningTest(GdSl4aBaseTestClass):
# Bluetooth MAC address must be upper case
return public_address_bytes.decode('utf-8').upper()
+ def set_cert_privacy_policy_with_public_address_but_advertise_resolvable(self, irk):
+ public_address_bytes = self.cert.hci_controller.GetMacAddress(empty_proto.Empty()).address
+ private_policy = le_initiator_address_facade.PrivacyPolicy(
+ address_policy=le_initiator_address_facade.AddressPolicy.USE_RESOLVABLE_ADDRESS,
+ address_with_type=common.BluetoothAddressWithType(
+ address=common.BluetoothAddress(address=public_address_bytes), type=common.PUBLIC_DEVICE_ADDRESS),
+ rotation_irk=irk)
+ self.cert.hci_le_initiator_address.SetPrivacyPolicyForInitiatorAddress(private_policy)
+ # Bluetooth MAC address must be upper case
+ return public_address_bytes.decode('utf-8').upper()
+
+ def test_scan_filter_device_public_address_with_irk_legacy_pdu(self):
+ """
+ The cert side will advertise an RPA derived from the adapter's public address.
+ """
+ data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]
+ byteArrayObject = bytearray(data)
+ irk = bytes(byteArrayObject)
+
+ DEVICE_NAME = 'Im_The_CERT!'
+ logging.info("Getting public address")
+ PUBLIC_ADDRESS = self.set_cert_privacy_policy_with_public_address_but_advertise_resolvable(irk)
+ logging.info("Done %s" % PUBLIC_ADDRESS)
+
+ # Setup cert side to advertise
+ gap_name = hci_packets.GapData()
+ gap_name.data_type = hci_packets.GapDataType.COMPLETE_LOCAL_NAME
+ gap_name.data = list(bytes(DEVICE_NAME, encoding='utf8'))
+ gap_data = le_advertising_facade.GapDataMsg(data=bytes(gap_name.Serialize()))
+ config = le_advertising_facade.AdvertisingConfig(
+ advertisement=[gap_data],
+ interval_min=512,
+ interval_max=768,
+ advertising_type=le_advertising_facade.AdvertisingEventType.ADV_IND,
+ own_address_type=common.USE_RANDOM_DEVICE_ADDRESS,
+ channel_map=7,
+ filter_policy=le_advertising_facade.AdvertisingFilterPolicy.ALL_DEVICES)
+ extended_config = le_advertising_facade.ExtendedAdvertisingConfig(
+ include_tx_power=True,
+ connectable=True,
+ legacy_pdus=True,
+ advertising_config=config,
+ secondary_advertising_phy=ble_scan_settings_phys["1m"])
+ request = le_advertising_facade.ExtendedCreateAdvertiserRequest(config=extended_config)
+ logging.info("Creating advertiser")
+ create_response = self.cert.hci_le_advertising_manager.ExtendedCreateAdvertiser(request)
+ logging.info("Created advertiser")
+
+ # Setup SL4A DUT side to scan
+ addr_type = ble_address_types["public"]
+ logging.info("Start scanning for PUBLIC_ADDRESS %s with address type %d and IRK %s" %
+ (PUBLIC_ADDRESS, addr_type, irk.decode("utf-8")))
+ self.dut.sl4a.bleSetScanSettingsScanMode(ble_scan_settings_modes['low_latency'])
+ filter_list, scan_settings, scan_callback = generate_ble_scan_objects(self.dut.sl4a)
+ expected_event_name = scan_result.format(scan_callback)
+
+ # Setup SL4A DUT filter
+ self.dut.sl4a.bleSetScanFilterDeviceAddressTypeAndIrk(PUBLIC_ADDRESS, int(addr_type), irk.decode("utf-8"))
+ self.dut.sl4a.bleBuildScanFilter(filter_list)
+
+ # Start scanning on SL4A DUT side
+ self.dut.sl4a.bleStartBleScan(filter_list, scan_settings, scan_callback)
+ logging.info("Started scanning")
+ try:
+ # Verify if there is scan result
+ event_info = self.dut.ed.pop_event(expected_event_name, self.default_timeout)
+ except queue.Empty as error:
+ logging.error("Could not find initial advertisement.")
+ return False
+ # Print out scan result
+ mac_address = event_info['data']['Result']['deviceInfo']['address']
+ logging.info("Filter advertisement with address {}".format(mac_address))
+
+ # Stop scanning
+ logging.info("Stop scanning")
+ self.dut.sl4a.bleStopBleScan(scan_callback)
+ logging.info("Stopped scanning")
+
+ # Stop advertising
+ logging.info("Stop advertising")
+ remove_request = le_advertising_facade.RemoveAdvertiserRequest(advertiser_id=create_response.advertiser_id)
+ self.cert.hci_le_advertising_manager.RemoveAdvertiser(remove_request)
+ logging.info("Stopped advertising")
+
+ return True
+
+ def test_scan_filter_device_public_address_with_irk_legacy_pdu_pending_intent(self):
+ """
+ The cert side will advertise an RPA derived from the adapter's public address.
+ """
+ data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]
+ byteArrayObject = bytearray(data)
+ irk = bytes(byteArrayObject)
+
+ DEVICE_NAME = 'Im_The_CERT!'
+ logging.info("Getting public address")
+ PUBLIC_ADDRESS = self.set_cert_privacy_policy_with_public_address_but_advertise_resolvable(irk)
+ logging.info("Done %s" % PUBLIC_ADDRESS)
+
+ # Setup cert side to advertise
+ gap_name = hci_packets.GapData()
+ gap_name.data_type = hci_packets.GapDataType.COMPLETE_LOCAL_NAME
+ gap_name.data = list(bytes(DEVICE_NAME, encoding='utf8'))
+ gap_data = le_advertising_facade.GapDataMsg(data=bytes(gap_name.Serialize()))
+ config = le_advertising_facade.AdvertisingConfig(
+ advertisement=[gap_data],
+ interval_min=512,
+ interval_max=768,
+ advertising_type=le_advertising_facade.AdvertisingEventType.ADV_IND,
+ own_address_type=common.USE_RANDOM_DEVICE_ADDRESS,
+ channel_map=7,
+ filter_policy=le_advertising_facade.AdvertisingFilterPolicy.ALL_DEVICES)
+ extended_config = le_advertising_facade.ExtendedAdvertisingConfig(
+ include_tx_power=True,
+ connectable=True,
+ legacy_pdus=True,
+ advertising_config=config,
+ secondary_advertising_phy=ble_scan_settings_phys["1m"])
+ request = le_advertising_facade.ExtendedCreateAdvertiserRequest(config=extended_config)
+ logging.info("Creating advertiser")
+ create_response = self.cert.hci_le_advertising_manager.ExtendedCreateAdvertiser(request)
+ logging.info("Created advertiser")
+
+ # Setup SL4A DUT side to scan
+ addr_type = ble_address_types["public"]
+ logging.info("Start scanning for PUBLIC_ADDRESS %s with address type %d and IRK %s" %
+ (PUBLIC_ADDRESS, addr_type, irk.decode("utf-8")))
+ self.dut.sl4a.bleSetScanSettingsScanMode(ble_scan_settings_modes['low_latency'])
+ filter_list, scan_settings, scan_callback = generate_ble_scan_objects(self.dut.sl4a)
+ # The event name needs to be set to this otherwise the index iterates from the scancallbacks
+ # being run consecutively. This is a Pending Intent callback but it hooks into the
+ # ScanCallback and uses just the 1 for the index.
+ expected_event_name = "BleScan1onScanResults"
+
+ # Setup SL4A DUT filter
+ self.dut.sl4a.bleSetScanFilterDeviceAddressTypeAndIrk(PUBLIC_ADDRESS, int(addr_type), irk.decode("utf-8"))
+ self.dut.sl4a.bleBuildScanFilter(filter_list)
+
+ # Start scanning on SL4A DUT side
+ self.dut.sl4a.bleStartBleScanPendingIntent(filter_list, scan_settings)
+ logging.info("Started scanning")
+ try:
+ # Verify if there is scan result
+ event_info = self.dut.ed.pop_event(expected_event_name, self.default_timeout)
+ except queue.Empty as error:
+ logging.error("Could not find initial advertisement.")
+ return False
+ # Print out scan result
+ mac_address = event_info['data']['Result']['deviceInfo']['address']
+ logging.info("Filter advertisement with address {}".format(mac_address))
+
+ # Stop scanning
+ logging.info("Stop scanning")
+ self.dut.sl4a.bleStopBleScan(scan_callback)
+ logging.info("Stopped scanning")
+
+ # Stop advertising
+ logging.info("Stop advertising")
+ remove_request = le_advertising_facade.RemoveAdvertiserRequest(advertiser_id=create_response.advertiser_id)
+ self.cert.hci_le_advertising_manager.RemoveAdvertiser(remove_request)
+ logging.info("Stopped advertising")
+
+ return True
+
+ def test_scan_filter_device_public_address_with_irk_extended_pdu(self):
+ """
+ The cert side will advertise an RPA derived from the adapter's public address.
+ """
+ data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]
+ byteArrayObject = bytearray(data)
+ irk = bytes(byteArrayObject)
+
+ DEVICE_NAME = 'Im_The_CERT!'
+ logging.info("Getting public address")
+ PUBLIC_ADDRESS = self.set_cert_privacy_policy_with_public_address_but_advertise_resolvable(irk)
+ logging.info("Done %s" % PUBLIC_ADDRESS)
+
+ # Setup cert side to advertise
+ gap_name = hci_packets.GapData()
+ gap_name.data_type = hci_packets.GapDataType.COMPLETE_LOCAL_NAME
+ gap_name.data = list(bytes(DEVICE_NAME, encoding='utf8'))
+ gap_data = le_advertising_facade.GapDataMsg(data=bytes(gap_name.Serialize()))
+ config = le_advertising_facade.AdvertisingConfig(
+ advertisement=[gap_data],
+ interval_min=512,
+ interval_max=768,
+ advertising_type=le_advertising_facade.AdvertisingEventType.ADV_IND,
+ own_address_type=common.USE_RANDOM_DEVICE_ADDRESS,
+ channel_map=7,
+ filter_policy=le_advertising_facade.AdvertisingFilterPolicy.ALL_DEVICES)
+ extended_config = le_advertising_facade.ExtendedAdvertisingConfig(
+ include_tx_power=True,
+ connectable=True,
+ legacy_pdus=False,
+ advertising_config=config,
+ secondary_advertising_phy=ble_scan_settings_phys["1m"])
+ request = le_advertising_facade.ExtendedCreateAdvertiserRequest(config=extended_config)
+ logging.info("Creating advertiser")
+ create_response = self.cert.hci_le_advertising_manager.ExtendedCreateAdvertiser(request)
+ logging.info("Created advertiser")
+
+ # Setup SL4A DUT side to scan
+ addr_type = ble_address_types["public"]
+ logging.info("Start scanning for PUBLIC_ADDRESS %s with address type %d and IRK %s" %
+ (PUBLIC_ADDRESS, addr_type, irk.decode("utf-8")))
+ self.dut.sl4a.bleSetScanSettingsScanMode(ble_scan_settings_modes['low_latency'])
+ self.dut.sl4a.bleSetScanSettingsLegacy(False)
+ filter_list, scan_settings, scan_callback = generate_ble_scan_objects(self.dut.sl4a)
+ expected_event_name = scan_result.format(scan_callback)
+
+ # Setup SL4A DUT filter
+ self.dut.sl4a.bleSetScanFilterDeviceAddressTypeAndIrk(PUBLIC_ADDRESS, int(addr_type), irk.decode("utf-8"))
+ self.dut.sl4a.bleBuildScanFilter(filter_list)
+
+ # Start scanning on SL4A DUT side
+ self.dut.sl4a.bleStartBleScan(filter_list, scan_settings, scan_callback)
+ logging.info("Started scanning")
+ try:
+ # Verify if there is scan result
+ event_info = self.dut.ed.pop_event(expected_event_name, self.default_timeout)
+ except queue.Empty as error:
+ logging.error("Could not find initial advertisement.")
+ return False
+ # Print out scan result
+ mac_address = event_info['data']['Result']['deviceInfo']['address']
+ logging.info("Filter advertisement with address {}".format(mac_address))
+
+ # Stop scanning
+ logging.info("Stop scanning")
+ self.dut.sl4a.bleStopBleScan(scan_callback)
+ logging.info("Stopped scanning")
+
+ # Stop advertising
+ logging.info("Stop advertising")
+ remove_request = le_advertising_facade.RemoveAdvertiserRequest(advertiser_id=create_response.advertiser_id)
+ self.cert.hci_le_advertising_manager.RemoveAdvertiser(remove_request)
+ logging.info("Stopped advertising")
+
+ return True
+
def test_scan_filter_device_name_legacy_pdu(self):
# Use public address on cert side
logging.info("Setting public address")
@@ -247,3 +498,428 @@ class LeAdvancedScanningTest(GdSl4aBaseTestClass):
logging.info("Stopped advertising")
return True
+
+ def test_scan_filter_device_public_address_with_irk_extended_pdu_pending_intent(self):
+ """
+ The cert side will advertise an RPA derived from the adapter's public address.
+ """
+ data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]
+ byteArrayObject = bytearray(data)
+ irk = bytes(byteArrayObject)
+
+ DEVICE_NAME = 'Im_The_CERT!'
+ logging.info("Getting public address")
+ PUBLIC_ADDRESS = self.set_cert_privacy_policy_with_public_address_but_advertise_resolvable(irk)
+ logging.info("Done %s" % PUBLIC_ADDRESS)
+
+ # Setup cert side to advertise
+ gap_name = hci_packets.GapData()
+ gap_name.data_type = hci_packets.GapDataType.COMPLETE_LOCAL_NAME
+ gap_name.data = list(bytes(DEVICE_NAME, encoding='utf8'))
+ gap_data = le_advertising_facade.GapDataMsg(data=bytes(gap_name.Serialize()))
+ config = le_advertising_facade.AdvertisingConfig(
+ advertisement=[gap_data],
+ interval_min=512,
+ interval_max=768,
+ advertising_type=le_advertising_facade.AdvertisingEventType.ADV_IND,
+ own_address_type=common.USE_RANDOM_DEVICE_ADDRESS,
+ channel_map=7,
+ filter_policy=le_advertising_facade.AdvertisingFilterPolicy.ALL_DEVICES)
+ extended_config = le_advertising_facade.ExtendedAdvertisingConfig(
+ include_tx_power=True,
+ connectable=True,
+ legacy_pdus=False,
+ advertising_config=config,
+ secondary_advertising_phy=ble_scan_settings_phys["1m"])
+ request = le_advertising_facade.ExtendedCreateAdvertiserRequest(config=extended_config)
+ logging.info("Creating advertiser")
+ create_response = self.cert.hci_le_advertising_manager.ExtendedCreateAdvertiser(request)
+ logging.info("Created advertiser")
+
+ # Setup SL4A DUT side to scan
+ addr_type = ble_address_types["public"]
+ logging.info("Start scanning for PUBLIC_ADDRESS %s with address type %d and IRK %s" %
+ (PUBLIC_ADDRESS, addr_type, irk.decode("utf-8")))
+ self.dut.sl4a.bleSetScanSettingsScanMode(ble_scan_settings_modes['low_latency'])
+ self.dut.sl4a.bleSetScanSettingsLegacy(False)
+ filter_list, scan_settings, scan_callback = generate_ble_scan_objects(self.dut.sl4a)
+ # Hard code here since callback index iterates and will cause this to fail if ran
+ # Second as the impl in SL4A sends this since its a single callback for broadcast.
+ expected_event_name = "BleScan1onScanResults"
+
+ # Setup SL4A DUT filter
+ self.dut.sl4a.bleSetScanFilterDeviceAddressTypeAndIrk(PUBLIC_ADDRESS, int(addr_type), irk.decode("utf-8"))
+ self.dut.sl4a.bleBuildScanFilter(filter_list)
+
+ # Start scanning on SL4A DUT side
+ self.dut.sl4a.bleStartBleScanPendingIntent(filter_list, scan_settings)
+ logging.info("Started scanning")
+ try:
+ # Verify if there is scan result
+ event_info = self.dut.ed.pop_event(expected_event_name, self.default_timeout)
+ except queue.Empty as error:
+ logging.error("Could not find initial advertisement.")
+ return False
+ # Print out scan result
+ mac_address = event_info['data']['Result']['deviceInfo']['address']
+ logging.info("Filter advertisement with address {}".format(mac_address))
+
+ # Stop scanning
+ logging.info("Stop scanning")
+ self.dut.sl4a.bleStopBleScan(scan_callback)
+ logging.info("Stopped scanning")
+
+ # Stop advertising
+ logging.info("Stop advertising")
+ remove_request = le_advertising_facade.RemoveAdvertiserRequest(advertiser_id=create_response.advertiser_id)
+ self.cert.hci_le_advertising_manager.RemoveAdvertiser(remove_request)
+ logging.info("Stopped advertising")
+
+ return True
+
+ def test_scan_filter_device_random_address_with_irk_extended_pdu(self):
+ """
+ The CERT side will advertise an RPA derived from the IRK.
+
+ The DUT (SL4A) side will scan for a RPA with matching IRK.
+ """
+ data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]
+ byteArrayObject = bytearray(data)
+ irk = bytes(byteArrayObject)
+
+ DEVICE_NAME = 'Im_The_CERT!'
+ logging.info("Getting public address")
+ RANDOM_ADDRESS = self.set_cert_privacy_policy_with_random_address_but_advertise_resolvable(irk)
+ logging.info("Done %s" % RANDOM_ADDRESS)
+
+ # Setup cert side to advertise
+ gap_name = hci_packets.GapData()
+ gap_name.data_type = hci_packets.GapDataType.COMPLETE_LOCAL_NAME
+ gap_name.data = list(bytes(DEVICE_NAME, encoding='utf8'))
+ gap_data = le_advertising_facade.GapDataMsg(data=bytes(gap_name.Serialize()))
+ config = le_advertising_facade.AdvertisingConfig(
+ advertisement=[gap_data],
+ interval_min=512,
+ interval_max=768,
+ advertising_type=le_advertising_facade.AdvertisingEventType.ADV_IND,
+ own_address_type=common.USE_RANDOM_DEVICE_ADDRESS,
+ channel_map=7,
+ filter_policy=le_advertising_facade.AdvertisingFilterPolicy.ALL_DEVICES)
+
+ extended_config = le_advertising_facade.ExtendedAdvertisingConfig(
+ include_tx_power=True,
+ connectable=True,
+ legacy_pdus=False,
+ advertising_config=config,
+ secondary_advertising_phy=ble_scan_settings_phys["1m"])
+ request = le_advertising_facade.ExtendedCreateAdvertiserRequest(config=extended_config)
+ logging.info("Creating advertiser")
+ create_response = self.cert.hci_le_advertising_manager.ExtendedCreateAdvertiser(request)
+ logging.info("Created advertiser")
+
+ # Setup SL4A DUT side to scan
+ addr_type = ble_address_types["random"]
+ logging.info("Start scanning for RANDOM_ADDRESS %s with address type %d and IRK %s" %
+ (RANDOM_ADDRESS, addr_type, irk.decode("utf-8")))
+ self.dut.sl4a.bleSetScanSettingsScanMode(ble_scan_settings_modes['low_latency'])
+ self.dut.sl4a.bleSetScanSettingsLegacy(False)
+ filter_list, scan_settings, scan_callback = generate_ble_scan_objects(self.dut.sl4a)
+ expected_event_name = scan_result.format(scan_callback)
+
+ # Setup SL4A DUT filter
+ self.dut.sl4a.bleSetScanFilterDeviceAddressTypeAndIrk(RANDOM_ADDRESS, int(addr_type), irk.decode("utf-8"))
+ self.dut.sl4a.bleBuildScanFilter(filter_list)
+
+ # Start scanning on SL4A DUT side
+ self.dut.sl4a.bleStartBleScan(filter_list, scan_settings, scan_callback)
+ logging.info("Started scanning")
+ try:
+ # Verify if there is scan result
+ event_info = self.dut.ed.pop_event(expected_event_name, self.default_timeout)
+ except queue.Empty as error:
+ logging.error("Could not find initial advertisement.")
+ return False
+ # Print out scan result
+ mac_address = event_info['data']['Result']['deviceInfo']['address']
+ logging.info("Filter advertisement with address {}".format(mac_address))
+
+ # Stop scanning
+ logging.info("Stop scanning")
+ self.dut.sl4a.bleStopBleScan(scan_callback)
+ logging.info("Stopped scanning")
+
+ # Stop advertising
+ logging.info("Stop advertising")
+ remove_request = le_advertising_facade.RemoveAdvertiserRequest(advertiser_id=create_response.advertiser_id)
+ self.cert.hci_le_advertising_manager.RemoveAdvertiser(remove_request)
+ logging.info("Stopped advertising")
+
+ return True
+
+ def test_scan_filter_device_random_address_with_irk_extended_pdu_pending_intent(self):
+ """
+ The cert side will advertise an RPA derived from the IRK.
+
+ The DUT (SL4A) side will scan for a RPA with matching IRK.
+
+ The DUT will get results via Pending Intent.
+ """
+ data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]
+ byteArrayObject = bytearray(data)
+ irk = bytes(byteArrayObject)
+
+ DEVICE_NAME = 'Im_The_CERT!'
+ logging.info("Getting public address")
+ RANDOM_ADDRESS = self.set_cert_privacy_policy_with_random_address_but_advertise_resolvable(irk)
+ logging.info("Done %s" % RANDOM_ADDRESS)
+
+ # Setup cert side to advertise
+ gap_name = hci_packets.GapData()
+ gap_name.data_type = hci_packets.GapDataType.COMPLETE_LOCAL_NAME
+ gap_name.data = list(bytes(DEVICE_NAME, encoding='utf8'))
+ gap_data = le_advertising_facade.GapDataMsg(data=bytes(gap_name.Serialize()))
+ config = le_advertising_facade.AdvertisingConfig(
+ advertisement=[gap_data],
+ interval_min=512,
+ interval_max=768,
+ advertising_type=le_advertising_facade.AdvertisingEventType.ADV_IND,
+ own_address_type=common.USE_RANDOM_DEVICE_ADDRESS,
+ channel_map=7,
+ filter_policy=le_advertising_facade.AdvertisingFilterPolicy.ALL_DEVICES)
+
+ extended_config = le_advertising_facade.ExtendedAdvertisingConfig(
+ include_tx_power=True,
+ connectable=True,
+ legacy_pdus=False,
+ advertising_config=config,
+ secondary_advertising_phy=ble_scan_settings_phys["1m"])
+ request = le_advertising_facade.ExtendedCreateAdvertiserRequest(config=extended_config)
+ logging.info("Creating advertiser")
+ create_response = self.cert.hci_le_advertising_manager.ExtendedCreateAdvertiser(request)
+ logging.info("Created advertiser")
+
+ # Setup SL4A DUT side to scan
+ addr_type = ble_address_types["random"]
+ logging.info("Start scanning for RANDOM_ADDRESS %s with address type %d and IRK %s" %
+ (RANDOM_ADDRESS, addr_type, irk.decode("utf-8")))
+ self.dut.sl4a.bleSetScanSettingsScanMode(ble_scan_settings_modes['low_latency'])
+ self.dut.sl4a.bleSetScanSettingsLegacy(False)
+ filter_list, scan_settings, scan_callback = generate_ble_scan_objects(self.dut.sl4a)
+ # Hard code here since callback index iterates and will cause this to fail if ran
+ # Second as the impl in SL4A sends this since its a single callback for broadcast.
+ expected_event_name = "BleScan1onScanResults"
+
+ # Setup SL4A DUT filter
+ self.dut.sl4a.bleSetScanFilterDeviceAddressTypeAndIrk(RANDOM_ADDRESS, int(addr_type), irk.decode("utf-8"))
+ self.dut.sl4a.bleBuildScanFilter(filter_list)
+
+ # Start scanning on SL4A DUT side
+ self.dut.sl4a.bleStartBleScanPendingIntent(filter_list, scan_settings)
+ logging.info("Started scanning")
+ try:
+ # Verify if there is scan result
+ event_info = self.dut.ed.pop_event(expected_event_name, self.default_timeout)
+ except queue.Empty as error:
+ logging.error("Could not find initial advertisement.")
+ return False
+ # Print out scan result
+ mac_address = event_info['data']['Result']['deviceInfo']['address']
+ logging.info("Filter advertisement with address {}".format(mac_address))
+
+ # Stop scanning
+ logging.info("Stop scanning")
+ self.dut.sl4a.bleStopBleScan(scan_callback)
+ logging.info("Stopped scanning")
+
+ # Stop advertising
+ logging.info("Stop advertising")
+ remove_request = le_advertising_facade.RemoveAdvertiserRequest(advertiser_id=create_response.advertiser_id)
+ self.cert.hci_le_advertising_manager.RemoveAdvertiser(remove_request)
+ logging.info("Stopped advertising")
+
+ return True
+
+ def test_scan_filter_device_random_address_with_irk_extended_pdu_scan_twice(self):
+ """
+ The cert side will advertise an RPA derived from the IRK.
+
+ The DUT (SL4A) side will scan for a RPA with matching IRK.
+
+ The DUT will get results via Scan Callback.
+
+ Scan will stop and then start again to verify sequential scanning with IRK.
+ """
+ data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]
+ byteArrayObject = bytearray(data)
+ irk = bytes(byteArrayObject)
+
+ DEVICE_NAME = 'Im_The_CERT!'
+ logging.info("Getting public address")
+ RANDOM_ADDRESS = self.set_cert_privacy_policy_with_random_address_but_advertise_resolvable(irk)
+ logging.info("Done %s" % RANDOM_ADDRESS)
+
+ # Setup cert side to advertise
+ gap_name = hci_packets.GapData()
+ gap_name.data_type = hci_packets.GapDataType.COMPLETE_LOCAL_NAME
+ gap_name.data = list(bytes(DEVICE_NAME, encoding='utf8'))
+ gap_data = le_advertising_facade.GapDataMsg(data=bytes(gap_name.Serialize()))
+ config = le_advertising_facade.AdvertisingConfig(
+ advertisement=[gap_data],
+ interval_min=512,
+ interval_max=768,
+ advertising_type=le_advertising_facade.AdvertisingEventType.ADV_IND,
+ own_address_type=common.USE_RANDOM_DEVICE_ADDRESS,
+ channel_map=7,
+ filter_policy=le_advertising_facade.AdvertisingFilterPolicy.ALL_DEVICES)
+
+ extended_config = le_advertising_facade.ExtendedAdvertisingConfig(
+ include_tx_power=True,
+ connectable=True,
+ legacy_pdus=False,
+ advertising_config=config,
+ secondary_advertising_phy=ble_scan_settings_phys["1m"])
+ request = le_advertising_facade.ExtendedCreateAdvertiserRequest(config=extended_config)
+ logging.info("Creating advertiser")
+ create_response = self.cert.hci_le_advertising_manager.ExtendedCreateAdvertiser(request)
+ logging.info("Created advertiser")
+
+ # Setup SL4A DUT side to scan
+ addr_type = ble_address_types["random"]
+ logging.info("Start scanning for RANDOM_ADDRESS %s with address type %d and IRK %s" %
+ (RANDOM_ADDRESS, addr_type, irk.decode("utf-8")))
+ self.dut.sl4a.bleSetScanSettingsScanMode(ble_scan_settings_modes['low_latency'])
+ self.dut.sl4a.bleSetScanSettingsLegacy(False)
+ filter_list, scan_settings, scan_callback = generate_ble_scan_objects(self.dut.sl4a)
+ expected_event_name = scan_result.format(scan_callback)
+
+ # Setup SL4A DUT filter
+ self.dut.sl4a.bleSetScanFilterDeviceAddressTypeAndIrk(RANDOM_ADDRESS, int(addr_type), irk.decode("utf-8"))
+ self.dut.sl4a.bleBuildScanFilter(filter_list)
+
+ # Start scanning on SL4A DUT side
+ self.dut.sl4a.bleStartBleScan(filter_list, scan_settings, scan_callback)
+ logging.info("Started scanning")
+ try:
+ # Verify if there is scan result
+ event_info = self.dut.ed.pop_event(expected_event_name, self.default_timeout)
+ except queue.Empty as error:
+ logging.error("Could not find initial advertisement.")
+ return False
+ # Print out scan result
+ mac_address = event_info['data']['Result']['deviceInfo']['address']
+ logging.info("Filter advertisement with address {}".format(mac_address))
+
+ # Stop scanning
+ logging.info("Stop scanning")
+ self.dut.sl4a.bleStopBleScan(scan_callback)
+ logging.info("Stopped scanning")
+
+ # Start scanning on SL4A DUT side
+ self.dut.sl4a.bleStartBleScan(filter_list, scan_settings, scan_callback)
+ logging.info("Started scanning...again")
+ try:
+ # Verify if there is scan result
+ event_info = self.dut.ed.pop_event(expected_event_name, self.default_timeout)
+ except queue.Empty as error:
+ logging.error("Could not find initial advertisement.")
+ return False
+ # Print out scan result
+ mac_address = event_info['data']['Result']['deviceInfo']['address']
+ logging.info("Filter advertisement with address {}".format(mac_address))
+
+ # Stop scanning
+ logging.info("Stop scanning")
+ self.dut.sl4a.bleStopBleScan(scan_callback)
+ logging.info("Stopped scanning")
+
+ # Stop advertising
+ logging.info("Stop advertising")
+ remove_request = le_advertising_facade.RemoveAdvertiserRequest(advertiser_id=create_response.advertiser_id)
+ self.cert.hci_le_advertising_manager.RemoveAdvertiser(remove_request)
+ logging.info("Stopped advertising")
+
+ return True
+
+ def test_scan_filter_device_random_address_with_irk_extended_pdu_pending_intent_128_640(self):
+ """
+ The CERT side will advertise an RPA derived from the IRK.
+
+ The DUT (SL4A) side will scan for a RPA with matching IRK.
+
+ Adjust the scan intervals to Digital Carkey specific timings.
+ """
+ data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]
+ byteArrayObject = bytearray(data)
+ irk = bytes(byteArrayObject)
+
+ DEVICE_NAME = 'Im_The_CERT!'
+ logging.info("Getting public address")
+ RANDOM_ADDRESS = self.set_cert_privacy_policy_with_random_address_but_advertise_resolvable(irk)
+ logging.info("Done %s" % RANDOM_ADDRESS)
+
+ # Setup cert side to advertise
+ gap_name = hci_packets.GapData()
+ gap_name.data_type = hci_packets.GapDataType.COMPLETE_LOCAL_NAME
+ gap_name.data = list(bytes(DEVICE_NAME, encoding='utf8'))
+ gap_data = le_advertising_facade.GapDataMsg(data=bytes(gap_name.Serialize()))
+ config = le_advertising_facade.AdvertisingConfig(
+ advertisement=[gap_data],
+ interval_min=128,
+ interval_max=640,
+ advertising_type=le_advertising_facade.AdvertisingEventType.ADV_IND,
+ own_address_type=common.USE_RANDOM_DEVICE_ADDRESS,
+ channel_map=7,
+ filter_policy=le_advertising_facade.AdvertisingFilterPolicy.ALL_DEVICES)
+
+ extended_config = le_advertising_facade.ExtendedAdvertisingConfig(
+ include_tx_power=True,
+ connectable=True,
+ legacy_pdus=False,
+ advertising_config=config,
+ secondary_advertising_phy=ble_scan_settings_phys["1m"])
+ request = le_advertising_facade.ExtendedCreateAdvertiserRequest(config=extended_config)
+ logging.info("Creating advertiser")
+ create_response = self.cert.hci_le_advertising_manager.ExtendedCreateAdvertiser(request)
+ logging.info("Created advertiser")
+
+ # Setup SL4A DUT side to scan
+ addr_type = ble_address_types["random"]
+ logging.info("Start scanning for RANDOM_ADDRESS %s with address type %d and IRK %s" %
+ (RANDOM_ADDRESS, addr_type, irk.decode("utf-8")))
+ self.dut.sl4a.bleSetScanSettingsScanMode(ble_scan_settings_modes['low_latency'])
+ self.dut.sl4a.bleSetScanSettingsLegacy(False)
+ self.dut.sl4a.bleSetScanSettingsScanMode(3) # ambient discovery
+ filter_list, scan_settings, scan_callback = generate_ble_scan_objects(self.dut.sl4a)
+ # Hard code here since callback index iterates and will cause this to fail if ran
+ # Second as the impl in SL4A sends this since its a single callback for broadcast.
+ expected_event_name = "BleScan1onScanResults"
+
+ # Setup SL4A DUT filter
+ self.dut.sl4a.bleSetScanFilterDeviceAddressTypeAndIrk(RANDOM_ADDRESS, int(addr_type), irk.decode("utf-8"))
+ self.dut.sl4a.bleBuildScanFilter(filter_list)
+
+ # Start scanning on SL4A DUT side
+ self.dut.sl4a.bleStartBleScanPendingIntent(filter_list, scan_settings)
+ logging.info("Started scanning")
+ try:
+ # Verify if there is scan result
+ event_info = self.dut.ed.pop_event(expected_event_name, self.default_timeout)
+ except queue.Empty as error:
+ logging.error("Could not find initial advertisement.")
+ return False
+ # Print out scan result
+ mac_address = event_info['data']['Result']['deviceInfo']['address']
+ logging.info("Filter advertisement with address {}".format(mac_address))
+
+ # Stop scanning
+ logging.info("Stop scanning")
+ self.dut.sl4a.bleStopBleScan(scan_callback)
+ logging.info("Stopped scanning")
+
+ # Stop advertising
+ logging.info("Stop advertising")
+ remove_request = le_advertising_facade.RemoveAdvertiserRequest(advertiser_id=create_response.advertiser_id)
+ self.cert.hci_le_advertising_manager.RemoveAdvertiser(remove_request)
+ logging.info("Stopped advertising")
+
+ return True
diff --git a/system/bta/Android.bp b/system/bta/Android.bp
index 0b48c77459..e354d0e27c 100644
--- a/system/bta/Android.bp
+++ b/system/bta/Android.bp
@@ -80,6 +80,7 @@ cc_library_static {
"gatt/bta_gatts_act.cc",
"gatt/bta_gatts_api.cc",
"gatt/bta_gatts_main.cc",
+ "gatt/bta_gatts_queue.cc",
"gatt/bta_gatts_utils.cc",
"gatt/database.cc",
"gatt/database_builder.cc",
diff --git a/system/bta/dm/bta_dm_pm.cc b/system/bta/dm/bta_dm_pm.cc
index 6f7c89c238..6a7d6577d6 100644
--- a/system/bta/dm/bta_dm_pm.cc
+++ b/system/bta/dm/bta_dm_pm.cc
@@ -856,8 +856,8 @@ void bta_dm_pm_active(const RawAddress& peer_addr) {
PRIVATE_ADDRESS(peer_addr));
break;
case BTM_SUCCESS:
- LOG_INFO("Active power mode already set for device:%s",
- PRIVATE_ADDRESS(peer_addr));
+ LOG_DEBUG("Active power mode already set for device:%s",
+ PRIVATE_ADDRESS(peer_addr));
break;
default:
LOG_WARN("Unable to set active power mode for device:%s status:%s",
diff --git a/system/bta/gatt/bta_gatts_queue.cc b/system/bta/gatt/bta_gatts_queue.cc
new file mode 100644
index 0000000000..d8c51adb2f
--- /dev/null
+++ b/system/bta/gatt/bta_gatts_queue.cc
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2017 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.
+ */
+
+#include <list>
+#include <unordered_map>
+#include <unordered_set>
+
+#include "bta_gatt_server_queue.h"
+
+using gatts_operation = BtaGattServerQueue::gatts_operation;
+using bluetooth::Uuid;
+
+constexpr uint8_t GATT_NOTIFY = 1;
+
+std::unordered_map<uint16_t, std::list<gatts_operation>>
+ BtaGattServerQueue::gatts_op_queue;
+std::unordered_set<uint16_t> BtaGattServerQueue::gatts_op_queue_executing;
+std::unordered_map<uint16_t, bool> BtaGattServerQueue::congestion_queue;
+
+void BtaGattServerQueue::mark_as_not_executing(uint16_t conn_id) {
+ gatts_op_queue_executing.erase(conn_id);
+}
+
+void BtaGattServerQueue::gatts_execute_next_op(uint16_t conn_id) {
+ APPL_TRACE_DEBUG("%s: conn_id=0x%x", __func__, conn_id);
+
+ if (gatts_op_queue.empty()) {
+ APPL_TRACE_DEBUG("%s: op queue is empty", __func__);
+ return;
+ }
+
+ auto ptr = congestion_queue.find(conn_id);
+
+ if (ptr != congestion_queue.end()) {
+ bool is_congested = ptr->second;
+ APPL_TRACE_DEBUG(
+ "%s: congestion queue exist, conn_id: %d, is_congested: %d", __func__,
+ conn_id, is_congested);
+ if (is_congested) {
+ APPL_TRACE_DEBUG("%s: lower layer is congested", __func__);
+ return;
+ }
+ }
+
+ auto map_ptr = gatts_op_queue.find(conn_id);
+
+ if (map_ptr == gatts_op_queue.end()) {
+ APPL_TRACE_DEBUG("%s: Queue is null", __func__);
+ return;
+ }
+
+ if (map_ptr->second.empty()) {
+ APPL_TRACE_DEBUG("%s: queue is empty for conn_id: %d", __func__, conn_id);
+ return;
+ }
+
+ if (gatts_op_queue_executing.count(conn_id)) {
+ APPL_TRACE_DEBUG("%s: can't enqueue next op, already executing", __func__);
+ return;
+ }
+
+ gatts_operation op = map_ptr->second.front();
+ APPL_TRACE_DEBUG("%s: op.type=%d, attr_id=%d", __func__, op.type, op.attr_id);
+
+ if (op.type == GATT_NOTIFY) {
+ BTA_GATTS_HandleValueIndication(conn_id, op.attr_id, op.value,
+ op.need_confirm);
+ gatts_op_queue_executing.insert(conn_id);
+ }
+}
+
+void BtaGattServerQueue::Clean(uint16_t conn_id) {
+ APPL_TRACE_DEBUG("%s: conn_id=0x%x", __func__, conn_id);
+
+ gatts_op_queue.erase(conn_id);
+ gatts_op_queue_executing.erase(conn_id);
+}
+
+void BtaGattServerQueue::SendNotification(uint16_t conn_id, uint16_t handle,
+ std::vector<uint8_t> value,
+ bool need_confirm) {
+ gatts_op_queue[conn_id].emplace_back(
+ gatts_operation{.type = GATT_NOTIFY,
+ .attr_id = handle,
+ .value = value,
+ .need_confirm = need_confirm});
+ gatts_execute_next_op(conn_id);
+}
+
+void BtaGattServerQueue::NotificationCallback(uint16_t conn_id) {
+ auto map_ptr = gatts_op_queue.find(conn_id);
+ if (map_ptr == gatts_op_queue.end() || map_ptr->second.empty()) {
+ APPL_TRACE_DEBUG("%s: no more operations queued for conn_id %d", __func__,
+ conn_id);
+ return;
+ }
+
+ gatts_operation op = map_ptr->second.front();
+ map_ptr->second.pop_front();
+ mark_as_not_executing(conn_id);
+ gatts_execute_next_op(conn_id);
+}
+
+void BtaGattServerQueue::CongestionCallback(uint16_t conn_id, bool congested) {
+ APPL_TRACE_DEBUG("%s: conn_id: %d, congested: %d", __func__, conn_id,
+ congested);
+
+ congestion_queue[conn_id] = congested;
+ if (!congested) {
+ gatts_execute_next_op(conn_id);
+ }
+}
diff --git a/system/bta/include/bta_gatt_server_queue.h b/system/bta/include/bta_gatt_server_queue.h
new file mode 100644
index 0000000000..a8bb4b4442
--- /dev/null
+++ b/system/bta/include/bta_gatt_server_queue.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 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.
+ */
+
+#include <list>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include "bta_gatt_api.h"
+
+class BtaGattServerQueue {
+ public:
+ static void Clean(uint16_t conn_id);
+ static void SendNotification(uint16_t conn_id, uint16_t handle,
+ std::vector<uint8_t> value, bool need_confirm);
+ static void NotificationCallback(uint16_t conn_id);
+ static void CongestionCallback(uint16_t conn_id, bool congested);
+
+ /* Holds pending GATT operations */
+ struct gatts_operation {
+ uint8_t type;
+ uint16_t attr_id;
+ std::vector<uint8_t> value;
+ bool need_confirm;
+ };
+
+ private:
+ static bool is_congested;
+ static void mark_as_not_executing(uint16_t conn_id);
+ static void gatts_execute_next_op(uint16_t conn_id);
+
+ // maps connection id to operations waiting for execution
+ static std::unordered_map<uint16_t, std::list<gatts_operation>>
+ gatts_op_queue;
+
+ // maps connection id to congestion status of each device
+ static std::unordered_map<uint16_t, bool> congestion_queue;
+
+ // contain connection ids that currently execute operations
+ static std::unordered_set<uint16_t> gatts_op_queue_executing;
+}; \ No newline at end of file
diff --git a/system/bta/le_audio/client.cc b/system/bta/le_audio/client.cc
index 999b1dd6c8..bb2c244587 100644
--- a/system/bta/le_audio/client.cc
+++ b/system/bta/le_audio/client.cc
@@ -2449,7 +2449,7 @@ class LeAudioClientImpl : public LeAudioClient {
if (lc3_decoder_left_mem) {
free(lc3_decoder_left_mem);
lc3_decoder_left_mem = nullptr;
- free(lc3_decoder_left_mem);
+ free(lc3_decoder_right_mem);
lc3_decoder_right_mem = nullptr;
}
}
diff --git a/system/btif/src/btif_storage.cc b/system/btif/src/btif_storage.cc
index 1f130d726f..7e4d72fcaf 100644
--- a/system/btif/src/btif_storage.cc
+++ b/system/btif/src/btif_storage.cc
@@ -952,8 +952,14 @@ void btif_storage_load_consolidate_devices(void) {
LOG_INFO("found consolidated device %s %s",
bonded_devices.devices[i].ToString().c_str(),
key.pid_key.identity_addr.ToString().c_str());
- consolidated_devices.emplace_back(bonded_devices.devices[i],
- key.pid_key.identity_addr);
+
+ if (bonded_devices.devices[i].IsEmpty() ||
+ key.pid_key.identity_addr.IsEmpty()) {
+ LOG_WARN("Address is empty! Skip");
+ } else {
+ consolidated_devices.emplace_back(bonded_devices.devices[i],
+ key.pid_key.identity_addr);
+ }
}
}
}
diff --git a/system/gd/rust/linux/client/src/command_handler.rs b/system/gd/rust/linux/client/src/command_handler.rs
index d10b005173..f2221cc5e8 100644
--- a/system/gd/rust/linux/client/src/command_handler.rs
+++ b/system/gd/rust/linux/client/src/command_handler.rs
@@ -487,18 +487,26 @@ impl CommandHandler {
name: String::from("Classic Device"),
};
- let (bonded, connected, uuids) = {
+ let (name, alias, device_type, class, bonded, connected, uuids) = {
let ctx = self.context.lock().unwrap();
let adapter = ctx.adapter_dbus.as_ref().unwrap();
+ let name = adapter.get_remote_name(device.clone());
+ let device_type = adapter.get_remote_type(device.clone());
+ let alias = adapter.get_remote_alias(device.clone());
+ let class = adapter.get_remote_class(device.clone());
let bonded = adapter.get_bond_state(device.clone());
let connected = adapter.get_connection_state(device.clone());
let uuids = adapter.get_remote_uuids(device.clone());
- (bonded, connected, uuids)
+ (name, alias, device_type, class, bonded, connected, uuids)
};
print_info!("Address: {}", &device.address);
+ print_info!("Name: {}", name);
+ print_info!("Alias: {}", alias);
+ print_info!("Type: {:?}", device_type);
+ print_info!("Class: {}", class);
print_info!("Bonded: {}", bonded);
print_info!("Connected: {}", connected);
print_info!(
diff --git a/system/gd/rust/linux/client/src/dbus_iface.rs b/system/gd/rust/linux/client/src/dbus_iface.rs
index 098963adda..5a93233bba 100644
--- a/system/gd/rust/linux/client/src/dbus_iface.rs
+++ b/system/gd/rust/linux/client/src/dbus_iface.rs
@@ -1,6 +1,6 @@
//! D-Bus proxy implementations of the APIs.
-use bt_topshim::btif::{BtSspVariant, BtTransport, Uuid128Bit};
+use bt_topshim::btif::{BtDeviceType, BtSspVariant, BtTransport, Uuid128Bit};
use bt_topshim::profiles::gatt::GattStatus;
use btstack::bluetooth::{
@@ -37,12 +37,13 @@ fn make_object_path(idx: i32, name: &str) -> dbus::Path {
dbus::Path::new(format!("/org/chromium/bluetooth/hci{}/{}", idx, name)).unwrap()
}
-impl_dbus_arg_enum!(BtTransport);
+impl_dbus_arg_enum!(BtDeviceType);
impl_dbus_arg_enum!(BtSspVariant);
+impl_dbus_arg_enum!(BtTransport);
impl_dbus_arg_enum!(GattStatus);
+impl_dbus_arg_enum!(GattWriteRequestStatus);
impl_dbus_arg_enum!(GattWriteType);
impl_dbus_arg_enum!(LePhy);
-impl_dbus_arg_enum!(GattWriteRequestStatus);
impl_dbus_arg_enum!(Profile);
// Represents Uuid128Bit as an array in D-Bus.
@@ -383,6 +384,26 @@ impl IBluetooth for BluetoothDBus {
dbus_generated!()
}
+ #[dbus_method("GetRemoteName")]
+ fn get_remote_name(&self, device: BluetoothDevice) -> String {
+ dbus_generated!()
+ }
+
+ #[dbus_method("GetRemoteType")]
+ fn get_remote_type(&self, device: BluetoothDevice) -> BtDeviceType {
+ dbus_generated!()
+ }
+
+ #[dbus_method("GetRemoteAlias")]
+ fn get_remote_alias(&self, device: BluetoothDevice) -> String {
+ dbus_generated!()
+ }
+
+ #[dbus_method("GetRemoteClass")]
+ fn get_remote_class(&self, device: BluetoothDevice) -> u32 {
+ dbus_generated!()
+ }
+
#[dbus_method("GetConnectionState")]
fn get_connection_state(&self, device: BluetoothDevice) -> u32 {
dbus_generated!()
diff --git a/system/gd/rust/linux/service/src/iface_bluetooth.rs b/system/gd/rust/linux/service/src/iface_bluetooth.rs
index 6ace31bf72..9d84b1d3c5 100644
--- a/system/gd/rust/linux/service/src/iface_bluetooth.rs
+++ b/system/gd/rust/linux/service/src/iface_bluetooth.rs
@@ -1,6 +1,6 @@
extern crate bt_shim;
-use bt_topshim::btif::{BtSspVariant, BtTransport, Uuid128Bit};
+use bt_topshim::btif::{BtDeviceType, BtSspVariant, BtTransport, Uuid128Bit};
use btstack::bluetooth::{
BluetoothDevice, IBluetooth, IBluetoothCallback, IBluetoothConnectionCallback,
@@ -63,8 +63,9 @@ impl IBluetoothCallback for BluetoothCallbackDBus {
}
}
-impl_dbus_arg_enum!(BtTransport);
+impl_dbus_arg_enum!(BtDeviceType);
impl_dbus_arg_enum!(BtSspVariant);
+impl_dbus_arg_enum!(BtTransport);
impl_dbus_arg_enum!(Profile);
#[allow(dead_code)]
@@ -232,6 +233,26 @@ impl IBluetooth for IBluetoothDBus {
dbus_generated!()
}
+ #[dbus_method("GetRemoteName")]
+ fn get_remote_name(&self, _device: BluetoothDevice) -> String {
+ dbus_generated!()
+ }
+
+ #[dbus_method("GetRemoteType")]
+ fn get_remote_type(&self, _device: BluetoothDevice) -> BtDeviceType {
+ dbus_generated!()
+ }
+
+ #[dbus_method("GetRemoteAlias")]
+ fn get_remote_alias(&self, _device: BluetoothDevice) -> String {
+ dbus_generated!()
+ }
+
+ #[dbus_method("GetRemoteClass")]
+ fn get_remote_class(&self, _device: BluetoothDevice) -> u32 {
+ dbus_generated!()
+ }
+
#[dbus_method("GetConnectionState")]
fn get_connection_state(&self, _device: BluetoothDevice) -> u32 {
dbus_generated!()
diff --git a/system/gd/rust/linux/stack/src/bluetooth.rs b/system/gd/rust/linux/stack/src/bluetooth.rs
index e7bff62c5c..f2a610d83b 100644
--- a/system/gd/rust/linux/stack/src/bluetooth.rs
+++ b/system/gd/rust/linux/stack/src/bluetooth.rs
@@ -2,8 +2,8 @@
use bt_topshim::btif::{
BaseCallbacks, BaseCallbacksDispatcher, BluetoothInterface, BluetoothProperty, BtAclState,
- BtBondState, BtDiscoveryState, BtHciErrorCode, BtPinCode, BtPropertyType, BtScanMode,
- BtSspVariant, BtState, BtStatus, BtTransport, RawAddress, Uuid, Uuid128Bit,
+ BtBondState, BtDeviceType, BtDiscoveryState, BtHciErrorCode, BtPinCode, BtPropertyType,
+ BtScanMode, BtSspVariant, BtState, BtStatus, BtTransport, RawAddress, Uuid, Uuid128Bit,
};
use bt_topshim::{
profiles::hid_host::{HHCallbacksDispatcher, HidHost},
@@ -122,6 +122,18 @@ pub trait IBluetooth {
/// Confirm that a pairing should be completed on a bonding device.
fn set_pairing_confirmation(&self, device: BluetoothDevice, accept: bool) -> bool;
+ /// Gets the name of the remote device.
+ fn get_remote_name(&self, device: BluetoothDevice) -> String;
+
+ /// Gets the type of the remote device.
+ fn get_remote_type(&self, device: BluetoothDevice) -> BtDeviceType;
+
+ /// Gets the alias of the remote device.
+ fn get_remote_alias(&self, device: BluetoothDevice) -> String;
+
+ /// Gets the class of the remote device.
+ fn get_remote_class(&self, device: BluetoothDevice) -> u32;
+
/// Gets the connection state of a single device.
fn get_connection_state(&self, device: BluetoothDevice) -> u32;
@@ -1082,6 +1094,34 @@ impl IBluetooth for Bluetooth {
) == 0
}
+ fn get_remote_name(&self, device: BluetoothDevice) -> String {
+ match self.get_remote_device_property(&device, &BtPropertyType::BdName) {
+ Some(BluetoothProperty::BdName(name)) => return name.clone(),
+ _ => return "".to_string(),
+ }
+ }
+
+ fn get_remote_type(&self, device: BluetoothDevice) -> BtDeviceType {
+ match self.get_remote_device_property(&device, &BtPropertyType::TypeOfDevice) {
+ Some(BluetoothProperty::TypeOfDevice(device_type)) => return device_type,
+ _ => return BtDeviceType::Unknown,
+ }
+ }
+
+ fn get_remote_alias(&self, device: BluetoothDevice) -> String {
+ match self.get_remote_device_property(&device, &BtPropertyType::RemoteFriendlyName) {
+ Some(BluetoothProperty::RemoteFriendlyName(name)) => return name.clone(),
+ _ => "".to_string(),
+ }
+ }
+
+ fn get_remote_class(&self, device: BluetoothDevice) -> u32 {
+ match self.get_remote_device_property(&device, &BtPropertyType::ClassOfDevice) {
+ Some(BluetoothProperty::ClassOfDevice(class)) => return class,
+ _ => 0,
+ }
+ }
+
fn get_connection_state(&self, device: BluetoothDevice) -> u32 {
let addr = RawAddress::from_string(device.address.clone());
diff --git a/system/gd/rust/topshim/src/btif.rs b/system/gd/rust/topshim/src/btif.rs
index 73a0460c91..625f815b08 100644
--- a/system/gd/rust/topshim/src/btif.rs
+++ b/system/gd/rust/topshim/src/btif.rs
@@ -121,6 +121,7 @@ pub enum BtDeviceType {
Bredr,
Ble,
Dual,
+ Unknown,
}
#[derive(Clone, Debug, Eq, Hash, FromPrimitive, ToPrimitive, PartialEq, PartialOrd)]
diff --git a/system/stack/include/btm_api_types.h b/system/stack/include/btm_api_types.h
index 981347ff4e..5b5fd0b4b9 100644
--- a/system/stack/include/btm_api_types.h
+++ b/system/stack/include/btm_api_types.h
@@ -78,81 +78,7 @@ typedef void(tBTM_VSC_CMPL_CB)(tBTM_VSC_CMPL* p1);
/* BTM service definitions
* Used for storing EIR data to bit mask
*/
-enum {
- BTM_EIR_UUID_SERVCLASS_SERVICE_DISCOVERY_SERVER,
- /* BTM_EIR_UUID_SERVCLASS_BROWSE_GROUP_DESCRIPTOR, */
- /* BTM_EIR_UUID_SERVCLASS_PUBLIC_BROWSE_GROUP, */
- BTM_EIR_UUID_SERVCLASS_SERIAL_PORT,
- BTM_EIR_UUID_SERVCLASS_LAN_ACCESS_USING_PPP,
- BTM_EIR_UUID_SERVCLASS_DIALUP_NETWORKING,
- BTM_EIR_UUID_SERVCLASS_IRMC_SYNC,
- BTM_EIR_UUID_SERVCLASS_OBEX_OBJECT_PUSH,
- BTM_EIR_UUID_SERVCLASS_OBEX_FILE_TRANSFER,
- BTM_EIR_UUID_SERVCLASS_IRMC_SYNC_COMMAND,
- BTM_EIR_UUID_SERVCLASS_HEADSET,
- BTM_EIR_UUID_SERVCLASS_CORDLESS_TELEPHONY,
- BTM_EIR_UUID_SERVCLASS_AUDIO_SOURCE,
- BTM_EIR_UUID_SERVCLASS_AUDIO_SINK,
- BTM_EIR_UUID_SERVCLASS_AV_REM_CTRL_TARGET,
- /* BTM_EIR_UUID_SERVCLASS_ADV_AUDIO_DISTRIBUTION, */
- BTM_EIR_UUID_SERVCLASS_AV_REMOTE_CONTROL,
- /* BTM_EIR_UUID_SERVCLASS_VIDEO_CONFERENCING, */
- BTM_EIR_UUID_SERVCLASS_INTERCOM,
- BTM_EIR_UUID_SERVCLASS_FAX,
- BTM_EIR_UUID_SERVCLASS_HEADSET_AUDIO_GATEWAY,
- /* BTM_EIR_UUID_SERVCLASS_WAP, */
- /* BTM_EIR_UUID_SERVCLASS_WAP_CLIENT, */
- BTM_EIR_UUID_SERVCLASS_PANU,
- BTM_EIR_UUID_SERVCLASS_NAP,
- BTM_EIR_UUID_SERVCLASS_GN,
- BTM_EIR_UUID_SERVCLASS_DIRECT_PRINTING,
- /* BTM_EIR_UUID_SERVCLASS_REFERENCE_PRINTING, */
- BTM_EIR_UUID_SERVCLASS_IMAGING,
- BTM_EIR_UUID_SERVCLASS_IMAGING_RESPONDER,
- BTM_EIR_UUID_SERVCLASS_IMAGING_AUTO_ARCHIVE,
- BTM_EIR_UUID_SERVCLASS_IMAGING_REF_OBJECTS,
- BTM_EIR_UUID_SERVCLASS_HF_HANDSFREE,
- BTM_EIR_UUID_SERVCLASS_AG_HANDSFREE,
- BTM_EIR_UUID_SERVCLASS_DIR_PRT_REF_OBJ_SERVICE,
- /* BTM_EIR_UUID_SERVCLASS_REFLECTED_UI, */
- BTM_EIR_UUID_SERVCLASS_BASIC_PRINTING,
- BTM_EIR_UUID_SERVCLASS_PRINTING_STATUS,
- BTM_EIR_UUID_SERVCLASS_HUMAN_INTERFACE,
- BTM_EIR_UUID_SERVCLASS_CABLE_REPLACEMENT,
- BTM_EIR_UUID_SERVCLASS_HCRP_PRINT,
- BTM_EIR_UUID_SERVCLASS_HCRP_SCAN,
- /* BTM_EIR_UUID_SERVCLASS_COMMON_ISDN_ACCESS, */
- /* BTM_EIR_UUID_SERVCLASS_VIDEO_CONFERENCING_GW, */
- /* BTM_EIR_UUID_SERVCLASS_UDI_MT, */
- /* BTM_EIR_UUID_SERVCLASS_UDI_TA, */
- /* BTM_EIR_UUID_SERVCLASS_VCP, */
- BTM_EIR_UUID_SERVCLASS_SAP,
- BTM_EIR_UUID_SERVCLASS_PBAP_PCE,
- BTM_EIR_UUID_SERVCLASS_PBAP_PSE,
- /* BTM_EIR_UUID_SERVCLASS_TE_PHONE_ACCESS, */
- /* BTM_EIR_UUID_SERVCLASS_ME_PHONE_ACCESS, */
- BTM_EIR_UUID_SERVCLASS_PHONE_ACCESS,
- BTM_EIR_UUID_SERVCLASS_HEADSET_HS,
- BTM_EIR_UUID_SERVCLASS_PNP_INFORMATION,
- /* BTM_EIR_UUID_SERVCLASS_GENERIC_NETWORKING, */
- /* BTM_EIR_UUID_SERVCLASS_GENERIC_FILETRANSFER, */
- /* BTM_EIR_UUID_SERVCLASS_GENERIC_AUDIO, */
- /* BTM_EIR_UUID_SERVCLASS_GENERIC_TELEPHONY, */
- /* BTM_EIR_UUID_SERVCLASS_UPNP_SERVICE, */
- /* BTM_EIR_UUID_SERVCLASS_UPNP_IP_SERVICE, */
- /* BTM_EIR_UUID_SERVCLASS_ESDP_UPNP_IP_PAN, */
- /* BTM_EIR_UUID_SERVCLASS_ESDP_UPNP_IP_LAP, */
- /* BTM_EIR_UUID_SERVCLASS_ESDP_UPNP_IP_L2CAP, */
- BTM_EIR_UUID_SERVCLASS_VIDEO_SOURCE,
- BTM_EIR_UUID_SERVCLASS_VIDEO_SINK,
- /* BTM_EIR_UUID_SERVCLASS_VIDEO_DISTRIBUTION */
- /* BTM_EIR_UUID_SERVCLASS_HDP_PROFILE */
- BTM_EIR_UUID_SERVCLASS_MESSAGE_ACCESS,
- BTM_EIR_UUID_SERVCLASS_MESSAGE_NOTIFICATION,
- BTM_EIR_UUID_SERVCLASS_HDP_SOURCE,
- BTM_EIR_UUID_SERVCLASS_HDP_SINK,
- BTM_EIR_MAX_SERVICES
-};
+#define BTM_EIR_MAX_SERVICES 46
/* search result in EIR of inquiry database */
#define BTM_EIR_FOUND 0
diff --git a/system/stack/test/btm/stack_btm_test.cc b/system/stack/test/btm/stack_btm_test.cc
index 44c2fd834c..f585d6be97 100644
--- a/system/stack/test/btm/stack_btm_test.cc
+++ b/system/stack/test/btm/stack_btm_test.cc
@@ -218,6 +218,8 @@ TEST(ScoTest, make_sco_packet) {
osi_free(p);
}
+TEST(BtmTest, BTM_EIR_MAX_SERVICES) { ASSERT_EQ(46, BTM_EIR_MAX_SERVICES); }
+
} // namespace
void btm_sec_rmt_name_request_complete(const RawAddress* p_bd_addr,
diff --git a/tools/rootcanal/model/controller/dual_mode_controller.cc b/tools/rootcanal/model/controller/dual_mode_controller.cc
index a27b9fa97c..acd413b368 100644
--- a/tools/rootcanal/model/controller/dual_mode_controller.cc
+++ b/tools/rootcanal/model/controller/dual_mode_controller.cc
@@ -355,7 +355,7 @@ void DualModeController::HandleAcl(
std::vector<bluetooth::hci::CompletedPackets> completed_packets;
bluetooth::hci::CompletedPackets cp;
cp.connection_handle_ = handle;
- cp.host_num_of_completed_packets_ = kNumCommandPackets;
+ cp.host_num_of_completed_packets_ = 1;
completed_packets.push_back(cp);
send_event_(bluetooth::hci::NumberOfCompletedPacketsBuilder::Create(
completed_packets));
@@ -379,7 +379,7 @@ void DualModeController::HandleSco(
std::vector<bluetooth::hci::CompletedPackets> completed_packets;
bluetooth::hci::CompletedPackets cp;
cp.connection_handle_ = handle;
- cp.host_num_of_completed_packets_ = kNumCommandPackets;
+ cp.host_num_of_completed_packets_ = 1;
completed_packets.push_back(cp);
if (properties_.GetSynchronousFlowControl()) {
send_event_(bluetooth::hci::NumberOfCompletedPacketsBuilder::Create(