aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/full/kotlin/org/meshbay
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-android/app/src/full/kotlin/org/meshbay')
-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
2 files changed, 80 insertions, 0 deletions
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
new file mode 100644
index 0000000..20882a5
--- /dev/null
+++ b/packages/meshbay-android/app/src/full/kotlin/org/meshbay/client/phonesync/Flavor.kt
@@ -0,0 +1,8 @@
+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
new file mode 100644
index 0000000..c24ef1f
--- /dev/null
+++ b/packages/meshbay-android/app/src/full/kotlin/org/meshbay/client/phonesync/SmsSource.kt
@@ -0,0 +1,72 @@
+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 }
+ }
+ }
+ }
+}