Skip to content

Commit

Permalink
Implement device list user interface
Browse files Browse the repository at this point in the history
  • Loading branch information
NiroDeveloper committed Jun 1, 2024
1 parent 3db0142 commit 7cd3ba8
Show file tree
Hide file tree
Showing 18 changed files with 411 additions and 169 deletions.
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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<String>) {
if (BluetoothPermission.hasBluetoothPermission(activity)) {
val localBluetoothCallback = bluetoothCallback
Expand All @@ -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()
}
Expand All @@ -84,6 +117,7 @@ object BluetoothController {
BluetoothPermission.requestBluetoothPermission(permissionLauncher)
}

@WorkerThread
fun registerBluetoothService(context: Context) {
bluetoothCallback?.let {
Log.w(null, "BluetoothService is already registered")
Expand All @@ -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)

Expand All @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down Expand Up @@ -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(
Expand All @@ -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<String, ConnectionState>()
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)
}
Expand All @@ -118,13 +168,22 @@ class BluetoothServiceCallback(private val connectionStateListener: IConnectionS
}
}

fun isDeviceConnected() = getConnectedDevices().isNotEmpty()
fun isDeviceConnected() = getDevices(BluetoothProfile.STATE_CONNECTED).isNotEmpty()

fun getConnectedDevices(): List<BluetoothDevice> {
private fun getDevices(
vararg states: Int = intArrayOf(
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_DISCONNECTING,
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_DISCONNECTING
)
): List<BluetoothDevice> {
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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}

}
Loading

0 comments on commit 7cd3ba8

Please sign in to comment.