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 }
}
}
}
}
|