diff options
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 Binary files differnew file mode 100644 index 0000000000..dd7f34037e --- /dev/null +++ b/android/leaudio/app/src/main/res/drawable-hdpi/ic_bluetooth_connected_black.png 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 Binary files differnew file mode 100644 index 0000000000..f1d77881f2 --- /dev/null +++ b/android/leaudio/app/src/main/res/drawable-hdpi/ic_bluetooth_dots_black.png 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 Binary files differnew file mode 100644 index 0000000000..93dcb60154 --- /dev/null +++ b/android/leaudio/app/src/main/res/drawable-mdpi/ic_bluetooth_connected_black.png 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 Binary files differnew file mode 100644 index 0000000000..2594d78b56 --- /dev/null +++ b/android/leaudio/app/src/main/res/drawable-mdpi/ic_bluetooth_dots_black.png 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 Binary files differnew file mode 100644 index 0000000000..d414bc6268 --- /dev/null +++ b/android/leaudio/app/src/main/res/drawable-xhdpi/ic_bluetooth_connected_black.png 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 Binary files differnew file mode 100644 index 0000000000..696668fe18 --- /dev/null +++ b/android/leaudio/app/src/main/res/drawable-xhdpi/ic_bluetooth_dots_black.png 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 Binary files differnew file mode 100644 index 0000000000..7869a8d9d7 --- /dev/null +++ b/android/leaudio/app/src/main/res/drawable-xxhdpi/ic_bluetooth_connected_black.png 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 Binary files differnew file mode 100644 index 0000000000..eb0cd35875 --- /dev/null +++ b/android/leaudio/app/src/main/res/drawable-xxhdpi/ic_bluetooth_dots_black.png 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 Binary files differnew file mode 100644 index 0000000000..898f3ed59a --- /dev/null +++ b/android/leaudio/app/src/main/res/mipmap-hdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..dffca3601e --- /dev/null +++ b/android/leaudio/app/src/main/res/mipmap-hdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 0000000000..64ba76f75e --- /dev/null +++ b/android/leaudio/app/src/main/res/mipmap-mdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..dae5e08234 --- /dev/null +++ b/android/leaudio/app/src/main/res/mipmap-mdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 0000000000..e5ed46597e --- /dev/null +++ b/android/leaudio/app/src/main/res/mipmap-xhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..14ed0af350 --- /dev/null +++ b/android/leaudio/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 0000000000..b0907cac3b --- /dev/null +++ b/android/leaudio/app/src/main/res/mipmap-xxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..d8ae031549 --- /dev/null +++ b/android/leaudio/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 0000000000..2c18de9e66 --- /dev/null +++ b/android/leaudio/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 0000000000..beed3cdd2c --- /dev/null +++ b/android/leaudio/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png 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&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 Binary files differnew file mode 100644 index 0000000000..f6b961fd5a --- /dev/null +++ b/android/leaudio/gradle/wrapper/gradle-wrapper.jar 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( |