Implement Android Home activity and HTTPS synchronization (#97)
Android ARM verification / rust (push) Failing after 46m50s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Successful in 23s

This commit is contained in:
hugo committed 2026-10-09 14:45:57 +02:00
1 parent 8c9341f214
commit 5c933a99a3
29 files changed
+2008 -110

No files matched your search

@@ -0,0 +1,66 @@
package de.rfc1437.ironstorage.core
import kotlinx.coroutines.flow.StateFlow
enum class HomeActionKind { Commit, Fetch, Pull, Push }
enum class HomeFreshness { NeverRefreshed, Cached, Current }
enum class HomeFailureKind {
MissingConfiguration,
Configuration,
Authentication,
Conflict,
DirtyLocalChanges,
NoChanges,
Offline,
Interrupted,
SecureStorage,
Repository,
PartialProgress
}
data class HomeAction(val kind: HomeActionKind, val title: String, val icon: ShellIcon)
data class HomeRow(
val id: String,
val title: String,
val detail: String,
val icon: ShellIcon,
val actions: List<HomeAction>
)
data class HomeChange(val id: String, val title: String, val detail: String, val icon: ShellIcon)
data class HomeCommit(
val id: String,
val title: String,
val detail: String,
val timestamp: Long,
val changes: List<HomeChange>,
val actions: List<HomeAction>
)
data class HomeNotice(val title: String, val detail: String)
data class HomePage(
val freshness: HomeFreshness,
val refreshedAt: Long?,
val summaries: List<HomeRow>,
val incoming: List<HomeCommit>,
val outgoing: List<HomeCommit>,
val incomingTotal: UInt,
val outgoingTotal: UInt,
val notice: HomeNotice?,
val activityKnown: Boolean = true
)
data class HomeFailure(val kind: HomeFailureKind, val title: String, val detail: String)
data class HomeState(
val page: HomePage? = null,
val name: String = "",
val email: String = "",
val busy: Boolean = false,
val progress: HomeNotice? = null,
val failure: HomeFailure? = null
)
interface HomeRepository {
val state: StateFlow<HomeState>
fun enter()
fun refresh()
fun perform(action: HomeActionKind, message: String = "")
fun reload()
fun cancel()
fun leave()
}
@@ -0,0 +1,203 @@
package de.rfc1437.ironstorage.core
import android.os.SystemClock
import de.rfc1437.ironstorage.bridge.NativeBridgeException
import de.rfc1437.ironstorage.bridge.generated.MobileClock
import de.rfc1437.ironstorage.bridge.generated.MobileHomeActionKind
import de.rfc1437.ironstorage.bridge.generated.MobileHomeErrorKind
import de.rfc1437.ironstorage.bridge.generated.MobileHomeFfiException
import de.rfc1437.ironstorage.bridge.generated.MobileHomeFreshness
import de.rfc1437.ironstorage.bridge.generated.MobileHomeOperation
import de.rfc1437.ironstorage.bridge.generated.MobileHomePage
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateStorageFfiException
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
class NativeHomeRepository(
private val session: NativePrivateSession,
private val passwords: PasswordRepository,
private val scope: CoroutineScope,
private val io: CoroutineDispatcher
) : HomeRepository {
private val mutable = MutableStateFlow(HomeState())
override val state: StateFlow<HomeState> = mutable
private val sequence = AtomicLong()
private val mutex = Mutex()
@Volatile private var active: MobileHomeOperation? = null
private var cancelTicket: Long? = null
private val clock = object : MobileClock {
override fun monotonicMilliseconds(): ULong = SystemClock.elapsedRealtime().toULong()
}
override fun enter() = run(Request.Enter)
override fun reload() = run(Request.Enter)
override fun refresh() = run(Request.Refresh)
override fun perform(action: HomeActionKind, message: String) = run(
when (action) {
HomeActionKind.Commit -> Request.Commit
HomeActionKind.Fetch -> Request.Fetch
HomeActionKind.Pull -> Request.Pull
HomeActionKind.Push -> Request.Push
},
message
)
override fun cancel() {
cancelTicket = sequence.get()
val operation = active
scope.launch(io) { operation?.cancel() }
}
override fun leave() {
sequence.incrementAndGet()
val operation = active
scope.launch(io) { operation?.cancel() }
mutable.value = mutable.value.copy(busy = false, progress = null)
}
private enum class Request { Enter, Refresh, Commit, Fetch, Pull, Push }
private fun run(request: Request, message: String = "") {
if (mutable.value.busy) return
val ticket = sequence.incrementAndGet()
mutable.value = mutable.value.copy(busy = true, failure = null, progress = null)
scope.launch {
mutex.withLock {
if (sequence.get() != ticket) return@withLock
val polling = launch {
while (sequence.get() == ticket) {
delay(150)
val progress = withContext(io) { active?.progress() }
if (sequence.get() == ticket && progress != null) {
mutable.value = mutable.value.copy(
progress = HomeNotice(progress.title, progress.detail)
)
}
}
}
try {
val current = withContext(io) {
session.storage().androidHomeOperation(session.backend(), clock)
}
active = current
if (sequence.get() != ticket || cancelTicket == ticket) {
withContext(io) { current.cancel() }
}
val identity = withContext(io) { current.identity() }
val cached = withContext(io) {
current.cached().toFacade(current.activityKnown())
}
if (sequence.get() == ticket) {
mutable.value = mutable.value.copy(
page = cached,
name = identity.name,
email = identity.email
)
}
val page = withContext(io) {
when (request) {
Request.Enter -> current.refreshIfStale()
Request.Refresh -> current.refreshGesture()
Request.Commit -> current.commit(message)
Request.Fetch -> current.fetch()
Request.Pull -> current.pull()
Request.Push -> current.push()
}.toFacade(current.activityKnown())
}
if (sequence.get() == ticket) {
mutable.value = HomeState(page, identity.name, identity.email)
if (request == Request.Pull || request == Request.Refresh) {
passwords.refresh()
}
}
} catch (error: MobileHomeFfiException.Failed) {
if (sequence.get() == ticket) {
mutable.value = mutable.value.copy(
failure = HomeFailure(error.kind.toFacade(), error.title, error.detail)
)
}
} catch (_: MobilePrivateStorageFfiException) {
failure(
ticket,
"Unlock the device or recover private storage, then reload Home."
)
} catch (_: NativeBridgeException) {
failure(ticket, "Update the app and reload Home.")
} catch (_: java.io.IOException) {
failure(
ticket,
"Private storage is unavailable. Unlock the device and reload Home."
)
} finally {
active = null
polling.cancelAndJoin()
if (sequence.get() == ticket) {
mutable.value = mutable.value.copy(busy = false, progress = null)
}
}
}
}
}
private fun failure(ticket: Long, detail: String) {
if (sequence.get() == ticket) {
mutable.value = mutable.value.copy(
failure = HomeFailure(HomeFailureKind.Repository, "Home is unavailable", detail)
)
}
}
}
private fun MobileHomeActionKind.toFacade(): HomeActionKind = when (this) {
MobileHomeActionKind.COMMIT -> HomeActionKind.Commit
MobileHomeActionKind.FETCH -> HomeActionKind.Fetch
MobileHomeActionKind.PULL -> HomeActionKind.Pull
MobileHomeActionKind.PUSH -> HomeActionKind.Push
}
private fun MobileHomeErrorKind.toFacade(): HomeFailureKind = when (this) {
MobileHomeErrorKind.MISSING_CONFIGURATION -> HomeFailureKind.MissingConfiguration
MobileHomeErrorKind.CONFIGURATION -> HomeFailureKind.Configuration
MobileHomeErrorKind.AUTHENTICATION -> HomeFailureKind.Authentication
MobileHomeErrorKind.CONFLICT -> HomeFailureKind.Conflict
MobileHomeErrorKind.DIRTY_LOCAL_CHANGES -> HomeFailureKind.DirtyLocalChanges
MobileHomeErrorKind.NO_CHANGES -> HomeFailureKind.NoChanges
MobileHomeErrorKind.OFFLINE -> HomeFailureKind.Offline
MobileHomeErrorKind.INTERRUPTED -> HomeFailureKind.Interrupted
MobileHomeErrorKind.SECURE_STORAGE -> HomeFailureKind.SecureStorage
MobileHomeErrorKind.REPOSITORY -> HomeFailureKind.Repository
MobileHomeErrorKind.PARTIAL_PROGRESS -> HomeFailureKind.PartialProgress
}
private fun MobileHomePage.toFacade(activityKnown: Boolean): HomePage {
fun action(value: de.rfc1437.ironstorage.bridge.generated.MobileHomeAction): HomeAction =
HomeAction(value.kind.toFacade(), value.title, value.icon.toFacade())
fun commit(value: de.rfc1437.ironstorage.bridge.generated.MobileHomeCommit): HomeCommit =
HomeCommit(
value.id,
value.title,
value.detail,
value.timestamp,
value.changes.map { HomeChange(it.id, it.title, it.detail, it.icon.toFacade()) },
value.actions.map(::action)
)
return HomePage(
when (freshness) {
MobileHomeFreshness.NEVER_REFRESHED -> HomeFreshness.NeverRefreshed
MobileHomeFreshness.CACHED -> HomeFreshness.Cached
MobileHomeFreshness.CURRENT -> HomeFreshness.Current
},
refreshedAt,
summaries.map {
HomeRow(it.id, it.title, it.detail, it.icon.toFacade(), it.actions.map(::action))
},
incoming.map(::commit),
outgoing.map(::commit),
incomingTotal,
outgoingTotal,
notice?.let { HomeNotice(it.title, it.detail) },
activityKnown
)
}