Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.homeassistant.companion.android.frontend.improv

import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.pm.PackageManager
import com.wifi.improv.ImprovManager
Expand Down Expand Up @@ -32,6 +33,10 @@ abstract class FrontendImprovModule {
@Provides
@Singleton
fun provideImprovManagerFactory(@ApplicationContext context: Context): ImprovManagerFactory =
ImprovManagerFactory { callback -> ImprovManager(context.applicationContext, callback) }
ImprovManagerFactory { callback ->
context.getSystemService(BluetoothManager::class.java)?.adapter?.let {
ImprovManager(context.applicationContext, callback)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import com.wifi.improv.ImprovManagerCallback
* Exists so the repository does not have to hold an Android `Context` — the factory closes over
* the application context at the Hilt provision site, leaving the repository fully unit-testable
* with a mock factory.
*
* Returns `null` on devices without a Bluetooth adapter: [ImprovManager] dereferences the adapter
* at construction and would crash.
*/
fun interface ImprovManagerFactory {
fun create(callback: ImprovManagerCallback): ImprovManager
fun create(callback: ImprovManagerCallback): ImprovManager?
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ class ImprovRepositoryImpl @VisibleForTesting constructor(
backgroundDispatcher = Dispatchers.IO,
)

private val manager: ImprovManager = improvManagerFactory.create(this)
// Null on devices without a Bluetooth adapter; scanning and provisioning are then inert.
private val manager: ImprovManager? = improvManagerFactory.create(this)

private val devices = MutableStateFlow(emptyList<ImprovDevice>())
private val stateEvents = MutableSharedFlow<DeviceState>(extraBufferCapacity = 16)
Expand Down Expand Up @@ -98,6 +99,11 @@ class ImprovRepositoryImpl @VisibleForTesting constructor(

override fun provisionDevice(device: ImprovDevice, ssid: String, password: String): Flow<ProvisioningEvent> =
channelFlow {
val manager = manager ?: run {
Timber.w("Ignoring Improv provisioning: device has no Bluetooth adapter")
close()
return@channelFlow
}
Comment thread
TimoPtr marked this conversation as resolved.
var credentialsSent = false

// Forward error events for the duration of the session.
Expand Down Expand Up @@ -190,6 +196,7 @@ class ImprovRepositoryImpl @VisibleForTesting constructor(
// endregion

private suspend fun startScanInternal() = withContext(backgroundDispatcher) {
val manager = manager ?: return@withContext
if (!hasPermissions()) return@withContext
try {
manager.findDevices()
Expand All @@ -206,7 +213,7 @@ class ImprovRepositoryImpl @VisibleForTesting constructor(
// `ensureActive` and the BLE scan would never be torn down.
private suspend fun stopScanInternal() = withContext(NonCancellable + backgroundDispatcher) {
try {
manager.stopScan()
manager?.stopScan()
} catch (e: Exception) {
Timber.w(e, "Cannot stop scanning")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ class ImprovRepositoryImplTest {
sdkInt: Int = Build.VERSION_CODES.S,
scope: CoroutineScope = backgroundScope,
ioDispatcher: CoroutineDispatcher = StandardTestDispatcher(testScheduler),
factory: ImprovManagerFactory = improvManagerFactory,
): ImprovRepositoryImpl {
SdkVersion.sdkInt = sdkInt
return ImprovRepositoryImpl(
permissionChecker = permissionChecker,
improvManagerFactory = improvManagerFactory,
improvManagerFactory = factory,
shareInScope = scope,
backgroundDispatcher = ioDispatcher,
)
Expand Down Expand Up @@ -348,4 +349,30 @@ class ImprovRepositoryImplTest {
}
}
}

@Nested
inner class WithoutBluetoothAdapter {

@Test
fun `Given no Bluetooth adapter when scanDevices subscribed then scan stays inert`() = runTest {
every { permissionChecker.hasPermission(any()) } returns true
val repository = createRepository(factory = { null })

repository.scanDevices().test {
assertEquals(emptyList<ImprovDevice>(), awaitItem())
cancelAndIgnoreRemainingEvents()
}
// Also covers the teardown path: stopScan on a missing manager must not throw.
advanceTimeBy(SCAN_IDLE_WINDOW_MS + 1)
}

@Test
fun `Given no Bluetooth adapter when provisionDevice then flow completes without events`() = runTest {
val repository = createRepository(factory = { null })

repository.provisionDevice(ImprovDevice("d", "AA"), "wifi", "pwd").test {
awaitComplete()
}
}
Comment thread
TimoPtr marked this conversation as resolved.
}
}
Loading