From 7cd3ba8cef755b2f722d405f2faad6d2c5ca37b0 Mon Sep 17 00:00:00 2001 From: NiroDev <23154768+NiroDeveloper@users.noreply.github.com> Date: Sat, 1 Jun 2024 19:02:14 +0200 Subject: [PATCH] Implement device list user interface --- .../bluetooth/BluetoothController.kt | 69 +++++++---- .../bluetooth/BluetoothServiceCallback.kt | 117 +++++++++++++----- .../cameraremote/bluetooth/DeviceWrapper.kt | 6 + .../bluetooth/HidDeviceCallback.kt | 23 +--- .../cameraremote/bluetooth/enums/BondState.kt | 21 ++++ .../bluetooth/enums/ConnectionState.kt | 23 ++++ .../bluetooth/helper/BluetoothHelper.kt | 55 ++++---- .../interfaces/IAmbientModeState.kt | 7 -- .../interfaces/IAppStateCallback.kt | 7 -- .../interfaces/IConnectionStateCallback.kt | 7 +- .../interfaces/IServiceStateCallback.kt | 9 ++ .../IUserInterfaceBluetoothCallback.kt | 8 +- .../interfaces/IUserInterfaceTimerCallback.kt | 6 +- .../cameraremote/ui/UserInputController.kt | 4 +- .../ui/activities/MainActivity.kt | 62 ++++++++-- .../niro/cameraremote/ui/pages/DevicesPage.kt | 106 ++++++++++++++++ .../niro/cameraremote/ui/pages/RemotePage.kt | 42 +------ .../dev/niro/cameraremote/utils/Vibrator.kt | 8 +- 18 files changed, 411 insertions(+), 169 deletions(-) create mode 100644 app/src/main/java/dev/niro/cameraremote/bluetooth/DeviceWrapper.kt create mode 100644 app/src/main/java/dev/niro/cameraremote/bluetooth/enums/BondState.kt create mode 100644 app/src/main/java/dev/niro/cameraremote/bluetooth/enums/ConnectionState.kt delete mode 100644 app/src/main/java/dev/niro/cameraremote/interfaces/IAmbientModeState.kt delete mode 100644 app/src/main/java/dev/niro/cameraremote/interfaces/IAppStateCallback.kt create mode 100644 app/src/main/java/dev/niro/cameraremote/interfaces/IServiceStateCallback.kt create mode 100644 app/src/main/java/dev/niro/cameraremote/ui/pages/DevicesPage.kt diff --git a/app/src/main/java/dev/niro/cameraremote/bluetooth/BluetoothController.kt b/app/src/main/java/dev/niro/cameraremote/bluetooth/BluetoothController.kt index 9a14c71..0f57045 100644 --- a/app/src/main/java/dev/niro/cameraremote/bluetooth/BluetoothController.kt +++ b/app/src/main/java/dev/niro/cameraremote/bluetooth/BluetoothController.kt @@ -1,16 +1,21 @@ package dev.niro.cameraremote.bluetooth import android.app.Activity +import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile import android.content.Context import android.content.Intent import android.util.Log import androidx.activity.result.ActivityResultLauncher +import androidx.annotation.WorkerThread import dev.niro.cameraremote.R +import dev.niro.cameraremote.bluetooth.enums.ConnectionState import dev.niro.cameraremote.bluetooth.helper.BluetoothPermission import dev.niro.cameraremote.bluetooth.helper.sendKeyboardPress +import dev.niro.cameraremote.bluetooth.helper.toDeviceWrapper import dev.niro.cameraremote.interfaces.IConnectionStateCallback +import dev.niro.cameraremote.interfaces.IServiceStateCallback import dev.niro.cameraremote.interfaces.IUserInterfaceBluetoothCallback import dev.niro.cameraremote.ui.activities.BluetoothPermissionActivity @@ -20,15 +25,21 @@ object BluetoothController { private var bluetoothCallback: BluetoothServiceCallback? = null - private val uiCallbackProxy = object : IConnectionStateCallback { - override fun onConnectionStateChanged(connected: Boolean) { - uiCallback?.onConnectionStateChanged(connected) + private val uiCallbackProxy = object : IConnectionStateCallback, IServiceStateCallback { + override fun onConnectionStateChange(device: BluetoothDevice, state: ConnectionState) { + uiCallback?.onConnectionStateChange(device.toDeviceWrapper(state)) } - override fun onConnectionError(message: Int) { - uiCallback?.onConnectionError(message) + + override fun onServiceStateChange(available: Boolean) { + uiCallback?.onServiceStateChange(available) + } + + override fun onServiceError(message: Int) { + uiCallback?.onServiceError(message) } } + @WorkerThread fun init(context: Context) { if (!BluetoothPermission.hasBluetoothPermission(context)) { return @@ -54,6 +65,28 @@ object BluetoothController { } } + @WorkerThread + fun isDeviceConnected() = bluetoothCallback?.isDeviceConnected() ?: false + + @WorkerThread + fun takePicture() { + Log.i(null, "Taking picture now") + + try { + val hidDevice = bluetoothCallback?.hidDevice + val connectedDevices = hidDevice?.connectedDevices + + connectedDevices?.forEach { bluetoothDevice -> + bluetoothDevice.sendKeyboardPress(hidDevice, 40.toByte()) + + Log.i(null, "Sent report signal to device: ${bluetoothDevice.name}") + } + } catch (ex: SecurityException) { + Log.wtf(null, "Failed calling BluetoothHidDevice.unregisterApp(): $ex") + } + } + + @WorkerThread fun handleBluetooth(activity: Activity, permissionLauncher: ActivityResultLauncher) { if (BluetoothPermission.hasBluetoothPermission(activity)) { val localBluetoothCallback = bluetoothCallback @@ -65,7 +98,7 @@ object BluetoothController { val localHidDevice = localBluetoothCallback.hidDevice if (localHidDevice == null) { Log.e(null, "Bluetooth service is not connected") - uiCallbackProxy.onConnectionError(R.string.error_service_register) + uiCallbackProxy.onServiceError(R.string.error_service_register) } else { localBluetoothCallback.startAutoConnect() } @@ -84,6 +117,7 @@ object BluetoothController { BluetoothPermission.requestBluetoothPermission(permissionLauncher) } + @WorkerThread fun registerBluetoothService(context: Context) { bluetoothCallback?.let { Log.w(null, "BluetoothService is already registered") @@ -92,7 +126,7 @@ object BluetoothController { val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager val bluetoothAdapter = bluetoothManager.adapter - val bluetoothCallback = BluetoothServiceCallback(uiCallbackProxy) + val bluetoothCallback = BluetoothServiceCallback(uiCallbackProxy, uiCallbackProxy) val buildSuccessful = bluetoothAdapter.getProfileProxy(context, bluetoothCallback, BluetoothProfile.HID_DEVICE) @@ -103,26 +137,7 @@ object BluetoothController { } else { Log.e(null, "BluetoothAdapter.getProfileProxy failed") - uiCallbackProxy.onConnectionError(R.string.error_adapter_register) - } - } - - fun isDeviceConnected() = bluetoothCallback?.isDeviceConnected() ?: false - - fun takePicture() { - Log.i(null, "Taking picture now") - - try { - val hidDevice = bluetoothCallback?.hidDevice - val connectedDevices = hidDevice?.connectedDevices - - connectedDevices?.forEach { bluetoothDevice -> - bluetoothDevice.sendKeyboardPress(hidDevice, 40.toByte()) - - Log.i(null, "Sent report signal to device: ${bluetoothDevice.name}") - } - } catch (ex: SecurityException) { - Log.wtf(null, "Failed calling BluetoothHidDevice.unregisterApp(): $ex") + uiCallbackProxy.onServiceError(R.string.error_adapter_register) } } diff --git a/app/src/main/java/dev/niro/cameraremote/bluetooth/BluetoothServiceCallback.kt b/app/src/main/java/dev/niro/cameraremote/bluetooth/BluetoothServiceCallback.kt index 875e376..f423ea4 100644 --- a/app/src/main/java/dev/niro/cameraremote/bluetooth/BluetoothServiceCallback.kt +++ b/app/src/main/java/dev/niro/cameraremote/bluetooth/BluetoothServiceCallback.kt @@ -5,19 +5,25 @@ import android.bluetooth.BluetoothHidDevice import android.bluetooth.BluetoothProfile import android.util.Log import dev.niro.cameraremote.R +import dev.niro.cameraremote.bluetooth.enums.ConnectionState import dev.niro.cameraremote.bluetooth.helper.BluetoothConstants -import dev.niro.cameraremote.bluetooth.helper.getNameWithState -import dev.niro.cameraremote.interfaces.IAppStateCallback +import dev.niro.cameraremote.bluetooth.helper.getAddressString +import dev.niro.cameraremote.bluetooth.helper.getConnectionStateEnum +import dev.niro.cameraremote.bluetooth.helper.toDebugString import dev.niro.cameraremote.interfaces.IConnectionStateCallback +import dev.niro.cameraremote.interfaces.IServiceStateCallback -class BluetoothServiceCallback(private val connectionStateListener: IConnectionStateCallback) : - BluetoothProfile.ServiceListener { +class BluetoothServiceCallback( + private val connectionStateListener: IConnectionStateCallback, + private val serviceStateListener: IServiceStateCallback +) : BluetoothProfile.ServiceListener { var hidDevice: BluetoothHidDevice? = null private set - var hidCallback: HidDeviceCallback? = null - private set + private var hidCallback: HidDeviceCallback? = null + + private var appRegistered = false override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) { Log.d(null, "onServiceConnected($profile, $proxy)") @@ -46,22 +52,31 @@ class BluetoothServiceCallback(private val connectionStateListener: IConnectionS hidDevice = null hidCallback = null + appRegistered = false - connectionStateListener.onConnectionStateChanged(false) + serviceStateListener.onServiceStateChange(false) } private fun registerApp(registerHidDevice: BluetoothHidDevice): HidDeviceCallback { - val appStateListener = object : IAppStateCallback { - override fun onAppStateChanged(registered: Boolean) { - if (registered) { + val serviceStateListener = object : IServiceStateCallback { + override fun onServiceStateChange(available: Boolean) { + appRegistered = available + + if (available) { startAutoConnect() - } else { - connectionStateListener.onConnectionStateChanged(false) + + startRadarThread() } + + serviceStateListener.onServiceStateChange(available) + } + + override fun onServiceError(message: Int) { + serviceStateListener.onServiceError(message) } } - val newHidCallback = HidDeviceCallback(registerHidDevice, connectionStateListener, appStateListener) + val newHidCallback = HidDeviceCallback(registerHidDevice, connectionStateListener, serviceStateListener) try { registerHidDevice.registerApp( @@ -80,36 +95,71 @@ class BluetoothServiceCallback(private val connectionStateListener: IConnectionS return newHidCallback } + private fun startRadarThread() { + val thread = Thread { + Log.i(null, "Starting radar thread") + + val foundDeviceAddresses = mutableMapOf() + while (true) { + val localHidDevice = hidDevice ?: return@Thread + val newDeviceList = getDevices() + + for (device in newDeviceList) { + val deviceAddress = device.getAddressString() + val connectionState = device.getConnectionStateEnum(localHidDevice) + + if (foundDeviceAddresses[deviceAddress] == connectionState) { + continue + } + + Log.d(null, "Radar detected device change: $device") + + connectionStateListener.onConnectionStateChange(device, connectionState) + foundDeviceAddresses[deviceAddress] = connectionState + } + + val sleepDelay = if (foundDeviceAddresses.isEmpty()) 1_000L else 10_000L + Thread.sleep(sleepDelay) + } + } + thread.name = "Radar" + thread.priority = Thread.MIN_PRIORITY + thread.start() + } + fun startAutoConnect() { val connectHidDevice = hidDevice if (connectHidDevice == null) { Log.e(null, "Bluetooth service is not connected") - connectionStateListener.onConnectionError(R.string.error_service_register) + serviceStateListener.onServiceError(R.string.error_service_register) return } - val appRegistered = hidCallback?.appRegistered ?: false if (!appRegistered) { Log.e(null, "Bluetooth app is not registered") - connectionStateListener.onConnectionError(R.string.error_app_register) + serviceStateListener.onServiceError(R.string.error_app_register) return } - try { - val connectionStates = intArrayOf(BluetoothProfile.STATE_DISCONNECTED) - val devices = connectHidDevice.getDevicesMatchingConnectionStates(connectionStates) + if (isDeviceConnected()) { + Log.w(null, "Device already connected, no auto connect required") + return + } - if (devices.isEmpty()) { - Log.e(null, "No devices found") - connectionStateListener.onConnectionError(R.string.error_no_devices_found) + val devices = getDevices(BluetoothProfile.STATE_DISCONNECTED) - return - } + if (devices.isEmpty()) { + Log.e(null, "No devices found") + serviceStateListener.onServiceError(R.string.error_no_devices_found) + + return + } + try { devices.forEach { device -> - Log.i(null, "Connect with device: ${device.getNameWithState(connectHidDevice)}") + Log.i(null, "Connect with device: ${device.toDebugString(connectHidDevice)}") connectHidDevice.connect(device) } @@ -118,13 +168,22 @@ class BluetoothServiceCallback(private val connectionStateListener: IConnectionS } } - fun isDeviceConnected() = getConnectedDevices().isNotEmpty() + fun isDeviceConnected() = getDevices(BluetoothProfile.STATE_CONNECTED).isNotEmpty() - fun getConnectedDevices(): List { + private fun getDevices( + vararg states: Int = intArrayOf( + BluetoothProfile.STATE_DISCONNECTED, + BluetoothProfile.STATE_DISCONNECTING, + BluetoothProfile.STATE_CONNECTED, + BluetoothProfile.STATE_DISCONNECTING + ) + ): List { try { - return hidDevice?.connectedDevices ?: listOf() + hidDevice?.let { + return it.getDevicesMatchingConnectionStates(states) + } } catch (ex: SecurityException) { - Log.wtf(null, "Failed auto connect: $ex") + Log.wtf(null, "Failed receiving devices: $ex") } return listOf() diff --git a/app/src/main/java/dev/niro/cameraremote/bluetooth/DeviceWrapper.kt b/app/src/main/java/dev/niro/cameraremote/bluetooth/DeviceWrapper.kt new file mode 100644 index 0000000..4e18a35 --- /dev/null +++ b/app/src/main/java/dev/niro/cameraremote/bluetooth/DeviceWrapper.kt @@ -0,0 +1,6 @@ +package dev.niro.cameraremote.bluetooth + +import dev.niro.cameraremote.bluetooth.enums.BondState +import dev.niro.cameraremote.bluetooth.enums.ConnectionState + +data class DeviceWrapper(val address: String, val name: String, val state: ConnectionState, val bond: BondState) \ No newline at end of file diff --git a/app/src/main/java/dev/niro/cameraremote/bluetooth/HidDeviceCallback.kt b/app/src/main/java/dev/niro/cameraremote/bluetooth/HidDeviceCallback.kt index cf71b33..840dd3e 100644 --- a/app/src/main/java/dev/niro/cameraremote/bluetooth/HidDeviceCallback.kt +++ b/app/src/main/java/dev/niro/cameraremote/bluetooth/HidDeviceCallback.kt @@ -2,34 +2,23 @@ package dev.niro.cameraremote.bluetooth import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothHidDevice -import android.bluetooth.BluetoothProfile import android.util.Log -import dev.niro.cameraremote.interfaces.IAppStateCallback +import dev.niro.cameraremote.bluetooth.enums.ConnectionState import dev.niro.cameraremote.interfaces.IConnectionStateCallback +import dev.niro.cameraremote.interfaces.IServiceStateCallback class HidDeviceCallback( private val hidDevice: BluetoothHidDevice, private val connectionStateListener: IConnectionStateCallback, - private val appStateListener: IAppStateCallback + private val serviceStateListener: IServiceStateCallback ) : BluetoothHidDevice.Callback() { - var appRegistered = false - private set - override fun onAppStatusChanged(pluggedDevice: BluetoothDevice?, registered: Boolean) { super.onAppStatusChanged(pluggedDevice, registered) Log.d(null, "onAppStatusChanged($pluggedDevice, $registered)") - if (appRegistered == registered) { - val variableInfo = "registered=$registered, appRegistered=$appRegistered" - Log.d(null, "App state of $pluggedDevice changed, but it is not relevant: $variableInfo") - - return - } - - appRegistered = registered - appStateListener.onAppStateChanged(registered) + serviceStateListener.onServiceStateChange(registered) } override fun onConnectionStateChanged(device: BluetoothDevice?, state: Int) { @@ -41,9 +30,7 @@ class HidDeviceCallback( return } - val connected = state == BluetoothProfile.STATE_CONNECTED - - connectionStateListener.onConnectionStateChanged(connected) + connectionStateListener.onConnectionStateChange(device, ConnectionState.fromBluetoothProfile(state)) } override fun onGetReport(device: BluetoothDevice?, type: Byte, id: Byte, bufferSize: Int) { diff --git a/app/src/main/java/dev/niro/cameraremote/bluetooth/enums/BondState.kt b/app/src/main/java/dev/niro/cameraremote/bluetooth/enums/BondState.kt new file mode 100644 index 0000000..290cc89 --- /dev/null +++ b/app/src/main/java/dev/niro/cameraremote/bluetooth/enums/BondState.kt @@ -0,0 +1,21 @@ +package dev.niro.cameraremote.bluetooth.enums + +import android.bluetooth.BluetoothDevice + +enum class BondState { + + BONDED, + BONDING, + UNBOUND, + ERROR; + + companion object { + fun fromBluetoothDevice(state: Int) = when(state) { + BluetoothDevice.BOND_BONDED -> BONDED + BluetoothDevice.BOND_BONDING -> BONDING + BluetoothDevice.BOND_NONE -> UNBOUND + else -> ERROR + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/dev/niro/cameraremote/bluetooth/enums/ConnectionState.kt b/app/src/main/java/dev/niro/cameraremote/bluetooth/enums/ConnectionState.kt new file mode 100644 index 0000000..b381dcb --- /dev/null +++ b/app/src/main/java/dev/niro/cameraremote/bluetooth/enums/ConnectionState.kt @@ -0,0 +1,23 @@ +package dev.niro.cameraremote.bluetooth.enums + +import android.bluetooth.BluetoothProfile + +enum class ConnectionState { + + CONNECTED, + DISCONNECTED, + CONNECTING, + DISCONNECTING, + ERROR; + + companion object { + fun fromBluetoothProfile(state: Int) = when(state) { + BluetoothProfile.STATE_CONNECTED -> CONNECTED + BluetoothProfile.STATE_CONNECTING -> CONNECTING + BluetoothProfile.STATE_DISCONNECTED -> DISCONNECTED + BluetoothProfile.STATE_DISCONNECTING -> DISCONNECTING + else -> ERROR + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/dev/niro/cameraremote/bluetooth/helper/BluetoothHelper.kt b/app/src/main/java/dev/niro/cameraremote/bluetooth/helper/BluetoothHelper.kt index e891ec4..5ff1a95 100644 --- a/app/src/main/java/dev/niro/cameraremote/bluetooth/helper/BluetoothHelper.kt +++ b/app/src/main/java/dev/niro/cameraremote/bluetooth/helper/BluetoothHelper.kt @@ -2,52 +2,63 @@ package dev.niro.cameraremote.bluetooth.helper import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothHidDevice -import android.bluetooth.BluetoothProfile import android.util.Log +import dev.niro.cameraremote.bluetooth.DeviceWrapper +import dev.niro.cameraremote.bluetooth.enums.BondState +import dev.niro.cameraremote.bluetooth.enums.ConnectionState -fun BluetoothDevice.getStateName(hidDevice: BluetoothHidDevice): String { +fun BluetoothDevice.getConnectionStateEnum(hidDevice: BluetoothHidDevice): ConnectionState { return try { val state = hidDevice.getConnectionState(this) - when (state) { - BluetoothProfile.STATE_CONNECTED -> "STATE_CONNECTED" - BluetoothProfile.STATE_CONNECTING -> "STATE_CONNECTING" - BluetoothProfile.STATE_DISCONNECTED -> "STATE_DISCONNECTED" - BluetoothProfile.STATE_DISCONNECTING -> "STATE_DISCONNECTING" - else -> "STATE_UNKNOWN" - } + ConnectionState.fromBluetoothProfile(state) } catch (ex: SecurityException) { Log.e(null, "Failed BluetoothHidDevice.getConnectionState: $ex") - "STATE_UNKNOWN" + ConnectionState.ERROR } } -fun BluetoothDevice.getBondStateName(): String { +fun BluetoothDevice.getBondStateEnum(): BondState { return try { - when (this.bondState) { - BluetoothDevice.BOND_BONDED -> "BOND_BONDED" - BluetoothDevice.BOND_BONDING -> "BOND_BONDING" - BluetoothDevice.BOND_NONE -> "BOND_NONE" - else -> "BOND_UNKNOWN" - } + BondState.fromBluetoothDevice(this.bondState) } catch (ex: SecurityException) { Log.e(null, "Failed BluetoothDevice.bondState: $ex") - "BOND_UNKNOWN" + BondState.ERROR } } -fun BluetoothDevice.getNameWithState(hidDevice: BluetoothHidDevice): String { - val deviceName = try { +fun BluetoothDevice.getNameString(): String { + return try { this.name } catch (ex: SecurityException) { Log.e(null, "Failed BluetoothDevice.name: $ex") - "NAME_UNKNOWN" + "NAME_ERROR" + } +} + +fun BluetoothDevice.getAddressString(): String { + return try { + this.address + } catch (ex: SecurityException) { + Log.e(null, "Failed BluetoothDevice.address: $ex") + + "ADDRESS_ERROR" } +} + +fun BluetoothDevice.toDeviceWrapper(hidDevice: BluetoothHidDevice): DeviceWrapper { + return toDeviceWrapper(getConnectionStateEnum(hidDevice)) +} + +fun BluetoothDevice.toDeviceWrapper(state: ConnectionState): DeviceWrapper { + return DeviceWrapper(getAddressString(), getNameString(), state, getBondStateEnum()) +} - return "$deviceName (${this.getStateName(hidDevice)}, ${this.getBondStateName()})" +fun BluetoothDevice.toDebugString(hidDevice: BluetoothHidDevice): String { + return "${this.getAddressString()} (${this.getConnectionStateEnum(hidDevice).name}, ${this.getBondStateEnum().name})" } fun BluetoothDevice.sendKeyboardPress(hidDevice: BluetoothHidDevice, key: Byte) { diff --git a/app/src/main/java/dev/niro/cameraremote/interfaces/IAmbientModeState.kt b/app/src/main/java/dev/niro/cameraremote/interfaces/IAmbientModeState.kt deleted file mode 100644 index c3718b5..0000000 --- a/app/src/main/java/dev/niro/cameraremote/interfaces/IAmbientModeState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.niro.cameraremote.interfaces - -interface IAmbientModeState { - - fun isAmbientModeActive(): Boolean - -} \ No newline at end of file diff --git a/app/src/main/java/dev/niro/cameraremote/interfaces/IAppStateCallback.kt b/app/src/main/java/dev/niro/cameraremote/interfaces/IAppStateCallback.kt deleted file mode 100644 index 197f461..0000000 --- a/app/src/main/java/dev/niro/cameraremote/interfaces/IAppStateCallback.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.niro.cameraremote.interfaces - -interface IAppStateCallback { - - fun onAppStateChanged(registered: Boolean) - -} \ No newline at end of file diff --git a/app/src/main/java/dev/niro/cameraremote/interfaces/IConnectionStateCallback.kt b/app/src/main/java/dev/niro/cameraremote/interfaces/IConnectionStateCallback.kt index 816e9ac..1fb202a 100644 --- a/app/src/main/java/dev/niro/cameraremote/interfaces/IConnectionStateCallback.kt +++ b/app/src/main/java/dev/niro/cameraremote/interfaces/IConnectionStateCallback.kt @@ -1,9 +1,10 @@ package dev.niro.cameraremote.interfaces -interface IConnectionStateCallback { +import android.bluetooth.BluetoothDevice +import dev.niro.cameraremote.bluetooth.enums.ConnectionState - fun onConnectionStateChanged(connected: Boolean) +interface IConnectionStateCallback { - fun onConnectionError(message: Int) + fun onConnectionStateChange(device: BluetoothDevice, state: ConnectionState) } \ No newline at end of file diff --git a/app/src/main/java/dev/niro/cameraremote/interfaces/IServiceStateCallback.kt b/app/src/main/java/dev/niro/cameraremote/interfaces/IServiceStateCallback.kt new file mode 100644 index 0000000..4127789 --- /dev/null +++ b/app/src/main/java/dev/niro/cameraremote/interfaces/IServiceStateCallback.kt @@ -0,0 +1,9 @@ +package dev.niro.cameraremote.interfaces + +interface IServiceStateCallback { + + fun onServiceStateChange(available: Boolean) + + fun onServiceError(message: Int) + +} \ No newline at end of file diff --git a/app/src/main/java/dev/niro/cameraremote/interfaces/IUserInterfaceBluetoothCallback.kt b/app/src/main/java/dev/niro/cameraremote/interfaces/IUserInterfaceBluetoothCallback.kt index eb83d2d..e39ade2 100644 --- a/app/src/main/java/dev/niro/cameraremote/interfaces/IUserInterfaceBluetoothCallback.kt +++ b/app/src/main/java/dev/niro/cameraremote/interfaces/IUserInterfaceBluetoothCallback.kt @@ -1,3 +1,9 @@ package dev.niro.cameraremote.interfaces -interface IUserInterfaceBluetoothCallback : IConnectionStateCallback \ No newline at end of file +import dev.niro.cameraremote.bluetooth.DeviceWrapper + +interface IUserInterfaceBluetoothCallback : IServiceStateCallback { + + fun onConnectionStateChange(device: DeviceWrapper) + +} \ No newline at end of file diff --git a/app/src/main/java/dev/niro/cameraremote/interfaces/IUserInterfaceTimerCallback.kt b/app/src/main/java/dev/niro/cameraremote/interfaces/IUserInterfaceTimerCallback.kt index df49681..0e53380 100644 --- a/app/src/main/java/dev/niro/cameraremote/interfaces/IUserInterfaceTimerCallback.kt +++ b/app/src/main/java/dev/niro/cameraremote/interfaces/IUserInterfaceTimerCallback.kt @@ -1,7 +1,9 @@ package dev.niro.cameraremote.interfaces -interface IUserInterfaceTimerCallback : IAmbientModeState { +interface IUserInterfaceTimerCallback { - fun shouldChangeProgressIndicator(progress: Float) + fun changeProgressIndicatorState(progress: Float) + + fun isAmbientModeActive(): Boolean } \ No newline at end of file diff --git a/app/src/main/java/dev/niro/cameraremote/ui/UserInputController.kt b/app/src/main/java/dev/niro/cameraremote/ui/UserInputController.kt index 189d9e5..28917a8 100644 --- a/app/src/main/java/dev/niro/cameraremote/ui/UserInputController.kt +++ b/app/src/main/java/dev/niro/cameraremote/ui/UserInputController.kt @@ -52,12 +52,12 @@ object UserInputController { val subTickProgress = subTick / tickFPS.toFloat() val progress = (waitCounter + subTickProgress) / configuredTimerDelay - uiCallback?.shouldChangeProgressIndicator(progress) + uiCallback?.changeProgressIndicatorState(progress) } } if (configuredTimerDelay == 0) { - uiCallback?.shouldChangeProgressIndicator(1f) + uiCallback?.changeProgressIndicatorState(1f) } Vibrator.shoot(context) diff --git a/app/src/main/java/dev/niro/cameraremote/ui/activities/MainActivity.kt b/app/src/main/java/dev/niro/cameraremote/ui/activities/MainActivity.kt index 55efe20..c16bfc8 100644 --- a/app/src/main/java/dev/niro/cameraremote/ui/activities/MainActivity.kt +++ b/app/src/main/java/dev/niro/cameraremote/ui/activities/MainActivity.kt @@ -7,30 +7,35 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.ActivityResultLauncher import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.wear.ambient.AmbientLifecycleObserver import androidx.wear.compose.material.HorizontalPageIndicator import androidx.wear.compose.material.PageIndicatorState -import androidx.wear.compose.material.Text import androidx.wear.tooling.preview.devices.WearDevices import dev.niro.cameraremote.bluetooth.BluetoothController +import dev.niro.cameraremote.bluetooth.DeviceWrapper import dev.niro.cameraremote.bluetooth.helper.BluetoothPermission -import dev.niro.cameraremote.interfaces.IAmbientModeState +import dev.niro.cameraremote.interfaces.IUserInterfaceBluetoothCallback +import dev.niro.cameraremote.interfaces.IUserInterfaceTimerCallback +import dev.niro.cameraremote.ui.UserInputController +import dev.niro.cameraremote.ui.pages.DevicesLayout +import dev.niro.cameraremote.ui.pages.DevicesPage import dev.niro.cameraremote.ui.pages.RemoteLayout import dev.niro.cameraremote.ui.pages.RemotePage import dev.niro.cameraremote.ui.pages.SettingsLayout +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch -class MainActivity : ComponentActivity(), AmbientLifecycleObserver.AmbientLifecycleCallback, IAmbientModeState { +class MainActivity : ComponentActivity(), AmbientLifecycleObserver.AmbientLifecycleCallback{ private val ambientObserver = AmbientLifecycleObserver(this, this) @@ -42,9 +47,12 @@ class MainActivity : ComponentActivity(), AmbientLifecycleObserver.AmbientLifecy super.onCreate(savedInstanceState) lifecycle.addObserver(ambientObserver) - BluetoothController.init(this) - RemotePage.registerCallbacks(this) - RemotePage.updateButtons() + registerUiCallbacks() + + CoroutineScope(Dispatchers.Default).launch { + RemotePage.updateUi() + BluetoothController.init(this@MainActivity) + } // Must be created at activity startup, otherwise the app will crash. val permissionLauncher = BluetoothPermission.buildPermissionLauncher(this) { permissionGranted -> @@ -63,6 +71,39 @@ class MainActivity : ComponentActivity(), AmbientLifecycleObserver.AmbientLifecy } } + private fun registerUiCallbacks() { + BluetoothController.uiCallback = object : IUserInterfaceBluetoothCallback { + override fun onConnectionStateChange(device: DeviceWrapper) { + Log.d(null, "onConnectionStateChange($device)") + + RemotePage.updateUi() + DevicesPage.updateDevice(device) + } + + override fun onServiceError(message: Int) { + val errorIntent = Intent(this@MainActivity, ErrorActivity::class.java) + errorIntent.putExtra("messageId", message) + startActivity(errorIntent) + } + + override fun onServiceStateChange(available: Boolean) { + Log.d(null, "onServiceStateChange($available)") + + RemotePage.updateUi() + } + } + + UserInputController.uiCallback = object : IUserInterfaceTimerCallback { + override fun changeProgressIndicatorState(progress: Float) { + RemotePage.progressIndicator.floatValue = progress + } + + override fun isAmbientModeActive(): Boolean { + return isInAmbientMode + } + } + } + override fun onDestroy() { super.onDestroy() lifecycle.removeObserver(ambientObserver) @@ -78,9 +119,6 @@ class MainActivity : ComponentActivity(), AmbientLifecycleObserver.AmbientLifecy isInAmbientMode = false } - override fun isAmbientModeActive(): Boolean { - return isInAmbientMode - } } @OptIn(ExperimentalFoundationApi::class) @@ -95,7 +133,7 @@ fun MainActivityLayout(permissionLauncher: ActivityResultLauncher? = nul HorizontalPager(state = pagerState) {page -> when(page) { 0 -> RemoteLayout(permissionLauncher) - 1 -> Text(text = "Devices List", modifier = Modifier.fillMaxSize(), textAlign = TextAlign.Center) + 1 -> DevicesLayout() 2 -> SettingsLayout() } } diff --git a/app/src/main/java/dev/niro/cameraremote/ui/pages/DevicesPage.kt b/app/src/main/java/dev/niro/cameraremote/ui/pages/DevicesPage.kt new file mode 100644 index 0000000..7a0ac98 --- /dev/null +++ b/app/src/main/java/dev/niro/cameraremote/ui/pages/DevicesPage.kt @@ -0,0 +1,106 @@ +package dev.niro.cameraremote.ui.pages + +import android.content.Intent +import android.provider.Settings +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.material.Chip +import androidx.wear.compose.material.ChipDefaults +import androidx.wear.compose.material.Text +import androidx.wear.tooling.preview.devices.WearDevices +import dev.niro.cameraremote.bluetooth.DeviceWrapper +import kotlinx.coroutines.flow.MutableStateFlow + + +object DevicesPage { + + val deviceList = MutableStateFlow(listOf()) + + fun updateDevice(device: DeviceWrapper) { + val mutableDeviceList = deviceList.value.toMutableList() + + val deviceIndex = mutableDeviceList.indexOfFirst { it.address == device.address } + + if (deviceIndex >= 0) { + mutableDeviceList[deviceIndex] = device + } else { + mutableDeviceList.add(device) + } + + deviceList.value = mutableDeviceList + } + +} + +@Preview(device = WearDevices.RECT, showSystemUi = true) +@Preview(device = WearDevices.SQUARE, showSystemUi = true) +@Preview(device = WearDevices.SMALL_ROUND, showSystemUi = true) +@Preview(device = WearDevices.LARGE_ROUND, showSystemUi = true) +@Composable +fun DevicesLayout() { + val context = LocalContext.current + + val deviceList = remember { DevicesPage.deviceList } + val deviceListState by remember { deviceList }.collectAsState() + + ScalingLazyColumn(modifier = Modifier.fillMaxSize()) { + + item { + Text( + text = "Devices", + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + } + + items(deviceListState.size) { + val device = deviceListState[it] + + Chip( + onClick = { }, + label = { Text(text = device.name) }, + secondaryLabel = { Text(text = "${device.state}, ${device.bond}") }, + modifier = Modifier.fillMaxWidth(), + colors = ChipDefaults.secondaryChipColors() + ) + + } + + + item { + val text = if (deviceListState.isEmpty()) { + "No devices paired" + } else { + "Connect more" + } + + Chip( + onClick = { + val intentOpenBluetoothSettings = Intent() + intentOpenBluetoothSettings.setAction(Settings.ACTION_BLUETOOTH_SETTINGS) + context.startActivity(intentOpenBluetoothSettings) + }, + label = { Text(text = text) }, + secondaryLabel = { Text(text = "Open Bluetooth settings") }, + colors = ChipDefaults.primaryChipColors() + ) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/dev/niro/cameraremote/ui/pages/RemotePage.kt b/app/src/main/java/dev/niro/cameraremote/ui/pages/RemotePage.kt index c112bd1..f84e6a6 100644 --- a/app/src/main/java/dev/niro/cameraremote/ui/pages/RemotePage.kt +++ b/app/src/main/java/dev/niro/cameraremote/ui/pages/RemotePage.kt @@ -1,10 +1,9 @@ package dev.niro.cameraremote.ui.pages import android.app.Activity -import android.content.Context -import android.content.Intent import android.util.Log import androidx.activity.result.ActivityResultLauncher +import androidx.annotation.WorkerThread import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column @@ -36,11 +35,7 @@ import androidx.wear.compose.material.TimeText import androidx.wear.tooling.preview.devices.WearDevices import dev.niro.cameraremote.R import dev.niro.cameraremote.bluetooth.BluetoothController -import dev.niro.cameraremote.interfaces.IAmbientModeState -import dev.niro.cameraremote.interfaces.IUserInterfaceBluetoothCallback -import dev.niro.cameraremote.interfaces.IUserInterfaceTimerCallback import dev.niro.cameraremote.ui.UserInputController -import dev.niro.cameraremote.ui.activities.ErrorActivity import kotlin.math.roundToInt object RemotePage { @@ -49,33 +44,8 @@ object RemotePage { val modeTextDecoration = mutableStateOf(TextDecoration.None) val progressIndicator = mutableFloatStateOf(0f) - fun registerCallbacks(context: Context) { - BluetoothController.uiCallback = object : IUserInterfaceBluetoothCallback { - override fun onConnectionStateChanged(connected: Boolean) { - Log.d(null, "onConnectionStateChanged($connected)") - - updateButtons() - } - - override fun onConnectionError(message: Int) { - val errorIntent = Intent(context, ErrorActivity::class.java) - errorIntent.putExtra("messageId", message) - context.startActivity(errorIntent) - } - } - - UserInputController.uiCallback = object : IUserInterfaceTimerCallback { - override fun shouldChangeProgressIndicator(progress: Float) { - progressIndicator.floatValue = progress - } - - override fun isAmbientModeActive(): Boolean { - return context is IAmbientModeState && context.isAmbientModeActive() - } - } - } - - fun updateButtons() { + @WorkerThread + fun updateUi() { triggerButtonIcon.intValue = if (!BluetoothController.isDeviceConnected()) { R.drawable.baseline_bluetooth_24 } else if (UserInputController.autoTriggerEnabled) { @@ -149,7 +119,7 @@ fun TriggerButton(permissionLauncher: ActivityResultLauncher?) { onClick = { if (BluetoothController.isDeviceConnected()) { UserInputController.clickTrigger(context) - RemotePage.updateButtons() + RemotePage.updateUi() } else { permissionLauncher?.let { if (context is Activity) { @@ -181,7 +151,7 @@ fun DelayButton() { OutlinedButton( onClick = { UserInputController.toggleTimer() - RemotePage.updateButtons() + RemotePage.updateUi() }, modifier = Modifier .width(50.dp) @@ -198,7 +168,7 @@ fun ModeButton() { OutlinedButton( onClick = { UserInputController.toggleAutoTrigger() - RemotePage.updateButtons() + RemotePage.updateUi() }, modifier = Modifier .width(50.dp) diff --git a/app/src/main/java/dev/niro/cameraremote/utils/Vibrator.kt b/app/src/main/java/dev/niro/cameraremote/utils/Vibrator.kt index 5076c96..74e1513 100644 --- a/app/src/main/java/dev/niro/cameraremote/utils/Vibrator.kt +++ b/app/src/main/java/dev/niro/cameraremote/utils/Vibrator.kt @@ -4,24 +4,26 @@ import android.content.Context import android.os.Build import android.os.VibrationEffect import android.os.VibratorManager +import androidx.annotation.WorkerThread import dev.niro.cameraremote.ui.pages.SettingsPage object Vibrator { + @WorkerThread fun tick(context: Context) { if (!SettingsPage.vibrationEnabled.value) { return } - getVibrator(context).vibrate(VibrationEffect.createOneShot(50, 100)) + getVibrator(context).vibrate(VibrationEffect.createOneShot(50, 80)) } + @WorkerThread fun shoot(context: Context) { if (!SettingsPage.vibrationEnabled.value) { return } - - getVibrator(context).vibrate(VibrationEffect.createOneShot(100, 200)) + getVibrator(context).vibrate(VibrationEffect.createOneShot(80, 255)) } private fun getVibrator(context: Context): android.os.Vibrator {