Skip to content

Commit

Permalink
add KMP version of appstore api
Browse files Browse the repository at this point in the history
  • Loading branch information
crc-32 committed Aug 30, 2024
1 parent 18d095d commit 3c8bd2a
Show file tree
Hide file tree
Showing 17 changed files with 446 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.rebble.cobble.bridges.common

import io.rebble.cobble.bridges.FlutterBridge
import io.rebble.cobble.bridges.ui.BridgeLifecycleController
import io.rebble.cobble.pigeons.Pigeons
import io.rebble.cobble.shared.domain.state.CurrentToken
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject

class KMPApiBridge @Inject constructor(
private val tokenState: MutableStateFlow<CurrentToken>,
bridgeLifecycleController: BridgeLifecycleController
): FlutterBridge, Pigeons.KMPApi {

init {
bridgeLifecycleController.setupControl(Pigeons.KMPApi::setup, this)
}

override fun updateToken(token: Pigeons.StringWrapper) {
tokenState.value = token.value?.let { CurrentToken.LoggedIn(it) } ?: CurrentToken.LoggedOut
}
}
5 changes: 5 additions & 0 deletions android/app/src/main/kotlin/io/rebble/cobble/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import io.rebble.cobble.shared.database.dao.NotificationChannelDao
import io.rebble.cobble.shared.database.dao.PersistedNotificationDao
import io.rebble.cobble.shared.datastore.KMPPrefs
import io.rebble.cobble.shared.domain.calendar.CalendarSync
import io.rebble.cobble.shared.domain.state.CurrentToken
import io.rebble.cobble.shared.handlers.CalendarActionHandler
import io.rebble.libpebblecommon.services.blobdb.BlobDBService
import kotlinx.coroutines.CoroutineExceptionHandler
Expand Down Expand Up @@ -62,6 +63,10 @@ abstract class AppModule {
return KMPPrefs()
}
@Provides
fun provideTokenState(): MutableStateFlow<CurrentToken> {
return KoinPlatformTools.defaultContext().get().get(named("currentToken"))
}
@Provides
fun providePersistedNotificationDao(context: Context): PersistedNotificationDao {
return AppDatabase.instance().persistedNotificationDao()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ abstract class CommonBridgesModule {
abstract fun bindIncomingPacketsBridge(
packetsBridge: RawIncomingPacketsBridge
): FlutterBridge

@Binds
@IntoSet
@CommonBridge
abstract fun bindKmpApiBridge(
kmpApiBridge: KMPApiBridge
): FlutterBridge
}

@Qualifier
Expand Down
64 changes: 64 additions & 0 deletions android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java
Original file line number Diff line number Diff line change
Expand Up @@ -6240,4 +6240,68 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable KeepUnused
}
}
}

private static class KMPApiCodec extends StandardMessageCodec {
public static final KMPApiCodec INSTANCE = new KMPApiCodec();

private KMPApiCodec() {}

@Override
protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
switch (type) {
case (byte) 128:
return StringWrapper.fromList((ArrayList<Object>) readValue(buffer));
default:
return super.readValueOfType(type, buffer);
}
}

@Override
protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
if (value instanceof StringWrapper) {
stream.write(128);
writeValue(stream, ((StringWrapper) value).toList());
} else {
super.writeValue(stream, value);
}
}
}

/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
public interface KMPApi {

void updateToken(@NonNull StringWrapper token);

/** The codec used by KMPApi. */
static @NonNull MessageCodec<Object> getCodec() {
return KMPApiCodec.INSTANCE;
}
/**Sets up an instance of `KMPApi` to handle messages through the `binaryMessenger`. */
static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable KMPApi api) {
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.KMPApi.updateToken", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
StringWrapper tokenArg = (StringWrapper) args.get(0);
try {
api.updateToken(tokenArg);
wrapped.add(0, null);
}
catch (Throwable exception) {
ArrayList<Object> wrappedError = wrapError(exception);
wrapped = wrappedError;
}
reply.reply(wrapped);
});
} else {
channel.setMessageHandler(null);
}
}
}
}
}
9 changes: 8 additions & 1 deletion android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ room-sqlite = "2.5.0-alpha05"
datastore = "1.1.1"
uuidVersion = "0.8.4"

