diff --git a/app/src/main/java/com/bnyro/contacts/ui/components/ContactsPage.kt b/app/src/main/java/com/bnyro/contacts/ui/components/ContactsPage.kt index 6eb1fc7c..2efa9880 100644 --- a/app/src/main/java/com/bnyro/contacts/ui/components/ContactsPage.kt +++ b/app/src/main/java/com/bnyro/contacts/ui/components/ContactsPage.kt @@ -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 @@ -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) } @@ -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( @@ -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) ), @@ -184,10 +192,14 @@ fun ContactsPage( } 2 -> { - showSettings = true + showImportSimDialog = true } 3 -> { + showSettings = true + } + + 4 -> { showAbout = true } } @@ -423,4 +435,10 @@ fun ContactsPage( availableGroups = viewModel.getAvailableGroups() ) } + + if (showImportSimDialog) { + SimImportDialog { + showImportSimDialog = false + } + } } diff --git a/app/src/main/java/com/bnyro/contacts/ui/components/dialogs/SimImportDialog.kt b/app/src/main/java/com/bnyro/contacts/ui/components/dialogs/SimImportDialog.kt new file mode 100644 index 00000000..653d7e11 --- /dev/null +++ b/app/src/main/java/com/bnyro/contacts/ui/components/dialogs/SimImportDialog.kt @@ -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() } + val selectedContacts = remember { mutableStateListOf() } + 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()) + } + } + } + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bnyro/contacts/ui/models/ContactsModel.kt b/app/src/main/java/com/bnyro/contacts/ui/models/ContactsModel.kt index 957b4516..d7ac2311 100644 --- a/app/src/main/java/com/bnyro/contacts/ui/models/ContactsModel.kt +++ b/app/src/main/java/com/bnyro/contacts/ui/models/ContactsModel.kt @@ -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) @@ -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() } diff --git a/app/src/main/java/com/bnyro/contacts/util/ContactsHelper.kt b/app/src/main/java/com/bnyro/contacts/util/ContactsHelper.kt index 34a4170c..53e51e6f 100644 --- a/app/src/main/java/com/bnyro/contacts/util/ContactsHelper.kt +++ b/app/src/main/java/com/bnyro/contacts/util/ContactsHelper.kt @@ -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 { + 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 "" } + } + } } } diff --git a/app/src/main/java/com/bnyro/contacts/util/DeviceContactsHelper.kt b/app/src/main/java/com/bnyro/contacts/util/DeviceContactsHelper.kt index 72e0bfc6..855894b6 100644 --- a/app/src/main/java/com/bnyro/contacts/util/DeviceContactsHelper.kt +++ b/app/src/main/java/com/bnyro/contacts/util/DeviceContactsHelper.kt @@ -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, @@ -65,7 +66,7 @@ class DeviceContactsHelper(private val context: Context) : ContactsHelper() { @Suppress("SameParameterValue") val cursor = contentResolver.query( - Data.CONTENT_URI, + contactsUri, projection, null, null, @@ -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( @@ -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 { val entries = mutableListOf() - 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()), @@ -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) @@ -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 @@ -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) @@ -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) diff --git a/app/src/main/java/com/bnyro/contacts/util/SimContactsHelper.kt b/app/src/main/java/com/bnyro/contacts/util/SimContactsHelper.kt new file mode 100644 index 00000000..89600b77 --- /dev/null +++ b/app/src/main/java/com/bnyro/contacts/util/SimContactsHelper.kt @@ -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 { + val simUri = Uri.parse("content://icc/adn") + val cursorSim = context.contentResolver.query(simUri, null, null, null, null) ?: return emptyList() + val contacts = mutableListOf() + + 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 + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3bab08a0..dce71c09 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,6 +77,7 @@ Import vCard Export vCard + Import from SIM Settings Start tab