Skip to content

Commit 848f5ad

Browse files
authored
Merge pull request #452 from yschimke/testing_remote_compose
Remote Compose ISS Map Widget
2 parents 75d7533 + 0378d96 commit 848f5ad

File tree

17 files changed

+518
-52
lines changed

17 files changed

+518
-52
lines changed

app/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ dependencies {
8383
implementation(libs.coil3.network.ktor)
8484

8585
implementation(libs.glance.appwidget)
86+
implementation(libs.androidx.remote.core)
87+
implementation(libs.androidx.remote.creation)
88+
implementation(libs.androidx.remote.creation.compose)
89+
implementation(libs.androidx.remote.player.view)
90+
implementation(libs.androidx.remote.player.core)
91+
implementation(libs.androidx.remote.player.compose)
92+
implementation(libs.androidx.wear.remote.material3)
8693

8794
implementation(libs.koin.core)
8895
implementation(libs.koin.android)

app/src/main/AndroidManifest.xml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
34
<uses-permission android:name="android.permission.INTERNET" />
45

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"/>
7+
58
<application
69
android:name=".PeopleInSpaceApplication"
710
android:allowBackup="true"
@@ -51,6 +54,21 @@
5154
android:resource="@xml/iss_widget_info" />
5255
</receiver>
5356

57+
<receiver
58+
android:name="com.surrus.peopleinspace.remotecompose.PeopleInSpaceWidgetReceiver"
59+
android:label="ISS Map"
60+
android:enabled="@bool/remotecompose_appwidget_available"
61+
android:exported="false"
62+
tools:targetApi="36">
63+
<intent-filter>
64+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
65+
</intent-filter>
66+
67+
<meta-data
68+
android:name="android.appwidget.provider"
69+
android:resource="@xml/rc_peopleinspace_info" />
70+
</receiver>
71+
5472
</application>
5573

5674
</manifest>
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: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
@file:SuppressLint("RestrictedApi")
2+
3+
package com.surrus.peopleinspace.remotecompose
4+
5+
import android.annotation.SuppressLint
6+
import android.graphics.BitmapFactory
7+
import androidx.compose.remote.creation.compose.layout.RemoteBox
8+
import androidx.compose.remote.creation.compose.layout.RemoteCanvas
9+
import androidx.compose.remote.creation.compose.layout.RemoteComposable
10+
import androidx.compose.remote.creation.compose.layout.RemoteOffset
11+
import androidx.compose.remote.creation.compose.layout.rotate
12+
import androidx.compose.remote.creation.compose.modifier.RemoteModifier
13+
import androidx.compose.remote.creation.compose.modifier.background
14+
import androidx.compose.remote.creation.compose.modifier.fillMaxSize
15+
import androidx.compose.runtime.Composable
16+
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
24+
import androidx.compose.ui.tooling.preview.Preview
25+
import androidx.core.graphics.drawable.toBitmap
26+
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
27+
import com.surrus.peopleinspace.remotecompose.util.RemotePreview
28+
import dev.johnoreilly.peopleinspace.R
29+
import org.osmdroid.util.GeoPoint
30+
31+
@RemoteComposable
32+
@Composable
33+
fun PeopleInSpaceCard(map: ImageBitmap, issPosition: GeoPoint) {
34+
val issVectorDrawable = issVectorDrawable()
35+
RemoteBox(
36+
modifier = RemoteModifier.fillMaxSize().background(Color.DarkGray)
37+
) {
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+
}
73+
}
74+
}
75+
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+
85+
@Composable
86+
@Preview(widthDp = 200, heightDp = 100)
87+
fun PeopleInSpaceCardPreview() {
88+
RemotePreview {
89+
val previewMap =
90+
BitmapFactory.decodeResource(LocalResources.current, R.drawable.anfield).asImageBitmap()
91+
PeopleInSpaceCard(previewMap, GeoPoint(0.0, 0.0))
92+
}
93+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.surrus.peopleinspace.remotecompose
2+
3+
import android.annotation.SuppressLint
4+
import android.appwidget.AppWidgetManager
5+
import android.content.Context
6+
import android.os.Build
7+
import android.widget.RemoteViews
8+
import androidx.annotation.RequiresApi
9+
import androidx.compose.remote.creation.profile.Profile
10+
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
14+
import com.surrus.peopleinspace.remotecompose.util.AsyncAppWidgetReceiver
15+
import com.surrus.peopleinspace.remotecompose.util.RemoteComposeRecorder
16+
import dev.johnoreilly.common.repository.PeopleInSpaceRepositoryInterface
17+
import kotlinx.coroutines.coroutineScope
18+
import kotlinx.coroutines.launch
19+
import okio.ByteString
20+
import org.koin.core.component.KoinComponent
21+
import org.koin.core.component.inject
22+
import org.osmdroid.util.GeoPoint
23+
24+
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
25+
@SuppressLint("RestrictedApi")
26+
class PeopleInSpaceWidgetReceiver : AsyncAppWidgetReceiver(), KoinComponent {
27+
private val repository: PeopleInSpaceRepositoryInterface by inject()
28+
29+
/** Called when widgets must provide remote views. */
30+
31+
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+
)
48+
49+
val bytes = recordPeopleInSpaceCard(
50+
profile = RcPlatformProfiles.WIDGETS_V6,
51+
recorder = RemoteComposeRecorder(appContext),
52+
issPosition = issPosition,
53+
map = bitmap
54+
)
55+
56+
val widget = RemoteViews(DrawInstructions(bytes))
57+
58+
wm.updateAppWidget(widgetId, widget)
59+
}
60+
}
61+
}
62+
}
63+
64+
private fun DrawInstructions(bytes: ByteString): RemoteViews.DrawInstructions {
65+
return RemoteViews.DrawInstructions.Builder(listOf(bytes.toByteArray())).build()
66+
}
67+
68+
suspend fun recordPeopleInSpaceCard(
69+
recorder: RemoteComposeRecorder,
70+
profile: Profile,
71+
issPosition: GeoPoint,
72+
map: ImageBitmap,
73+
): ByteString {
74+
return recorder.record(profile) { PeopleInSpaceCard(map, issPosition) }
75+
}
76+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.surrus.peopleinspace.remotecompose.util
2+
3+
import android.appwidget.AppWidgetManager
4+
import android.appwidget.AppWidgetProvider
5+
import android.content.Context
6+
7+
abstract class AsyncAppWidgetReceiver : AppWidgetProvider() {
8+
override fun onUpdate(context: Context, wm: AppWidgetManager, widgetIds: IntArray) {
9+
goAsync {
10+
update(context, wm, widgetIds)
11+
}
12+
}
13+
14+
abstract suspend fun update(context: Context, wm: AppWidgetManager, widgetIds: IntArray)
15+
}

0 commit comments

Comments
 (0)