Skip to content

Commit a7c6b55

Browse files
committed
feat: make example app work with the sandbox
1 parent 4e8d773 commit a7c6b55

6 files changed

Lines changed: 174 additions & 70 deletions

File tree

examples/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ dependencies {
6464
implementation(libs.ui.graphics)
6565
implementation(libs.ui.tooling.preview)
6666
implementation(libs.material3)
67+
implementation(libs.ktor.client.core)
68+
implementation(libs.ktor.client.cio)
6769

6870
implementation(project(":live-objects"))
6971
implementation(project(":android"))

examples/src/main/kotlin/com/ably/example/MainActivity.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,31 @@ import androidx.activity.enableEdgeToEdge
77
import com.ably.example.screen.MainScreen
88
import com.ably.example.ui.theme.AblyTheme
99
import io.ably.lib.realtime.AblyRealtime
10+
import io.ably.lib.rest.AblyRest
11+
import io.ably.lib.rest.Auth
1012
import io.ably.lib.types.ClientOptions
1113
import io.ably.lib.util.Log
14+
import kotlinx.coroutines.runBlocking
1215

1316
class MainActivity : ComponentActivity() {
1417
private val realtimeClient: AblyRealtime by lazy {
1518
AblyRealtime(
1619
ClientOptions().apply {
17-
key = BuildConfig.ABLY_KEY
20+
if (BuildConfig.ABLY_KEY.isBlank()) {
21+
authCallback = Auth.TokenCallback {
22+
val apiKey = runBlocking {
23+
val sandbox = Sandbox.getInstance()
24+
sandbox.apiKey
25+
}
26+
AblyRest(ClientOptions().apply {
27+
key = apiKey
28+
environment = "sandbox"
29+
}).auth.requestToken(null, null)
30+
}
31+
environment = "sandbox"
32+
} else {
33+
key = BuildConfig.ABLY_KEY
34+
}
1835
logLevel = Log.VERBOSE
1936
autoConnect = false
2037
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.ably.example
2+
3+
import com.google.gson.JsonElement
4+
import com.google.gson.JsonParser
5+
import io.ktor.client.*
6+
import io.ktor.client.engine.cio.*
7+
import io.ktor.client.network.sockets.*
8+
import io.ktor.client.network.sockets.SocketTimeoutException
9+
import io.ktor.client.plugins.*
10+
import io.ktor.client.request.*
11+
import io.ktor.client.statement.*
12+
import io.ktor.http.*
13+
14+
private val client = HttpClient(CIO) {
15+
install(HttpRequestRetry) {
16+
maxRetries = 5
17+
retryIf { _, response ->
18+
!response.status.isSuccess()
19+
}
20+
retryOnExceptionIf { _, cause ->
21+
cause is ConnectTimeoutException ||
22+
cause is HttpRequestTimeoutException ||
23+
cause is SocketTimeoutException
24+
}
25+
exponentialDelay()
26+
}
27+
}
28+
29+
class Sandbox private constructor(val appId: String, val apiKey: String) {
30+
companion object {
31+
private var cachedInstance: Sandbox? = null
32+
33+
suspend fun createInstance(): Sandbox {
34+
val response: HttpResponse = client.post("https://sandbox.realtime.ably-nonprod.net/apps") {
35+
contentType(ContentType.Application.Json)
36+
setBody(loadAppCreationRequestBody().toString())
37+
}
38+
val body = JsonParser.parseString(response.bodyAsText())
39+
40+
return Sandbox(
41+
appId = body.asJsonObject["appId"].asString,
42+
apiKey = body.asJsonObject["keys"].asJsonArray[0].asJsonObject["keyStr"].asString,
43+
)
44+
}
45+
46+
suspend fun getInstance(): Sandbox {
47+
cachedInstance?.let { return it }
48+
val created = createInstance()
49+
cachedInstance = created
50+
return created
51+
}
52+
}
53+
}
54+
55+
private suspend fun loadAppCreationRequestBody(): JsonElement =
56+
JsonParser.parseString(
57+
client.get("https://raw.githubusercontent.com/ably/ably-common/refs/heads/main/test-resources/test-app-setup.json") {
58+
contentType(ContentType.Application.Json)
59+
}.bodyAsText(),
60+
).asJsonObject.get("post_apps")

examples/src/main/kotlin/com/ably/example/Utils.kt

Lines changed: 86 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
package com.ably.example
22

3-
import androidx.compose.runtime.Composable
4-
import androidx.compose.runtime.DisposableEffect
5-
import androidx.compose.runtime.LaunchedEffect
6-
import androidx.compose.runtime.getValue
7-
import androidx.compose.runtime.mutableStateOf
8-
import androidx.compose.runtime.remember
9-
import androidx.compose.runtime.setValue
10-
import io.ably.lib.objects.RealtimeObjects
3+
import androidx.compose.runtime.*
114
import io.ably.lib.objects.ObjectsCallback
5+
import io.ably.lib.objects.RealtimeObjects
126
import io.ably.lib.objects.type.counter.LiveCounter
137
import io.ably.lib.objects.type.counter.LiveCounterUpdate
148
import io.ably.lib.objects.type.map.LiveMap
@@ -28,7 +22,7 @@ import kotlinx.coroutines.launch
2822
import kotlinx.coroutines.suspendCancellableCoroutine
2923
import kotlin.coroutines.resume
3024

31-
suspend fun RealtimeObjects.getRootCoroutines(): LiveMap = suspendCancellableCoroutine { continuation ->
25+
private suspend fun RealtimeObjects.getRootCoroutines(): LiveMap = suspendCancellableCoroutine { continuation ->
3226
getRootAsync(object : ObjectsCallback<LiveMap> {
3327
override fun onSuccess(result: LiveMap?) {
3428
continuation.resume(result!!)
@@ -40,19 +34,20 @@ suspend fun RealtimeObjects.getRootCoroutines(): LiveMap = suspendCancellableCor
4034
})
4135
}
4236

43-
suspend fun RealtimeObjects.createCounterCoroutine(): LiveCounter = suspendCancellableCoroutine { continuation ->
44-
createCounterAsync(object : ObjectsCallback<LiveCounter> {
45-
override fun onSuccess(result: LiveCounter?) {
46-
continuation.resume(result!!)
47-
}
37+
private suspend fun RealtimeObjects.createCounterCoroutine(): LiveCounter =
38+
suspendCancellableCoroutine { continuation ->
39+
createCounterAsync(object : ObjectsCallback<LiveCounter> {
40+
override fun onSuccess(result: LiveCounter?) {
41+
continuation.resume(result!!)
42+
}
4843

49-
override fun onError(exception: AblyException?) {
50-
continuation.cancel(exception)
51-
}
52-
})
53-
}
44+
override fun onError(exception: AblyException?) {
45+
continuation.cancel(exception)
46+
}
47+
})
48+
}
5449

55-
suspend fun RealtimeObjects.createMapCoroutine(): LiveMap = suspendCancellableCoroutine { continuation ->
50+
private suspend fun RealtimeObjects.createMapCoroutine(): LiveMap = suspendCancellableCoroutine { continuation ->
5651
createMapAsync(object : ObjectsCallback<LiveMap> {
5752
override fun onSuccess(result: LiveMap?) {
5853
continuation.resume(result!!)
@@ -64,40 +59,46 @@ suspend fun RealtimeObjects.createMapCoroutine(): LiveMap = suspendCancellableCo
6459
})
6560
}
6661

67-
suspend fun LiveCounter.incrementCoroutine(amount: Int): Unit = suspendCancellableCoroutine { continuation ->
68-
incrementAsync(amount, object : ObjectsCallback<Void> {
69-
override fun onSuccess(result: Void?) {
70-
continuation.resume(Unit)
71-
}
62+
suspend fun LiveCounter.incrementCoroutine(amount: Int): Unit = supressCoroutineExceptions {
63+
suspendCancellableCoroutine { continuation ->
64+
incrementAsync(amount, object : ObjectsCallback<Void> {
65+
override fun onSuccess(result: Void?) {
66+
continuation.resume(Unit)
67+
}
7268

73-
override fun onError(exception: AblyException?) {
74-
continuation.cancel(exception)
75-
}
76-
})
69+
override fun onError(exception: AblyException?) {
70+
continuation.cancel(exception)
71+
}
72+
})
73+
}
7774
}
7875

79-
suspend fun LiveCounter.decrementCoroutine(amount: Int): Unit = suspendCancellableCoroutine { continuation ->
80-
decrementAsync(amount, object : ObjectsCallback<Void> {
81-
override fun onSuccess(result: Void?) {
82-
continuation.resume(Unit)
83-
}
76+
suspend fun LiveCounter.decrementCoroutine(amount: Int): Unit = supressCoroutineExceptions {
77+
suspendCancellableCoroutine { continuation ->
78+
decrementAsync(amount, object : ObjectsCallback<Void> {
79+
override fun onSuccess(result: Void?) {
80+
continuation.resume(Unit)
81+
}
8482

85-
override fun onError(exception: AblyException?) {
86-
continuation.cancel(exception)
87-
}
88-
})
83+
override fun onError(exception: AblyException?) {
84+
continuation.cancel(exception)
85+
}
86+
})
87+
}
8988
}
9089

91-
suspend fun Channel.updateOptions(options: ChannelOptions): Unit = suspendCancellableCoroutine { continuation ->
92-
setOptions(options, object : io.ably.lib.realtime.CompletionListener {
93-
override fun onSuccess() {
94-
continuation.resume(Unit)
95-
}
90+
suspend fun Channel.updateOptions(options: ChannelOptions): Unit = supressCoroutineExceptions {
91+
suspendCancellableCoroutine { continuation ->
92+
setOptions(options, object : io.ably.lib.realtime.CompletionListener {
93+
override fun onSuccess() {
94+
continuation.resume(Unit)
95+
}
9696

97-
override fun onError(reason: ErrorInfo?) {
98-
continuation.cancel(AblyException.fromErrorInfo(reason))
99-
}
100-
})
97+
override fun onError(reason: ErrorInfo?) {
98+
continuation.cancel(AblyException.fromErrorInfo(reason))
99+
}
100+
})
101+
}
101102
}
102103

103104
suspend fun getOrCreateCounter(channel: Channel, root: LiveMap?, path: String): LiveCounter {
@@ -122,28 +123,32 @@ suspend fun getOrCreateMap(channel: Channel, root: LiveMap?, path: String): Live
122123
}
123124
}
124125

125-
suspend fun LiveMap.setCoroutine(key: String, value: LiveMapValue) = suspendCancellableCoroutine<Unit> { continuation ->
126-
setAsync(key, value, object : ObjectsCallback<Void> {
127-
override fun onSuccess(result: Void?) {
128-
continuation.resume(Unit)
129-
}
126+
suspend fun LiveMap.setCoroutine(key: String, value: LiveMapValue) = supressCoroutineExceptions {
127+
suspendCancellableCoroutine<Unit> { continuation ->
128+
setAsync(key, value, object : ObjectsCallback<Void> {
129+
override fun onSuccess(result: Void?) {
130+
continuation.resume(Unit)
131+
}
130132

131-
override fun onError(exception: AblyException?) {
132-
continuation.cancel(exception)
133-
}
134-
})
133+
override fun onError(exception: AblyException?) {
134+
continuation.cancel(exception)
135+
}
136+
})
137+
}
135138
}
136139

137-
suspend fun LiveMap.removeCoroutine(key: String) = suspendCancellableCoroutine<Unit> { continuation ->
138-
removeAsync(key, object : ObjectsCallback<Void> {
139-
override fun onSuccess(result: Void?) {
140-
continuation.resume(Unit)
141-
}
140+
suspend fun LiveMap.removeCoroutine(key: String) = supressCoroutineExceptions {
141+
suspendCancellableCoroutine<Unit> { continuation ->
142+
removeAsync(key, object : ObjectsCallback<Void> {
143+
override fun onSuccess(result: Void?) {
144+
continuation.resume(Unit)
145+
}
142146

143-
override fun onError(exception: AblyException?) {
144-
continuation.cancel(exception)
145-
}
146-
})
147+
override fun onError(exception: AblyException?) {
148+
continuation.cancel(exception)
149+
}
150+
})
151+
}
147152
}
148153

149154
@Composable
@@ -152,7 +157,9 @@ fun observeCounter(channel: Channel, root: LiveMap?, path: String): CounterState
152157
var counterValue by remember { mutableStateOf<Int?>(null) }
153158

154159
LaunchedEffect(root) {
155-
counter = getOrCreateCounter(channel, root, path)
160+
supressCoroutineExceptions {
161+
counter = getOrCreateCounter(channel, root, path)
162+
}
156163
}
157164

158165
DisposableEffect(counter) {
@@ -225,7 +232,9 @@ fun observeMap(channel: Channel, root: LiveMap?, path: String): Pair<Map<String,
225232
var mapValue by remember { mutableStateOf<Map<String, String>>(mapOf()) }
226233

227234
LaunchedEffect(root) {
228-
map = getOrCreateMap(channel, root, path)
235+
supressCoroutineExceptions {
236+
map = getOrCreateMap(channel, root, path)
237+
}
229238
}
230239

231240
DisposableEffect(map) {
@@ -256,7 +265,9 @@ fun observeRootObject(channel: Channel): LiveMap? {
256265

257266
LaunchedEffect(channelState) {
258267
if (channelState == ChannelState.attached) {
259-
root = channel.objects.getRootCoroutines()
268+
supressCoroutineExceptions {
269+
root = channel.objects.getRootCoroutines()
270+
}
260271
}
261272
}
262273

@@ -282,3 +293,10 @@ fun getRealtimeChannel(realtimeClient: AblyRealtime, channelName: String): Chann
282293

283294
return channel
284295
}
296+
297+
suspend fun supressCoroutineExceptions(block: suspend () -> Unit) {
298+
try {
299+
block()
300+
} catch (_: Exception) {
301+
}
302+
}

examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ fun ColorVotingScreen(realtimeClient: AblyRealtime) {
5050
color = Color.Red,
5151
colorName = "Red",
5252
count = redCount ?: 0,
53+
enabled = greenCounter != null,
5354
onVote = {
5455
scope.launch {
5556
redCounter?.incrementCoroutine(1)
@@ -61,6 +62,7 @@ fun ColorVotingScreen(realtimeClient: AblyRealtime) {
6162
color = Color.Green,
6263
colorName = "Green",
6364
count = greenCount ?: 0,
65+
enabled = greenCounter != null,
6466
onVote = {
6567
scope.launch {
6668
greenCounter?.incrementCoroutine(1)
@@ -72,6 +74,7 @@ fun ColorVotingScreen(realtimeClient: AblyRealtime) {
7274
color = Color.Blue,
7375
colorName = "Blue",
7476
count = blueCount ?: 0,
77+
enabled = blueCounter != null,
7578
onVote = {
7679
scope.launch {
7780
blueCounter?.incrementCoroutine(1)
@@ -80,6 +83,7 @@ fun ColorVotingScreen(realtimeClient: AblyRealtime) {
8083
)
8184

8285
Button(
86+
enabled = redCounter != null && greenCounter != null && blueCounter != null,
8387
onClick = {
8488
scope.launch {
8589
resetRed()
@@ -102,6 +106,7 @@ fun ColorVoteCard(
102106
color: Color,
103107
colorName: String,
104108
count: Int,
109+
enabled: Boolean,
105110
onVote: () -> Unit
106111
) {
107112
Card(
@@ -145,6 +150,7 @@ fun ColorVoteCard(
145150
)
146151
OutlinedButton(
147152
onClick = onVote,
153+
enabled = enabled,
148154
) {
149155
Text(
150156
text = "Vote",

examples/src/main/kotlin/com/ably/example/screen/MainScreen.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import io.ably.lib.realtime.AblyRealtime
1414
@Composable
1515
fun MainScreen(realtimeClient: AblyRealtime) {
1616
var selectedTab by remember { mutableIntStateOf(0) }
17+
val isSandbox = realtimeClient.options.environment == "sandbox"
1718

1819
val tabs = listOf(
1920
TabItem("Color Voting", Icons.Default.Favorite),
@@ -23,7 +24,7 @@ fun MainScreen(realtimeClient: AblyRealtime) {
2324
Column(modifier = Modifier.fillMaxSize()) {
2425
TopAppBar(
2526
title = {
26-
Text("Ably Live Objects Demo")
27+
Text("Ably Live Objects Demo ${if (isSandbox) "(sandbox)" else ""}")
2728
},
2829
colors = TopAppBarDefaults.topAppBarColors(
2930
containerColor = MaterialTheme.colorScheme.primaryContainer,

0 commit comments

Comments
 (0)