Skip to content

Commit

Permalink
pkjs timeline token support
Browse files Browse the repository at this point in the history
  • Loading branch information
crc-32 committed Feb 8, 2025
1 parent 8611dee commit 71504c6
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import com.benasher44.uuid.Uuid
import io.rebble.cobble.shared.api.RWS
import io.rebble.cobble.shared.database.dao.LockerDao
import io.rebble.cobble.shared.domain.common.PebbleDevice
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.security.MessageDigest
import java.util.Locale
import kotlin.time.Duration.Companion.seconds

object JsTokenUtil: KoinComponent {
private val lockerDao: LockerDao by inject()
Expand Down Expand Up @@ -36,6 +41,22 @@ object JsTokenUtil: KoinComponent {
}

suspend fun getAccountToken(uuid: Uuid): String? {
return RWS.authClient?.getCurrentAccount()?.uid?.toString()?.let { generateToken(uuid, it) }
return try {
withTimeout(5.seconds) {
RWS.authClientFlow.filterNotNull().first().getCurrentAccount().uid.toString().let { generateToken(uuid, it) }
}
} catch (e: TimeoutCancellationException) {
null
}
}

suspend fun getSandboxTimelineToken(uuid: Uuid): String? {
return try {
withTimeout(5.seconds) {
RWS.timelineClientFlow.filterNotNull().first().getSandboxUserToken(uuid.toString())
}
} catch (e: TimeoutCancellationException) {
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json


class WebViewJsRunner(val context: Context, device: PebbleDevice, private val scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner(appInfo, jsPath, device) {
class WebViewJsRunner(val context: Context, device: PebbleDevice, val scope: CoroutineScope, appInfo: PbwAppInfo, jsPath: String): JsRunner(appInfo, jsPath, device) {

companion object {
const val API_NAMESPACE = "Pebble"
Expand Down Expand Up @@ -238,6 +238,20 @@ class WebViewJsRunner(val context: Context, device: PebbleDevice, private val sc
} ?: error("WebView not initialized")
}

suspend fun signalTimelineToken(callId: String, token: String) {
val tokenJson = Json.encodeToString(mapOf("userToken" to token, "callId" to callId))
withContext(Dispatchers.Main) {
webView?.loadUrl("javascript:signalTimelineTokenSuccess('${Uri.encode(tokenJson)}')")
}
}

suspend fun signalTimelineTokenFail(callId: String) {
val tokenJson = Json.encodeToString(mapOf("userToken" to null, "callId" to callId))
withContext(Dispatchers.Main) {
webView?.loadUrl("javascript:signalTimelineTokenFailure('${Uri.encode(tokenJson)}')")
}
}

suspend fun signalReady() {
val readyDeviceIds = listOf(device.address)
val readyJson = Json.encodeToString(readyDeviceIds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@ package io.rebble.cobble.shared.js

import android.net.Uri
import android.webkit.JavascriptInterface
import com.benasher44.uuid.Uuid
import io.rebble.cobble.shared.Logging
import io.rebble.cobble.shared.data.js.ActivePebbleWatchInfo
import io.rebble.cobble.shared.data.js.fromDevice
import io.rebble.cobble.shared.database.dao.LockerDao
import io.rebble.cobble.shared.domain.state.ConnectionStateManager
import io.rebble.cobble.shared.domain.state.watchOrNull
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

class WebViewPrivatePKJSInterface(private val jsRunner: WebViewJsRunner, private val scope: CoroutineScope, private val outgoingAppMessages: MutableSharedFlow<String>): PrivatePKJSInterface {
class WebViewPrivatePKJSInterface(private val jsRunner: WebViewJsRunner, private val scope: CoroutineScope, private val outgoingAppMessages: MutableSharedFlow<String>): PrivatePKJSInterface, KoinComponent {
private val lockerDao: LockerDao by inject()

@JavascriptInterface
override fun privateLog(message: String) {
Expand Down Expand Up @@ -41,6 +46,35 @@ class WebViewPrivatePKJSInterface(private val jsRunner: WebViewJsRunner, private
Logging.v("logLocationRequest")
}

@JavascriptInterface
override fun getTimelineTokenAsync(): String {
val uuid = Uuid.fromString(jsRunner.appInfo.uuid)
jsRunner.scope.launch {
var token: String? = null
val entry = lockerDao.getEntryByUuid(uuid.toString())
if (entry != null) {
token = entry.entry.userToken
if (entry.entry.local /*&& token == null*/) {
Logging.d("App is local, getting sandbox timeline token")
token = JsTokenUtil.getSandboxTimelineToken(uuid)
if (token == null) {
Logging.w("Failed to get sandbox timeline token")
} else {
lockerDao.update(entry.entry.copy(userToken = token))
}
}
} else {
Logging.e("App not found in locker")
}
if (token == null) {
jsRunner.signalTimelineTokenFail(uuid.toString())
} else {
jsRunner.signalTimelineToken(uuid.toString(), token)
}
}
return uuid.toString()
}

@JavascriptInterface
fun startupScriptHasLoaded(url: String) {
Logging.v("Startup script has loaded: $url")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@ object RWS: KoinComponent {
private val token: StateFlow<CurrentToken> by inject(named("currentToken"))
private val scope = CoroutineScope(Dispatchers.Default)

private val _appstoreClient = token.map {
val appstoreClientFlow = token.map {
it.tokenOrNull?.let { t -> AppstoreClient("https://appstore-api.$domainSuffix/api", t) }
}.stateIn(scope, SharingStarted.Eagerly, null)
private val _authClient = token.map {
val authClientFlow = token.map {
it.tokenOrNull?.let { t -> AuthClient("https://auth.$domainSuffix/api", t) }
}.stateIn(scope, SharingStarted.Eagerly, null)
val timelineClientFlow = token.map {
it.tokenOrNull?.let { t -> TimelineClient("https://timeline-sync.$domainSuffix", t) }
}.stateIn(scope, SharingStarted.Eagerly, null)
val appstoreClient: AppstoreClient?
get() = _appstoreClient.value
get() = appstoreClientFlow.value
val authClient: AuthClient?
get() = _authClient.value
get() = authClientFlow.value
val timelineClient: TimelineClient?
get() = timelineClientFlow.value
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.rebble.cobble.shared.api

import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.http.HttpHeaders
import io.rebble.cobble.shared.domain.api.timeline.TimelineTokenResponse
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

class TimelineClient(
val syncBaseUrl: String,
private val token: String
): KoinComponent {
private val client: HttpClient by inject()
private val version = "v1"
suspend fun getSandboxUserToken(uuid: String): String {
val res = client.get("$syncBaseUrl/$version/tokens/sandbox/$uuid") {
headers {
append(HttpHeaders.Accept, "application/json")
append(HttpHeaders.Authorization, "Bearer $token")
}
}
if (res.status.value != 200) {
error("Failed to get sandbox user token: ${res.status}")
}
val body: TimelineTokenResponse = res.body()
return body.token
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import org.koin.mp.KoinPlatformTools
SyncedLockerEntry::class,
SyncedLockerEntryPlatform::class
],
version = 12,
version = 13,
autoMigrations = [
AutoMigration(1, 2),
AutoMigration(2, 3),
Expand All @@ -31,7 +31,8 @@ import org.koin.mp.KoinPlatformTools
AutoMigration(8, 9),
AutoMigration(9, 10),
AutoMigration(10, 11),
AutoMigration(11, 12)
AutoMigration(11, 12),
AutoMigration(12, 13)
]
)
@TypeConverters(Converters::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ data class SyncedLockerEntry(
val order: Int,
val lastOpened: Instant?,
@ColumnInfo(defaultValue = "0")
val local: Boolean = false
val local: Boolean = false,
@ColumnInfo(defaultValue = "null")
val userToken: String? = null,
)

data class SyncedLockerEntryWithPlatforms(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ fun LockerEntry.toEntity(): SyncedLockerEntry {
pbwIconResourceId = pbw.iconResourceId,
nextSyncAction = if (type == "watchface") NextSyncAction.Ignore else NextSyncAction.Upload,
order = -1,
lastOpened = null
lastOpened = null,
userToken = userToken
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.rebble.cobble.shared.domain.api.timeline

import kotlinx.serialization.Serializable

@Serializable
data class TimelineTokenResponse(
val token: String,
val uuid: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ class PbwApp(uri: String): KoinComponent {
NextSyncAction.Upload,
order = -1,
lastOpened = null,
local = true
local = true,
userToken = null
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ interface PrivatePKJSInterface {
fun logInterceptedRequest()
fun logLocationRequest()
fun getVersionCode(): Int
fun getTimelineTokenAsync(): String
}

0 comments on commit 71504c6

Please sign in to comment.