diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 20f221ed..21b10bcb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -83,6 +83,13 @@ dependencies { implementation(libs.coil3.network.ktor) implementation(libs.glance.appwidget) + implementation(libs.androidx.remote.core) + implementation(libs.androidx.remote.creation) + implementation(libs.androidx.remote.creation.compose) + implementation(libs.androidx.remote.player.view) + implementation(libs.androidx.remote.player.core) + implementation(libs.androidx.remote.player.compose) + implementation(libs.androidx.wear.remote.material3) implementation(libs.koin.core) implementation(libs.koin.android) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6cbd0d51..25c8c8e2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,10 @@ - + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/surrus/peopleinspace/glance/Fetch.kt b/app/src/main/java/com/surrus/peopleinspace/glance/Fetch.kt new file mode 100644 index 00000000..26bcb7a2 --- /dev/null +++ b/app/src/main/java/com/surrus/peopleinspace/glance/Fetch.kt @@ -0,0 +1,69 @@ +package com.surrus.peopleinspace.glance + +import android.content.Context +import android.graphics.Bitmap +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import dev.johnoreilly.common.remote.IssPosition +import dev.johnoreilly.common.repository.PeopleInSpaceRepositoryInterface +import dev.johnoreilly.peopleinspace.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.osmdroid.tileprovider.MapTileProviderBasic +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.Projection +import org.osmdroid.views.drawing.MapSnapshot +import org.osmdroid.views.overlay.IconOverlay +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + + +suspend fun fetchIssPosition(repository: PeopleInSpaceRepositoryInterface): GeoPoint { + val issPosition: IssPosition = repository.pollISSPosition().first() + + val issPositionPoint = GeoPoint(issPosition.latitude, issPosition.longitude) + println("ISS Position: $issPositionPoint") + return issPositionPoint +} + +suspend fun fetchMapBitmap( + issPositionPoint: GeoPoint, + context: Context, + includeStationMarker: Boolean = true, + zoomLevel: Double = 1.0, + pWidth: Int = 480, + pHeight: Int = 240, +): ImageBitmap { + val stationMarker = IconOverlay( + issPositionPoint, + context.resources.getDrawable(R.drawable.ic_iss, context.theme) + ) + + val source = TileSourceFactory.DEFAULT_TILE_SOURCE + val projection = Projection(zoomLevel, pWidth, pHeight, issPositionPoint, 0f, true, false, 0, 0) + + val bitmap = withContext(Dispatchers.Main) { + suspendCoroutine { cont -> + val mapSnapshot = MapSnapshot( + { + if (it.status == MapSnapshot.Status.CANVAS_OK) { + val bitmap = Bitmap.createBitmap(it.bitmap) + cont.resume(bitmap) + } + }, + MapSnapshot.INCLUDE_FLAG_UPTODATE or MapSnapshot.INCLUDE_FLAG_SCALED, + MapTileProviderBasic(context, source, null), + if (includeStationMarker) listOf(stationMarker) else listOf(), + projection + ) + + launch(Dispatchers.IO) { + mapSnapshot.run() + } + } + } + return bitmap.asImageBitmap() +} \ No newline at end of file diff --git a/app/src/main/java/com/surrus/peopleinspace/glance/ISSMapWidget.kt b/app/src/main/java/com/surrus/peopleinspace/glance/ISSMapWidget.kt index 07f28cdd..12d1e6f1 100644 --- a/app/src/main/java/com/surrus/peopleinspace/glance/ISSMapWidget.kt +++ b/app/src/main/java/com/surrus/peopleinspace/glance/ISSMapWidget.kt @@ -1,8 +1,8 @@ package dev.johnoreilly.peopleinspace.glance import android.content.Context -import android.graphics.Bitmap import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asAndroidBitmap import androidx.glance.GlanceId import androidx.glance.GlanceModifier import androidx.glance.Image @@ -14,64 +14,20 @@ import androidx.glance.appwidget.provideContent import androidx.glance.background import androidx.glance.layout.Box import androidx.glance.layout.fillMaxSize -import dev.johnoreilly.common.remote.IssPosition +import com.surrus.peopleinspace.glance.fetchIssPosition +import com.surrus.peopleinspace.glance.fetchMapBitmap import dev.johnoreilly.common.repository.PeopleInSpaceRepositoryInterface import dev.johnoreilly.peopleinspace.MainActivity -import dev.johnoreilly.peopleinspace.R -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import org.osmdroid.tileprovider.MapTileProviderBasic -import org.osmdroid.tileprovider.tilesource.TileSourceFactory -import org.osmdroid.util.GeoPoint -import org.osmdroid.views.Projection -import org.osmdroid.views.drawing.MapSnapshot -import org.osmdroid.views.overlay.IconOverlay -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine class ISSMapWidget: GlanceAppWidget(), KoinComponent { private val repository: PeopleInSpaceRepositoryInterface by inject() override suspend fun provideGlance(context: Context, id: GlanceId) { - val issPosition: IssPosition = withContext(Dispatchers.Main) { - repository.pollISSPosition().first() - } - - val issPositionPoint = GeoPoint(issPosition.latitude, issPosition.longitude) - println("ISS Position: $issPositionPoint") - - val stationMarker = IconOverlay( - issPositionPoint, - context.resources.getDrawable(R.drawable.ic_iss, context.theme) - ) - - val source = TileSourceFactory.DEFAULT_TILE_SOURCE - val projection = Projection(1.0, 480, 240, issPositionPoint, 0f, true, false, 0, 0) + val issPositionPoint = fetchIssPosition(repository) - val bitmap = withContext(Dispatchers.Main) { - suspendCoroutine { cont -> - val mapSnapshot = MapSnapshot( - { - if (it.status == MapSnapshot.Status.CANVAS_OK) { - val bitmap = Bitmap.createBitmap(it.bitmap) - cont.resume(bitmap) - } - }, - MapSnapshot.INCLUDE_FLAG_UPTODATE or MapSnapshot.INCLUDE_FLAG_SCALED, - MapTileProviderBasic(context, source, null), - listOf(stationMarker), - projection - ) - - launch(Dispatchers.IO) { - mapSnapshot.run() - } - } - } + val bitmap = fetchMapBitmap(issPositionPoint, context) provideContent { Box( @@ -81,7 +37,7 @@ class ISSMapWidget: GlanceAppWidget(), KoinComponent { ) { Image( modifier = GlanceModifier.fillMaxSize(), - provider = ImageProvider(bitmap), + provider = ImageProvider(bitmap.asAndroidBitmap()), contentDescription = "ISS Location" ) } diff --git a/app/src/main/java/com/surrus/peopleinspace/remotecompose/PeopleInSpaceCard.kt b/app/src/main/java/com/surrus/peopleinspace/remotecompose/PeopleInSpaceCard.kt new file mode 100644 index 00000000..93e09ef0 --- /dev/null +++ b/app/src/main/java/com/surrus/peopleinspace/remotecompose/PeopleInSpaceCard.kt @@ -0,0 +1,93 @@ +@file:SuppressLint("RestrictedApi") + +package com.surrus.peopleinspace.remotecompose + +import android.annotation.SuppressLint +import android.graphics.BitmapFactory +import androidx.compose.remote.creation.compose.layout.RemoteBox +import androidx.compose.remote.creation.compose.layout.RemoteCanvas +import androidx.compose.remote.creation.compose.layout.RemoteComposable +import androidx.compose.remote.creation.compose.layout.RemoteOffset +import androidx.compose.remote.creation.compose.layout.rotate +import androidx.compose.remote.creation.compose.modifier.RemoteModifier +import androidx.compose.remote.creation.compose.modifier.background +import androidx.compose.remote.creation.compose.modifier.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.graphics.drawable.toBitmap +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat +import com.surrus.peopleinspace.remotecompose.util.RemotePreview +import dev.johnoreilly.peopleinspace.R +import org.osmdroid.util.GeoPoint + +@RemoteComposable +@Composable +fun PeopleInSpaceCard(map: ImageBitmap, issPosition: GeoPoint) { + val issVectorDrawable = issVectorDrawable() + RemoteBox( + modifier = RemoteModifier.fillMaxSize().background(Color.DarkGray) + ) { + val bitmap = issVectorDrawable + .apply { + setTint(Color.Black.toArgb()) + } + .toBitmap(256, 256) + + RemoteCanvas(modifier = RemoteModifier.fillMaxSize()) { + val mapBitmapId = canvas.document.addBitmap(map.asAndroidBitmap()) + canvas.document.drawBitmap( + mapBitmapId, + 0f, + 0f, + remote.component.width.id, + remote.component.height.id, + null + ) + + val centerX = remote.component.centerX + val centerY = remote.component.centerY + drawCircle(Color.White.copy(alpha = 0.3f), radius = 48f, RemoteOffset(centerX, centerY)) + drawCircle(Color.Black, radius = 48f, RemoteOffset(centerX, centerY), 1f, Stroke(1f)) + + val issBitmapId = canvas.document.addBitmap(bitmap) + val angle = remote.time.ContinuousSec() * 10f % 360f + rotate(angle, centerX, centerY) { + canvas.document.drawBitmap( + issBitmapId, + (centerX - 32f).id, + (centerY - 32f).id, + (centerX + 32f).id, + (centerY + 32f).id, + null + ) + } + } + } +} + +@Composable +private fun issVectorDrawable(): VectorDrawableCompat { + val drawable = VectorDrawableCompat.create( + LocalResources.current, R.drawable.ic_iss, + LocalContext.current.theme + )!! + return drawable +} + +@Composable +@Preview(widthDp = 200, heightDp = 100) +fun PeopleInSpaceCardPreview() { + RemotePreview { + val previewMap = + BitmapFactory.decodeResource(LocalResources.current, R.drawable.anfield).asImageBitmap() + PeopleInSpaceCard(previewMap, GeoPoint(0.0, 0.0)) + } +} diff --git a/app/src/main/java/com/surrus/peopleinspace/remotecompose/PeopleInSpaceWidgetReceiver.kt b/app/src/main/java/com/surrus/peopleinspace/remotecompose/PeopleInSpaceWidgetReceiver.kt new file mode 100644 index 00000000..48a19e2e --- /dev/null +++ b/app/src/main/java/com/surrus/peopleinspace/remotecompose/PeopleInSpaceWidgetReceiver.kt @@ -0,0 +1,76 @@ +package com.surrus.peopleinspace.remotecompose + +import android.annotation.SuppressLint +import android.appwidget.AppWidgetManager +import android.content.Context +import android.os.Build +import android.widget.RemoteViews +import androidx.annotation.RequiresApi +import androidx.compose.remote.creation.profile.Profile +import androidx.compose.remote.creation.profile.RcPlatformProfiles +import androidx.compose.ui.graphics.ImageBitmap +import com.surrus.peopleinspace.glance.fetchIssPosition +import com.surrus.peopleinspace.glance.fetchMapBitmap +import com.surrus.peopleinspace.remotecompose.util.AsyncAppWidgetReceiver +import com.surrus.peopleinspace.remotecompose.util.RemoteComposeRecorder +import dev.johnoreilly.common.repository.PeopleInSpaceRepositoryInterface +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import okio.ByteString +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.osmdroid.util.GeoPoint + +@RequiresApi(Build.VERSION_CODES.BAKLAVA) +@SuppressLint("RestrictedApi") +class PeopleInSpaceWidgetReceiver : AsyncAppWidgetReceiver(), KoinComponent { + private val repository: PeopleInSpaceRepositoryInterface by inject() + + /** Called when widgets must provide remote views. */ + + override suspend fun update(context: Context, wm: AppWidgetManager, widgetIds: IntArray) { + // receiver context is restricted + val appContext = context.applicationContext + + val issPosition = fetchIssPosition(repository) + + coroutineScope { + widgetIds.forEach { widgetId -> + launch { + val bitmap = fetchMapBitmap( + issPosition, + appContext, + includeStationMarker = false, + zoomLevel = 3.0, + pWidth = 400, + pHeight = 400 + ) + + val bytes = recordPeopleInSpaceCard( + profile = RcPlatformProfiles.WIDGETS_V6, + recorder = RemoteComposeRecorder(appContext), + issPosition = issPosition, + map = bitmap + ) + + val widget = RemoteViews(DrawInstructions(bytes)) + + wm.updateAppWidget(widgetId, widget) + } + } + } + } + + private fun DrawInstructions(bytes: ByteString): RemoteViews.DrawInstructions { + return RemoteViews.DrawInstructions.Builder(listOf(bytes.toByteArray())).build() + } + + suspend fun recordPeopleInSpaceCard( + recorder: RemoteComposeRecorder, + profile: Profile, + issPosition: GeoPoint, + map: ImageBitmap, + ): ByteString { + return recorder.record(profile) { PeopleInSpaceCard(map, issPosition) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/surrus/peopleinspace/remotecompose/util/AsyncAppWidgetReceiver.kt b/app/src/main/java/com/surrus/peopleinspace/remotecompose/util/AsyncAppWidgetReceiver.kt new file mode 100644 index 00000000..e4a761be --- /dev/null +++ b/app/src/main/java/com/surrus/peopleinspace/remotecompose/util/AsyncAppWidgetReceiver.kt @@ -0,0 +1,15 @@ +package com.surrus.peopleinspace.remotecompose.util + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context + +abstract class AsyncAppWidgetReceiver : AppWidgetProvider() { + override fun onUpdate(context: Context, wm: AppWidgetManager, widgetIds: IntArray) { + goAsync { + update(context, wm, widgetIds) + } + } + + abstract suspend fun update(context: Context, wm: AppWidgetManager, widgetIds: IntArray) +} \ No newline at end of file diff --git a/app/src/main/java/com/surrus/peopleinspace/remotecompose/util/RemoteComposeRecorder.kt b/app/src/main/java/com/surrus/peopleinspace/remotecompose/util/RemoteComposeRecorder.kt new file mode 100644 index 00000000..d690ec06 --- /dev/null +++ b/app/src/main/java/com/surrus/peopleinspace/remotecompose/util/RemoteComposeRecorder.kt @@ -0,0 +1,50 @@ +package com.surrus.peopleinspace.remotecompose.util + +import android.annotation.SuppressLint +import android.content.Context +import androidx.compose.remote.creation.compose.capture.CreationDisplayInfo +import androidx.compose.remote.creation.compose.capture.RemoteComposeCapture +import androidx.compose.remote.creation.compose.layout.RemoteComposable +import androidx.compose.remote.creation.profile.Profile +import androidx.compose.remote.creation.profile.RcPlatformProfiles +import androidx.compose.runtime.Composable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import okio.ByteString +import okio.ByteString.Companion.toByteString + +class RemoteComposeRecorder(private val context: Context) { + @SuppressLint("RestrictedApi") + suspend fun record( + profile: Profile = RcPlatformProfiles.ANDROIDX, + content: @RemoteComposable @Composable () -> Unit + ): ByteString = + withContext(Dispatchers.Main) { + suspendCancellableCoroutine { continuation -> + val connection = CreationDisplayInfo() + RemoteComposeCapture( + context = context, + creationDisplayInfo = connection, + immediateCapture = true, + onPaint = { view, writer -> + val rcDocBytes = writer.encodeToByteArray().toByteString() + if (continuation.isActive) { + continuation.resume( + rcDocBytes, { _, _, _ -> + println("Cancelled during execution") + } + ) + } + true + }, + onCaptureReady = @Composable {}, + profile = profile, + content = content, + ) + continuation.invokeOnCancellation { + println("Cancellation") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/surrus/peopleinspace/remotecompose/util/RemotePreview.kt b/app/src/main/java/com/surrus/peopleinspace/remotecompose/util/RemotePreview.kt new file mode 100644 index 00000000..5a8ae727 --- /dev/null +++ b/app/src/main/java/com/surrus/peopleinspace/remotecompose/util/RemotePreview.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.surrus.peopleinspace.remotecompose.util + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.remote.creation.compose.capture.RememberRemoteDocumentInline +import androidx.compose.remote.creation.compose.layout.RemoteComposable +import androidx.compose.remote.player.compose.RemoteDocumentPlayer +import androidx.compose.remote.player.core.RemoteDocument +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalWindowInfo + +/** + * Display a RemoteCompose Composable in the Android Studio Preview. + * + * Currently only works in single Preview mode, where previews presumably run longer. + */ +@SuppressLint("RestrictedApi") +@Composable +fun RemotePreview( + modifier: Modifier = Modifier, + content: @RemoteComposable @Composable () -> Unit +) { + var documentState by remember { mutableStateOf(null) } + + Box(modifier = modifier.fillMaxSize()) { + RememberRemoteDocumentInline( + onDocument = { doc -> + println("Document generated: $doc") + if (documentState == null) { + // Generate seems to get called again with a partial document + // Essentially re-recording but with existing state, so document is incomplete + documentState = RemoteDocument(doc) + } + } + ) { + content() + } + + if (documentState != null) { + val windowInfo = LocalWindowInfo.current + RemoteDocumentPlayer( + document = documentState!!.document, + windowInfo.containerSize.width, + windowInfo.containerSize.height, + modifier = Modifier.fillMaxSize(), + debugMode = 0, + ) + } + } +} diff --git a/app/src/main/java/com/surrus/peopleinspace/remotecompose/util/goAsync.kt b/app/src/main/java/com/surrus/peopleinspace/remotecompose/util/goAsync.kt new file mode 100644 index 00000000..28021743 --- /dev/null +++ b/app/src/main/java/com/surrus/peopleinspace/remotecompose/util/goAsync.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * From https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:glance/glance-appwidget/src/main/java/androidx/glance/appwidget/CoroutineBroadcastReceiver.kt + */ +package com.surrus.peopleinspace.remotecompose.util + +import android.content.BroadcastReceiver +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.cancellation.CancellationException + +/** + * Execute the block asynchronously in a scope with the lifetime of the broadcast. + * + * The coroutine scope will finish once the block return, as the broadcast will finish at that point + * too, allowing the system to kill the broadcast. + */ +internal fun BroadcastReceiver.goAsync( + coroutineContext: CoroutineContext = Dispatchers.Default, + block: suspend CoroutineScope.() -> Unit, +) { + val parentScope = CoroutineScope(coroutineContext) + val pendingResult = goAsync() + + parentScope.launch { + try { + try { + // Use `coroutineScope` so that errors within `block` are rethrown at the end of + // this scope, instead of propagating up the Job hierarchy. If we use `parentScope` + // directly, then errors in child jobs `launch`ed by `block` would trigger the + // CoroutineExceptionHandler and crash the process. + coroutineScope { this.block() } + } catch (e: Throwable) { + if (e is CancellationException && e.cause == null) { + // Regular cancellation, do nothing. The scope will always be cancelled below. + } else { + println("BroadcastReceiver execution failed $e") + } + } finally { + // Make sure the parent scope is cancelled in all cases. Nothing can be in the + // `finally` block after this, as this throws a `CancellationException`. + parentScope.cancel() + } + } finally { + // Notify ActivityManager that we are finished with this broadcast. This must be the + // last call, as the process may be killed after calling this. + try { + pendingResult.finish() + } catch (e: IllegalStateException) { + // On some OEM devices, this may throw an error about "Broadcast already finished". + // See b/257513022. + println("Error thrown when trying to finish broadcast $e") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/anfield.png b/app/src/main/res/drawable/anfield.png new file mode 100644 index 00000000..e4d94cf4 Binary files /dev/null and b/app/src/main/res/drawable/anfield.png differ diff --git a/app/src/main/res/values-v36/bools.xml b/app/src/main/res/values-v36/bools.xml new file mode 100644 index 00000000..a2a7fc8d --- /dev/null +++ b/app/src/main/res/values-v36/bools.xml @@ -0,0 +1,6 @@ + + + + true + \ No newline at end of file diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml new file mode 100644 index 00000000..518208e1 --- /dev/null +++ b/app/src/main/res/values/bools.xml @@ -0,0 +1,6 @@ + + + + false + \ No newline at end of file diff --git a/app/src/main/res/xml/rc_peopleinspace_info.xml b/app/src/main/res/xml/rc_peopleinspace_info.xml new file mode 100644 index 00000000..5ebbe913 --- /dev/null +++ b/app/src/main/res/xml/rc_peopleinspace_info.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/common/src/commonMain/kotlin/dev/johnoreilly/common/repository/PeopleInSpaceRepository.kt b/common/src/commonMain/kotlin/dev/johnoreilly/common/repository/PeopleInSpaceRepository.kt index 1d071ff7..38e0d84a 100644 --- a/common/src/commonMain/kotlin/dev/johnoreilly/common/repository/PeopleInSpaceRepository.kt +++ b/common/src/commonMain/kotlin/dev/johnoreilly/common/repository/PeopleInSpaceRepository.kt @@ -72,6 +72,8 @@ class PeopleInSpaceRepository( ) } } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { // TODO report error up to UI logger.w(e) { "Exception during fetchAndStorePeople: $e" } @@ -83,8 +85,12 @@ class PeopleInSpaceRepository( while (true) { try { val position = peopleInSpaceApi.fetchISSPosition().iss_position - emit(position) + if (currentCoroutineContext().isActive) { + emit(position) + } logger.d { position.toString() } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { // TODO report error up to UI logger.w(e) { "Exception during pollISSPosition: $e" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6314e7f..f143b3c3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ slf4j = "2.0.17" sqlDelight = "2.1.0" sqlJs = "1.8.0" webPackPlugin = "9.1.0" +remoteCompose = "1.0.0-SNAPSHOT" androidxActivity = "1.10.1" @@ -155,6 +156,14 @@ androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui- mcp-kotlin = { group = "io.modelcontextprotocol", name = "kotlin-sdk", version.ref = "mcp" } +androidx-remote-core = { module = "androidx.compose.remote:remote-core", version.ref = "remoteCompose" } +androidx-remote-creation = { module = "androidx.compose.remote:remote-creation", version.ref = "remoteCompose" } +androidx-remote-creation-compose = { module = "androidx.compose.remote:remote-creation-compose", version.ref = "remoteCompose" } +androidx-remote-player-view = { module = "androidx.compose.remote:remote-player-view", version.ref = "remoteCompose" } +androidx-remote-player-core = { module = "androidx.compose.remote:remote-player-core", version.ref = "remoteCompose" } +androidx-remote-player-compose = { module = "androidx.compose.remote:remote-player-compose", version.ref = "remoteCompose" } +androidx-wear-remote-material3 = { module = "androidx.wear.compose.remote:remote-material3", version.ref = "remoteCompose" } + [bundles] ktor-common = ["ktor-client-core", "ktor-client-json", "ktor-client-logging", "ktor-client-serialization", "ktor-client-content-negotiation", "ktor-serialization-kotlinx-json"] diff --git a/settings.gradle.kts b/settings.gradle.kts index b92a7245..1458d1dd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,6 +4,9 @@ pluginManagement { google() mavenCentral() gradlePluginPortal() + maven { + url = uri("https://androidx.dev/snapshots/builds/14405968/artifacts/repository") + } } }