aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-15 15:09:58 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-15 15:09:58 +0200
commit39cac2878de059992cb09a1ccf09b6bcece99fae (patch)
treec4686f00fdf40e90b4b74144c1e8fd8fb8b8ed0b /packages
parent7fc2fa04d7d88a1cdf0d290ddd2de3815a7d52a5 (diff)
downloadmeshbay-39cac2878de059992cb09a1ccf09b6bcece99fae.tar.gz
feat(android): no text messages backup, and one build again
READ_SMS sits behind restricted settings outside a store and Play keeps it for the default SMS application: not worth a second build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-android/README.md12
-rw-r--r--packages/meshbay-android/app/build.gradle.kts11
-rw-r--r--packages/meshbay-android/app/src/full/AndroidManifest.xml6
-rw-r--r--packages/meshbay-android/app/src/full/kotlin/org/meshbay/client/phonesync/Flavor.kt8
-rw-r--r--packages/meshbay-android/app/src/full/kotlin/org/meshbay/client/phonesync/SmsSource.kt72
-rw-r--r--packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js20
-rw-r--r--packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt10
-rw-r--r--packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocChannels.kt18
-rw-r--r--packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/DocPlan.kt6
-rw-r--r--packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/Profiles.kt2
-rw-r--r--packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/SmsXml.kt69
-rw-r--r--packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/photos/PhotoChannels.kt2
-rw-r--r--packages/meshbay-android/app/src/play/kotlin/org/meshbay/client/phonesync/Flavor.kt11
-rw-r--r--packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/DocSyncTest.kt2
-rw-r--r--packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/SmsXmlTest.kt66
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js16
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/phone-sync.js6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/platform.js5
-rw-r--r--packages/meshbay-hub/tests/harness/photo_sync_probe.py9
-rw-r--r--packages/meshbay-hub/tests/test_android_shell.py25
-rw-r--r--packages/meshbay-hub/tests/test_phone_sync.py19
-rw-r--r--packages/meshbay-hub/tests/test_photo_sync_ui.py6
34 files changed, 28 insertions, 432 deletions
diff --git a/packages/meshbay-android/README.md b/packages/meshbay-android/README.md
index 02297c9..d44b58c 100644
--- a/packages/meshbay-android/README.md
+++ b/packages/meshbay-android/README.md
@@ -30,17 +30,11 @@ holds the same service and the same visibility, for as long as it plays
```bash
# needs JDK 17+ and an Android SDK (ANDROID_HOME, or sdk.dir in local.properties)
-./gradlew assembleDebug # app/build/outputs/apk/{full,play}/debug/
-./gradlew testFullDebugUnitTest # JVM unit tests
-./gradlew assembleRelease # app/build/outputs/apk/full/release/app-full-release.apk
- # app/build/outputs/apk/play/release/app-play-release.apk
+./gradlew assembleDebug # app/build/outputs/apk/debug/app-debug.apk
+./gradlew testDebugUnitTest # JVM unit tests
+./gradlew assembleRelease # app/build/outputs/apk/release/app-release.apk
```
-Two builds of the same application, same id and key: `full` is the one
-distributed directly; `play` is for the Play Store, whose policy gives
-READ_SMS to the default SMS application only, so it has no text messages
-backup (no permission, no code: `src/full`, `src/play`).
-
A release is signed with the release key, which never enters the repository.
`assembleRelease` reads it from `~/.gradle/gradle.properties`, and stops if
any of these is missing rather than signing with the debug key:
diff --git a/packages/meshbay-android/app/build.gradle.kts b/packages/meshbay-android/app/build.gradle.kts
index cd5e1a5..7887d04 100644
--- a/packages/meshbay-android/app/build.gradle.kts
+++ b/packages/meshbay-android/app/build.gradle.kts
@@ -32,16 +32,6 @@ android {
keyPassword = providers.gradleProperty("meshbayReleaseKeyPassword").get()
}
}
- // Two builds of the same application, the same id and key. `full` is the
- // one distributed directly; `play` is for the Play Store, whose policy
- // gives READ_SMS to the default SMS application only, so it has no
- // messages backup: not the permission, and not the code that reads them
- // (src/full, src/play: Flavor.kt).
- flavorDimensions += "store"
- productFlavors {
- create("full") { dimension = "store"; isDefault = true }
- create("play") { dimension = "store" }
- }
buildTypes {
getByName("release") {
isMinifyEnabled = false
@@ -110,7 +100,6 @@ val syncUi = tasks.register<SyncUi>("syncUi") {
// Checked when a release is built, not when the project is configured, so a
// debug build and the unit tests need no key.
tasks.configureEach {
- // One per build: preFullReleaseBuild, prePlayReleaseBuild.
if (name.startsWith("pre") && name.endsWith("ReleaseBuild")) doFirst {
val missing = releaseSigning.filter { !providers.gradleProperty(it).isPresent }
if (missing.isNotEmpty()) throw GradleException(
diff --git a/packages/meshbay-android/app/src/full/AndroidManifest.xml b/packages/meshbay-android/app/src/full/AndroidManifest.xml
deleted file mode 100644
index 552902a..0000000
--- a/packages/meshbay-android/app/src/full/AndroidManifest.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
- <!-- Messages backup, `full` build only: asked for when the person turns it
- on, never at start. The `play` build has no such permission. -->
- <uses-permission android:name="android.permission.READ_SMS" />
-</manifest>
diff --git a/packages/meshbay-android/app/src/full/kotlin/org/meshbay/client/phonesync/Flavor.kt b/packages/meshbay-android/app/src/full/kotlin/org/meshbay/client/phonesync/Flavor.kt
deleted file mode 100644
index 20882a5..0000000
--- a/packages/meshbay-android/app/src/full/kotlin/org/meshbay/client/phonesync/Flavor.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.meshbay.client.phonesync
-
-import android.content.Context
-
-/** What this build backs up beyond photos and contacts: the `full` build reads messages. */
-object Flavor {
- fun messageSource(context: Context): DocSource? = SmsSource(context)
-}
diff --git a/packages/meshbay-android/app/src/full/kotlin/org/meshbay/client/phonesync/SmsSource.kt b/packages/meshbay-android/app/src/full/kotlin/org/meshbay/client/phonesync/SmsSource.kt
deleted file mode 100644
index c24ef1f..0000000
--- a/packages/meshbay-android/app/src/full/kotlin/org/meshbay/client/phonesync/SmsSource.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-package org.meshbay.client.phonesync
-
-import android.Manifest
-import android.content.Context
-import android.content.pm.PackageManager
-import android.net.Uri
-import android.provider.ContactsContract
-import android.provider.Telephony
-import java.io.File
-import java.util.TimeZone
-
-/**
- * The phone's text messages, from the platform's SMS provider (not MMS).
- *
- * Each run writes the messages added since the last file the node took:
- * `marker` is the highest provider id it held, and ids only grow, so a
- * message restored onto the phone later counts as new and is sent too. The
- * first file holds the whole history. Files go under the year they were
- * written in, `<account>-messages/YYYY/`.
- *
- * Only in the `full` build: Play's policy keeps READ_SMS for the default SMS
- * application, so the `play` build has neither the permission nor this.
- */
-class SmsSource(private val context: Context) : DocSource {
- override val permissions = arrayOf(Manifest.permission.READ_SMS)
- override val suffix = "messages"
-
- override fun export(marker: String?, dir: File, now: Long, zone: TimeZone): Export? {
- val after = marker?.toLongOrNull() ?: 0L
- val names = Names(context)
- val out = ArrayList<Sms>()
- val cols = arrayOf(
- Telephony.Sms._ID, Telephony.Sms.ADDRESS, Telephony.Sms.DATE, Telephony.Sms.DATE_SENT,
- Telephony.Sms.TYPE, Telephony.Sms.BODY, Telephony.Sms.READ, Telephony.Sms.STATUS,
- Telephony.Sms.LOCKED, Telephony.Sms.PROTOCOL, Telephony.Sms.SUBJECT, Telephony.Sms.SERVICE_CENTER,
- )
- context.contentResolver.query(Telephony.Sms.CONTENT_URI, cols, "${Telephony.Sms._ID} > ?",
- arrayOf(after.toString()), "${Telephony.Sms.DATE} ASC")?.use { c ->
- fun str(i: Int) = if (c.isNull(i)) null else c.getString(i)
- fun int(i: Int, d: Int) = if (c.isNull(i)) d else c.getInt(i)
- while (c.moveToNext()) {
- val address = str(1) ?: ""
- out += Sms(c.getLong(0), address, c.getLong(2), if (c.isNull(3)) 0 else c.getLong(3),
- int(4, 1), str(5) ?: "", int(6, 1), int(7, -1), int(8, 0), int(9, 0),
- str(10), str(11), names.of(address))
- }
- }
- if (out.isEmpty()) return null
- val file = File(dir, "sms.xml")
- file.bufferedWriter(Charsets.UTF_8).use { SmsXml.write(out, it, zone) }
- return Export(file, DocPlan.nameFor("sms", "xml", now, zone), "application/xml",
- out.maxOf { it.id }.toString(), out.size, DocPlan.yearOf(now, zone))
- }
-
- /** Who a number is, when the person also let the application read contacts; once per number. */
- private class Names(private val context: Context) {
- private val allowed = context.checkSelfPermission(Manifest.permission.READ_CONTACTS) ==
- PackageManager.PERMISSION_GRANTED
- private val known = HashMap<String, String?>()
-
- fun of(address: String): String? {
- if (!allowed || address.isBlank()) return null
- return known.getOrPut(address) {
- try {
- val uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address))
- context.contentResolver.query(uri, arrayOf(ContactsContract.PhoneLookup.DISPLAY_NAME),
- null, null, null)?.use { c -> if (c.moveToFirst()) c.getString(0) else null }
- } catch (e: Exception) { null }
- }
- }
- }
-}
diff --git a/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js b/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js
index a07399f..d829df3 100644
--- a/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js
+++ b/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js
@@ -15,12 +15,11 @@
* that refuses: `platform.js` decides what to show from whether an object
* exists (`platform.node.available`, `platform.folder.available`, …).
*
- * `HUB_BASE`, `BINARY`, `CAST`, `BACKUP` and `MESSAGES` are prepended by the
+ * `HUB_BASE`, `BINARY`, `CAST` and `BACKUP` are prepended by the
* shell when it injects this file: the interface asks for the hub while its
* modules load, before anything can await; BINARY says whether the WebView carries ArrayBuffer
* messages; CAST whether this device can cast at all; BACKUP whether this is
- * the personal profile, the only one backed up; MESSAGES whether this build
- * backs text messages up (not the Play build).
+ * the personal profile, the only one backed up.
*/
(function () {
'use strict';
@@ -238,7 +237,6 @@
sent: (token, dir, name) => call('contactsync:sent', token, dir, name),
completed: () => call('contactsync:completed'),
failed: (code, text) => call('contactsync:failed', code, text),
- openSettings: () => call('contactsync:open-settings'),
},
// Calendar backup (§9.13), the same way: one iCalendar file of every
@@ -251,7 +249,6 @@
sent: (token, dir, name) => call('calendarsync:sent', token, dir, name),
completed: () => call('calendarsync:completed'),
failed: (code, text) => call('calendarsync:failed', code, text),
- openSettings: () => call('calendarsync:open-settings'),
},
// Files backup (§9.13): folders the person chose through the system's
@@ -287,19 +284,6 @@
},
} : {}),
- // Messages backup (§9.13), the same way as contacts: the messages added
- // since the last file the node took. Only in a build that may read them.
- ...(MESSAGES ? { messageSync: {
- status: () => call('messagesync:status'),
- permit: () => call('messagesync:permit'),
- configure: (settings) => call('messagesync:configure', settings || null),
- plan: () => call('messagesync:plan'),
- sent: (token, dir, name) => call('messagesync:sent', token, dir, name),
- completed: () => call('messagesync:completed'),
- failed: (code, text) => call('messagesync:failed', code, text),
- openSettings: () => call('messagesync:open-settings'),
- } } : {}),
-
// Where downloads go, chosen once. A display name comes back, never a URI.
folder: {
choose: () => call('folder:choose'),
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 d523da7..5f225e2 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
@@ -42,7 +42,6 @@ import org.meshbay.client.phonesync.CalendarSource
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.phonesync.Profiles
import org.meshbay.client.photos.PhotoChannels
import org.meshbay.client.save.SaveSinks
@@ -128,11 +127,7 @@ class MainActivity : Activity() {
DocChannels("calendarsync", this, getSharedPreferences("calendarsync", Context.MODE_PRIVATE), destinations,
java.io.File(cacheDir, "calendarsync"), CalendarSource(this),
notifyId = 12, permissionRequest = 4211),
- ) + 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() } },
@@ -279,8 +274,7 @@ class MainActivity : Activity() {
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 BACKUP = $backups;\n" +
- "const MESSAGES = ${docs.any { it.handles("messagesync:") }};\n"
+ "const BACKUP = $backups;\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 f4da64f..dd74256 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
@@ -16,7 +16,7 @@ import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
- * Contacts or messages backup (docs/MESHBAY_DESIGN.md §9.13): what the page
+ * Contacts or calendar backup (docs/MESHBAY_DESIGN.md §9.13): what the page
* needs from the phone for one kind of data, and nothing it could use to read
* anything else.
*
@@ -64,7 +64,6 @@ class DocChannels(
"sent" -> { sent(args.optString(0, ""), args.optString(1, ""), args.optString(2, "")); true }
"completed" -> { completed(); status() }
"failed" -> failed(args.optString(0, ""), args.optString(1, ""))
- "open-settings" -> { openSettings(); true }
else -> throw Refused("Refused: no such channel")
}
@@ -126,7 +125,7 @@ class DocChannels(
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) + (if (export.subdir.isEmpty()) "" else "/${export.subdir}"))
+ .put("dir", DocPlan.dirFor(c, source.suffix))
.put("size", export.file.length()).put("count", export.count))
}
@@ -168,19 +167,6 @@ class DocChannels(
permission = null
}
- /**
- * The application's page in the system's settings. Android keeps some
- * permissions (text messages) behind "restricted settings" for an
- * application installed outside a store; that page is where the person
- * lifts it (⋮ > Allow restricted settings), and nowhere in here can.
- */
- private fun openSettings() {
- val intent = android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
- android.net.Uri.fromParts("package", activity.packageName, null))
- .addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
- activity.runOnUiThread { activity.startActivity(intent) }
- }
-
/** From Activity.onRequestPermissionsResult; true when the request was ours. */
fun deliverPermission(requestCode: Int): Boolean {
if (requestCode != permissionRequest) return false
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 1a2a359..ecf643b 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
@@ -13,14 +13,12 @@ data class Export(
val mime: String,
/** Recorded only when the node took the file: what the next export starts from. */
val marker: String,
- /** Contacts or messages in it, for the page to say. */
+ /** Contacts or events 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 = "",
)
/**
- * One kind of the phone's data (the contacts, the messages) written out as
+ * One kind of the phone's data (the contacts, the calendar) written out as
* a file of what is new since `marker`, the value the last file sent left.
*/
interface DocSource {
diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/Profiles.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/Profiles.kt
index 660cfaa..305e705 100644
--- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/Profiles.kt
+++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/Profiles.kt
@@ -9,7 +9,7 @@ import android.os.UserManager
* of the application installed inside a work profile offers none of them.
*
* In the personal profile, every source reads that profile alone: MediaStore,
- * ContactsContract, the SMS and calendar providers answer for the profile
+ * ContactsContract and the calendar provider answer for the profile
* they are asked from, and none of the cross-profile (`ENTERPRISE_*`) URIs
* is used (`test_android_shell.py` checks for them).
*/
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
deleted file mode 100644
index 624e1f1..0000000
--- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/SmsXml.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-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("&amp;")
- c == '<' -> b.append("&lt;")
- c == '>' -> b.append("&gt;")
- c == '"' -> b.append("&quot;")
- c == '\n' -> b.append("&#10;")
- c == '\r' -> b.append("&#13;")
- c == '\t' -> b.append("&#9;")
- c < ' ' || c == '￾' || c == '￿' -> {}
- else -> b.append(c)
- }
- return b.append('"').toString()
- }
-}
diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/photos/PhotoChannels.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/photos/PhotoChannels.kt
index c1de859..21734fb 100644
--- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/photos/PhotoChannels.kt
+++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/photos/PhotoChannels.kt
@@ -348,7 +348,7 @@ class PhotoChannels(
private const val FAILURE_AT = "failure_at"
private const val NOTIFIED = "notified"
- /** Created, or renamed from "Photo backup" now that contacts and messages use it too. */
+ /** Created, or renamed from "Photo backup" now that contacts and the calendar use it too. */
fun ensureChannel(context: Context) {
context.getSystemService(NotificationManager::class.java)
.createNotificationChannel(NotificationChannel(CHANNEL, "Backup", NotificationManager.IMPORTANCE_LOW))
diff --git a/packages/meshbay-android/app/src/play/kotlin/org/meshbay/client/phonesync/Flavor.kt b/packages/meshbay-android/app/src/play/kotlin/org/meshbay/client/phonesync/Flavor.kt
deleted file mode 100644
index 76dde1f..0000000
--- a/packages/meshbay-android/app/src/play/kotlin/org/meshbay/client/phonesync/Flavor.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.meshbay.client.phonesync
-
-import android.content.Context
-
-/**
- * What this build backs up beyond photos and contacts: nothing, in the Play
- * build. Play's policy keeps READ_SMS for the default SMS application.
- */
-object Flavor {
- fun messageSource(context: Context): DocSource? = null
-}
diff --git a/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/DocSyncTest.kt b/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/DocSyncTest.kt
index f53110b..6a9e4e6 100644
--- a/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/DocSyncTest.kt
+++ b/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/DocSyncTest.kt
@@ -24,7 +24,7 @@ class DocSyncTest {
@Test fun `each kind goes into a folder of its own named for the account`() {
val c = Destination.fromJson(settings(folder = "/Backups/Phone/"))
assertEquals("Backups/Phone/bob-contacts", DocPlan.dirFor(c, "contacts"))
- assertEquals("Backups/Phone/bob-messages", DocPlan.dirFor(c, "messages"))
+ assertEquals("Backups/Phone/bob-calendar", DocPlan.dirFor(c, "calendar"))
}
@Test fun `a file is named for when it was taken, in the phone's time`() {
diff --git a/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/SmsXmlTest.kt b/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/SmsXmlTest.kt
deleted file mode 100644
index 9945ec1..0000000
--- a/packages/meshbay-android/app/src/test/kotlin/org/meshbay/client/SmsXmlTest.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package org.meshbay.client
-
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.meshbay.client.phonesync.DocPlan
-import org.meshbay.client.phonesync.Sms
-import org.meshbay.client.phonesync.SmsXml
-import org.w3c.dom.Element
-import java.io.StringWriter
-import java.util.TimeZone
-import javax.xml.parsers.DocumentBuilderFactory
-
-class SmsXmlTest {
- private val utc = TimeZone.getTimeZone("UTC")
- // 2026-10-09 12:00:00 UTC
- private val oct9 = 1791547200000L
-
- private fun sms(id: Long, body: String, address: String = "+33600000000", name: String? = null) =
- Sms(id, address, oct9, oct9 - 5000, 1, body, 1, -1, 0, 0, null, null, name)
-
- private fun parse(xml: String): List<Element> {
- val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(xml.byteInputStream())
- assertEquals("smses", doc.documentElement.tagName)
- val list = doc.getElementsByTagName("sms")
- return (0 until list.length).map { list.item(it) as Element }
- }
-
- private fun write(vararg m: Sms) = StringWriter().also { SmsXml.write(m.toList(), it, utc) }.toString()
-
- @Test fun `what a restore reads is there, with the count`() {
- val xml = write(sms(1, "hello"), sms(2, "again", name = "Ada"))
- assertTrue(xml.contains("<smses count=\"2\">"))
- val rows = parse(xml)
- assertEquals("hello", rows[0].getAttribute("body"))
- assertEquals("+33600000000", rows[0].getAttribute("address"))
- assertEquals(oct9.toString(), rows[0].getAttribute("date"))
- assertEquals("1", rows[0].getAttribute("type"))
- assertEquals("(Unknown)", rows[0].getAttribute("contact_name"))
- assertEquals("Ada", rows[1].getAttribute("contact_name"))
- assertEquals("9 Oct 2026 12:00:00", rows[0].getAttribute("readable_date"))
- assertEquals("null", rows[0].getAttribute("subject"))
- }
-
- @Test fun `markup, quotes, line breaks and emoji come back as they were`() {
- val body = "a < b & \"c\" > 'd'\nsecond line\r\n\ttab 🎉 é"
- assertEquals(body, parse(write(sms(1, body)))[0].getAttribute("body"))
- }
-
- @Test fun `characters XML cannot carry are left out rather than breaking the file`() {
- val rows = parse(write(sms(1, "a\u0000b\u0007c￿d")))
- assertEquals("abcd", rows[0].getAttribute("body"))
- }
-
- @Test fun `an empty body or address is still a well-formed row`() {
- val rows = parse(write(sms(1, "", address = "")))
- assertEquals("", rows[0].getAttribute("body"))
- assertFalse(rows[0].hasAttribute("missing"))
- }
-
- @Test fun `a file goes under the year it was written in`() {
- assertEquals("2026", DocPlan.yearOf(oct9, utc))
- assertEquals("sms-2026-10-09-1200.xml", DocPlan.nameFor("sms", "xml", oct9, utc))
- }
-}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 195660c..5fcd6e7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -714,7 +714,7 @@ const SearchPage = lazy(() => import('./search-page.js'), 'SearchPage',
// ── Lazy-loaded Android Sync page ────────────────────────────────────────────
//
-// The Android application's backups of its photos, contacts and messages
+// The Android application's backups of its photos, contacts, calendar and files
// (docs/MESHBAY_DESIGN.md §9.12, §9.13): no browser or desktop ever fetches it.
const PHONE_SYNC = platform.phoneSync.available;
const AndroidSyncPage = lazy(() => import('./android-sync-page.js'), 'AndroidSyncPage',
@@ -962,10 +962,9 @@ function App() {
return () => { gone = true; if (stop) stop(); };
}, [syncAccount]);
- // Contacts, calendar and messages backup (§9.13), the same way.
+ // Contacts and calendar backup (§9.13), the same way.
useEffect(() => {
- if (!syncAccount || !(platform.contactSync.available || platform.calendarSync.available
- || platform.messageSync.available)) {
+ if (!syncAccount || !(platform.contactSync.available || platform.calendarSync.available)) {
return undefined;
}
let stop = null;
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index 7bd3528..0489b95 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -1369,10 +1369,6 @@ export default {
'calendarsync.heading': "Kalender-Sicherung",
'calendarsync.intro': "Einmal täglich, wenn sie sich geändert haben, eine Kopie der Kalender dieses Telefons senden: die, die du bearbeiten kannst, nicht die nur abonnierten. Jede Kopie ist eine .ics-Datei, die jeder Kalender importieren kann.",
'calendarsync.permission_needed': "MeshBay braucht Zugriff auf die Kalender dieses Telefons.",
- 'messagesync.heading': "SMS-Sicherung",
- 'messagesync.intro': "Die SMS dieses Telefons einmal täglich senden: Die erste Kopie enthält alle, jede weitere nur die neuen. Die Dateien sind im XML-Format, das SMS-Sicherungs-Apps wiederherstellen können. MMS sind nicht enthalten.",
- 'messagesync.permission_needed': "MeshBay braucht Zugriff auf die SMS dieses Telefons.",
- 'messagesync.restricted_hint': "Wenn Android von einer eingeschränkten Einstellung spricht: Öffne die App-Einstellungen, tippe oben auf ⋮, „Eingeschränkte Einstellungen zulassen“, und schalte es dann hier wieder ein. Android verlangt das von Apps, die nicht aus einem Store installiert wurden.",
'phonesync.state_sending': "Wird gesendet…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Gesendete Kopien: {n}",
@@ -1389,7 +1385,6 @@ export default {
'phonesync.fail_not_private': "Gestoppt: Jemand anderes ist der Gruppe beigetreten.",
'phonesync.fail_permission': "Gestoppt: MeshBay hat keinen Zugriff mehr.",
'phonesync.fail_other': "Gestoppt.",
- 'phonesync.open_settings': "App-Einstellungen öffnen",
'drivesync.heading': "Datei-Sicherung",
'drivesync.intro': "Die Dateien der Ordner, die du auf diesem Telefon auswählst, einmal täglich über WLAN senden. Eine auf dem Telefon geänderte Datei wird neben der früheren Kopie erneut gesendet; nichts wird je ersetzt.",
'drivesync.folders': "Gesicherte Ordner",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index 1ba570a..cb74f18 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -1354,10 +1354,6 @@ export default {
'calendarsync.heading': "Calendar backup",
'calendarsync.intro': "Send a copy of this phone's calendars once a day when they have changed: the ones you can edit, not those you only subscribe to. Each copy is an .ics file any calendar can import.",
'calendarsync.permission_needed': "MeshBay needs access to this phone's calendars.",
- 'messagesync.heading': "Messages backup",
- 'messagesync.intro': "Send the text messages on this phone once a day: the first copy holds them all, each next one only the new ones. The files are in the XML format SMS backup applications restore. Multimedia messages are not included.",
- 'messagesync.permission_needed': "MeshBay needs access to this phone's text messages.",
- 'messagesync.restricted_hint': "If Android says this is a restricted setting: open the application's settings, tap ⋮ at the top, “Allow restricted settings”, then come back and turn this on again. Android asks this of applications installed outside a store.",
'phonesync.state_sending': "Sending…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Copies sent: {n}",
@@ -1374,7 +1370,6 @@ export default {
'phonesync.fail_not_private': "Stopped: somebody else has joined the group.",
'phonesync.fail_permission': "Stopped: MeshBay no longer has access.",
'phonesync.fail_other': "Stopped.",
- 'phonesync.open_settings': "Open the application's settings",
'drivesync.heading': "Files backup",
'drivesync.intro': "Send the files of folders you choose on this phone, once a day, on Wi-Fi. A file changed on the phone is sent again beside the earlier copy; nothing is ever replaced.",
'drivesync.folders': "Folders backed up",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index e04d09d..b8d1b89 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -1364,10 +1364,6 @@ export default {
'calendarsync.heading': "Copia de seguridad del calendario",
'calendarsync.intro': "Enviar una copia de los calendarios de este teléfono una vez al día si han cambiado: los que puedes editar, no aquellos a los que solo estás suscrito. Cada copia es un archivo .ics que cualquier calendario puede importar.",
'calendarsync.permission_needed': "MeshBay necesita acceso a los calendarios de este teléfono.",
- 'messagesync.heading': "Copia de seguridad de SMS",
- 'messagesync.intro': "Enviar los SMS de este teléfono una vez al día: la primera copia los contiene todos y cada una de las siguientes solo los nuevos. Los archivos están en el formato XML que restauran las aplicaciones de copia de seguridad de SMS. Los MMS no se incluyen.",
- 'messagesync.permission_needed': "MeshBay necesita acceso a los SMS de este teléfono.",
- 'messagesync.restricted_hint': "Si Android habla de un ajuste restringido: abre los ajustes de la aplicación, toca ⋮ arriba, «Permitir ajustes restringidos» y vuelve para activarlo. Android lo exige a las aplicaciones instaladas fuera de una tienda.",
'phonesync.state_sending': "Enviando…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Copias enviadas: {n}",
@@ -1384,7 +1380,6 @@ export default {
'phonesync.fail_not_private': "Detenida: otra persona se ha unido al grupo.",
'phonesync.fail_permission': "Detenida: MeshBay ya no tiene acceso.",
'phonesync.fail_other': "Detenida.",
- 'phonesync.open_settings': "Abrir los ajustes de la aplicación",
'drivesync.heading': "Copia de seguridad de archivos",
'drivesync.intro': "Enviar los archivos de las carpetas que elijas en este teléfono, una vez al día, por Wi-Fi. Un archivo modificado en el teléfono se envía de nuevo junto a la copia anterior; nunca se reemplaza nada.",
'drivesync.folders': "Carpetas con copia",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 9165d6d..c895b63 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -1379,10 +1379,6 @@ export default {
'calendarsync.heading': "Sauvegarde de l'agenda",
'calendarsync.intro': "Envoyer une copie des agendas de ce téléphone une fois par jour s'ils ont changé : ceux que vous pouvez modifier, pas ceux auxquels vous êtes seulement abonné. Chaque copie est un fichier .ics que tout agenda sait importer.",
'calendarsync.permission_needed': "MeshBay a besoin d'accéder aux agendas de ce téléphone.",
- 'messagesync.heading': "Sauvegarde des SMS",
- 'messagesync.intro': "Envoyer les SMS de ce téléphone une fois par jour : la première copie les contient tous, les suivantes seulement les nouveaux. Les fichiers sont au format XML que les applis de sauvegarde SMS savent restaurer. Les MMS ne sont pas inclus.",
- 'messagesync.permission_needed': "MeshBay a besoin d'accéder aux SMS de ce téléphone.",
- 'messagesync.restricted_hint': "Si Android parle d'un paramètre restreint : ouvrez les réglages de l'appli, touchez ⋮ en haut, « Autoriser les paramètres restreints », puis revenez l'activer. Android l'exige des applis installées hors d'un store.",
'phonesync.state_sending': "Envoi…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Copies envoyées : {n}",
@@ -1399,7 +1395,6 @@ export default {
'phonesync.fail_not_private': "Arrêtée : quelqu'un d'autre a rejoint le groupe.",
'phonesync.fail_permission': "Arrêtée : MeshBay n'y a plus accès.",
'phonesync.fail_other': "Arrêtée.",
- 'phonesync.open_settings': "Ouvrir les réglages de l'appli",
'drivesync.heading': "Sauvegarde des fichiers",
'drivesync.intro': "Envoyer les fichiers des dossiers que vous choisissez sur ce téléphone, une fois par jour, en Wi-Fi. Un fichier modifié sur le téléphone est renvoyé à côté de la copie précédente ; rien n'est jamais remplacé.",
'drivesync.folders': "Dossiers sauvegardés",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index b7ae9e9..8da2953 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -1378,10 +1378,6 @@ export default {
'calendarsync.heading': "Backup del calendario",
'calendarsync.intro': "Invia una copia dei calendari di questo telefono una volta al giorno se sono cambiati: quelli che puoi modificare, non quelli a cui sei solo iscritto. Ogni copia è un file .ics che qualsiasi calendario può importare.",
'calendarsync.permission_needed': "MeshBay ha bisogno di accedere ai calendari di questo telefono.",
- 'messagesync.heading': "Backup degli SMS",
- 'messagesync.intro': "Invia gli SMS di questo telefono una volta al giorno: la prima copia li contiene tutti, le successive solo quelli nuovi. I file sono nel formato XML che le app di backup degli SMS sanno ripristinare. Gli MMS non sono inclusi.",
- 'messagesync.permission_needed': "MeshBay ha bisogno di accedere agli SMS di questo telefono.",
- 'messagesync.restricted_hint': "Se Android parla di un'impostazione con restrizioni: apri le impostazioni dell'app, tocca ⋮ in alto, «Consenti impostazioni con restrizioni», poi torna qui ad attivarlo. Android lo richiede alle app installate fuori da uno store.",
'phonesync.state_sending': "Invio…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Copie inviate: {n}",
@@ -1398,7 +1394,6 @@ export default {
'phonesync.fail_not_private': "Interrotto: qualcun altro si è unito al gruppo.",
'phonesync.fail_permission': "Interrotto: MeshBay non ha più accesso.",
'phonesync.fail_other': "Interrotto.",
- 'phonesync.open_settings': "Apri le impostazioni dell'app",
'drivesync.heading': "Backup dei file",
'drivesync.intro': "Invia i file delle cartelle che scegli su questo telefono, una volta al giorno, in Wi-Fi. Un file modificato sul telefono viene inviato di nuovo accanto alla copia precedente; non viene mai sostituito nulla.",
'drivesync.folders': "Cartelle salvate",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index 286051e..c1f9523 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -1362,10 +1362,6 @@ export default {
'calendarsync.heading': "カレンダーのバックアップ",
'calendarsync.intro': "このスマートフォンのカレンダーのコピーを、変更があった日に1日1回送ります。対象は編集できるカレンダーで、購読しているだけのものは含みません。各コピーはどのカレンダーでも読み込める .ics ファイルです。",
'calendarsync.permission_needed': "MeshBay にこのスマートフォンのカレンダーへのアクセスを許可してください。",
- 'messagesync.heading': "SMS のバックアップ",
- 'messagesync.intro': "このスマートフォンの SMS を1日1回送ります。最初のコピーにはすべてが含まれ、以降は新しいものだけが含まれます。ファイルは SMS バックアップアプリで復元できる XML 形式です。MMS は含まれません。",
- 'messagesync.permission_needed': "MeshBay にこのスマートフォンの SMS へのアクセスを許可してください。",
- 'messagesync.restricted_hint': "Android が制限付き設定と表示した場合は、アプリの設定を開き、上部の ⋮ から「制限付き設定を許可」を選び、ここに戻ってもう一度オンにしてください。ストア以外からインストールしたアプリには Android がこれを求めます。",
'phonesync.state_sending': "送信中…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "送信したコピー: {n}",
@@ -1382,7 +1378,6 @@ export default {
'phonesync.fail_not_private': "停止しました: 他の人がグループに参加しました。",
'phonesync.fail_permission': "停止しました: MeshBay はアクセスできなくなりました。",
'phonesync.fail_other': "停止しました。",
- 'phonesync.open_settings': "アプリの設定を開く",
'drivesync.heading': "ファイルのバックアップ",
'drivesync.intro': "このスマートフォンで選んだフォルダーのファイルを、1日1回 Wi-Fi で送ります。スマートフォンで変更したファイルは、以前のコピーの横にもう一度送られます。何も置き換えられません。",
'drivesync.folders': "バックアップするフォルダー",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 5bf1a3b..66505da 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -1380,10 +1380,6 @@ export default {
'calendarsync.heading': "Back-up van de agenda",
'calendarsync.intro': "Stuur eenmaal per dag, als ze veranderd zijn, een kopie van de agenda's op deze telefoon: de agenda's die je kunt bewerken, niet de agenda's waarop je alleen geabonneerd bent. Elke kopie is een .ics-bestand dat elke agenda kan importeren.",
'calendarsync.permission_needed': "MeshBay heeft toegang nodig tot de agenda's op deze telefoon.",
- 'messagesync.heading': "Back-up van sms'jes",
- 'messagesync.intro': "Stuur de sms'jes op deze telefoon eenmaal per dag: de eerste kopie bevat ze allemaal, elke volgende alleen de nieuwe. De bestanden zijn in het XML-formaat dat sms-back-upapps kunnen terugzetten. Mms'jes zijn niet inbegrepen.",
- 'messagesync.permission_needed': "MeshBay heeft toegang nodig tot de sms'jes op deze telefoon.",
- 'messagesync.restricted_hint': "Als Android het over een beperkte instelling heeft: open de instellingen van de app, tik bovenaan op ⋮, ‘Beperkte instellingen toestaan’, en zet het daarna hier weer aan. Android vraagt dit van apps die buiten een winkel zijn geïnstalleerd.",
'phonesync.state_sending': "Bezig met verzenden…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Verzonden kopieën: {n}",
@@ -1400,7 +1396,6 @@ export default {
'phonesync.fail_not_private': "Gestopt: iemand anders is lid geworden van de groep.",
'phonesync.fail_permission': "Gestopt: MeshBay heeft geen toegang meer.",
'phonesync.fail_other': "Gestopt.",
- 'phonesync.open_settings': "Instellingen van de app openen",
'drivesync.heading': "Back-up van bestanden",
'drivesync.intro': "Stuur de bestanden van mappen die je op deze telefoon kiest, eenmaal per dag, via wifi. Een bestand dat op de telefoon is gewijzigd, wordt opnieuw verstuurd naast de eerdere kopie; er wordt nooit iets vervangen.",
'drivesync.folders': "Geback-upte mappen",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index e5bc31a..e4fbda8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -1408,10 +1408,6 @@ export default {
'calendarsync.heading': "Kopia kalendarza",
'calendarsync.intro': "Wysyłaj kopię kalendarzy z tego telefonu raz dziennie, jeśli się zmieniły: tych, które możesz edytować, a nie tylko subskrybowanych. Każda kopia to plik .ics, który zaimportuje każdy kalendarz.",
'calendarsync.permission_needed': "MeshBay potrzebuje dostępu do kalendarzy w tym telefonie.",
- 'messagesync.heading': "Kopia SMS-ów",
- 'messagesync.intro': "Wysyłaj SMS-y z tego telefonu raz dziennie: pierwsza kopia zawiera wszystkie, każda kolejna tylko nowe. Pliki są w formacie XML, który przywracają aplikacje do kopii SMS-ów. MMS-y nie są uwzględniane.",
- 'messagesync.permission_needed': "MeshBay potrzebuje dostępu do SMS-ów w tym telefonie.",
- 'messagesync.restricted_hint': "Jeśli Android mówi o ustawieniu z ograniczeniami: otwórz ustawienia aplikacji, dotknij ⋮ u góry, „Zezwól na ustawienia z ograniczeniami”, a potem wróć i włącz to ponownie. Android wymaga tego od aplikacji zainstalowanych spoza sklepu.",
'phonesync.state_sending': "Wysyłanie…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Wysłane kopie: {n}",
@@ -1428,7 +1424,6 @@ export default {
'phonesync.fail_not_private': "Zatrzymano: ktoś inny dołączył do grupy.",
'phonesync.fail_permission': "Zatrzymano: MeshBay nie ma już dostępu.",
'phonesync.fail_other': "Zatrzymano.",
- 'phonesync.open_settings': "Otwórz ustawienia aplikacji",
'drivesync.heading': "Kopia plików",
'drivesync.intro': "Wysyłaj pliki z folderów wybranych w tym telefonie raz dziennie przez Wi-Fi. Plik zmieniony w telefonie jest wysyłany ponownie obok wcześniejszej kopii; nic nie jest nigdy zastępowane.",
'drivesync.folders': "Kopiowane foldery",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index f21997b..bb7271c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -1365,10 +1365,6 @@ export default {
'calendarsync.heading': "Backup da agenda",
'calendarsync.intro': "Enviar uma cópia das agendas deste celular uma vez por dia se tiverem mudado: as que você pode editar, não as que você só assina. Cada cópia é um arquivo .ics que qualquer agenda consegue importar.",
'calendarsync.permission_needed': "O MeshBay precisa de acesso às agendas deste celular.",
- 'messagesync.heading': "Backup de SMS",
- 'messagesync.intro': "Enviar os SMS deste celular uma vez por dia: a primeira cópia contém todos, cada uma das seguintes só os novos. Os arquivos estão no formato XML que os apps de backup de SMS conseguem restaurar. MMS não são incluídos.",
- 'messagesync.permission_needed': "O MeshBay precisa de acesso aos SMS deste celular.",
- 'messagesync.restricted_hint': "Se o Android falar de uma configuração restrita: abra as configurações do app, toque em ⋮ no alto, “Permitir configurações restritas” e volte para ativar. O Android exige isso de apps instalados fora de uma loja.",
'phonesync.state_sending': "Enviando…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "Cópias enviadas: {n}",
@@ -1385,7 +1381,6 @@ export default {
'phonesync.fail_not_private': "Parado: outra pessoa entrou no grupo.",
'phonesync.fail_permission': "Parado: o MeshBay não tem mais acesso.",
'phonesync.fail_other': "Parado.",
- 'phonesync.open_settings': "Abrir as configurações do app",
'drivesync.heading': "Backup de arquivos",
'drivesync.intro': "Enviar os arquivos das pastas que você escolher neste celular, uma vez por dia, pelo Wi-Fi. Um arquivo alterado no celular é enviado de novo ao lado da cópia anterior; nada é substituído.",
'drivesync.folders': "Pastas com backup",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 1d07025..9d922fe 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -1351,10 +1351,6 @@ export default {
'calendarsync.heading': "日历备份",
'calendarsync.intro': "在日历有变动的日子里,每天一次发送本机日历的副本:包括你可以编辑的日历,不包括仅订阅的日历。每份副本都是任何日历都能导入的 .ics 文件。",
'calendarsync.permission_needed': "MeshBay 需要访问本机的日历。",
- 'messagesync.heading': "短信备份",
- 'messagesync.intro': "每天一次发送本机的短信:第一份副本包含全部短信,之后每份只包含新短信。文件采用短信备份应用可以恢复的 XML 格式。不包括彩信。",
- 'messagesync.permission_needed': "MeshBay 需要访问本机的短信。",
- 'messagesync.restricted_hint': "如果 Android 提示这是受限设置:打开应用设置,点按顶部的 ⋮,选择“允许受限设置”,然后回到这里再次开启。Android 对不是从应用商店安装的应用有此要求。",
'phonesync.state_sending': "正在发送…",
'phonesync.summary': "{group} → {dir}",
'phonesync.copies_sent': "已发送副本:{n}",
@@ -1371,7 +1367,6 @@ export default {
'phonesync.fail_not_private': "已停止:有其他人加入了群组。",
'phonesync.fail_permission': "已停止:MeshBay 已无法访问。",
'phonesync.fail_other': "已停止。",
- 'phonesync.open_settings': "打开应用设置",
'drivesync.heading': "文件备份",
'drivesync.intro': "每天一次通过 Wi-Fi 发送你在本机选择的文件夹中的文件。在手机上修改过的文件会在之前的副本旁边再次发送;不会替换任何内容。",
'drivesync.folders': "已备份的文件夹",
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js
index 998a3b2..9ee8b2b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync-settings.js
@@ -1,4 +1,4 @@
-// The contacts, calendar and messages sections of the Android Sync page, on
+// The contacts and calendar sections of the Android Sync page, on
// the Android application only (docs/MESHBAY_DESIGN.md §9.13). Loaded on
// demand: no other client has it. Where the copies go is the page's destination section; these
// only turn each backup on or off.
@@ -17,13 +17,6 @@ const TEXT = {
intro: 'calendarsync.intro',
permission: 'calendarsync.permission_needed',
},
- messages: {
- intro: 'messagesync.intro',
- permission: 'messagesync.permission_needed',
- // Android keeps text messages behind "restricted settings" for an
- // application installed outside a store: said, with the way to lift it.
- restricted: 'messagesync.restricted_hint',
- },
};
function stateLine(st, status) {
@@ -87,12 +80,7 @@ export function DocSyncSection({ kind, destination }) {
<p class="settings-hint">${t(TEXT[kind].intro)}</p>
${!destination && html`<p class="settings-hint">${t('androidsync.dest_needed')}</p>`}
${destination && status.permission !== 'granted' && html`
- <p class="settings-hint">${t(TEXT[kind].permission)}</p>
- ${TEXT[kind].restricted && html`
- <p class="settings-hint">${t(TEXT[kind].restricted)}</p>
- <button class="btn-secondary" style="margin-bottom:8px"
- onClick=${() => sync.openSettings().catch((e) => setError(e.message))}>
- ${t('phonesync.open_settings')}</button>`}`}
+ <p class="settings-hint">${t(TEXT[kind].permission)}</p>`}
${destination && html`
<button class="admin-btn" disabled=${busy} onClick=${turnOn}>${t('phonesync.turn_on')}</button>`}
`}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/phone-sync.js b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync.js
index dcfb6f9..26334a8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/phone-sync.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/phone-sync.js
@@ -1,6 +1,5 @@
-// Contacts, calendar and messages backup: a copy of this phone's address book
-// and calendars, and the text messages added since the last copy, sent to
-// `<folder>/<account>-contacts`, `-calendar` and `-messages` of the destination
+// Contacts and calendar backup: a copy of this phone's address book and
+// calendars, sent to `<folder>/<account>-contacts` and `-calendar` of the destination
// every backup shares (sync-destination.js), once a day when there is
// something new (docs/MESHBAY_DESIGN.md §9.13).
//
@@ -32,7 +31,6 @@ const RETRY_MS = 15 * 60 * 1000;
export const KINDS = {
contacts: { sync: platform.contactSync, heading: 'contactsync.heading', suffix: 'contacts' },
calendar: { sync: platform.calendarSync, heading: 'calendarsync.heading', suffix: 'calendar' },
- messages: { sync: platform.messageSync, heading: 'messagesync.heading', suffix: 'messages' },
};
// Refusals that will hold at the next run too: said once (a notification,
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js
index 9a45cf8..3cee220 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/photo-sync.js
@@ -303,7 +303,7 @@ export const isDue = (status, now) => photos.isDue(status, now);
/**
* One photo, under a slot the node granted: the backup queues like anyone.
- * `file` is how its bytes come from the phone; contacts and messages pass theirs.
+ * `file` is how its bytes come from the phone; contacts and the calendar pass theirs.
*/
export async function upload(transport, item, file = platform.photoSync.file) {
const lease = transport.openTransfer({ kind: 'upload', bytes: item.size });
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
index cf2acf3..3a91417 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
@@ -356,7 +356,7 @@ export const phoneSync = {
};
/**
- * Backing up one kind of this phone's data (contacts, calendar, messages) to the destination
+ * Backing up one kind of this phone's data (contacts, calendar) to the destination
* every kind shares (docs/MESHBAY_DESIGN.md §9.13). Android only, like
* `photoSync`. See phone-sync.js.
*/
@@ -371,7 +371,6 @@ function docSync(name) {
sent(token, dir, n) { return b.sent(token, dir, n); },
completed() { return b.completed(); },
failed(code, text) { return b.failed(code, text); },
- openSettings() { return b.openSettings(); },
/** The file the last plan wrote, as a File named for the node. */
async file(token, fileName) {
const r = await fetch(`/phonesync/${encodeURIComponent(token)}`, { cache: 'no-store' });
@@ -383,8 +382,6 @@ function docSync(name) {
export const contactSync = docSync('contactSync');
export const calendarSync = docSync('calendarSync');
-/** Absent from the Play build, whose policy keeps text messages from it. */
-export const messageSync = docSync('messageSync');
/** Pick a directory to add as a group root. Returns { path, name } or null. */
export const rootPicker = {
diff --git a/packages/meshbay-hub/tests/harness/photo_sync_probe.py b/packages/meshbay-hub/tests/harness/photo_sync_probe.py
index 122103a..ac3365c 100644
--- a/packages/meshbay-hub/tests/harness/photo_sync_probe.py
+++ b/packages/meshbay-hub/tests/harness/photo_sync_probe.py
@@ -129,15 +129,6 @@ window.meshbay = {
manifestSent: async () => true, completed: async () => ({}), failed: async () => true,
keepAlive: async () => true,
},
- messageSync: {
- status: async () => ({
- permission: 'granted', on: MODE === 'configured', destination: dest,
- dir: dest && '__FOLDER__/bob-messages', sent: 2, lastCompleted: Date.now(),
- lastSent: dest && '__FOLDER__/bob-messages/2026/sms-2026-10-10-0900.xml', failure: null,
- }),
- permit: async () => ({}), configure: async () => ({}), plan: async () => ({ item: null }),
- sent: async () => true, completed: async () => ({}), failed: async () => true,
- },
};
</script>
<main class="main"><div id=root></div></main>
diff --git a/packages/meshbay-hub/tests/test_android_shell.py b/packages/meshbay-hub/tests/test_android_shell.py
index c68234d..520fa7e 100644
--- a/packages/meshbay-hub/tests/test_android_shell.py
+++ b/packages/meshbay-hub/tests/test_android_shell.py
@@ -139,7 +139,7 @@ def test_the_shim_offers_desktop_channels_and_native_answers_each():
SRC / "cast" / "CastChannels.kt", SRC / "notify" / "PushChannels.kt",
SRC / "photos" / "PhotoChannels.kt", SRC / "phonesync" / "Destination.kt"):
native |= set(re.findall(r'^\s*"([\w:-]+)" ->', _read(path), flags=re.M))
- # Contacts and messages share one class, its channels named by a prefix.
+ # Contacts and the calendar share one class, its channels named by a prefix.
docs = set(re.findall(r'^\s*"([\w-]+)" ->', _read(SRC / "phonesync" / "DocChannels.kt"), flags=re.M))
for prefix in re.findall(r'DocChannels\("(\w+)"', _read(SRC / "MainActivity.kt")):
native |= {f"{prefix}:{name}" for name in docs}
@@ -153,7 +153,7 @@ def test_the_shim_offers_desktop_channels_and_native_answers_each():
# address book to back up, and a desktop has none of them; those families are the ones the desktop lacks
# rather than the ones it shares.
phone_only = {c for c in shim if c.startswith(("push:", "photosync:", "phonesync:", "contactsync:",
- "calendarsync:", "messagesync:", "drivesync:",
+ "calendarsync:", "drivesync:",
"wasync:"))}
assert any(c.startswith("push:") for c in phone_only), "the push channels are gone"
assert any(c.startswith("photosync:") for c in phone_only), "the backup channels are gone"
@@ -277,30 +277,13 @@ def test_only_the_personal_profile_is_backed_up():
"whatsappSync:"):
assert name in gated, name
for path in [*(SRC / "phonesync").rglob("*.kt"), *(SRC / "photos").rglob("*.kt"),
- *(SRC / "drive").rglob("*.kt"),
- *(APP / "src" / "full").rglob("*.kt")]:
+ *(SRC / "drive").rglob("*.kt")]:
assert not re.search(r"\.ENTERPRISE_\w+", _read(path)), path.name
-def test_the_play_build_cannot_read_text_messages():
- """Play's policy keeps READ_SMS for the default SMS application: the Play
- build has neither the permission nor the code that reads messages."""
- main = _read(APP / "src" / "main" / "AndroidManifest.xml")
- assert "READ_SMS" not in main
- assert "READ_SMS" in _read(APP / "src" / "full" / "AndroidManifest.xml")
- assert not (APP / "src" / "play" / "AndroidManifest.xml").exists() or \
- "READ_SMS" not in _read(APP / "src" / "play" / "AndroidManifest.xml")
- play = list((APP / "src" / "play").rglob("*.kt"))
- assert play and all("Telephony" not in _read(f) and "SmsSource" not in _read(f) for f in play)
- assert not list((APP / "src" / "main").rglob("SmsSource.kt"))
-
-
@pytest.mark.skipif(not os.environ.get("ANDROID_HOME") or shutil.which("java") is None,
reason="no Android SDK in the environment")
def test_the_jvm_unit_tests_pass():
- # The tests are shared; the Play build is compiled too, so a flavor file
- # missing from it fails here rather than at release time.
- result = subprocess.run(["./gradlew", "--no-daemon", "-q", "testFullDebugUnitTest",
- "compilePlayDebugKotlin"],
+ result = subprocess.run(["./gradlew", "--no-daemon", "-q", "testDebugUnitTest"],
cwd=ANDROID, capture_output=True, text=True, timeout=900)
assert result.returncode == 0, result.stdout[-3000:] + result.stderr[-3000:]
diff --git a/packages/meshbay-hub/tests/test_phone_sync.py b/packages/meshbay-hub/tests/test_phone_sync.py
index 0798ef0..1a35614 100644
--- a/packages/meshbay-hub/tests/test_phone_sync.py
+++ b/packages/meshbay-hub/tests/test_phone_sync.py
@@ -99,13 +99,6 @@ win.meshbay = { contactSync: {
return fake.status;
},
failed: async (code, text) => { fake.log.push(['failed', code, text]); return true; },
-}, messageSync: {
- status: async () => ({ ...fake.status, on: !!fake.messagesOn, dir: 'Backups/bob-messages' }),
- plan: async () => ({ item: { token: 'm1', name: 'sms-2026-10-10-0900.xml',
- dir: 'Backups/bob-messages/2026', size: 4, count: 300 } }),
- sent: async (token, dir, name) => { fake.log.push(['sent', token, dir, name]); return true; },
- completed: async () => { fake.log.push(['completed']); return fake.status; },
- failed: async (code, text) => { fake.log.push(['failed', code, text]); return true; },
} };
"""
@@ -184,18 +177,6 @@ await m.runDocSync('contacts', { force: true });
assert len(_of(log, "upload")) == 1
-def test_messages_go_under_the_year_in_their_own_folder(tmp_path):
- log = _run("""
-fake.messagesOn = true;
-m.startPhoneSync(ctx);
-await m.runDocSync('contacts');
-await m.runDocSync('messages');
-""", tmp_path)
- assert _of(log, "mkdir")[-2:] == [["Backups/bob-messages"], ["Backups/bob-messages/2026"]]
- assert ["upload", "Backups/bob-messages/2026", "sms-2026-10-10-0900.xml", "tr-4"] in log
- assert ["sent", "m1", "Backups/bob-messages/2026", "sms-2026-10-10-0900.xml"] in log
-
-
# ── alone in the group, at every run ────────────────────────────────────────
def test_the_group_is_asked_about_at_every_run(tmp_path):
diff --git a/packages/meshbay-hub/tests/test_photo_sync_ui.py b/packages/meshbay-hub/tests/test_photo_sync_ui.py
index da0ceeb..f432fe9 100644
--- a/packages/meshbay-hub/tests/test_photo_sync_ui.py
+++ b/packages/meshbay-hub/tests/test_photo_sync_ui.py
@@ -57,7 +57,7 @@ def test_limited_access_is_said_rather_than_failing_quietly(probe):
def test_where_backups_go_lists_each_kinds_folder(probe):
text = probe["dest360"]["text"]
- assert all(f"Media/bob-{k}" in text for k in ("photos", "contacts", "calendar", "messages", "drive", "whatsapp"))
+ assert all(f"Media/bob-{k}" in text for k in ("photos", "contacts", "calendar", "drive", "whatsapp"))
def test_photos_the_photos_tab_will_not_show_are_said(probe):
@@ -68,10 +68,6 @@ def test_the_contacts_backup_says_where_its_copies_go(probe):
assert "bob-contacts/contacts-2026-10-10-0900.vcf" in probe["configured360"]["text"]
-def test_the_messages_backup_says_where_its_copies_go(probe):
- assert "bob-messages/2026/sms-2026-10-10-0900.xml" in probe["configured360"]["text"]
-
-
def test_the_calendar_backup_says_where_its_copies_go(probe):
assert "bob-calendar/calendar-2026-10-10-0900.ics" in probe["configured360"]["text"]