imports contacts helper & extensions

This commit is contained in:
fatih ergin
2023-09-18 23:51:34 +03:00
parent 307ed586d3
commit 2bfeae72cf
8 changed files with 3336 additions and 38 deletions

View File

@@ -5,6 +5,7 @@ import org.jetbrains.kotlin.konan.properties.Properties
plugins {
alias(libs.plugins.android)
alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.ksp)
}
@@ -88,6 +89,7 @@ android {
}
dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.simple.tools.commons)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.autofittextview)

View File

@@ -24,6 +24,7 @@ import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.extensions.*
//import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.FAQItem
import com.simplemobiletools.commons.models.PhoneNumber
@@ -38,11 +39,16 @@ import com.simplemobiletools.contacts.pro.dialogs.ChangeSortingDialog
import com.simplemobiletools.contacts.pro.dialogs.ExportContactsDialog
import com.simplemobiletools.contacts.pro.dialogs.FilterContactSourcesDialog
import com.simplemobiletools.contacts.pro.dialogs.ImportContactsDialog
import com.simplemobiletools.contacts.pro.extensions.config
import com.simplemobiletools.contacts.pro.extensions.handleGenericContactClick
import com.simplemobiletools.contacts.pro.extensions.*
import com.simplemobiletools.contacts.pro.extensions.getTempFile
import com.simplemobiletools.contacts.pro.extensions.isPackageInstalled
import com.simplemobiletools.contacts.pro.extensions.showErrorToast
import com.simplemobiletools.contacts.pro.extensions.toast
import com.simplemobiletools.contacts.pro.extensions.updateBottomTabItemColors
import com.simplemobiletools.contacts.pro.fragments.FavoritesFragment
import com.simplemobiletools.contacts.pro.fragments.MyViewPagerFragment
import com.simplemobiletools.contacts.pro.helpers.ALL_TABS_MASK
import com.simplemobiletools.contacts.pro.helpers.ContactsHelper
import com.simplemobiletools.contacts.pro.helpers.VcfExporter
import com.simplemobiletools.contacts.pro.helpers.tabsList
import com.simplemobiletools.contacts.pro.interfaces.RefreshContactsListener
@@ -624,44 +630,48 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
binding.viewPager.currentItem = getDefaultTab()
}
val initToLoad = System.currentTimeMillis() - timeInMs
ContactsHelper(this).getContacts { contacts ->
val diff = System.currentTimeMillis() - timeInMs
val msg = "loaded ${contacts.size} in $diff ms (init: $initToLoad + load: ${diff - initToLoad})";
Log.e("TAGG", msg)
toast(msg)
isGettingContacts = false
if (isDestroyed || isFinishing) {
return@getContacts
}
if (refreshTabsMask and TAB_CONTACTS != 0) {
findViewById<MyViewPagerFragment<*>>(R.id.contacts_fragment)?.apply {
skipHashComparing = true
refreshContacts(contacts)
Handler(Looper.getMainLooper()).postDelayed({
val initToLoad = System.currentTimeMillis() - timeInMs
ContactsHelper(this).getContacts { contacts ->
val diff = System.currentTimeMillis() - timeInMs
val msg = "loaded ${contacts.size} in $diff ms (init: $initToLoad + load: ${diff - initToLoad})";
Log.e("TAGG", msg)
toast(msg)
isGettingContacts = false
if (isDestroyed || isFinishing) {
return@getContacts
}
}
if (refreshTabsMask and TAB_FAVORITES != 0) {
findViewById<MyViewPagerFragment<*>>(R.id.favorites_fragment)?.apply {
skipHashComparing = true
refreshContacts(contacts)
}
}
if (refreshTabsMask and TAB_GROUPS != 0) {
findViewById<MyViewPagerFragment<*>>(R.id.groups_fragment)?.apply {
if (refreshTabsMask == TAB_GROUPS) {
if (refreshTabsMask and TAB_CONTACTS != 0) {
findViewById<MyViewPagerFragment<*>>(R.id.contacts_fragment)?.apply {
skipHashComparing = true
refreshContacts(contacts)
}
refreshContacts(contacts)
}
if (refreshTabsMask and TAB_FAVORITES != 0) {
findViewById<MyViewPagerFragment<*>>(R.id.favorites_fragment)?.apply {
skipHashComparing = true
refreshContacts(contacts)
}
}
if (refreshTabsMask and TAB_GROUPS != 0) {
findViewById<MyViewPagerFragment<*>>(R.id.groups_fragment)?.apply {
if (refreshTabsMask == TAB_GROUPS) {
skipHashComparing = true
}
refreshContacts(contacts)
}
}
if (binding.mainMenu.isSearchOpen) {
getCurrentFragment()?.onSearchQueryChanged(binding.mainMenu.getCurrentQuery())
}
}
if (binding.mainMenu.isSearchOpen) {
getCurrentFragment()?.onSearchQueryChanged(binding.mainMenu.getCurrentQuery())
}
}
}, 3000)
}
override fun contactClicked(contact: Contact) {

View File

@@ -20,6 +20,7 @@ import com.simplemobiletools.commons.dialogs.CallConfirmationDialog
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.dialogs.SelectAlarmSoundDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.contacts.pro.helpers.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.PhoneNumber
import com.simplemobiletools.commons.models.contacts.*
@@ -32,6 +33,7 @@ import com.simplemobiletools.contacts.pro.extensions.editContact
import com.simplemobiletools.contacts.pro.extensions.getPackageDrawable
import com.simplemobiletools.contacts.pro.extensions.startCallIntent
import com.simplemobiletools.contacts.pro.helpers.*
import com.simplemobiletools.contacts.pro.helpers.ContactsHelper
class ViewContactActivity : ContactActivity() {
private var isViewIntent = false

View File

@@ -0,0 +1,400 @@
package com.simplemobiletools.contacts.pro.extensions
import android.annotation.TargetApi
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.ContactsContract
import android.telephony.PhoneNumberUtils
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.databases.ContactsDatabase
import com.simplemobiletools.commons.dialogs.CallConfirmationDialog
import com.simplemobiletools.commons.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.extensions.getIntValue
import com.simplemobiletools.commons.extensions.getLongValue
import com.simplemobiletools.commons.extensions.getStringValue
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.interfaces.ContactsDao
import com.simplemobiletools.commons.interfaces.GroupsDao
import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.commons.models.contacts.Contact
import com.simplemobiletools.commons.models.contacts.ContactSource
import com.simplemobiletools.commons.models.contacts.Organization
import com.simplemobiletools.commons.models.contacts.SocialAction
import com.simplemobiletools.contacts.pro.helpers.ContactsHelper
import java.io.File
val Context.contactsDB: ContactsDao get() = ContactsDatabase.getInstance(applicationContext).ContactsDao()
val Context.groupsDB: GroupsDao get() = ContactsDatabase.getInstance(applicationContext).GroupsDao()
fun Context.getEmptyContact(): Contact {
val originalContactSource = if (hasContactPermissions()) baseConfig.lastUsedContactSource else SMT_PRIVATE
val organization = Organization("", "")
return Contact(
0, "", "", "", "", "", "", "", ArrayList(), ArrayList(), ArrayList(), ArrayList(), originalContactSource, 0, 0, "",
null, "", ArrayList(), organization, ArrayList(), ArrayList(), DEFAULT_MIMETYPE, null
)
}
fun Context.sendAddressIntent(address: String) {
val location = Uri.encode(address)
val uri = Uri.parse("geo:0,0?q=$location")
Intent(Intent.ACTION_VIEW, uri).apply {
launchActivityIntent(this)
}
}
fun Context.openWebsiteIntent(url: String) {
val website = if (url.startsWith("http")) {
url
} else {
"https://$url"
}
Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(website)
launchActivityIntent(this)
}
}
fun Context.getLookupUriRawId(dataUri: Uri): Int {
val lookupKey = getLookupKeyFromUri(dataUri)
if (lookupKey != null) {
val uri = lookupContactUri(lookupKey, this)
if (uri != null) {
return getContactUriRawId(uri)
}
}
return -1
}
fun Context.getContactUriRawId(uri: Uri): Int {
val projection = arrayOf(ContactsContract.Contacts.NAME_RAW_CONTACT_ID)
var cursor: Cursor? = null
try {
cursor = contentResolver.query(uri, projection, null, null, null)
if (cursor!!.moveToFirst()) {
return cursor.getIntValue(ContactsContract.Contacts.NAME_RAW_CONTACT_ID)
}
} catch (ignored: Exception) {
} finally {
cursor?.close()
}
return -1
}
// from https://android.googlesource.com/platform/packages/apps/Dialer/+/68038172793ee0e2ab3e2e56ddfbeb82879d1f58/java/com/android/contacts/common/util/UriUtils.java
fun getLookupKeyFromUri(lookupUri: Uri): String? {
return if (!isEncodedContactUri(lookupUri)) {
val segments = lookupUri.pathSegments
if (segments.size < 3) null else Uri.encode(segments[2])
} else {
null
}
}
fun isEncodedContactUri(uri: Uri?): Boolean {
if (uri == null) {
return false
}
val lastPathSegment = uri.lastPathSegment ?: return false
return lastPathSegment == "encoded"
}
fun lookupContactUri(lookup: String, context: Context): Uri? {
val lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookup)
return try {
ContactsContract.Contacts.lookupContact(context.contentResolver, lookupUri)
} catch (e: Exception) {
null
}
}
fun Context.getCachePhoto(): File {
val imagesFolder = File(cacheDir, "my_cache")
if (!imagesFolder.exists()) {
imagesFolder.mkdirs()
}
val file = File(imagesFolder, "Photo_${System.currentTimeMillis()}.jpg")
file.createNewFile()
return file
}
fun Context.getPhotoThumbnailSize(): Int {
val uri = ContactsContract.DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI
val projection = arrayOf(ContactsContract.DisplayPhoto.THUMBNAIL_MAX_DIM)
var cursor: Cursor? = null
try {
cursor = contentResolver.query(uri, projection, null, null, null)
if (cursor?.moveToFirst() == true) {
return cursor.getIntValue(ContactsContract.DisplayPhoto.THUMBNAIL_MAX_DIM)
}
} catch (ignored: Exception) {
} finally {
cursor?.close()
}
return 0
}
fun Context.hasContactPermissions() = hasPermission(PERMISSION_READ_CONTACTS) && hasPermission(PERMISSION_WRITE_CONTACTS)
fun Context.getPublicContactSource(source: String, callback: (String) -> Unit) {
when (source) {
SMT_PRIVATE -> callback(getString(R.string.phone_storage_hidden))
else -> {
ContactsHelper(this).getContactSources {
var newSource = source
for (contactSource in it) {
if (contactSource.name == source && contactSource.type == TELEGRAM_PACKAGE) {
newSource = getString(R.string.telegram)
break
} else if (contactSource.name == source && contactSource.type == VIBER_PACKAGE) {
newSource = getString(R.string.viber)
break
}
}
Handler(Looper.getMainLooper()).post {
callback(newSource)
}
}
}
}
}
fun Context.getPublicContactSourceSync(source: String, contactSources: ArrayList<ContactSource>): String {
return when (source) {
SMT_PRIVATE -> getString(R.string.phone_storage_hidden)
else -> {
var newSource = source
for (contactSource in contactSources) {
if (contactSource.name == source && contactSource.type == TELEGRAM_PACKAGE) {
newSource = getString(R.string.telegram)
break
} else if (contactSource.name == source && contactSource.type == VIBER_PACKAGE) {
newSource = getString(R.string.viber)
break
}
}
return newSource
}
}
}
fun Context.sendSMSToContacts(contacts: ArrayList<Contact>) {
val numbers = StringBuilder()
contacts.forEach {
val number = it.phoneNumbers.firstOrNull { it.type == ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE }
?: it.phoneNumbers.firstOrNull()
if (number != null) {
numbers.append("${Uri.encode(number.value)};")
}
}
val uriString = "smsto:${numbers.toString().trimEnd(';')}"
Intent(Intent.ACTION_SENDTO, Uri.parse(uriString)).apply {
launchActivityIntent(this)
}
}
fun Context.sendEmailToContacts(contacts: ArrayList<Contact>) {
val emails = ArrayList<String>()
contacts.forEach {
it.emails.forEach {
if (it.value.isNotEmpty()) {
emails.add(it.value)
}
}
}
Intent(Intent.ACTION_SEND_MULTIPLE).apply {
type = "message/rfc822"
putExtra(Intent.EXTRA_EMAIL, emails.toTypedArray())
launchActivityIntent(this)
}
}
fun Context.getTempFile(filename: String = DEFAULT_FILE_NAME): File? {
val folder = File(cacheDir, "contacts")
if (!folder.exists()) {
if (!folder.mkdir()) {
toast(R.string.unknown_error_occurred)
return null
}
}
return File(folder, filename)
}
fun Context.addContactsToGroup(contacts: ArrayList<Contact>, groupId: Long) {
val publicContacts = contacts.filter { !it.isPrivate() }.toMutableList() as ArrayList<Contact>
val privateContacts = contacts.filter { it.isPrivate() }.toMutableList() as ArrayList<Contact>
if (publicContacts.isNotEmpty()) {
ContactsHelper(this).addContactsToGroup(publicContacts, groupId)
}
if (privateContacts.isNotEmpty()) {
LocalContactsHelper(this).addContactsToGroup(privateContacts, groupId)
}
}
fun Context.removeContactsFromGroup(contacts: ArrayList<Contact>, groupId: Long) {
val publicContacts = contacts.filter { !it.isPrivate() }.toMutableList() as ArrayList<Contact>
val privateContacts = contacts.filter { it.isPrivate() }.toMutableList() as ArrayList<Contact>
if (publicContacts.isNotEmpty() && hasContactPermissions()) {
ContactsHelper(this).removeContactsFromGroup(publicContacts, groupId)
}
if (privateContacts.isNotEmpty()) {
LocalContactsHelper(this).removeContactsFromGroup(privateContacts, groupId)
}
}
fun Context.getContactPublicUri(contact: Contact): Uri {
val lookupKey = if (contact.isPrivate()) {
"local_${contact.id}"
} else {
SimpleContactsHelper(this).getContactLookupKey(contact.id.toString())
}
return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey)
}
fun Context.getVisibleContactSources(): ArrayList<String> {
val sources = getAllContactSources()
val ignoredContactSources = baseConfig.ignoredContactSources
return ArrayList(sources).filter { !ignoredContactSources.contains(it.getFullIdentifier()) }
.map { it.name }.toMutableList() as ArrayList<String>
}
fun Context.getAllContactSources(): ArrayList<ContactSource> {
val sources = ContactsHelper(this).getDeviceContactSources()
sources.add(getPrivateContactSource())
return sources.toMutableList() as ArrayList<ContactSource>
}
fun Context.getPrivateContactSource() = ContactSource(SMT_PRIVATE, SMT_PRIVATE, getString(R.string.phone_storage_hidden))
fun Context.getSocialActions(id: Int): ArrayList<SocialAction> {
val uri = ContactsContract.Data.CONTENT_URI
val projection = arrayOf(
ContactsContract.Data._ID,
ContactsContract.Data.DATA3,
ContactsContract.Data.MIMETYPE,
ContactsContract.Data.ACCOUNT_TYPE_AND_DATA_SET
)
val socialActions = ArrayList<SocialAction>()
var curActionId = 0
val selection = "${ContactsContract.Data.RAW_CONTACT_ID} = ?"
val selectionArgs = arrayOf(id.toString())
queryCursor(uri, projection, selection, selectionArgs, null, true) { cursor ->
val mimetype = cursor.getStringValue(ContactsContract.Data.MIMETYPE)
val type = when (mimetype) {
// WhatsApp
"vnd.android.cursor.item/vnd.com.whatsapp.profile" -> SOCIAL_MESSAGE
"vnd.android.cursor.item/vnd.com.whatsapp.voip.call" -> SOCIAL_VOICE_CALL
"vnd.android.cursor.item/vnd.com.whatsapp.video.call" -> SOCIAL_VIDEO_CALL
// Viber
"vnd.android.cursor.item/vnd.com.viber.voip.viber_number_call" -> SOCIAL_VOICE_CALL
"vnd.android.cursor.item/vnd.com.viber.voip.viber_out_call_viber" -> SOCIAL_VOICE_CALL
"vnd.android.cursor.item/vnd.com.viber.voip.viber_out_call_none_viber" -> SOCIAL_VOICE_CALL
"vnd.android.cursor.item/vnd.com.viber.voip.viber_number_message" -> SOCIAL_MESSAGE
// Signal
"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact" -> SOCIAL_MESSAGE
"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call" -> SOCIAL_VOICE_CALL
// Telegram
"vnd.android.cursor.item/vnd.org.telegram.messenger.android.call" -> SOCIAL_VOICE_CALL
"vnd.android.cursor.item/vnd.org.telegram.messenger.android.call.video" -> SOCIAL_VIDEO_CALL
"vnd.android.cursor.item/vnd.org.telegram.messenger.android.profile" -> SOCIAL_MESSAGE
// Threema
"vnd.android.cursor.item/vnd.ch.threema.app.profile" -> SOCIAL_MESSAGE
"vnd.android.cursor.item/vnd.ch.threema.app.call" -> SOCIAL_VOICE_CALL
else -> return@queryCursor
}
val label = cursor.getStringValue(ContactsContract.Data.DATA3)
val realID = cursor.getLongValue(ContactsContract.Data._ID)
val packageName = cursor.getStringValue(ContactsContract.Data.ACCOUNT_TYPE_AND_DATA_SET)
val socialAction = SocialAction(curActionId++, type, label, mimetype, realID, packageName)
socialActions.add(socialAction)
}
return socialActions
}
fun BaseSimpleActivity.initiateCall(contact: Contact, onStartCallIntent: (phoneNumber: String) -> Unit) {
val numbers = contact.phoneNumbers
if (numbers.size == 1) {
onStartCallIntent(numbers.first().value)
} else if (numbers.size > 1) {
val primaryNumber = contact.phoneNumbers.find { it.isPrimary }
if (primaryNumber != null) {
onStartCallIntent(primaryNumber.value)
} else {
val items = ArrayList<RadioItem>()
numbers.forEachIndexed { index, phoneNumber ->
items.add(RadioItem(index, "${phoneNumber.value} (${getPhoneNumberTypeText(phoneNumber.type, phoneNumber.label)})", phoneNumber.value))
}
RadioGroupDialog(this, items) {
onStartCallIntent(it as String)
}
}
}
}
fun BaseSimpleActivity.tryInitiateCall(contact: Contact, onStartCallIntent: (phoneNumber: String) -> Unit) {
if (baseConfig.showCallConfirmation) {
CallConfirmationDialog(this, contact.getNameToDisplay()) {
initiateCall(contact, onStartCallIntent)
}
} else {
initiateCall(contact, onStartCallIntent)
}
}
fun Context.isContactBlocked(contact: Contact, callback: (Boolean) -> Unit) {
val phoneNumbers = contact.phoneNumbers.map { PhoneNumberUtils.stripSeparators(it.value) }
getBlockedNumbersWithContact { blockedNumbersWithContact ->
val blockedNumbers = blockedNumbersWithContact.map { it.number }
val allNumbersBlocked = phoneNumbers.all { it in blockedNumbers }
callback(allNumbersBlocked)
}
}
@TargetApi(Build.VERSION_CODES.N)
fun Context.blockContact(contact: Contact): Boolean {
var contactBlocked = true
ensureBackgroundThread {
contact.phoneNumbers.forEach {
val numberBlocked = addBlockedNumber(PhoneNumberUtils.stripSeparators(it.value))
contactBlocked = contactBlocked && numberBlocked
}
}
return contactBlocked
}
@TargetApi(Build.VERSION_CODES.N)
fun Context.unblockContact(contact: Contact): Boolean {
var contactUnblocked = true
ensureBackgroundThread {
contact.phoneNumbers.forEach {
val numberUnblocked = deleteBlockedNumber(PhoneNumberUtils.stripSeparators(it.value))
contactUnblocked = contactUnblocked && numberUnblocked
}
}
return contactUnblocked
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,4 +2,6 @@ plugins {
alias(libs.plugins.android).apply(false)
alias(libs.plugins.kotlinAndroid).apply(false)
alias(libs.plugins.ksp).apply(false)
alias(libs.plugins.kotlinSerialization).apply(false)
}

View File

@@ -3,6 +3,8 @@
kotlin = "1.9.0"
#KSP
ksp = "1.9.0-1.0.12"
kotlinxSerializationJson = "1.5.1"
#AndroidX
androidx-swiperefreshlayout = "1.1.0"
#AutoFitTextView
@@ -30,6 +32,8 @@ app-version-versionName = "6.22.7"
[libraries]
#AndroidX
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefreshlayout" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
#AutoFitTextView
autofittextview = { module = "me.grantland:autofittextview", version.ref = "autofittextview" }
#EzVcard
@@ -51,3 +55,4 @@ room = [
android = { id = "com.android.application", version.ref = "gradlePlugins-agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }