Annual refactor
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import de.mm20.launcher2.icons.ColorLayer
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.icons.StaticIconLayer
|
||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||
import de.mm20.launcher2.icons.TextLayer
|
||||
import de.mm20.launcher2.ktx.asBitmap
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.ContactInfo
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal data class AndroidContact(
|
||||
internal val id: Long,
|
||||
override val firstName: String,
|
||||
override val lastName: String,
|
||||
override val displayName: String,
|
||||
override val contactInfos: Iterable<ContactInfo>,
|
||||
internal val lookupKey: String,
|
||||
override val labelOverride: String? = null,
|
||||
) : Contact {
|
||||
|
||||
|
||||
override val domain: String = Domain
|
||||
override val key: String
|
||||
get() = "$Domain://$id"
|
||||
override val label: String
|
||||
get() = displayName.takeIf { it.isNotBlank() } ?: "$firstName $lastName"
|
||||
|
||||
override val summary: String
|
||||
get() {
|
||||
return contactInfos.distinctBy { it.label }.take(5).joinToString(separator = ", ") { it.label }
|
||||
}
|
||||
|
||||
override fun overrideLabel(label: String): Contact {
|
||||
return copy(labelOverride = label)
|
||||
}
|
||||
|
||||
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||
val uri =
|
||||
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id)
|
||||
val intent = Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
return context.tryStartActivity(intent, options)
|
||||
}
|
||||
|
||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||
val iconText =
|
||||
if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else ""
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = TextLayer(text = iconText, color = 0xFF2364AA.toInt()),
|
||||
backgroundLayer = ColorLayer(0xFF2364AA.toInt())
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun loadIcon(
|
||||
context: Context,
|
||||
size: Int,
|
||||
themed: Boolean,
|
||||
): LauncherIcon? {
|
||||
val contentResolver = context.contentResolver
|
||||
val bmp = withContext(Dispatchers.IO) {
|
||||
val uri =
|
||||
ContactsContract.Contacts.getLookupUri(id, lookupKey) ?: return@withContext null
|
||||
ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, false)
|
||||
?.asBitmap()
|
||||
} ?: return null
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = StaticIconLayer(
|
||||
icon = bmp.toDrawable(context.resources),
|
||||
),
|
||||
backgroundLayer = ColorLayer(0xFF2364AA.toInt())
|
||||
)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return ContactSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "contact"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.ContactsContract
|
||||
import de.mm20.launcher2.search.ContactInfo
|
||||
import de.mm20.launcher2.search.ContactInfoType
|
||||
import java.net.URLEncoder
|
||||
|
||||
|
||||
internal data class PhoneContactInfo(
|
||||
val number: String,
|
||||
) : ContactInfo {
|
||||
override val label: String
|
||||
get() = number
|
||||
|
||||
override val type: ContactInfoType = ContactInfoType.Phone
|
||||
|
||||
override val intent: Intent
|
||||
get() = Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse("tel:$number"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
internal data class MailContactInfo(
|
||||
val address: String,
|
||||
) : ContactInfo {
|
||||
override val label: String
|
||||
get() = address
|
||||
|
||||
override val type: ContactInfoType = ContactInfoType.Email
|
||||
|
||||
override val intent: Intent
|
||||
get() = Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse("mailto:$address"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
internal data class PostalContactInfo(
|
||||
val address: String,
|
||||
) : ContactInfo {
|
||||
override val label: String
|
||||
get() = address.replace("\n", ", ")
|
||||
|
||||
override val type: ContactInfoType = ContactInfoType.Postal
|
||||
|
||||
override val intent: Intent
|
||||
get() = Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse("geo:0,0?q=${URLEncoder.encode(address, "utf8")}"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
internal data class TelegramContactInfo(
|
||||
override val label: String,
|
||||
val userId: String,
|
||||
) : ContactInfo {
|
||||
|
||||
override val type: ContactInfoType = ContactInfoType.Telegram
|
||||
|
||||
override val intent: Intent
|
||||
get() = Intent(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse("tg:openmessage?user_id=$userId"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
internal companion object {
|
||||
const val ItemType = "vnd.android.cursor.item/vnd.org.telegram.messenger.android.profile"
|
||||
}
|
||||
}
|
||||
|
||||
internal data class SignalContactInfo(
|
||||
override val label: String,
|
||||
val dataId: Long,
|
||||
) : ContactInfo {
|
||||
|
||||
override val type: ContactInfoType = ContactInfoType.Signal
|
||||
|
||||
override val intent: Intent
|
||||
get() = Intent(Intent.ACTION_VIEW)
|
||||
.setData(
|
||||
Uri.withAppendedPath(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
dataId.toString()
|
||||
)
|
||||
)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
internal companion object {
|
||||
const val ItemType = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact"
|
||||
}
|
||||
}
|
||||
|
||||
internal data class WhatsAppContactInfo(
|
||||
override val label: String,
|
||||
val dataId: Long,
|
||||
) : ContactInfo {
|
||||
|
||||
override val type: ContactInfoType = ContactInfoType.Whatsapp
|
||||
|
||||
override val intent: Intent
|
||||
get() = Intent(Intent.ACTION_VIEW)
|
||||
.setData(
|
||||
Uri.withAppendedPath(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
dataId.toString()
|
||||
)
|
||||
)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
internal companion object {
|
||||
const val ItemType = "vnd.android.cursor.item/vnd.com.whatsapp.profile"
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,12 @@ package de.mm20.launcher2.contacts
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.data.Contact
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.ContactInfo
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -12,14 +15,143 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
interface ContactRepository {
|
||||
fun search(query: String): Flow<ImmutableList<Contact>>
|
||||
}
|
||||
|
||||
internal class ContactRepositoryImpl(
|
||||
internal class ContactRepository(
|
||||
private val context: Context,
|
||||
private val permissionsManager: PermissionsManager
|
||||
) : ContactRepository {
|
||||
) : SearchableRepository<Contact> {
|
||||
|
||||
fun get(id: Long): Flow<Contact?> = flow {
|
||||
val rawContactsCursor = context.contentResolver.query(
|
||||
ContactsContract.RawContacts.CONTENT_URI,
|
||||
arrayOf(ContactsContract.RawContacts._ID),
|
||||
"${ContactsContract.RawContacts.CONTACT_ID} = ?",
|
||||
arrayOf(id.toString()),
|
||||
null
|
||||
)
|
||||
if (rawContactsCursor == null) {
|
||||
emit(null)
|
||||
return@flow
|
||||
}
|
||||
val rawContacts = mutableSetOf<Long>()
|
||||
while (rawContactsCursor.moveToNext()) {
|
||||
rawContacts.add(rawContactsCursor.getLong(0))
|
||||
}
|
||||
rawContactsCursor.close()
|
||||
if (rawContacts.isEmpty()) {
|
||||
emit(null)
|
||||
return@flow
|
||||
}
|
||||
emit(getWithRawIds(id, rawContacts))
|
||||
}
|
||||
|
||||
private suspend fun getWithRawIds(id: Long, rawIds: Set<Long>): Contact? = withContext(Dispatchers.IO) {
|
||||
val s = "(" + rawIds.joinToString(separator = " OR ",
|
||||
transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" +
|
||||
" AND (${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${TelegramContactInfo.ItemType}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${WhatsAppContactInfo.ItemType}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${SignalContactInfo.ItemType}\"" +
|
||||
")"
|
||||
val dataCursor = context.contentResolver.query(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
null, s, null, null
|
||||
) ?: return@withContext null
|
||||
val contactInfos = mutableSetOf<ContactInfo>()
|
||||
var firstName = ""
|
||||
var lastName = ""
|
||||
var displayName = ""
|
||||
val mimeTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.MIMETYPE)
|
||||
val emailAddressColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)
|
||||
val numberColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||
val addressColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS)
|
||||
val displayNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)
|
||||
val givenNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)
|
||||
val familyNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)
|
||||
val data1Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA1)
|
||||
val data3Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA3)
|
||||
val idColumn = dataCursor.getColumnIndex(ContactsContract.Data._ID)
|
||||
loop@ while (dataCursor.moveToNext()) {
|
||||
when (dataCursor.getStringOrNull(mimeTypeColumn)) {
|
||||
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(emailAddressColumn)?.let {
|
||||
contactInfos.add(MailContactInfo(it))
|
||||
}
|
||||
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(numberColumn)?.let {
|
||||
val phone = it.replace(Regex("[^+0-9]"), "")
|
||||
contactInfos.add(PhoneContactInfo(phone))
|
||||
}
|
||||
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(addressColumn)?.let {
|
||||
contactInfos.add(PostalContactInfo(it))
|
||||
}
|
||||
|
||||
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
|
||||
firstName = dataCursor.getStringOrNull(givenNameColumn) ?: ""
|
||||
lastName = dataCursor.getStringOrNull(familyNameColumn) ?: ""
|
||||
displayName = dataCursor.getStringOrNull(displayNameColumn) ?: ""
|
||||
}
|
||||
|
||||
TelegramContactInfo.ItemType -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val data3 = dataCursor.getStringOrNull(data3Column)
|
||||
?: continue@loop
|
||||
contactInfos.add(
|
||||
TelegramContactInfo(data3.substringAfterLast(" "), data1)
|
||||
)
|
||||
}
|
||||
|
||||
WhatsAppContactInfo.ItemType -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val dataId = dataCursor.getLong(idColumn)
|
||||
contactInfos.add(WhatsAppContactInfo("+${data1.substringBefore('@')}", dataId))
|
||||
}
|
||||
|
||||
SignalContactInfo.ItemType -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val dataId = dataCursor.getLong(idColumn)
|
||||
contactInfos.add(SignalContactInfo(data1, dataId))
|
||||
}
|
||||
}
|
||||
}
|
||||
dataCursor.close()
|
||||
|
||||
val lookupKeyCursor = context.contentResolver.query(
|
||||
ContactsContract.Contacts.CONTENT_URI,
|
||||
arrayOf(ContactsContract.Contacts.LOOKUP_KEY),
|
||||
"${ContactsContract.Contacts._ID} = ?",
|
||||
arrayOf(id.toString()),
|
||||
null
|
||||
) ?: return@withContext null
|
||||
var lookUpKey = ""
|
||||
if (lookupKeyCursor.moveToNext()) {
|
||||
lookUpKey = lookupKeyCursor.getString(0)
|
||||
}
|
||||
lookupKeyCursor.close()
|
||||
|
||||
return@withContext AndroidContact(
|
||||
id = id,
|
||||
firstName = firstName,
|
||||
lastName = lastName,
|
||||
displayName = displayName,
|
||||
contactInfos = contactInfos,
|
||||
lookupKey = lookUpKey
|
||||
)
|
||||
}
|
||||
|
||||
override fun search(query: String): Flow<ImmutableList<Contact>> {
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
|
||||
@@ -45,7 +177,8 @@ internal class ContactRepositoryImpl(
|
||||
ContactsContract.RawContacts.CONTACT_ID,
|
||||
ContactsContract.RawContacts._ID
|
||||
)
|
||||
val sel = "${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ? OR ${ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE} LIKE ? OR ${ContactsContract.RawContacts.PHONETIC_NAME} LIKE ?"
|
||||
val sel =
|
||||
"${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ? OR ${ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE} LIKE ? OR ${ContactsContract.RawContacts.PHONETIC_NAME} LIKE ?"
|
||||
val selArgs = arrayOf("%$query%", "%$query%", "%$query%")
|
||||
val cursor = context.contentResolver.query(
|
||||
ContactsContract.RawContacts.CONTENT_URI, proj, sel, selArgs, null
|
||||
@@ -58,7 +191,7 @@ internal class ContactRepositoryImpl(
|
||||
cursor.close()
|
||||
val results = mutableListOf<Contact>()
|
||||
for ((id, rawIds) in contactMap) {
|
||||
Contact.contactById(context, id, rawIds)?.let { results.add(it) }
|
||||
getWithRawIds(id, rawIds)?.let { results.add(it) }
|
||||
if (results.size > 15) break
|
||||
}
|
||||
results
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.data.Contact
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.json.JSONObject
|
||||
|
||||
class ContactSerializer : SearchableSerializer {
|
||||
internal class ContactSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as Contact
|
||||
searchable as AndroidContact
|
||||
return jsonObjectOf(
|
||||
"id" to searchable.id
|
||||
).toString()
|
||||
@@ -24,28 +21,15 @@ class ContactSerializer : SearchableSerializer {
|
||||
get() = "contact"
|
||||
}
|
||||
|
||||
class ContactDeserializer(val context: Context) : SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable? {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.READ_CONTACTS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) return null
|
||||
val id = JSONObject(serialized).getLong("id")
|
||||
val rawContactsCursor = context.contentResolver.query(
|
||||
ContactsContract.RawContacts.CONTENT_URI,
|
||||
arrayOf(ContactsContract.RawContacts._ID),
|
||||
"${ContactsContract.RawContacts.CONTACT_ID} = ?",
|
||||
arrayOf(id.toString()),
|
||||
null
|
||||
) ?: return null
|
||||
val rawContacts = mutableSetOf<Long>()
|
||||
while (rawContactsCursor.moveToNext()) {
|
||||
rawContacts.add(rawContactsCursor.getLong(0))
|
||||
}
|
||||
rawContactsCursor.close()
|
||||
if (rawContacts.isEmpty()) return null
|
||||
internal class ContactDeserializer(
|
||||
private val contactRepository: ContactRepository,
|
||||
private val permissionsManager: PermissionsManager
|
||||
) : SearchableDeserializer {
|
||||
|
||||
return Contact.contactById(context, id, rawContacts)
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Contacts)) return null
|
||||
val id = JSONObject(serialized).getLong("id")
|
||||
|
||||
return contactRepository.get(id).first()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val contactsModule = module {
|
||||
single<ContactRepository> { ContactRepositoryImpl(androidContext(), get()) }
|
||||
factory { ContactRepository(androidContext(), get()) }
|
||||
factory<SearchableRepository<Contact>>(named<Contact>()) { ContactRepository(androidContext(), get()) }
|
||||
factory<SearchableDeserializer>(named(AndroidContact.Domain)) { ContactDeserializer(get(), get()) }
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.database.getStringOrNull
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import de.mm20.launcher2.icons.*
|
||||
import de.mm20.launcher2.ktx.asBitmap
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URLEncoder
|
||||
|
||||
data class Contact(
|
||||
val id: Long,
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val displayName: String,
|
||||
val lookupKey: String,
|
||||
val phones: Set<ContactInfo>,
|
||||
val emails: Set<ContactInfo>,
|
||||
val telegram: Set<ContactInfo>,
|
||||
val whatsapp: Set<ContactInfo>,
|
||||
val signal: Set<ContactInfo>,
|
||||
val postals: Set<ContactInfo>,
|
||||
override val labelOverride: String? = null
|
||||
) : Searchable, SavableSearchable {
|
||||
|
||||
override val domain: String = Domain
|
||||
override val key: String
|
||||
get() = "${Domain}://$id"
|
||||
override val label: String
|
||||
get() = "$firstName $lastName"
|
||||
|
||||
override fun overrideLabel(label: String): Contact {
|
||||
return this.copy(labelOverride = label)
|
||||
}
|
||||
|
||||
override val preferDetailsOverLaunch: Boolean = true
|
||||
|
||||
val summary: String
|
||||
get() {
|
||||
return phones.union(emails).joinToString(separator = ", ") { it.label }
|
||||
}
|
||||
|
||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||
val iconText =
|
||||
if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else ""
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = TextLayer(text = iconText, color = 0xFF2364AA.toInt()),
|
||||
backgroundLayer = ColorLayer(0xFF2364AA.toInt())
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun loadIcon(
|
||||
context: Context,
|
||||
size: Int,
|
||||
themed: Boolean,
|
||||
): LauncherIcon? {
|
||||
val contentResolver = context.contentResolver
|
||||
val bmp = withContext(Dispatchers.IO) {
|
||||
val uri =
|
||||
ContactsContract.Contacts.getLookupUri(id, lookupKey) ?: return@withContext null
|
||||
ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, false)
|
||||
?.asBitmap()
|
||||
} ?: return null
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = StaticIconLayer(
|
||||
icon = bmp.toDrawable(context.resources),
|
||||
),
|
||||
backgroundLayer = ColorLayer(0xFF2364AA.toInt())
|
||||
)
|
||||
}
|
||||
|
||||
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||
val intent = getLaunchIntent()
|
||||
return context.tryStartActivity(intent, options)
|
||||
}
|
||||
|
||||
private fun getLaunchIntent(): Intent {
|
||||
val uri =
|
||||
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id)
|
||||
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
internal fun contactById(context: Context, id: Long, rawIds: Set<Long>): Contact? {
|
||||
val s = "(" + rawIds.joinToString(separator = " OR ",
|
||||
transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" +
|
||||
" AND (${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"vnd.android.cursor.item/vnd.org.telegram.messenger.android.profile\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"vnd.android.cursor.item/vnd.com.whatsapp.profile\"" +
|
||||
" OR ${ContactsContract.Data.MIMETYPE} = \"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact\"" +
|
||||
")"
|
||||
val dataCursor = context.contentResolver.query(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
null, s, null, null
|
||||
) ?: return null
|
||||
val phones = mutableSetOf<ContactInfo>()
|
||||
val emails = mutableSetOf<ContactInfo>()
|
||||
val telegram = mutableSetOf<ContactInfo>()
|
||||
val whatsapp = mutableSetOf<ContactInfo>()
|
||||
val signal = mutableSetOf<ContactInfo>()
|
||||
val postals = mutableSetOf<ContactInfo>()
|
||||
var firstName = ""
|
||||
var lastName = ""
|
||||
var displayName = ""
|
||||
val mimeTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.MIMETYPE)
|
||||
val emailAddressColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)
|
||||
val numberColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||
val addressColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS)
|
||||
val displayNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)
|
||||
val givenNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)
|
||||
val familyNameColumn =
|
||||
dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)
|
||||
val data1Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA1)
|
||||
val data3Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA3)
|
||||
val idColumn = dataCursor.getColumnIndex(ContactsContract.Data._ID)
|
||||
loop@ while (dataCursor.moveToNext()) {
|
||||
when (dataCursor.getStringOrNull(mimeTypeColumn)) {
|
||||
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(emailAddressColumn)?.let {
|
||||
emails.add(ContactInfo(it, "mailto:$it"))
|
||||
}
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(numberColumn)?.let {
|
||||
val phone = it.replace(Regex("[^+0-9]"), "")
|
||||
phones.add(
|
||||
ContactInfo(
|
||||
phone,
|
||||
"tel:$phone"
|
||||
)
|
||||
)
|
||||
}
|
||||
ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE ->
|
||||
dataCursor.getStringOrNull(addressColumn)?.let {
|
||||
postals.add(
|
||||
ContactInfo(
|
||||
it.replace("\n", ", "),
|
||||
"geo:0,0?q=${URLEncoder.encode(it, "utf8")}"
|
||||
)
|
||||
)
|
||||
}
|
||||
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
|
||||
firstName = dataCursor.getStringOrNull(givenNameColumn) ?: ""
|
||||
lastName = dataCursor.getStringOrNull(familyNameColumn) ?: ""
|
||||
displayName = dataCursor.getStringOrNull(displayNameColumn) ?: ""
|
||||
}
|
||||
"vnd.android.cursor.item/vnd.org.telegram.messenger.android.profile" -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val data3 = dataCursor.getStringOrNull(data3Column)
|
||||
?: continue@loop
|
||||
telegram.add(
|
||||
ContactInfo(
|
||||
data3.substringAfterLast(" "),
|
||||
"tg:openmessage?user_id=$data1"
|
||||
)
|
||||
)
|
||||
}
|
||||
"vnd.android.cursor.item/vnd.com.whatsapp.profile" -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val dataId = dataCursor.getLong(idColumn)
|
||||
whatsapp.add(
|
||||
ContactInfo(
|
||||
"+${data1.substringBefore('@')}",
|
||||
Uri.withAppendedPath(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
dataId.toString()
|
||||
).toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact" -> {
|
||||
val data1 = dataCursor.getStringOrNull(data1Column)
|
||||
?: continue@loop
|
||||
val dataId = dataCursor.getLong(idColumn)
|
||||
signal.add(
|
||||
ContactInfo(
|
||||
data1,
|
||||
Uri.withAppendedPath(
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
dataId.toString()
|
||||
).toString(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
dataCursor.close()
|
||||
|
||||
val lookupKeyCursor = context.contentResolver.query(
|
||||
ContactsContract.Contacts.CONTENT_URI,
|
||||
arrayOf(ContactsContract.Contacts.LOOKUP_KEY),
|
||||
"${ContactsContract.Contacts._ID} = ?",
|
||||
arrayOf(id.toString()),
|
||||
null
|
||||
) ?: return null
|
||||
var lookUpKey = ""
|
||||
if (lookupKeyCursor.moveToNext()) {
|
||||
lookUpKey = lookupKeyCursor.getString(0)
|
||||
}
|
||||
lookupKeyCursor.close()
|
||||
|
||||
return Contact(
|
||||
id = id,
|
||||
emails = emails,
|
||||
phones = phones,
|
||||
firstName = firstName,
|
||||
lastName = lastName,
|
||||
displayName = displayName,
|
||||
postals = postals,
|
||||
telegram = telegram,
|
||||
whatsapp = whatsapp,
|
||||
signal = signal,
|
||||
lookupKey = lookUpKey
|
||||
)
|
||||
}
|
||||
|
||||
const val Domain = "contact"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class ContactInfo(
|
||||
val label: String,
|
||||
val data: String
|
||||
)
|
||||
Reference in New Issue
Block a user