package org.meshbay.client.phonesync import android.Manifest import android.content.Context import android.net.Uri import android.provider.ContactsContract import org.meshbay.client.photos.PhotoChannels import java.io.File import java.security.MessageDigest import java.util.TimeZone /** * The phone's contacts, as the platform itself exports them: one vCard per * contact from ContactsContract, the format any phone imports back. * * Every run writes the whole address book and sends it only when it differs * from the last one sent (`marker` is its SHA-256): a file is a complete * copy as of its date, so restoring is importing the newest one. */ class ContactSource(private val context: Context) : DocSource { override val permissions = arrayOf(Manifest.permission.READ_CONTACTS) override val suffix = "contacts" override fun export(marker: String?, dir: File, now: Long, zone: TimeZone): Export? { val keys = ArrayList() context.contentResolver.query( ContactsContract.Contacts.CONTENT_URI, arrayOf(ContactsContract.Contacts.LOOKUP_KEY), null, null, ContactsContract.Contacts._ID, )?.use { c -> while (c.moveToNext()) c.getString(0)?.let { keys += it } } if (keys.isEmpty()) return null val file = File(dir, "contacts.vcf") val digest = MessageDigest.getInstance("SHA-256") var count = 0 file.outputStream().buffered().use { out -> for (key in keys) { val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, Uri.encode(key)) // A contact removed between the list and its card is simply not in this copy. val bytes = try { context.contentResolver.openInputStream(uri)?.use { it.readBytes() } } catch (e: Exception) { null } ?: continue out.write(bytes) digest.update(bytes) count += 1 } } val sha = PhotoChannels.hex(digest.digest()) if (count == 0 || sha == marker) { file.delete(); return null } return Export(file, DocPlan.nameFor("contacts", "vcf", now, zone), "text/vcard", sha, count) } }