diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-10 14:40:05 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-10 14:40:05 +0200 |
| commit | aed32f20625a6206628b577d743d96552f81e91a (patch) | |
| tree | af2eaf3336804f2e4af37445326e52a19b2af169 /packages/meshbay-android/app/src/main/kotlin/org/meshbay | |
| parent | 2f2a9a6542d2194e50ffba6f382e4fdffd481838 (diff) | |
| download | meshbay-aed32f20625a6206628b577d743d96552f81e91a.tar.gz | |
feat(android): back text messages up, in a build Play does not get
A Messages section sends the SMS added since the last copy, as restorable
<smses> XML, into <folder>/<account>-messages/YYYY. A play flavor has neither
READ_SMS nor the code that reads messages; full is the default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-android/app/src/main/kotlin/org/meshbay')
4 files changed, 84 insertions, 3 deletions
diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt index 3fa1146..0545ead 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt @@ -39,6 +39,7 @@ import org.meshbay.client.photos.BackupService import org.meshbay.client.phonesync.ContactSource import org.meshbay.client.phonesync.DestinationChannels import org.meshbay.client.phonesync.DocChannels +import org.meshbay.client.phonesync.Flavor import org.meshbay.client.photos.PhotoChannels import org.meshbay.client.save.SaveSinks import org.meshbay.client.shell.Pickers @@ -108,7 +109,11 @@ class MainActivity : Activity() { DocChannels("contactsync", this, getSharedPreferences("contactsync", Context.MODE_PRIVATE), destinations, java.io.File(cacheDir, "contactsync"), ContactSource(this), notifyId = 10, permissionRequest = 4209), - ) + ) + listOfNotNull(Flavor.messageSource(this)?.let { source -> + DocChannels("messagesync", this, getSharedPreferences("messagesync", Context.MODE_PRIVATE), destinations, + java.io.File(cacheDir, "messagesync"), source, + notifyId = 11, permissionRequest = 4210) + }) channels = Channels(hub, onHubChanged = { runOnUiThread { reloadForHub() } }, hasCatalogue = { code -> hasAsset("ui/locales/$code.js") }, keys = keys, saves = saves, cast = cast, onPlayback = { on -> runOnUiThread { playing = on; keepAlive() } }, @@ -247,7 +252,8 @@ class MainActivity : Activity() { val source = assets.open("bridge/meshbay-bridge.js").bufferedReader().use { it.readText() } val binary = WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) val prelude = "const HUB_BASE = ${JSONObject.quote(hub.base)};\nconst BINARY = $binary;\n" + - "const CAST = ${cast.control.available()};\n" + "const CAST = ${cast.control.available()};\n" + + "const MESSAGES = ${docs.any { it.handles("messagesync:") }};\n" shim = WebViewCompat.addDocumentStartJavaScript(web, "(function(){$prelude$source\n})();", setOf(UiAssets.ORIGIN)) } diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocChannels.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocChannels.kt index 6877912..2efe558 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocChannels.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocChannels.kt @@ -124,7 +124,8 @@ class DocChannels( val token = PhotoChannels.hex(ByteArray(16).also { random.nextBytes(it) }) issued = Issued(token, export, c.ledgerKey) return JSONObject().put("item", JSONObject() - .put("token", token).put("name", export.name).put("dir", DocPlan.dirFor(c, source.suffix)) + .put("token", token).put("name", export.name) + .put("dir", DocPlan.dirFor(c, source.suffix) + (if (export.subdir.isEmpty()) "" else "/${export.subdir}")) .put("size", export.file.length()).put("count", export.count)) } diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocPlan.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocPlan.kt index b1563da..1a2a359 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocPlan.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocPlan.kt @@ -15,6 +15,8 @@ data class Export( val marker: String, /** Contacts or messages in it, for the page to say. */ val count: Int, + /** A folder under the kind's own, such as the year, or empty. */ + val subdir: String = "", ) /** @@ -46,6 +48,9 @@ object DocPlan { * `<stem>-YYYY-MM-DD-HHmm.<ext>`, in the phone's time: a run never replaces * an earlier file, and the name says when it was taken. */ + fun yearOf(now: Long, zone: TimeZone): String = + SimpleDateFormat("yyyy", Locale.ROOT).apply { timeZone = zone }.format(Date(now)) + fun nameFor(stem: String, ext: String, now: Long, zone: TimeZone): String = "$stem-" + SimpleDateFormat("yyyy-MM-dd-HHmm", Locale.ROOT).apply { timeZone = zone }.format(Date(now)) + ".$ext" } diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/SmsXml.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/SmsXml.kt new file mode 100644 index 0000000..624e1f1 --- /dev/null +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/SmsXml.kt @@ -0,0 +1,69 @@ +package org.meshbay.client.phonesync + +import java.io.Writer +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +/** One text message, as the platform's SMS provider stores it. */ +data class Sms( + val id: Long, + val address: String, + /** Milliseconds, received or sent. */ + val date: Long, + val dateSent: Long, + /** 1 received, 2 sent, 3 draft, 4 outbox, 5 failed, 6 queued. */ + val type: Int, + val body: String, + val read: Int, + val status: Int, + val locked: Int, + val protocol: Int, + val subject: String?, + val serviceCenter: String?, + val contactName: String?, +) + +/** + * Messages written as the `<smses>` XML that SMS backup applications read and + * restore, so a copy on the node goes back onto a phone with tools that exist. + * Pure, so the JVM tests hold the format and its escaping. + */ +object SmsXml { + fun write(messages: List<Sms>, out: Writer, zone: TimeZone) { + val readable = SimpleDateFormat("d MMM yyyy HH:mm:ss", Locale.ROOT).apply { timeZone = zone } + out.write("<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>\n") + out.write("<smses count=\"${messages.size}\">\n") + for (m in messages) { + out.write(" <sms protocol=\"${m.protocol}\" address=${q(m.address)} date=\"${m.date}\"" + + " type=\"${m.type}\" subject=${q(m.subject ?: "null")} body=${q(m.body)}" + + " toa=\"null\" sc_toa=\"null\" service_center=${q(m.serviceCenter ?: "null")}" + + " read=\"${m.read}\" status=\"${m.status}\" locked=\"${m.locked}\" date_sent=\"${m.dateSent}\"" + + " readable_date=${q(readable.format(Date(m.date)))}" + + " contact_name=${q(m.contactName ?: "(Unknown)")} />\n") + } + out.write("</smses>\n") + } + + /** + * A quoted attribute value. Line breaks are kept as character references, + * which an attribute would otherwise fold into spaces; characters XML 1.0 + * cannot carry at all, even escaped, are left out. + */ + fun q(text: String): String { + val b = StringBuilder(text.length + 2).append('"') + for (c in text) when { + c == '&' -> b.append("&") + c == '<' -> b.append("<") + c == '>' -> b.append(">") + c == '"' -> b.append(""") + c == '\n' -> b.append(" ") + c == '\r' -> b.append(" ") + c == '\t' -> b.append("	") + c < ' ' || c == '' || c == '' -> {} + else -> b.append(c) + } + return b.append('"').toString() + } +} |