Skip to content

Commit

Permalink
Option to import contacts from Sim card (#178)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bnyro committed May 31, 2023
1 parent 363c288 commit 3156c56
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 35 deletions.
22 changes: 20 additions & 2 deletions app/src/main/java/com/bnyro/contacts/ui/components/ContactsPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import com.bnyro.contacts.ui.components.base.OptionMenu
import com.bnyro.contacts.ui.components.base.SearchBar
import com.bnyro.contacts.ui.components.dialogs.ConfirmationDialog
import com.bnyro.contacts.ui.components.dialogs.FilterDialog
import com.bnyro.contacts.ui.components.dialogs.SimImportDialog
import com.bnyro.contacts.ui.components.modifier.scrollbar
import com.bnyro.contacts.ui.models.ContactsModel
import com.bnyro.contacts.ui.screens.AboutScreen
Expand Down Expand Up @@ -110,6 +111,10 @@ fun ContactsPage(
mutableStateOf(false)
}

var showImportSimDialog by remember {
mutableStateOf(false)
}

val importVcard =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let { viewModel.importVcf(context, it) }
Expand Down Expand Up @@ -137,7 +142,9 @@ fun ContactsPage(
when (state) {
true -> {
SearchBar(
modifier = Modifier.padding(horizontal = 10.dp).padding(top = 15.dp),
modifier = Modifier
.padding(horizontal = 10.dp)
.padding(top = 15.dp),
state = searchQuery
) {
Box(
Expand Down Expand Up @@ -167,6 +174,7 @@ fun ContactsPage(
options = listOf(
stringResource(R.string.import_vcf),
stringResource(R.string.export_vcf),
stringResource(R.string.import_sim),
stringResource(R.string.settings),
stringResource(R.string.about)
),
Expand All @@ -184,10 +192,14 @@ fun ContactsPage(
}

2 -> {
showSettings = true
showImportSimDialog = true
}

3 -> {
showSettings = true
}

4 -> {
showAbout = true
}
}
Expand Down Expand Up @@ -423,4 +435,10 @@ fun ContactsPage(
availableGroups = viewModel.getAvailableGroups()
)
}

if (showImportSimDialog) {
SimImportDialog {
showImportSimDialog = false
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.bnyro.contacts.ui.components.dialogs

import android.widget.Toast
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.bnyro.contacts.R
import com.bnyro.contacts.obj.ContactData
import com.bnyro.contacts.ui.models.ContactsModel
import com.bnyro.contacts.util.SimContactsHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

@Composable
fun SimImportDialog(
onDismissRequest: () -> Unit
) {
val simContacts = remember { mutableStateListOf<ContactData>() }
val selectedContacts = remember { mutableStateListOf<ContactData>() }
var isLoading by remember {
mutableStateOf(true)
}
val context = LocalContext.current
val contactsModel: ContactsModel = viewModel()

LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
try {
val contacts = SimContactsHelper.getSimContacts(context)
simContacts.addAll(contacts)
selectedContacts.addAll(contacts)
} catch (e: Exception) {
Toast.makeText(context, e.localizedMessage, Toast.LENGTH_LONG).show()
}
isLoading = false
}
}

AlertDialog(
title = { Text(stringResource(R.string.import_sim)) },
onDismissRequest = onDismissRequest,
dismissButton = {
DialogButton(stringResource(R.string.cancel)) {
onDismissRequest.invoke()
}
},
confirmButton = {
DialogButton(text = stringResource(R.string.okay)) {
selectedContacts.forEach { contact ->
contactsModel.createContact(context, contact)
}
onDismissRequest.invoke()
}
},
text = {
if (isLoading) {
Box(
modifier = Modifier
.height(300.dp)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else {
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
items(simContacts) { contact ->
Row(
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = selectedContacts.contains(contact),
onCheckedChange = {
if (it) selectedContacts.add(contact)
else selectedContacts.remove(contact)
}
)
Spacer(modifier = Modifier.width(10.dp))
Column {
Text(text = contact.displayName.orEmpty())
Spacer(modifier = Modifier.height(3.dp))
Text(text = contact.numbers.firstOrNull()?.value.orEmpty())
}
}
}
}
}
}
)
}
23 changes: 11 additions & 12 deletions app/src/main/java/com/bnyro/contacts/ui/models/ContactsModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ class ContactsModel : ViewModel() {
Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS
)
private var sessionId = 0
var initialContactId: Long? by mutableStateOf(null)
var initialContactData: ContactData? by mutableStateOf(null)

Expand All @@ -53,19 +52,19 @@ class ContactsModel : ViewModel() {
return
}
viewModelScope.launch(Dispatchers.IO) {
sessionId += 1
val currentSession = sessionId
contacts.clear()
contacts.addAll(contactsHelper?.getContactList().orEmpty())
isLoading = true
try {
val ct = contactsHelper?.getContactList().orEmpty()
contacts.clear()
contacts.addAll(ct)
} catch (e: Exception) {
return@launch
}
isLoading = false
CoroutineScope(Dispatchers.IO + Job()).launch {
(0 until contacts.size).map { i ->
CoroutineScope(Dispatchers.IO).launch {
contacts.map {
async {
contacts.getOrNull(i)?.let {
val data = contactsHelper?.loadAdvancedData(it) ?: return@async
if (currentSession != sessionId || it.displayName != data.displayName) return@async
runCatching { contacts[i] = data }.onFailure { return@async }
}
contactsHelper?.loadAdvancedData(it)
}
}.awaitAll()
}
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/com/bnyro/contacts/util/ContactsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,21 @@ abstract class ContactsHelper {
TranslatedType(ContactsContract.CommonDataKinds.Website.TYPE_CUSTOM, R.string.custom),
TranslatedType(ContactsContract.CommonDataKinds.Website.TYPE_OTHER, R.string.other)
)


fun splitFullName(displayName: String?): Pair<String, String> {
val displayNameParts = displayName.orEmpty().split(" ")
return when {
displayNameParts.size >= 2 -> {
displayNameParts.subList(0, displayNameParts.size - 1).joinToString(
" "
) to displayNameParts.last()
}
displayNameParts.size == 1 -> {
displayNameParts.first() to ""
}
else -> { "" to "" }
}
}
}
}
32 changes: 11 additions & 21 deletions app/src/main/java/com/bnyro/contacts/util/DeviceContactsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class DeviceContactsHelper(private val context: Context) : ContactsHelper() {
private val contentResolver = context.contentResolver
private val androidAccountType = "com.android.contacts"
private val deviceContactName = "DEVICE"
private val contactsUri = Data.CONTENT_URI

private val projection = arrayOf(
Data.RAW_CONTACT_ID,
Expand All @@ -65,7 +66,7 @@ class DeviceContactsHelper(private val context: Context) : ContactsHelper() {

@Suppress("SameParameterValue")
val cursor = contentResolver.query(
Data.CONTENT_URI,
contactsUri,
projection,
null,
null,
Expand All @@ -86,19 +87,9 @@ class DeviceContactsHelper(private val context: Context) : ContactsHelper() {

// try parsing the display name to a proper name
if (firstName.notAName() || surName.notAName()) {
val displayNameParts = displayName.orEmpty().split(" ")
when {
displayNameParts.size >= 2 -> {
firstName = displayNameParts.subList(0, displayNameParts.size - 1).joinToString(
" "
)
surName = displayNameParts.last()
}
displayNameParts.size == 1 -> {
firstName = displayNameParts.first()
surName = ""
}
}
val nameParts = splitFullName(displayName)
firstName = nameParts.first
surName = nameParts.second
}

val contact = ContactData(
Expand Down Expand Up @@ -259,11 +250,10 @@ class DeviceContactsHelper(private val context: Context) : ContactsHelper() {
@Suppress("SameParameterValue")
private fun getExtras(contactId: Long, valueIndex: String, typeIndex: String?, itemType: String): List<ValueWithType> {
val entries = mutableListOf<ValueWithType>()
val uri = Data.CONTENT_URI
val projection = arrayOf(Data.CONTACT_ID, valueIndex, typeIndex ?: "data2")

contentResolver.query(
uri,
contactsUri,
projection,
"${Data.MIMETYPE} = ? AND ${Data.CONTACT_ID} = ?",
arrayOf(itemType, contactId.toString()),
Expand Down Expand Up @@ -406,7 +396,7 @@ class DeviceContactsHelper(private val context: Context) : ContactsHelper() {
val rawContactId = contact.rawContactId.toString()

val selection = "${Data.RAW_CONTACT_ID} = ? AND ${Data.MIMETYPE} = ?"
ContentProviderOperation.newUpdate(Data.CONTENT_URI).apply {
ContentProviderOperation.newUpdate(contactsUri).apply {
val selectionArgs = arrayOf(rawContactId, StructuredName.CONTENT_ITEM_TYPE)
withSelection(selection, selectionArgs)
withValue(StructuredName.GIVEN_NAME, contact.firstName)
Expand Down Expand Up @@ -527,7 +517,7 @@ class DeviceContactsHelper(private val context: Context) : ContactsHelper() {
type: Int? = null,
rawContactId: Int? = null
): ContentProviderOperation {
return ContentProviderOperation.newInsert(Data.CONTENT_URI)
return ContentProviderOperation.newInsert(contactsUri)
.let { builder ->
// if creating a new contact, the previous contact id is going to be taken
// if updating an already existing contact, don't worry about the previous batch id
Expand Down Expand Up @@ -571,14 +561,14 @@ class DeviceContactsHelper(private val context: Context) : ContactsHelper() {
val selection = "${Data.RAW_CONTACT_ID} = ? AND ${Data.MIMETYPE} = ?"
val selectionArgs = arrayOf(contactId, mimeType)

ContentProviderOperation.newDelete(Data.CONTENT_URI).apply {
ContentProviderOperation.newDelete(contactsUri).apply {
withSelection(selection, selectionArgs)
operations.add(build())
}

// add new entries
entries.forEach {
ContentProviderOperation.newInsert(Data.CONTENT_URI).apply {
ContentProviderOperation.newInsert(contactsUri).apply {
withValue(Data.RAW_CONTACT_ID, contactId)
withValue(Data.MIMETYPE, mimeType)
withValue(valueIndex, it.value)
Expand Down Expand Up @@ -631,7 +621,7 @@ class DeviceContactsHelper(private val context: Context) : ContactsHelper() {
}

private fun deletePhoto(rawContactId: Int): ContentProviderOperation {
return ContentProviderOperation.newDelete(Data.CONTENT_URI).apply {
return ContentProviderOperation.newDelete(contactsUri).apply {
val selection = "${Data.RAW_CONTACT_ID} = ? AND ${Data.MIMETYPE} = ?"
val selectionArgs = arrayOf(rawContactId.toString(), Photo.CONTENT_ITEM_TYPE)
withSelection(selection, selectionArgs)
Expand Down
38 changes: 38 additions & 0 deletions app/src/main/java/com/bnyro/contacts/util/SimContactsHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.bnyro.contacts.util

import android.content.Context
import android.net.Uri
import com.bnyro.contacts.ext.longValue
import com.bnyro.contacts.ext.stringValue
import com.bnyro.contacts.obj.ContactData
import com.bnyro.contacts.obj.ValueWithType


object SimContactsHelper {
fun getSimContacts(context: Context): List<ContactData> {
val simUri = Uri.parse("content://icc/adn")
val cursorSim = context.contentResolver.query(simUri, null, null, null, null) ?: return emptyList()
val contacts = mutableListOf<ContactData>()

while (cursorSim.moveToNext()) {
val name = cursorSim.stringValue("name")
val phoneNumber = cursorSim.stringValue("number")
?.replace("\\D","")
?.replace("&", "")
// skip empty sim contacts
if (name.isNullOrBlank() && phoneNumber.isNullOrBlank()) continue

val nameParts = ContactsHelper.splitFullName(name)
val contact = ContactData(
displayName = name,
firstName = nameParts.first,
surName = nameParts.second,
alternativeName = "${nameParts.first} ${nameParts.second}",
numbers = listOfNotNull(phoneNumber?.let { ValueWithType(it, 0) })
)
contacts.add(contact)
}
cursorSim.close()
return contacts
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
<!-- Import & Export -->
<string name="import_vcf">Import vCard</string>
<string name="export_vcf">Export vCard</string>
<string name="import_sim">Import from SIM</string>
<!-- Settings -->
<string name="settings">Settings</string>
<string name="start_tab">Start tab</string>
Expand Down

0 comments on commit 3156c56

Please sign in to comment.