Implement Android Home activity and HTTPS synchronization (#97)
This commit is contained in:
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
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user