aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-android/app')
-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
14 files changed, 11 insertions, 292 deletions
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))
- }
-}