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, `-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() 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() 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 } } } } }