Skip to content

Commit

Permalink
external app installs via kmp
Browse files Browse the repository at this point in the history
  • Loading branch information
crc-32 committed Oct 15, 2024
1 parent 3df39fa commit 6d0759c
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 24 deletions.
17 changes: 2 additions & 15 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,8 @@
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="content" />
<data android:mimeType="application/octet-stream" />
<data android:mimeType="application/zip" />
<data android:mimeType="binary/octet-stream" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.pbw" />
</intent-filter>
</activity>

Expand Down Expand Up @@ -162,18 +161,6 @@
<data android:scheme="rebble" />
</intent-filter>

<!-- PBW Install -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="content" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.pbw" />
</intent-filter>

<!-- PBZ Install -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
Expand Down
48 changes: 46 additions & 2 deletions android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
package io.rebble.cobble

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import io.rebble.cobble.shared.AndroidPlatformContext
import io.rebble.cobble.shared.Logging
import io.rebble.cobble.shared.PlatformContext
import io.rebble.cobble.shared.ui.nav.Routes
import io.rebble.cobble.shared.ui.view.MainView
import io.rebble.cobble.shared.util.File
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.plus
import kotlinx.datetime.Clock
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

class MainActivity : AppCompatActivity() {
lateinit var coroutineScope: CoroutineScope
var navHostController: NavHostController? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand All @@ -32,13 +44,45 @@ class MainActivity : AppCompatActivity() {
setContent {
navHostController = rememberNavController()
MainView(navHostController!!)
handleIntent(intent)
}
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (intent.hasExtra("navigationPath")) {
navHostController?.navigate(intent.getStringExtra("navigationPath")!!)
handleIntent(intent)
}

private fun handleIntent(intent: Intent) {
val uri = intent.data
if (intent.scheme == "content" && uri != null) {
if (uri.path?.endsWith("pbw") == true) {
Logging.d("Received pbw install intent")
val fileSize = contentResolver.openAssetFileDescriptor(uri, "r").use {
it?.length
}
if (fileSize == null || fileSize > 10_000_000) {
Logging.e("Invalid PBW file size, ignoring")
return
}
val cachedUri = cacheIncomingPbw(uri)
navHostController?.navigate("${Routes.DIALOG_APP_INSTALL}?uri=$cachedUri")
}
} else {
if (intent.hasExtra("navigationPath")) {
navHostController?.navigate(intent.getStringExtra("navigationPath")!!)
}
}
}

private fun cacheIncomingPbw(uri: Uri): Uri {
val cached = applicationContext.cacheDir.resolve("local-${Clock.System.now().toEpochMilliseconds()}.pbw")
cached.deleteOnExit()
applicationContext.contentResolver.openInputStream(uri).use { input ->
cached.outputStream().use { output ->
input!!.copyTo(output)
}
}
return cached.toUri()
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package io.rebble.cobble.shared

import android.content.Context
import io.rebble.libpebblecommon.packets.PhoneAppVersion

class AndroidPlatformContext(
val applicationContext: android.content.Context
val applicationContext: Context
) : PlatformContext {
override val osType: PhoneAppVersion.OSType = PhoneAppVersion.OSType.Android
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package io.rebble.cobble.shared.util

import android.net.Uri
import androidx.core.net.toFile
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.jvm.javaio.toByteReadChannel

actual class File actual constructor(uri: String) {
val file = Uri.parse(uri).toFile()
actual fun exists(): Boolean = file.exists()
actual fun readChannel(): ByteReadChannel = file.inputStream().toByteReadChannel()
}

fun java.io.File.toKMPFile(): File {
Expand Down
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 = 11,
version = 12,
autoMigrations = [
AutoMigration(1, 2),
AutoMigration(2, 3),
Expand All @@ -30,7 +30,8 @@ import org.koin.mp.KoinPlatformTools
AutoMigration(7, 8),
AutoMigration(8, 9),
AutoMigration(9, 10),
AutoMigration(10, 11)
AutoMigration(10, 11),
AutoMigration(11, 12)
]
)
@TypeConverters(Converters::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ data class SyncedLockerEntry(
@ColumnInfo(defaultValue = "-1")
val order: Int,
val lastOpened: Instant?,
@ColumnInfo(defaultValue = "0")
val local: Boolean = false
)

data class SyncedLockerEntryWithPlatforms(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package io.rebble.cobble.shared.domain.pbw

import io.rebble.cobble.shared.Logging
import io.rebble.cobble.shared.PlatformContext
import io.rebble.cobble.shared.database.AppDatabase
import io.rebble.cobble.shared.database.NextSyncAction
import io.rebble.cobble.shared.database.dao.LockerDao
import io.rebble.cobble.shared.database.entity.SyncedLockerEntry
import io.rebble.cobble.shared.database.entity.SyncedLockerEntryPlatform
import io.rebble.cobble.shared.database.entity.SyncedLockerEntryPlatformImages
import io.rebble.cobble.shared.database.getDatabase
import io.rebble.cobble.shared.handlers.savePbwFile
import io.rebble.cobble.shared.jobs.LockerSyncJob
import io.rebble.cobble.shared.util.*
import io.rebble.libpebblecommon.disk.PbwBinHeader
import io.rebble.libpebblecommon.metadata.WatchType
import io.rebble.libpebblecommon.util.DataBuffer
import okio.buffer
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

class PbwApp(uri: String): KoinComponent {
private val platformContext: PlatformContext by inject()
private val file = File(uri)
private val lockerDao: LockerDao by inject()
init {
require(file.exists())
}

val info by lazy { requirePbwAppInfo(file) }

fun getManifest(watchType: WatchType) = requirePbwManifest(file, watchType)

suspend fun installToLockerCache(): Boolean {
val exists = lockerDao.getEntryByUuid(info.uuid)
lockerDao.insertOrReplace(
SyncedLockerEntry(
id = exists?.entry?.id ?: "",
uuid = info.uuid,
version = info.versionLabel,
title = info.shortName,
type = if (info.watchapp.watchface) "watchface" else "watchapp",
hearts = 0,
developerName = info.companyName,
developerId = null,
configurable = info.capabilities.any { it == "configurable" },
timelineEnabled = false, //TODO
removeLink = "",
shareLink = "",
pbwLink = "",
pbwReleaseId = info.versionCode.toString(),
pbwIconResourceId = 0, //TODO,
NextSyncAction.Upload,
order = -1,
lastOpened = null,
local = true
)
)

val inserted = lockerDao.getEntryByUuid(info.uuid) ?: return false

if (exists != null) {
lockerDao.clearPlatformsFor(exists.entry.id)
}

val platforms = WatchType.entries.mapNotNull {
val manifest = getPbwManifest(file, it)
if (manifest != null) {
val header = PbwBinHeader().apply {
fromBytes(DataBuffer(requirePbwBinaryBlob(file, it, "pebble-app.bin")
.buffer()
.readByteArray((PbwBinHeader.SIZE+1).toLong()).asUByteArray()
))
}

SyncedLockerEntryPlatform(
platformEntryId = 0,
lockerEntryId = inserted.entry.id,
sdkVersion = "${header.sdkVersionMajor.get()}.${header.sdkVersionMinor.get()}",
processInfoFlags = header.flags.get().toInt(),
name = it.codename,
description = "",
images = SyncedLockerEntryPlatformImages(
null,
null,
null
)
)
} else {
null
}
}
if (platforms.isEmpty()) {
Logging.e("No platforms in PBW")
return false
}

lockerDao.insertOrReplaceAllPlatforms(platforms)

savePbwFile(platformContext, info.uuid, file.readChannel())
if (!LockerSyncJob().syncToDevice()) {
Logging.e("Failed to sync locker to device")
return false
}
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ class LockerSyncJob: KoinComponent {
}

val changedEntries = locker.filter { new ->
val newPlat = new.hardwarePlatforms.map { it.toEntity(new.id) }
val newPlat = new.hardwarePlatforms.map { it.toEntity(new.id) }
storedLocker.any { old ->
old.entry.nextSyncAction != NextSyncAction.Ignore && old.entry.id == new.id && (old.entry != new.toEntity() || old.platforms.any { oldPlat -> newPlat.none { newPlat -> oldPlat.dataEqualTo(newPlat) } })
old.entry.nextSyncAction != NextSyncAction.Ignore && old.entry.id == new.id && old.entry.version != new.version
}
}
val newEntries = locker.filter { new -> storedLocker.none { old -> old.entry.id == new.id } }
val removedEntries = storedLocker.filter { old -> locker.none { nw -> nw.id == old.entry.id } }
val removedEntries = storedLocker.filter { old -> locker.none { nw -> nw.id == old.entry.id } && !old.entry.local }

lockerDao.insertOrReplaceAll(newEntries.map { it.toEntity() })
changedEntries.forEach {
Expand Down Expand Up @@ -83,7 +83,7 @@ class LockerSyncJob: KoinComponent {
return syncToDevice()
}

private suspend fun syncToDevice(): Boolean {
suspend fun syncToDevice(): Boolean {
val entries = lockerDao.getEntriesForSync().sortedBy { it.entry.title }
val connectedWatch = ConnectionStateManager.connectionState.value.watchOrNull
connectedWatch?.let {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.rebble.cobble.shared.ui.nav

object Routes {
const val DIALOG_APP_INSTALL = "dialog_app_install"
object Home {
const val LOCKER_APPS = "locker_apps"
const val LOCKER_WATCHFACES = "locker_watchfaces"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@ import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.dialog
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import io.rebble.cobble.shared.ui.LocalTheme
import io.rebble.cobble.shared.ui.Theme
import io.rebble.cobble.shared.ui.nav.Routes
import io.rebble.cobble.shared.ui.view.dialogs.AppInstallDialog
import io.rebble.cobble.shared.ui.view.home.HomePage
import io.rebble.cobble.shared.ui.view.home.HomeScaffold
import io.rebble.cobble.shared.ui.view.home.locker.LockerTabs
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.Json.Default.decodeFromString
import org.koin.compose.KoinContext

@Composable
Expand All @@ -39,6 +46,15 @@ fun MainView(navController: NavHostController = rememberNavController()) {
composable(Routes.Home.TEST_PAGE) {
HomeScaffold(HomePage.TestPage, onNavChange = navController::navigate)
}
dialog("${Routes.DIALOG_APP_INSTALL}?uri={uri}", arguments = listOf(navArgument("uri") {
nullable = false
type = NavType.StringType
})) {
val uri = it.arguments?.getString("uri") ?: return@dialog
AppInstallDialog(uri) {
navController.popBackStack()
}
}
}
}
}
Expand Down
Loading

0 comments on commit 6d0759c

Please sign in to comment.