Skip to content

Commit 0378d96

Browse files
committed
More interesting data
1 parent 54b50a0 commit 0378d96

File tree

10 files changed

+201
-85
lines changed

10 files changed

+201
-85
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ dependencies {
8989
implementation(libs.androidx.remote.player.view)
9090
implementation(libs.androidx.remote.player.core)
9191
implementation(libs.androidx.remote.player.compose)
92+
implementation(libs.androidx.wear.remote.material3)
9293

9394
implementation(libs.koin.core)
9495
implementation(libs.koin.android)

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
xmlns:tools="http://schemas.android.com/tools">
44
<uses-permission android:name="android.permission.INTERNET" />
55

6-
<uses-sdk tools:overrideLibrary="androidx.compose.remote.creation,androidx.compose.remote.player.compose,androidx.compose.remote.player.core,androidx.compose.remote.player.view,androidx.compose.remote.creation.compose"/>
6+
<uses-sdk tools:overrideLibrary="androidx.compose.remote.creation,androidx.compose.remote.player.compose,androidx.compose.remote.player.core,androidx.compose.remote.player.view,androidx.compose.remote.creation.compose,androidx.wear.compose.remote.material3,androidx.wear.compose.material.core,androidx.wear.compose.foundation,androidx.wear.compose.material3"/>
77

88
<application
99
android:name=".PeopleInSpaceApplication"
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.surrus.peopleinspace.glance
2+
3+
import android.content.Context
4+
import android.graphics.Bitmap
5+
import androidx.compose.ui.graphics.ImageBitmap
6+
import androidx.compose.ui.graphics.asImageBitmap
7+
import dev.johnoreilly.common.remote.IssPosition
8+
import dev.johnoreilly.common.repository.PeopleInSpaceRepositoryInterface
9+
import dev.johnoreilly.peopleinspace.R
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.flow.first
12+
import kotlinx.coroutines.launch
13+
import kotlinx.coroutines.withContext
14+
import org.osmdroid.tileprovider.MapTileProviderBasic
15+
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
16+
import org.osmdroid.util.GeoPoint
17+
import org.osmdroid.views.Projection
18+
import org.osmdroid.views.drawing.MapSnapshot
19+
import org.osmdroid.views.overlay.IconOverlay
20+
import kotlin.coroutines.resume
21+
import kotlin.coroutines.suspendCoroutine
22+
23+
24+
suspend fun fetchIssPosition(repository: PeopleInSpaceRepositoryInterface): GeoPoint {
25+
val issPosition: IssPosition = repository.pollISSPosition().first()
26+
27+
val issPositionPoint = GeoPoint(issPosition.latitude, issPosition.longitude)
28+
println("ISS Position: $issPositionPoint")
29+
return issPositionPoint
30+
}
31+
32+
suspend fun fetchMapBitmap(
33+
issPositionPoint: GeoPoint,
34+
context: Context,
35+
includeStationMarker: Boolean = true,
36+
zoomLevel: Double = 1.0,
37+
pWidth: Int = 480,
38+
pHeight: Int = 240,
39+
): ImageBitmap {
40+
val stationMarker = IconOverlay(
41+
issPositionPoint,
42+
context.resources.getDrawable(R.drawable.ic_iss, context.theme)
43+
)
44+
45+
val source = TileSourceFactory.DEFAULT_TILE_SOURCE
46+
val projection = Projection(zoomLevel, pWidth, pHeight, issPositionPoint, 0f, true, false, 0, 0)
47+
48+
val bitmap = withContext(Dispatchers.Main) {
49+
suspendCoroutine<Bitmap> { cont ->
50+
val mapSnapshot = MapSnapshot(
51+
{
52+
if (it.status == MapSnapshot.Status.CANVAS_OK) {
53+
val bitmap = Bitmap.createBitmap(it.bitmap)
54+
cont.resume(bitmap)
55+
}
56+
},
57+
MapSnapshot.INCLUDE_FLAG_UPTODATE or MapSnapshot.INCLUDE_FLAG_SCALED,
58+
MapTileProviderBasic(context, source, null),
59+
if (includeStationMarker) listOf(stationMarker) else listOf(),
60+
projection
61+
)
62+
63+
launch(Dispatchers.IO) {
64+
mapSnapshot.run()
65+
}
66+
}
67+
}
68+
return bitmap.asImageBitmap()
69+
}

app/src/main/java/com/surrus/peopleinspace/glance/ISSMapWidget.kt

Lines changed: 6 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package dev.johnoreilly.peopleinspace.glance
22

33
import android.content.Context
4-
import android.graphics.Bitmap
54
import androidx.compose.ui.graphics.Color
5+
import androidx.compose.ui.graphics.asAndroidBitmap
66
import androidx.glance.GlanceId
77
import androidx.glance.GlanceModifier
88
import androidx.glance.Image
@@ -14,64 +14,20 @@ import androidx.glance.appwidget.provideContent
1414
import androidx.glance.background
1515
import androidx.glance.layout.Box
1616
import androidx.glance.layout.fillMaxSize
17-
import dev.johnoreilly.common.remote.IssPosition
17+
import com.surrus.peopleinspace.glance.fetchIssPosition
18+
import com.surrus.peopleinspace.glance.fetchMapBitmap
1819
import dev.johnoreilly.common.repository.PeopleInSpaceRepositoryInterface
1920
import dev.johnoreilly.peopleinspace.MainActivity
20-
import dev.johnoreilly.peopleinspace.R
21-
import kotlinx.coroutines.Dispatchers
22-
import kotlinx.coroutines.flow.first
23-
import kotlinx.coroutines.launch
24-
import kotlinx.coroutines.withContext
2521
import org.koin.core.component.KoinComponent
2622
import org.koin.core.component.inject
27-
import org.osmdroid.tileprovider.MapTileProviderBasic
28-
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
29-
import org.osmdroid.util.GeoPoint
30-
import org.osmdroid.views.Projection
31-
import org.osmdroid.views.drawing.MapSnapshot
32-
import org.osmdroid.views.overlay.IconOverlay
33-
import kotlin.coroutines.resume
34-
import kotlin.coroutines.suspendCoroutine
3523

3624
class ISSMapWidget: GlanceAppWidget(), KoinComponent {
3725
private val repository: PeopleInSpaceRepositoryInterface by inject()
3826

3927
override suspend fun provideGlance(context: Context, id: GlanceId) {
40-
val issPosition: IssPosition = withContext(Dispatchers.Main) {
41-
repository.pollISSPosition().first()
42-
}
43-
44-
val issPositionPoint = GeoPoint(issPosition.latitude, issPosition.longitude)
45-
println("ISS Position: $issPositionPoint")
46-
47-
val stationMarker = IconOverlay(
48-
issPositionPoint,
49-
context.resources.getDrawable(R.drawable.ic_iss, context.theme)
50-
)
51-
52-
val source = TileSourceFactory.DEFAULT_TILE_SOURCE
53-
val projection = Projection(1.0, 480, 240, issPositionPoint, 0f, true, false, 0, 0)
28+
val issPositionPoint = fetchIssPosition(repository)
5429

55-
val bitmap = withContext(Dispatchers.Main) {
56-
suspendCoroutine<Bitmap> { cont ->
57-
val mapSnapshot = MapSnapshot(
58-
{
59-
if (it.status == MapSnapshot.Status.CANVAS_OK) {
60-
val bitmap = Bitmap.createBitmap(it.bitmap)
61-
cont.resume(bitmap)
62-
}
63-
},
64-
MapSnapshot.INCLUDE_FLAG_UPTODATE or MapSnapshot.INCLUDE_FLAG_SCALED,
65-
MapTileProviderBasic(context, source, null),
66-
listOf(stationMarker),
67-
projection
68-
)
69-
70-
launch(Dispatchers.IO) {
71-
mapSnapshot.run()
72-
}
73-
}
74-
}
30+
val bitmap = fetchMapBitmap(issPositionPoint, context)
7531

7632
provideContent {
7733
Box(
@@ -81,7 +37,7 @@ class ISSMapWidget: GlanceAppWidget(), KoinComponent {
8137
) {
8238
Image(
8339
modifier = GlanceModifier.fillMaxSize(),
84-
provider = ImageProvider(bitmap),
40+
provider = ImageProvider(bitmap.asAndroidBitmap()),
8541
contentDescription = "ISS Location"
8642
)
8743
}
Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,93 @@
1+
@file:SuppressLint("RestrictedApi")
2+
13
package com.surrus.peopleinspace.remotecompose
24

35
import android.annotation.SuppressLint
4-
import androidx.compose.remote.creation.compose.capture.LocalRemoteComposeCreationState
5-
import androidx.compose.remote.creation.compose.layout.RemoteColumn
6+
import android.graphics.BitmapFactory
7+
import androidx.compose.remote.creation.compose.layout.RemoteBox
8+
import androidx.compose.remote.creation.compose.layout.RemoteCanvas
69
import androidx.compose.remote.creation.compose.layout.RemoteComposable
7-
import androidx.compose.remote.creation.compose.layout.RemoteText
8-
import androidx.compose.remote.creation.compose.layout.remoteComponentHeight
9-
import androidx.compose.remote.creation.compose.layout.remoteComponentWidth
10+
import androidx.compose.remote.creation.compose.layout.RemoteOffset
11+
import androidx.compose.remote.creation.compose.layout.rotate
1012
import androidx.compose.remote.creation.compose.modifier.RemoteModifier
1113
import androidx.compose.remote.creation.compose.modifier.background
1214
import androidx.compose.remote.creation.compose.modifier.fillMaxSize
13-
import androidx.compose.remote.creation.compose.state.RemoteColor
14-
import androidx.compose.remote.creation.compose.state.RemoteString
15-
import androidx.compose.remote.creation.compose.state.rememberRemoteIntValue
1615
import androidx.compose.runtime.Composable
1716
import androidx.compose.ui.graphics.Color
17+
import androidx.compose.ui.graphics.ImageBitmap
18+
import androidx.compose.ui.graphics.asAndroidBitmap
19+
import androidx.compose.ui.graphics.asImageBitmap
20+
import androidx.compose.ui.graphics.drawscope.Stroke
21+
import androidx.compose.ui.graphics.toArgb
22+
import androidx.compose.ui.platform.LocalContext
23+
import androidx.compose.ui.platform.LocalResources
1824
import androidx.compose.ui.tooling.preview.Preview
25+
import androidx.core.graphics.drawable.toBitmap
26+
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
1927
import com.surrus.peopleinspace.remotecompose.util.RemotePreview
28+
import dev.johnoreilly.peopleinspace.R
29+
import org.osmdroid.util.GeoPoint
2030

21-
@SuppressLint("RestrictedApi")
2231
@RemoteComposable
2332
@Composable
24-
fun PeopleInSpaceCard() {
25-
RemoteColumn(
33+
fun PeopleInSpaceCard(map: ImageBitmap, issPosition: GeoPoint) {
34+
val issVectorDrawable = issVectorDrawable()
35+
RemoteBox(
2636
modifier = RemoteModifier.fillMaxSize().background(Color.DarkGray)
2737
) {
28-
val state = LocalRemoteComposeCreationState.current
29-
// TODO real content
30-
val count = rememberRemoteIntValue { 5 }
31-
val width = remoteComponentWidth(state)
32-
val height = remoteComponentHeight(state)
33-
val text = RemoteString("People in Space: ") + count.toRemoteString(1, 0)
34-
RemoteText(text, color = RemoteColor(Color.White))
35-
36-
val sizeText = RemoteString("Size: ") + width.toRemoteString(3, 0) + RemoteString("x") + height.toRemoteString(1, 0)
37-
RemoteText(sizeText, color = RemoteColor(Color.White))
38+
val bitmap = issVectorDrawable
39+
.apply {
40+
setTint(Color.Black.toArgb())
41+
}
42+
.toBitmap(256, 256)
43+
44+
RemoteCanvas(modifier = RemoteModifier.fillMaxSize()) {
45+
val mapBitmapId = canvas.document.addBitmap(map.asAndroidBitmap())
46+
canvas.document.drawBitmap(
47+
mapBitmapId,
48+
0f,
49+
0f,
50+
remote.component.width.id,
51+
remote.component.height.id,
52+
null
53+
)
54+
55+
val centerX = remote.component.centerX
56+
val centerY = remote.component.centerY
57+
drawCircle(Color.White.copy(alpha = 0.3f), radius = 48f, RemoteOffset(centerX, centerY))
58+
drawCircle(Color.Black, radius = 48f, RemoteOffset(centerX, centerY), 1f, Stroke(1f))
59+
60+
val issBitmapId = canvas.document.addBitmap(bitmap)
61+
val angle = remote.time.ContinuousSec() * 10f % 360f
62+
rotate(angle, centerX, centerY) {
63+
canvas.document.drawBitmap(
64+
issBitmapId,
65+
(centerX - 32f).id,
66+
(centerY - 32f).id,
67+
(centerX + 32f).id,
68+
(centerY + 32f).id,
69+
null
70+
)
71+
}
72+
}
3873
}
3974
}
4075

76+
@Composable
77+
private fun issVectorDrawable(): VectorDrawableCompat {
78+
val drawable = VectorDrawableCompat.create(
79+
LocalResources.current, R.drawable.ic_iss,
80+
LocalContext.current.theme
81+
)!!
82+
return drawable
83+
}
84+
4185
@Composable
4286
@Preview(widthDp = 200, heightDp = 100)
4387
fun PeopleInSpaceCardPreview() {
4488
RemotePreview {
45-
PeopleInSpaceCard()
89+
val previewMap =
90+
BitmapFactory.decodeResource(LocalResources.current, R.drawable.anfield).asImageBitmap()
91+
PeopleInSpaceCard(previewMap, GeoPoint(0.0, 0.0))
4692
}
4793
}

app/src/main/java/com/surrus/peopleinspace/remotecompose/PeopleInSpaceWidgetReceiver.kt

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,69 @@ import android.widget.RemoteViews
88
import androidx.annotation.RequiresApi
99
import androidx.compose.remote.creation.profile.Profile
1010
import androidx.compose.remote.creation.profile.RcPlatformProfiles
11+
import androidx.compose.ui.graphics.ImageBitmap
12+
import com.surrus.peopleinspace.glance.fetchIssPosition
13+
import com.surrus.peopleinspace.glance.fetchMapBitmap
1114
import com.surrus.peopleinspace.remotecompose.util.AsyncAppWidgetReceiver
1215
import com.surrus.peopleinspace.remotecompose.util.RemoteComposeRecorder
16+
import dev.johnoreilly.common.repository.PeopleInSpaceRepositoryInterface
17+
import kotlinx.coroutines.coroutineScope
18+
import kotlinx.coroutines.launch
1319
import okio.ByteString
20+
import org.koin.core.component.KoinComponent
21+
import org.koin.core.component.inject
22+
import org.osmdroid.util.GeoPoint
1423

1524
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
1625
@SuppressLint("RestrictedApi")
17-
class PeopleInSpaceWidgetReceiver : AsyncAppWidgetReceiver() {
26+
class PeopleInSpaceWidgetReceiver : AsyncAppWidgetReceiver(), KoinComponent {
27+
private val repository: PeopleInSpaceRepositoryInterface by inject()
28+
1829
/** Called when widgets must provide remote views. */
1930

2031
override suspend fun update(context: Context, wm: AppWidgetManager, widgetIds: IntArray) {
32+
// receiver context is restricted
33+
val appContext = context.applicationContext
34+
35+
val issPosition = fetchIssPosition(repository)
36+
37+
coroutineScope {
38+
widgetIds.forEach { widgetId ->
39+
launch {
40+
val bitmap = fetchMapBitmap(
41+
issPosition,
42+
appContext,
43+
includeStationMarker = false,
44+
zoomLevel = 3.0,
45+
pWidth = 400,
46+
pHeight = 400
47+
)
2148

22-
val bytes = recordPeopleInSpaceCard(
23-
profile = RcPlatformProfiles.WIDGETS_V6,
24-
recorder = RemoteComposeRecorder(context)
25-
)
49+
val bytes = recordPeopleInSpaceCard(
50+
profile = RcPlatformProfiles.WIDGETS_V6,
51+
recorder = RemoteComposeRecorder(appContext),
52+
issPosition = issPosition,
53+
map = bitmap
54+
)
2655

27-
val widget =
28-
RemoteViews(RemoteViews.DrawInstructions.Builder(listOf(bytes.toByteArray())).build())
56+
val widget = RemoteViews(DrawInstructions(bytes))
57+
58+
wm.updateAppWidget(widgetId, widget)
59+
}
60+
}
61+
}
62+
}
2963

30-
widgetIds.forEach { widgetId -> wm.updateAppWidget(widgetId, widget) }
64+
private fun DrawInstructions(bytes: ByteString): RemoteViews.DrawInstructions {
65+
return RemoteViews.DrawInstructions.Builder(listOf(bytes.toByteArray())).build()
3166
}
3267

3368
suspend fun recordPeopleInSpaceCard(
3469
recorder: RemoteComposeRecorder,
35-
profile: Profile
70+
profile: Profile,
71+
issPosition: GeoPoint,
72+
map: ImageBitmap,
3673
): ByteString {
37-
return recorder.record(profile) { PeopleInSpaceCard() }
74+
return recorder.record(profile) { PeopleInSpaceCard(map, issPosition) }
3875
}
3976
}
329 KB
Loading

common/src/commonMain/kotlin/dev/johnoreilly/common/repository/PeopleInSpaceRepository.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ class PeopleInSpaceRepository(
7272
)
7373
}
7474
}
75+
} catch (e: CancellationException) {
76+
throw e
7577
} catch (e: Exception) {
7678
// TODO report error up to UI
7779
logger.w(e) { "Exception during fetchAndStorePeople: $e" }
@@ -83,8 +85,12 @@ class PeopleInSpaceRepository(
8385
while (true) {
8486
try {
8587
val position = peopleInSpaceApi.fetchISSPosition().iss_position
86-
emit(position)
88+
if (currentCoroutineContext().isActive) {
89+
emit(position)
90+
}
8791
logger.d { position.toString() }
92+
} catch (e: CancellationException) {
93+
throw e
8894
} catch (e: Exception) {
8995
// TODO report error up to UI
9096
logger.w(e) { "Exception during pollISSPosition: $e" }

0 commit comments

Comments
 (0)