Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 3 additions & 3 deletions demos/kotlin/kmp_sdk/composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ plugins {
kotlin {
androidTarget {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions { jvmTarget.set(JvmTarget.JVM_11) }
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
}

// jvm("desktop")
Expand Down Expand Up @@ -108,8 +108,8 @@ android {
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
buildTypes { getByName("release") { isMinifyEnabled = false } }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures { compose = true }
dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import androidx.lifecycle.viewModelScope
import com.gopro.open_gopro.OgpSdk
import com.gopro.open_gopro.operations.AccessPointState
import data.IAppPreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
Expand All @@ -32,58 +32,80 @@ class AccessPointViewModel(
private var _state = MutableStateFlow<ApUiState>(ApUiState.Idle)
val state = _state.asStateFlow()

private var _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()

private fun setErrorMessage(message: String) {
logger.e(message)
_errorMessage.update { message }
}

private fun setState(newState: ApUiState) {
logger.d("Setting state to: ${newState.name}")
_state.update { newState }
}

fun scanForSsids() {
if (_state.value == ApUiState.Idle) {
viewModelScope.launch {
_state.update { ApUiState.Scanning }
setState(ApUiState.Scanning)
gopro.features.accessPoint
.scanForAccessPoints()
.getOrThrow()
.map { it.ssid }
.let { ssids -> _state.update { ApUiState.WaitingConnect(ssids) } }
.let { ssids -> setState(ApUiState.WaitingConnect(ssids)) }
}
} else {
logger.w("Can only scan from idle state")
setErrorMessage("Can only scan from idle state")
}
}

private suspend fun processConnectNotifications(notifications: Flow<AccessPointState>) =
_state.value.let { s ->
if (s is ApUiState.Connecting) {
notifications.collect { notification ->
logger.d("Received AP Connect notification: $notification")
when (notification) {
is AccessPointState.Connected -> _state.update { ApUiState.Connected(s.ssid) }
is AccessPointState.InProgress -> _state.update { ApUiState.Connecting(s.ssid) }
else -> _state.update { ApUiState.Idle }
}
}
} else {
logger.w("Can not process connect notifications if not connecting")
}
}

fun connectToSsid(ssid: String) {
if (_state.value is ApUiState.WaitingConnect) {
viewModelScope.launch {
_state.update { ApUiState.Connecting(ssid) }
processConnectNotifications(
gopro.features.accessPoint.connectAccessPoint(ssid).getOrThrow())
setState(ApUiState.Connecting(ssid))
gopro.features.accessPoint
.connectAccessPoint(ssid)
.onSuccess { setState(ApUiState.Connected(ssid)) }
.onFailure { setState(ApUiState.Idle) }
}
} else {
logger.w("Can only connect after scanning.")
setErrorMessage("Can only connect after scanning.")
}
}

fun connectToSsid(ssid: String, password: String) {
if (_state.value is ApUiState.WaitingConnect) {
viewModelScope.launch {
_state.update { ApUiState.Connecting(ssid) }
processConnectNotifications(
gopro.features.accessPoint.connectAccessPoint(ssid, password).getOrThrow())
setState(ApUiState.Connecting(ssid))
gopro.features.accessPoint
.connectAccessPoint(ssid, password)
.onSuccess { setState(ApUiState.Connected(ssid)) }
.onFailure { setState(ApUiState.Idle) }
}
} else {
setErrorMessage("Can only connect after scanning.")
}
}

fun disconnect() {
if (_state.value is ApUiState.Connected) {
viewModelScope.launch {
gopro.features.accessPoint
.disconnectAccessPoint()
.onSuccess { setState(ApUiState.Idle) }
.onFailure { setErrorMessage("Failed to disconnect from access point") }
}
} else {
logger.w("Can only connect after scanning.")
setErrorMessage("Can only disconnect when connected.")
}
}

override fun onStart() {
when (val featureState = gopro.accessPointState.value) {
AccessPointState.Disconnected -> ApUiState.Idle
is AccessPointState.InProgress -> ApUiState.Connecting(featureState.ssid)
is AccessPointState.Connected -> ApUiState.Connected(featureState.ssid)
}.let { setState(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import presenter.ApUiState
import ui.common.Screen
import ui.components.CommonTopBar
import ui.components.IndeterminateCircularProgressIndicator
import ui.components.SnackbarMessageHandler

@Composable
fun AccessPointScreen(
Expand All @@ -39,10 +40,13 @@ fun AccessPointScreen(
modifier: Modifier = Modifier,
) {
val state by viewModel.state.collectAsStateWithLifecycle()
val errorMessage by viewModel.errorMessage.collectAsStateWithLifecycle()

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

errorMessage?.let { SnackbarMessageHandler(it) }

CommonTopBar(
navController = navController,
title = Screen.AccessPoint.route,
Expand All @@ -59,17 +63,21 @@ fun AccessPointScreen(
is ApUiState.Connected ->
ScanScreen(
isScanning = (s is ApUiState.Scanning),
isConnected = (s is ApUiState.Connected),
onScanStart = { viewModel.scanForSsids() },
onSsidSelect = { ssid = it },
selectedSsid = ssid)
selectedSsid = ssid,
onDisconnect = { viewModel.disconnect() })

is ApUiState.WaitingConnect -> {
ScanScreen(
isScanning = false,
isConnected = false,
onScanStart = { viewModel.scanForSsids() },
onSsidSelect = { ssid = it },
ssids = s.ssids,
selectedSsid = ssid)
selectedSsid = ssid,
onDisconnect = { viewModel.disconnect() })
ConnectScreen(
password = password,
onConnectSsid = {
Expand All @@ -87,13 +95,18 @@ fun AccessPointScreen(
@Composable
fun ScanScreen(
isScanning: Boolean,
isConnected: Boolean,
ssids: List<String> = listOf(),
onScanStart: (() -> Unit),
onSsidSelect: ((String) -> Unit),
selectedSsid: String,
onDisconnect: (() -> Unit)
) {
Column {
Button(onScanStart) { Text("Start Scanning for SSIDS") }
if (isConnected) {
Button(onDisconnect) { Text("Disconnect") }
}
if (isScanning) {
IndeterminateCircularProgressIndicator()
}
Expand Down
7 changes: 2 additions & 5 deletions demos/kotlin/kmp_sdk/docs/sdk_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,10 @@ Here is a (naive) example of using the [AccessPointFeature] to connect the GoPro

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

Expand Down
4 changes: 2 additions & 2 deletions demos/kotlin/kmp_sdk/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ androidx-lifecycle = "2.8.4"
androidx-material = "1.12.0"
# https://plugins.gradle.org/plugin/org.jetbrains.compose
compose-plugin = "1.7.3"
uiToolingPreview = "1.9.0+dev2451"
uiToolingPreview = "1.9.0-alpha02"
junit = "4.13.2"
# https://central.sonatype.com/artifact/io.insert-koin/koin-test-junit4/versions
koinTestJunit4 = "4.0.0"
Expand All @@ -33,7 +33,7 @@ uuid = "0.8.4"
# https://ktor.io/changelog/
ktor = "3.1.1"
# https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-navigation-routing.html
navigationCompose = "2.9.10-alpha01"
navigationCompose = "2.9.0-beta03"
lifecycleViewModel = "2.8.7"
# https://insert-koin.io/docs/setup/koin/
koin = "4.1.0-Beta5"
Expand Down
6 changes: 3 additions & 3 deletions demos/kotlin/kmp_sdk/simplifiedApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ plugins {
kotlin {
androidTarget {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions { jvmTarget.set(JvmTarget.JVM_11) }
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
}

sourceSets {
Expand Down Expand Up @@ -47,8 +47,8 @@ android {
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
buildTypes { getByName("release") { isMinifyEnabled = false } }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures { compose = false }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.gopro.open_gopro.OgpSdk
import com.gopro.open_gopro.OgpSdkAppContext
import com.gopro.open_gopro.gopro.GoPro
import com.gopro.open_gopro.operations.VideoResolution
import com.gopro.open_gopro.operations.isFinished
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
Expand Down Expand Up @@ -77,13 +76,10 @@ private suspend fun examples(gopro: GoPro) {

// Use access point feature
with(gopro.features.accessPoint) {
// Get all available access opints and filter to find our target.
// Get all available access points and filter to find our target.
val entry = scanForAccessPoints().getOrThrow().first { it.ssid == "TARGET_SSID" }
// Start connecting to the access point..
connectAccessPoint(entry.ssid, "password").onSuccess {
// Wait to collect a finished element from the flow
it.first { state -> state.isFinished() }
}
connectAccessPoint(entry.ssid, "password")
}

// Monitor disconnects
Expand Down
8 changes: 3 additions & 5 deletions demos/kotlin/kmp_sdk/wsdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.vanniktech.maven.publish.SonatypeHost
import org.jetbrains.dokka.base.DokkaBase
import org.jetbrains.dokka.base.DokkaBaseConfiguration
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
Expand All @@ -21,8 +20,7 @@ plugins {

kotlin {
androidTarget {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions { jvmTarget.set(JvmTarget.JVM_11) }
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
publishLibraryVariants("release")
}

Expand Down Expand Up @@ -109,8 +107,8 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ package com.gopro.open_gopro.gopro

import com.gopro.open_gopro.domain.connector.ICameraConnector
import com.gopro.open_gopro.domain.gopro.IGoProFactory
import kotlinx.coroutines.CoroutineScope

internal interface IFeatureContext {
val gopro: GoPro
val gpDescriptorManager: GpDescriptorManager
val connector: ICameraConnector
val facadeFactory: IGoProFactory
val scope: CoroutineScope
}

internal data class FeatureContext(
override val gopro: GoPro,
override val gpDescriptorManager: GpDescriptorManager,
override val connector: ICameraConnector,
override val facadeFactory: IGoProFactory
override val facadeFactory: IGoProFactory,
override val scope: CoroutineScope
) : IFeatureContext

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,6 @@ class GoPro internal constructor(override val id: GoProId) : IGpDescriptor {

private val scope = CoroutineScope(dispatcher + coroutineExceptionHandler)

private val gpDescriptorManager =
object : GpDescriptorManager {
override fun getDescriptor(): IGpDescriptor = this@GoPro

override fun setAccessPointState(state: AccessPointState) =
_accessPointState.update { state }

override fun setCohnState(state: CohnState) = _cohnState.update { state }
}

override val communicators: List<CommunicationType>
get() = operationMarshaller.communicators

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

/** Container delegate to access all camera features */
val features =
FeaturesContainer(
FeatureContext(this, this.gpDescriptorManager, cameraConnector, facadeFactory))
val features = FeaturesContainer(FeatureContext(this, cameraConnector, facadeFactory, scope))

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

private val _accessPointState: MutableStateFlow<AccessPointState> =
MutableStateFlow(AccessPointState.Disconnected)
override val accessPointState: StateFlow<AccessPointState>
get() = _accessPointState
override val accessPointState: StateFlow<AccessPointState> = features.accessPoint.state

private val _cohnState: MutableStateFlow<CohnState> = MutableStateFlow(CohnState.Unprovisioned)
override val cohnState: StateFlow<CohnState>
get() = _cohnState
override val cohnState: StateFlow<CohnState> = features.cohn.state

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

is HttpCommunicator -> {
Expand Down
Loading