aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/Destination.kt
blob: e140fd08be1d946dd915ae100a9630f1e37cf46b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package org.meshbay.client.phonesync

import android.content.SharedPreferences
import org.json.JSONArray
import org.json.JSONObject
import org.meshbay.client.bridge.Refused

/**
 * Where everything this phone backs up goes: one folder of one group the
 * account owns and is the only member of (docs/MESHBAY_DESIGN.md §9.12).
 * Each kind of data goes into its own folder under it, `<account>-<kind>`.
 */
data class Destination(
    val account: String,
    val groupId: String,
    val groupName: String,
    /** A folder among the group's own, as a virtual path (`Backups`). */
    val folder: String,
) {
    /** What was sent belongs to this, so a different group or folder starts again from nothing. */
    val ledgerKey: String get() = "$account\n$groupId\n$folder"

    fun toJson(): JSONObject = JSONObject()
        .put("account", account).put("groupId", groupId).put("groupName", groupName).put("folder", folder)

    companion object {
        /** From the page, so checked like any input: names it chose, never a path on this phone. */
        fun fromJson(o: JSONObject): Destination {
            val account = o.optString("account", "").take(64)
            val groupId = o.optString("groupId", "").take(64)
            val folder = o.optString("folder", "").trim().trim('/').take(1024)
            require(account.isNotEmpty() && groupId.isNotEmpty() && folder.isNotEmpty()) { "incomplete" }
            require(DocPlan.SAFE_ACCOUNT.matches(account)) { "bad account" }
            require(folder.split('/').none { it.isEmpty() || it == "." || it == ".." }) { "bad folder" }
            return Destination(account, groupId, o.optString("groupName", "").take(256), folder)
        }

        fun parse(text: String?): Destination? = try {
            val o = JSONObject(text ?: return null)
            Destination(o.getString("account"), o.getString("groupId"), o.optString("groupName"), o.getString("folder"))
        } catch (e: Exception) { null }
    }
}

/**
 * The destination, kept once for every kind. Changing it is a new backup for
 * each of them: what they remember having sent belongs to the old place, so
 * each is told and starts again from nothing.
 */
class DestinationChannels(private val prefs: SharedPreferences) {
    private val listeners = mutableListOf<() -> Unit>()

    fun onChange(listener: () -> Unit) { listeners += listener }

    fun get(): Destination? = Destination.parse(prefs.getString(KEY, null))

    fun handles(channel: String) = channel.startsWith("phonesync:")

    fun call(channel: String, args: JSONArray): Any? = when (channel) {
        "phonesync:destination" -> get()?.toJson() ?: JSONObject.NULL
        "phonesync:set-destination" -> { set(args.optJSONObject(0)); get()?.toJson() ?: JSONObject.NULL }
        else -> throw Refused("Refused: no such channel")
    }

    private fun set(o: JSONObject?) {
        val previous = get()
        val next = o?.let {
            try { Destination.fromJson(it) } catch (e: IllegalArgumentException) { throw Refused("Refused: ${e.message}") }
        }
        if (next == null) prefs.edit().remove(KEY).apply()
        else prefs.edit().putString(KEY, next.toJson().toString()).apply()
        if (previous?.ledgerKey != next?.ledgerKey) listeners.forEach { it() }
    }

    companion object {
        const val PREFS = "phonesync"
        private const val KEY = "destination"
    }
}