- shared/ = portable Android-origin sources vendored from deferred/desktop-server (app/build.gradle.kts srcDir repointed; PlaybackController.kt excluded as Android-only) - backend/ = bundled-lite engine (SQLite + inline queue); .venv symlinked from the old checkout, PYTHONPATH pins THIS backend's code over any editable install - repoRoot() resolves this project dir (env SHONAR_REPO still wins); desktop-dev.sh watches shared/ + backend/ - Verified: :app:compileKotlin + :app:test green (23 tests); engine boots on :8010, self-migrates, /healthz ok
77 lines
3 KiB
Kotlin
77 lines
3 KiB
Kotlin
package com.shonar.provider
|
|
|
|
import java.io.File
|
|
|
|
/**
|
|
* The single contract between SHONAR and any server (or the device itself).
|
|
*
|
|
* Everything the app needs from "the cloud" goes through this interface;
|
|
* UI, sync engine, and database reference providers by descriptor id only.
|
|
* LocalOnlyProvider is a first-class implementation, so a missing server is
|
|
* never a special case.
|
|
*
|
|
* Contract rules (enforced by the shared provider contract test suite):
|
|
* - all suspends are safe to cancel; partial uploads leave no visible object
|
|
* - upload() is idempotent per RecordingDraft.id (re-upload replaces the
|
|
* same key, never duplicates)
|
|
* - originals are immutable after successful upload until delete()
|
|
* - no method ever logs credentials, tokens, audio bytes, or transcripts
|
|
* - errors are ProviderError subtypes with secret-free messages
|
|
*/
|
|
interface ShonarProvider {
|
|
|
|
val descriptor: ProviderDescriptor
|
|
|
|
/** Current auth lifecycle; providers push updates here. */
|
|
val authState: kotlinx.coroutines.flow.StateFlow<AuthState>
|
|
|
|
/**
|
|
* Is there a SHONAR-compatible service at [baseUrl]? Purely read-only;
|
|
* must not require or request credentials. TlsFailure carries the SPKI
|
|
* fingerprint so the UI can run explicit trust-on-first-use approval —
|
|
* never silently accept.
|
|
*/
|
|
suspend fun probe(baseUrl: ServerUrl): ProbeResult
|
|
|
|
/** Validate [credential] against the server, then hand it to the secure store. */
|
|
suspend fun connect(credential: ProviderCredential): Unit
|
|
|
|
/** Re-validate stored credential (app start / after network return). */
|
|
suspend fun reconnect(): AuthState
|
|
|
|
/**
|
|
* Disconnect locally; if [revokeOnServer] and the provider supports it,
|
|
* revoke the credential server-side first (best effort — local state is
|
|
* cleared even if revoke fails, with the failure surfaced).
|
|
*/
|
|
suspend fun disconnect(revokeOnServer: Boolean)
|
|
|
|
/** Provider-native account/data deletion, then wipe local state. */
|
|
suspend fun deleteAccountAndData()
|
|
|
|
// ---- storage -----------------------------------------------------------
|
|
|
|
/**
|
|
* Upload the original audio for [draft], reporting [onProgress] 0..1.
|
|
* Implementations use chunked/resumable transfers when the capability is
|
|
* advertised. Returns the ref for later download/delete/sidecars.
|
|
*/
|
|
suspend fun upload(draft: RecordingDraft, onProgress: (Float) -> Unit): RemoteRef
|
|
|
|
suspend fun download(ref: RemoteRef, dest: File, onProgress: (Float) -> Unit)
|
|
|
|
suspend fun delete(ref: RemoteRef)
|
|
|
|
suspend fun list(cursor: String?): Page<RemoteRecording>
|
|
|
|
// ---- sidecars (transcript/summary/... JSON, synced independently) ------
|
|
|
|
suspend fun putSidecar(ref: RemoteRef, kind: SidecarKind, bytes: ByteArray)
|
|
|
|
/** null when the sidecar does not exist remotely. */
|
|
suspend fun getSidecar(ref: RemoteRef, kind: SidecarKind): ByteArray?
|
|
|
|
// ---- status ------------------------------------------------------------
|
|
|
|
suspend fun storageLocationSummary(): StorageLocation
|
|
}
|