Skip to content

Commit f5310a5

Browse files
authored
KMP SDK: Improve Robustness of Access Point Feature (#781)
1 parent 2e80ac9 commit f5310a5

File tree

12 files changed

+235
-101
lines changed

12 files changed

+235
-101
lines changed

demos/kotlin/kmp_sdk/composeApp/build.gradle.kts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ plugins {
1616
kotlin {
1717
androidTarget {
1818
@OptIn(ExperimentalKotlinGradlePluginApi::class)
19-
compilerOptions { jvmTarget.set(JvmTarget.JVM_11) }
19+
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
2020
}
2121

2222
// jvm("desktop")
@@ -108,8 +108,8 @@ android {
108108
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
109109
buildTypes { getByName("release") { isMinifyEnabled = false } }
110110
compileOptions {
111-
sourceCompatibility = JavaVersion.VERSION_11
112-
targetCompatibility = JavaVersion.VERSION_11
111+
sourceCompatibility = JavaVersion.VERSION_17
112+
targetCompatibility = JavaVersion.VERSION_17
113113
}
114114
buildFeatures { compose = true }
115115
dependencies {

demos/kotlin/kmp_sdk/composeApp/src/commonMain/kotlin/presenter/AccessPointViewModel.kt

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import androidx.lifecycle.viewModelScope
77
import com.gopro.open_gopro.OgpSdk
88
import com.gopro.open_gopro.operations.AccessPointState
99
import data.IAppPreferences
10-
import kotlinx.coroutines.flow.Flow
1110
import kotlinx.coroutines.flow.MutableStateFlow
11+
import kotlinx.coroutines.flow.StateFlow
1212
import kotlinx.coroutines.flow.asStateFlow
1313
import kotlinx.coroutines.flow.update
1414
import kotlinx.coroutines.launch
@@ -32,58 +32,80 @@ class AccessPointViewModel(
3232
private var _state = MutableStateFlow<ApUiState>(ApUiState.Idle)
3333
val state = _state.asStateFlow()
3434

35+
private var _errorMessage = MutableStateFlow<String?>(null)
36+
val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()
37+
38+
private fun setErrorMessage(message: String) {
39+
logger.e(message)
40+
_errorMessage.update { message }
41+
}
42+
43+
private fun setState(newState: ApUiState) {
44+
logger.d("Setting state to: ${newState.name}")
45+
_state.update { newState }
46+
}
47+
3548
fun scanForSsids() {
3649
if (_state.value == ApUiState.Idle) {
3750
viewModelScope.launch {
38-
_state.update { ApUiState.Scanning }
51+
setState(ApUiState.Scanning)
3952
gopro.features.accessPoint
4053
.scanForAccessPoints()
4154
.getOrThrow()
4255
.map { it.ssid }
43-
.let { ssids -> _state.update { ApUiState.WaitingConnect(ssids) } }
56+
.let { ssids -> setState(ApUiState.WaitingConnect(ssids)) }
4457
}
4558
} else {
46-
logger.w("Can only scan from idle state")
59+
setErrorMessage("Can only scan from idle state")
4760
}
4861
}
4962

50-
private suspend fun processConnectNotifications(notifications: Flow<AccessPointState>) =
51-
_state.value.let { s ->
52-
if (s is ApUiState.Connecting) {
53-
notifications.collect { notification ->
54-
logger.d("Received AP Connect notification: $notification")
55-
when (notification) {
56-
is AccessPointState.Connected -> _state.update { ApUiState.Connected(s.ssid) }
57-
is AccessPointState.InProgress -> _state.update { ApUiState.Connecting(s.ssid) }
58-
else -> _state.update { ApUiState.Idle }
59-
}
60-
}
61-
} else {
62-
logger.w("Can not process connect notifications if not connecting")
63-
}
64-
}
65-
6663
fun connectToSsid(ssid: String) {
6764
if (_state.value is ApUiState.WaitingConnect) {
6865
viewModelScope.launch {
69-
_state.update { ApUiState.Connecting(ssid) }
70-
processConnectNotifications(
71-
gopro.features.accessPoint.connectAccessPoint(ssid).getOrThrow())
66+
setState(ApUiState.Connecting(ssid))
67+
gopro.features.accessPoint
68+
.connectAccessPoint(ssid)
69+
.onSuccess { setState(ApUiState.Connected(ssid)) }
70+
.onFailure { setState(ApUiState.Idle) }
7271
}
7372
} else {
74-
logger.w("Can only connect after scanning.")
73+
setErrorMessage("Can only connect after scanning.")
7574
}
7675
}
7776

7877
fun connectToSsid(ssid: String, password: String) {
7978
if (_state.value is ApUiState.WaitingConnect) {
8079
viewModelScope.launch {
81-
_state.update { ApUiState.Connecting(ssid) }
82-
processConnectNotifications(
83-
gopro.features.accessPoint.connectAccessPoint(ssid, password).getOrThrow())
80+
setState(ApUiState.Connecting(ssid))
81+
gopro.features.accessPoint
82+
.connectAccessPoint(ssid, password)
83+
.onSuccess { setState(ApUiState.Connected(ssid)) }
84+
.onFailure { setState(ApUiState.Idle) }
85+
}
86+
} else {
87+
setErrorMessage("Can only connect after scanning.")
88+
}
89+
}
90+
91+
fun disconnect() {
92+
if (_state.value is ApUiState.Connected) {
93+
viewModelScope.launch {
94+
gopro.features.accessPoint
95+
.disconnectAccessPoint()
96+
.onSuccess { setState(ApUiState.Idle) }
97+
.onFailure { setErrorMessage("Failed to disconnect from access point") }
8498
}
8599
} else {
86-
logger.w("Can only connect after scanning.")
100+
setErrorMessage("Can only disconnect when connected.")
87101
}
88102
}
103+
104+
override fun onStart() {
105+
when (val featureState = gopro.accessPointState.value) {
106+
AccessPointState.Disconnected -> ApUiState.Idle
107+
is AccessPointState.InProgress -> ApUiState.Connecting(featureState.ssid)
108+
is AccessPointState.Connected -> ApUiState.Connected(featureState.ssid)
109+
}.let { setState(it) }
110+
}
89111
}

demos/kotlin/kmp_sdk/composeApp/src/commonMain/kotlin/ui/screens/connected/AccessPointScreen.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import presenter.ApUiState
3131
import ui.common.Screen
3232
import ui.components.CommonTopBar
3333
import ui.components.IndeterminateCircularProgressIndicator
34+
import ui.components.SnackbarMessageHandler
3435

3536
@Composable
3637
fun AccessPointScreen(
@@ -39,10 +40,13 @@ fun AccessPointScreen(
3940
modifier: Modifier = Modifier,
4041
) {
4142
val state by viewModel.state.collectAsStateWithLifecycle()
43+
val errorMessage by viewModel.errorMessage.collectAsStateWithLifecycle()
4244

4345
var ssid by remember { mutableStateOf("") }
4446
var password: String? by remember { mutableStateOf(null) }
4547

48+
errorMessage?.let { SnackbarMessageHandler(it) }
49+
4650
CommonTopBar(
4751
navController = navController,
4852
title = Screen.AccessPoint.route,
@@ -59,17 +63,21 @@ fun AccessPointScreen(
5963
is ApUiState.Connected ->
6064
ScanScreen(
6165
isScanning = (s is ApUiState.Scanning),
66+
isConnected = (s is ApUiState.Connected),
6267
onScanStart = { viewModel.scanForSsids() },
6368
onSsidSelect = { ssid = it },
64-
selectedSsid = ssid)
69+
selectedSsid = ssid,
70+
onDisconnect = { viewModel.disconnect() })
6571

6672
is ApUiState.WaitingConnect -> {
6773
ScanScreen(
6874
isScanning = false,
75+
isConnected = false,
6976
onScanStart = { viewModel.scanForSsids() },
7077
onSsidSelect = { ssid = it },
7178
ssids = s.ssids,
72-
selectedSsid = ssid)
79+
selectedSsid = ssid,
80+
onDisconnect = { viewModel.disconnect() })
7381
ConnectScreen(
7482
password = password,
7583
onConnectSsid = {
@@ -87,13 +95,18 @@ fun AccessPointScreen(
8795
@Composable
8896
fun ScanScreen(
8997
isScanning: Boolean,
98+
isConnected: Boolean,
9099
ssids: List<String> = listOf(),
91100
onScanStart: (() -> Unit),
92101
onSsidSelect: ((String) -> Unit),
93102
selectedSsid: String,
103+
onDisconnect: (() -> Unit)
94104
) {
95105
Column {
96106
Button(onScanStart) { Text("Start Scanning for SSIDS") }
107+
if (isConnected) {
108+
Button(onDisconnect) { Text("Disconnect") }
109+
}
97110
if (isScanning) {
98111
IndeterminateCircularProgressIndicator()
99112
}

demos/kotlin/kmp_sdk/docs/sdk_documentation.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,10 @@ Here is a (naive) example of using the [AccessPointFeature] to connect the GoPro
211211

212212
```kotlin
213213
with(gopro.features.accessPoint) {
214-
// Get all available access opints and filter to find our target.
214+
// Get all available access points and filter to find our target.
215215
val entry = scanForAccessPoints().getOrThrow().first { it.ssid == "TARGET_SSID" }
216216
// Start connecting to the access point..
217-
connectAccessPoint(entry.ssid, "password").onSuccess {
218-
// Wait to collect a finished element from the flow
219-
it.first { state -> state.isFinished() }
220-
}
217+
connectAccessPoint(entry.ssid, "password")
221218
}
222219
```
223220

demos/kotlin/kmp_sdk/gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ androidx-lifecycle = "2.8.4"
1111
androidx-material = "1.12.0"
1212
# https://plugins.gradle.org/plugin/org.jetbrains.compose
1313
compose-plugin = "1.7.3"
14-
uiToolingPreview = "1.9.0+dev2451"
14+
uiToolingPreview = "1.9.0-alpha02"
1515
junit = "4.13.2"
1616
# https://central.sonatype.com/artifact/io.insert-koin/koin-test-junit4/versions
1717
koinTestJunit4 = "4.0.0"
@@ -33,7 +33,7 @@ uuid = "0.8.4"
3333
# https://ktor.io/changelog/
3434
ktor = "3.1.1"
3535
# https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-navigation-routing.html
36-
navigationCompose = "2.9.10-alpha01"
36+
navigationCompose = "2.9.0-beta03"
3737
lifecycleViewModel = "2.8.7"
3838
# https://insert-koin.io/docs/setup/koin/
3939
koin = "4.1.0-Beta5"

demos/kotlin/kmp_sdk/simplifiedApp/build.gradle.kts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ plugins {
1313
kotlin {
1414
androidTarget {
1515
@OptIn(ExperimentalKotlinGradlePluginApi::class)
16-
compilerOptions { jvmTarget.set(JvmTarget.JVM_11) }
16+
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
1717
}
1818

1919
sourceSets {
@@ -47,8 +47,8 @@ android {
4747
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
4848
buildTypes { getByName("release") { isMinifyEnabled = false } }
4949
compileOptions {
50-
sourceCompatibility = JavaVersion.VERSION_11
51-
targetCompatibility = JavaVersion.VERSION_11
50+
sourceCompatibility = JavaVersion.VERSION_17
51+
targetCompatibility = JavaVersion.VERSION_17
5252
}
5353
buildFeatures { compose = false }
5454
}

demos/kotlin/kmp_sdk/simplifiedApp/src/commonMain/kotlin/App.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import com.gopro.open_gopro.OgpSdk
66
import com.gopro.open_gopro.OgpSdkAppContext
77
import com.gopro.open_gopro.gopro.GoPro
88
import com.gopro.open_gopro.operations.VideoResolution
9-
import com.gopro.open_gopro.operations.isFinished
109
import kotlinx.coroutines.CoroutineScope
1110
import kotlinx.coroutines.Dispatchers
1211
import kotlinx.coroutines.flow.first
@@ -77,13 +76,10 @@ private suspend fun examples(gopro: GoPro) {
7776

7877
// Use access point feature
7978
with(gopro.features.accessPoint) {
80-
// Get all available access opints and filter to find our target.
79+
// Get all available access points and filter to find our target.
8180
val entry = scanForAccessPoints().getOrThrow().first { it.ssid == "TARGET_SSID" }
8281
// Start connecting to the access point..
83-
connectAccessPoint(entry.ssid, "password").onSuccess {
84-
// Wait to collect a finished element from the flow
85-
it.first { state -> state.isFinished() }
86-
}
82+
connectAccessPoint(entry.ssid, "password")
8783
}
8884

8985
// Monitor disconnects

demos/kotlin/kmp_sdk/wsdk/build.gradle.kts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.vanniktech.maven.publish.SonatypeHost
55
import org.jetbrains.dokka.base.DokkaBase
66
import org.jetbrains.dokka.base.DokkaBaseConfiguration
7-
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
87
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
98

109
plugins {
@@ -21,8 +20,7 @@ plugins {
2120

2221
kotlin {
2322
androidTarget {
24-
@OptIn(ExperimentalKotlinGradlePluginApi::class)
25-
compilerOptions { jvmTarget.set(JvmTarget.JVM_11) }
23+
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
2624
publishLibraryVariants("release")
2725
}
2826

@@ -109,8 +107,8 @@ android {
109107
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
110108
}
111109
compileOptions {
112-
sourceCompatibility = JavaVersion.VERSION_11
113-
targetCompatibility = JavaVersion.VERSION_11
110+
sourceCompatibility = JavaVersion.VERSION_17
111+
targetCompatibility = JavaVersion.VERSION_17
114112
}
115113
}
116114

demos/kotlin/kmp_sdk/wsdk/src/commonMain/kotlin/com/gopro/open_gopro/gopro/FeaturesContainer.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@ package com.gopro.open_gopro.gopro
55

66
import com.gopro.open_gopro.domain.connector.ICameraConnector
77
import com.gopro.open_gopro.domain.gopro.IGoProFactory
8+
import kotlinx.coroutines.CoroutineScope
89

910
internal interface IFeatureContext {
1011
val gopro: GoPro
11-
val gpDescriptorManager: GpDescriptorManager
1212
val connector: ICameraConnector
1313
val facadeFactory: IGoProFactory
14+
val scope: CoroutineScope
1415
}
1516

1617
internal data class FeatureContext(
1718
override val gopro: GoPro,
18-
override val gpDescriptorManager: GpDescriptorManager,
1919
override val connector: ICameraConnector,
20-
override val facadeFactory: IGoProFactory
20+
override val facadeFactory: IGoProFactory,
21+
override val scope: CoroutineScope
2122
) : IFeatureContext
2223

2324
/**

demos/kotlin/kmp_sdk/wsdk/src/commonMain/kotlin/com/gopro/open_gopro/gopro/GoProFacade.kt

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,6 @@ class GoPro internal constructor(override val id: GoProId) : IGpDescriptor {
6262

6363
private val scope = CoroutineScope(dispatcher + coroutineExceptionHandler)
6464

65-
private val gpDescriptorManager =
66-
object : GpDescriptorManager {
67-
override fun getDescriptor(): IGpDescriptor = this@GoPro
68-
69-
override fun setAccessPointState(state: AccessPointState) =
70-
_accessPointState.update { state }
71-
72-
override fun setCohnState(state: CohnState) = _cohnState.update { state }
73-
}
74-
7565
override val communicators: List<CommunicationType>
7666
get() = operationMarshaller.communicators
7767

@@ -85,9 +75,7 @@ class GoPro internal constructor(override val id: GoProId) : IGpDescriptor {
8575
val statuses = StatusesContainer(operationMarshaller)
8676

8777
/** Container delegate to access all camera features */
88-
val features =
89-
FeaturesContainer(
90-
FeatureContext(this, this.gpDescriptorManager, cameraConnector, facadeFactory))
78+
val features = FeaturesContainer(FeatureContext(this, cameraConnector, facadeFactory, scope))
9179

9280
private var _ipAddress: String? = null
9381
override val ipAddress: String?
@@ -105,14 +93,9 @@ class GoPro internal constructor(override val id: GoProId) : IGpDescriptor {
10593
override val isReady: StateFlow<Boolean>
10694
get() = _isReady
10795

108-
private val _accessPointState: MutableStateFlow<AccessPointState> =
109-
MutableStateFlow(AccessPointState.Disconnected)
110-
override val accessPointState: StateFlow<AccessPointState>
111-
get() = _accessPointState
96+
override val accessPointState: StateFlow<AccessPointState> = features.accessPoint.state
11297

113-
private val _cohnState: MutableStateFlow<CohnState> = MutableStateFlow(CohnState.Unprovisioned)
114-
override val cohnState: StateFlow<CohnState>
115-
get() = _cohnState
98+
override val cohnState: StateFlow<CohnState> = features.cohn.state
11699

117100
// TODO do we need network type instead of communication type?
118101
private val _disconnects = MutableStateFlow<CommunicationType?>(null)
@@ -174,11 +157,6 @@ class GoPro internal constructor(override val id: GoProId) : IGpDescriptor {
174157
commands.sendKeepAlive().onFailure { logger.w("Failed to send keep alive.") }
175158
}
176159
}
177-
// // Register for COHN status updates
178-
// scope.launch {
179-
// features.cohn.getCohnStatus().collect {
180-
// gpDescriptorManager.setCohnState(it) }
181-
// }
182160
}
183161

184162
is HttpCommunicator -> {

0 commit comments

Comments
 (0)