aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/full/kotlin/org/meshbay/client/phonesync/SmsSource.kt
blob: c24ef1fea4623e31566175683d063901d219b738 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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 }
            }
        }
    }
}