ktorVersion = "2.3.12"

[plugins]
multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Expand Down Expand Up @@ -52,4 +54,9 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationJson" }
libpebblecommon = { module = "io.rebble.libpebblecommon:libpebblecommon", version.ref = "libpebblecommonVersion" }
uuid = { module = "com.benasher44:uuid", version.ref = "uuidVersion" }
uuid = { module = "com.benasher44:uuid", version.ref = "uuidVersion" }

ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorVersion" }
ktor-client-contentnegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorVersion" }
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorVersion" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorVersion" }
4 changes: 4 additions & 0 deletions android/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,12 @@ kotlin {
implementation(libs.androidx.sqlite.bundled)
implementation(libs.androidx.datastore)
implementation(libs.androidx.datastore.preferences)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.serialization.json)
}
androidMain.dependencies {
implementation(libs.ktor.client.okhttp)
implementation("io.insert-koin:koin-android:$koinVersion")
implementation("androidx.core:core-ktx:$androidxVersion")
implementation("com.jakewharton.timber:timber:$timberVersion")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.rebble.cobble.shared.api

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.*
import io.ktor.http.HttpHeaders
import io.ktor.serialization.kotlinx.json.json
import io.rebble.cobble.shared.domain.api.appstore.LockerEntry

class AppstoreClient(
val baseUrl: String,
private val token: String
) {
private val version = "v1"
private val client = HttpClient {
install(ContentNegotiation) {
json()
}
}

suspend fun getLocker(): List<LockerEntry> {
val body: Map<String, List<LockerEntry>> = client.get("$baseUrl/$version/locker") {
headers {
append(HttpHeaders.Accept, "application/json")
append(HttpHeaders.Authorization, "Bearer $token")
}
}.body()
return body["applications"] ?: emptyList()
}

suspend fun addToLocker(uuid: String) {
client.put("$baseUrl/$version/locker/$uuid") {
headers {
append(HttpHeaders.Accept, "application/json")
append(HttpHeaders.Authorization, "Bearer $token")
}
}
}

suspend fun removeFromLocker(uuid: String) {
client.delete("$baseUrl/$version/locker/$uuid") {
headers {
append(HttpHeaders.Accept, "application/json")
append(HttpHeaders.Authorization, "Bearer $token")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.rebble.cobble.shared.api

import io.rebble.cobble.shared.domain.state.CurrentToken
import io.rebble.cobble.shared.domain.state.CurrentToken.LoggedOut.tokenOrNull
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.qualifier.named

object RWS: KoinComponent {
private val domainSuffix = "rebble.io"
private val token: StateFlow<CurrentToken> by inject(named("currentToken"))
private val scope = CoroutineScope(Dispatchers.Default)

private val _appstoreClient = token.map {
it.tokenOrNull?.let { t -> AppstoreClient("https://appstore-api.$domainSuffix", t) }
}.stateIn(scope, SharingStarted.Eagerly, null)
val appstoreClient: AppstoreClient?
get() = _appstoreClient.value
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.rebble.cobble.shared.di

import io.rebble.cobble.shared.domain.state.ConnectionState
import io.rebble.cobble.shared.domain.state.CurrentToken
import io.rebble.cobble.shared.domain.state.watchOrNull
import kotlinx.coroutines.flow.*
import org.koin.core.qualifier.named
Expand All @@ -16,4 +17,8 @@ val stateModule = module {
.flatMapLatest { it.watchOrNull?.metadata?.take(1) ?: flowOf(null) }
.filterNotNull()
}

single(named("currentToken")) {
MutableStateFlow<CurrentToken>(CurrentToken.LoggedOut)
} bind StateFlow::class
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package io.rebble.cobble.shared.domain.api.appstore

import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName

@Serializable
data class LockerEntry(
val id: String,
val uuid: String,
@SerialName("user_token") val userToken: String,
val title: String,
val type: String,
val category: String,
val version: String? = null,
val hearts: Int,
@SerialName("is_configurable") val isConfigurable: Boolean,
@SerialName("is_timeline_enabled") val isTimelineEnabled: Boolean,
val links: LockerEntryLinks,
val developer: LockerEntryDeveloper,
@SerialName("hardware_platforms") val hardwarePlatforms: List<LockerEntryPlatform>,
val compatibility: LockerEntryCompatibility,
val companions: Map<String, LockerEntryCompanionApp?>,
val pbw: LockerEntryPBW? = null
)

@Serializable
data class LockerEntryLinks(
val remove: String,
val href: String,
val share: String
)

@Serializable
data class LockerEntryDeveloper(
val id: String,
val name: String,
@SerialName("contact_email") val contactEmail: String
)

@Serializable
data class LockerEntryPlatform(
@SerialName("sdk_version") val sdkVersion: String,
@SerialName("pebble_process_info_flags") val pebbleProcessInfoFlags: Int,
val name: String,
val description: String,
val images: LockerEntryPlatformImages
)

@Serializable
data class LockerEntryPlatformImages(
val icon: String,
val list: String,
val screenshot: String
)

@Serializable
data class LockerEntryCompatibility(
val ios: LockerEntryCompatibilityPhonePlatformDetails,
val android: LockerEntryCompatibilityPhonePlatformDetails,
val aplite: LockerEntryCompatibilityWatchPlatformDetails,
val basalt: LockerEntryCompatibilityWatchPlatformDetails,
val chalk: LockerEntryCompatibilityWatchPlatformDetails,
val diorite: LockerEntryCompatibilityWatchPlatformDetails,
val emery: LockerEntryCompatibilityWatchPlatformDetails
)

@Serializable
data class LockerEntryCompatibilityPhonePlatformDetails(
val supported: Boolean,
@SerialName("min_js_version") val minJsVersion: Int? = null
)

@Serializable
data class LockerEntryCompatibilityWatchPlatformDetails(
val supported: Boolean,
val firmware: LockerEntryFirmwareVersion
)

@Serializable
data class LockerEntryFirmwareVersion(
val major: Int,
val minor: Int? = null,
val patch: Int? = null
)

@Serializable
data class LockerEntryCompanionApp(
val id: Int,
val icon: String,
val name: String,
val url: String,
val required: Boolean,
@SerialName("pebblekit_version") val pebblekitVersion: String
)

@Serializable
data class LockerEntryPBW(
val file: String,
@SerialName("icon_resource_id") val iconResourceId: Int,
@SerialName("release_id") val releaseId: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.rebble.cobble.shared.domain.state

open class CurrentToken {
object LoggedOut : CurrentToken()
data class LoggedIn(val token: String) : CurrentToken()

val CurrentToken.tokenOrNull: String?
get() = when (this) {
is LoggedIn -> token
else -> null
}
}
9 changes: 9 additions & 0 deletions ios/Runner/Pigeon/Pigeons.h
Original file line number Diff line number Diff line change
Expand Up @@ -703,4 +703,13 @@ NSObject<FlutterMessageCodec> *KeepUnusedHackGetCodec(void);

extern void KeepUnusedHackSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<KeepUnusedHack> *_Nullable api);

/// The codec used by KMPApi.
NSObject<FlutterMessageCodec> *KMPApiGetCodec(void);

@protocol KMPApi
- (void)updateTokenToken:(StringWrapper *)token error:(FlutterError *_Nullable *_Nonnull)error;
@end

extern void KMPApiSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<KMPApi> *_Nullable api);

NS_ASSUME_NONNULL_END
Loading

0 comments on commit 3c8bd2a

Please sign in to comment.