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, `-`. */ 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" } }