package com.tutpro.baresip import android.content.Context import android.database.Cursor import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.provider.ContactsContract import androidx.core.net.toUri import com.tutpro.baresip.BaresipService.Companion.contactNames import java.io.File import java.util.ArrayList sealed class Contact { data class ContactUri(var uri: String, var label: String = "") abstract fun name(): String abstract fun id(): Long abstract fun favorite(): Boolean abstract fun colorInt(): Int abstract fun uris(): List abstract fun email(): String class BaresipContact(var name: String, val uris: ArrayList, var email: String, var color: Int, var id: Long, var favorite: Boolean): Contact() { var avatarImage: Bitmap? = null override fun name() = name override fun id() = id override fun favorite() = favorite override fun colorInt() = color override fun uris() = uris override fun email() = email } class AndroidContact(var name: String, val uris: ArrayList, var email: String, var color: Int, var thumbnailUri: Uri?, var id: Long, var favorite: Boolean): Contact() { override fun name() = name override fun id() = id override fun favorite() = favorite override fun colorInt() = color override fun uris() = uris override fun email() = email } fun color(): String { // Mask with 0xFFFFFF to ensure we get the standard 6-character hex code (RRGGBB) // ignoring the alpha channel for compatibility with simple string parsers. return String.format("#%06X", 0xFFFFFF and colorInt()) } fun copy(): Contact { val copy = when (this) { is BaresipContact -> BaresipContact(name, ArrayList(uris), email, color, id, favorite) is AndroidContact -> AndroidContact(name, ArrayList(uris), email, color, thumbnailUri, id, favorite) } if (this is BaresipContact) (copy as BaresipContact).avatarImage = this.avatarImage return copy } companion object { // Return contact name of uri or uri itself if contact with uri is not found fun contactName(uri: String): String { var contact = findContact(uri) if (contact == null) { val userPart = Utils.uriUserPart(uri).replace("%23", "#") if (Utils.isTelNumber(userPart)) { contact = findContact("tel:$userPart") } } if (contact != null) return contact.name() return uri } fun baresipContact(name: String): BaresipContact? { synchronized(BaresipService.baresipContacts) { for (c in BaresipService.baresipContacts.value) if (c.name == name) return c } return null } fun androidContact(name: String): AndroidContact? { synchronized(BaresipService.androidContacts) { for (c in BaresipService.androidContacts.value) if (c.name == name) return c } return null } fun contactContactUris(name: String, tel: Boolean = false): List { synchronized(BaresipService.contacts) { for (c in BaresipService.contacts) when (c) { is BaresipContact -> { if (c.name.equals(name, ignoreCase = true)) { return if (tel) c.uris.filter { it.uri.startsWith("tel:") } else c.uris } } is AndroidContact -> { if (c.name == name) { return if (tel) c.uris.filter { it.uri.startsWith("tel:") } else c.uris } } } } return emptyList() } fun findContact(uri: String): Contact? { synchronized(BaresipService.contacts) { for (c in BaresipService.contacts) when (c) { is BaresipContact -> { for (u in c.uris) if (Utils.uriMatch(u.uri, uri)) return c } is AndroidContact -> { val cleanUri = uri.filterNot { setOf('-', ' ', '(', ')').contains(it) } for (u in c.uris) if (Utils.uriMatch( u.uri.filterNot { setOf('-', ' ', '(', ')').contains(it) }, cleanUri ) ) return c } } } return null } fun nameExists(name: String, list: List, ignoreCase: Boolean): Boolean { for (c in list) if (c.name().equals(name, ignoreCase = ignoreCase)) return true return false } fun saveBaresipContacts() { val avatarFiles = avatarFileNames() var contents = "" val contacts = synchronized(BaresipService.baresipContacts) { BaresipService.baresipContacts.value.toList() } for (c in contacts) { val uris = c.uris.joinToString(",") { if (it.label.isNotEmpty()) "${it.uri}[${it.label}]" else it.uri } contents += "\"${c.name}\" <$uris>;email=${c.email};id=${c.id}" + ";color=${c.color};favorite=${if (c.favorite) "yes" else "no"}\n" avatarFiles.remove(c.id.toString() + ".png") } Utils.putFileContents(BaresipService.filesPath + "/contacts", contents.toByteArray()) for (f in avatarFiles) File(BaresipService.filesPath + "/" + f).delete() } fun loadAndroidContacts(ctx: Context) { // If phone type is needed, add DATA2 to projection. Then phone type can be got from // cursor using getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE) val projection = arrayOf(ContactsContract.Data.CONTACT_ID, ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data.MIMETYPE, ContactsContract.Data.DATA1, ContactsContract.Data.DATA2, ContactsContract.Data.PHOTO_THUMBNAIL_URI, ContactsContract.Contacts.STARRED) val selection = ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + "' OR " + ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "' OR " + ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE + "'" val cur: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, null, null) BaresipService.androidContacts.value = listOf() val contacts = HashMap() while (cur != null && cur.moveToNext()) { val id = cur.getLong(0) val name = cur.getString(1) ?: "" val mime = cur.getString(2) val data = cur.getString(3) ?: continue val type = cur.getInt(4) val thumb = cur.getString(5)?.toUri() val starred = cur.getInt(6) val contact = if (contacts.containsKey(id)) contacts[id]!! else AndroidContact(name, ArrayList(), "", Utils.randomColor(), thumb, id, starred == 1) if (contact.name == "" && name != "") contact.name = name if (contact.thumbnailUri == null && thumb != null) contact.thumbnailUri = thumb when (mime) { ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> { val uri = "tel:${data.filterNot { setOf('-', ' ', '(', ')').contains(it) }}" val label = typeToString(type) if (contact.uris.none { it.uri == uri }) contact.uris.add(ContactUri(uri, label)) } ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> { val uri = "sip:$data" if (contact.uris.none { it.uri == uri }) contact.uris.add(ContactUri(uri, "")) } ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE -> { if (contact.email == "") contact.email = data } } if (!contacts.containsKey(id)) contacts[id] = contact } cur?.close() val newList = mutableListOf() for ((_, value) in contacts) if (value.name != "" && (value.uris.isNotEmpty() || value.email != "")) newList.add(value) BaresipService.androidContacts.value = newList.toList() } private fun typeToString(type: Int): String { return when(type) { ContactsContract.CommonDataKinds.Phone.TYPE_HOME -> "Home" ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE -> "Mobile" ContactsContract.CommonDataKinds.Phone.TYPE_WORK -> "Work" else -> "" } } fun stringToType(label: String): Int { return when (label) { "Home" -> ContactsContract.CommonDataKinds.Phone.TYPE_HOME "Mobile" -> ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE "Work" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK "" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER else -> ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM } } fun restoreBaresipContacts(): Boolean { val content = Utils.getFileContents(BaresipService.filesPath + "/contacts") ?: return false val contacts = String(content) var contactNo = 0 val baseId = System.currentTimeMillis() val newBaresipContacts = mutableListOf() contacts.lines().forEach { val parts = it.split("\"") if (parts.size == 3) { contactNo++ val name = parts[1] val uriParams = parts[2].trim() val urisPart = uriParams.substringAfter("<").substringBefore(">") val uris = ArrayList() for (uPart in urisPart.split(",")) { if (uPart.isEmpty()) continue if (uPart.contains("[") && uPart.contains("]")) { val uri = uPart.substringBefore("[") val label = uPart.substringAfter("[").substringBefore("]") uris.add(ContactUri(uri, label)) } else { uris.add(ContactUri(uPart, "")) } } val params = uriParams.substringAfter(">;") val email = Utils.paramValue(params, "email") val colorValue = Utils.paramValue(params, "color" ) val color: Int = if (colorValue != "") colorValue.toInt() else Utils.randomColor() val idValue = Utils.paramValue(params, "id" ) val id: Long = if (idValue != "") idValue.toLong() else baseId + contactNo val favorite = Utils.paramValue(params, "favorite" ) == "yes" // Log.d(TAG, "Restoring contact $name, $urisPart, $color, $id") val contact = BaresipContact(name, uris, email, color, id, favorite) val avatarFilePath = BaresipService.filesPath + "/$id.png" if (File(avatarFilePath).exists()) { try { contact.avatarImage = BitmapFactory.decodeFile(avatarFilePath) Log.d(TAG, "Set avatarImage") if (contact.avatarImage == null) Log.d(TAG, "Contact $id avatarImage is null") } catch (e: Exception) { Log.e(TAG, "Could not read avatar image from file $id.png: ${e.message}") } } newBaresipContacts.add(contact) } } BaresipService.baresipContacts.value = newBaresipContacts.toList() return true } fun contactsUpdate() { val newContacts = mutableListOf() if (BaresipService.contactsMode != "android") { val baresipContacts = synchronized(BaresipService.baresipContacts) { BaresipService.baresipContacts.value.toList() } for (c in baresipContacts) newContacts.add(c.copy()) } if (BaresipService.contactsMode != "baresip") { val androidContacts = synchronized(BaresipService.androidContacts) { BaresipService.androidContacts.value.toList() } for (c in androidContacts) if (!nameExists(c.name, newContacts, true)) newContacts.add(c.copy()) } newContacts.sortBy { when (it) { is BaresipContact -> if (it.favorite) "0" + it.name else "1" + it.name is AndroidContact -> if (it.favorite) "0" + it.name else "1" + it.name } } BaresipService.contacts = newContacts generateContactNames() } fun addBaresipContact(contact: BaresipContact) { synchronized(BaresipService.baresipContacts) { BaresipService.baresipContacts.value += contact } saveBaresipContacts() contactsUpdate() } fun updateBaresipContact(id: Long, contact: BaresipContact) { synchronized(BaresipService.baresipContacts) { val updatedContacts = BaresipService.baresipContacts.value.toMutableList() updatedContacts.removeIf { it.id == id } updatedContacts.add(contact) BaresipService.baresipContacts.value = updatedContacts.toList() } saveBaresipContacts() contactsUpdate() } fun removeBaresipContact(contact: BaresipContact) { synchronized(BaresipService.baresipContacts) { val removed = BaresipService.baresipContacts.value.toMutableList() removed.removeIf { it.id == contact.id } BaresipService.baresipContacts.value = removed.toList() } saveBaresipContacts() contactsUpdate() } private fun generateContactNames () { val newList = mutableListOf() for (c in BaresipService.contacts) when (c) { is BaresipContact -> newList.add(c.name) is AndroidContact -> newList.add(c.name) } contactNames.value = newList.toList() } private fun avatarFileNames(): MutableList { return File(BaresipService.filesPath).list()!!.filter{ it.endsWith(".png")}.toMutableList() } } }