diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 70211083b..b19c729f3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -48,10 +48,18 @@
android:exported="false"
android:theme="@style/SplashTheme" />
+
+ android:exported="true"
+ android:grantUriPermissions="true">
@@ -60,6 +68,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/kotlin/org/fossify/contacts/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/contacts/activities/MainActivity.kt
index a2022aa17..15855f2b8 100644
--- a/app/src/main/kotlin/org/fossify/contacts/activities/MainActivity.kt
+++ b/app/src/main/kotlin/org/fossify/contacts/activities/MainActivity.kt
@@ -7,6 +7,8 @@ import android.content.pm.ShortcutInfo
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Icon
import android.graphics.drawable.LayerDrawable
+import android.net.Uri
+import android.os.Build
import android.os.Bundle
import androidx.viewpager.widget.ViewPager
import me.grantland.widget.AutofitHelper
@@ -366,19 +368,90 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
refreshMenuItems()
}
- if (intent?.action == Intent.ACTION_VIEW && intent.data != null) {
- tryImportContactsFromFile(intent.data!!) {
- if (it) {
- runOnUiThread {
- refreshContacts(ALL_TABS_MASK)
+ handleContactImportIntent(intent)
+
+ binding.mainDialpadButton.setOnClickListener {
+ launchDialpad()
+ }
+ }
+
+ /**
+ * Handles incoming intents to import contacts. This function processes intents with actions
+ * ACTION_VIEW, ACTION_SEND, and ACTION_SEND_MULTIPLE for vCard MIME types. It extracts the
+ * relevant URIs and passes them to the contact import handler.
+ *
+ * @param intent The incoming intent to process.
+ */
+ private fun handleContactImportIntent(intent: Intent?) {
+ if (intent?.action == null) {
+ return
+ }
+
+ val contactUris = getUrisFromIntent(intent)
+
+ if (contactUris.isNotEmpty()) {
+ for (uri in contactUris) {
+ tryImportContactsFromFile(uri) { isSuccess ->
+ if (isSuccess) {
+ runOnUiThread {
+ refreshContacts(ALL_TABS_MASK)
+ }
}
}
}
- intent.data = null
+ // Prevents the intent from being processed again on activity recreation (e.g., orientation change).
+ intent.action = null
}
+ }
- binding.mainDialpadButton.setOnClickListener {
- launchDialpad()
+ /**
+ * Extracts a list of URIs from an intent, supporting various actions and data types.
+ *
+ * @param intent The intent to parse.
+ * @return A list of URIs found in the intent, or an empty list if none are found.
+ */
+ private fun getUrisFromIntent(intent: Intent): List {
+ val supportedMimeTypes = listOf("text/x-vcard", "text/vcard")
+
+ return when (intent.action) {
+ Intent.ACTION_VIEW -> {
+ intent.data?.let { listOf(it) } ?: emptyList()
+ }
+
+ Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE -> {
+ // Ensure the MIME type is supported before proceeding.
+ if (!supportedMimeTypes.contains(intent.type)) {
+ return emptyList()
+ }
+
+ if (intent.action == Intent.ACTION_SEND) {
+ getParcelableUri(intent, Intent.EXTRA_STREAM)?.let { listOf(it) } ?: emptyList()
+ } else {
+ getParcelableUriList(intent, Intent.EXTRA_STREAM) ?: emptyList()
+ }
+ }
+
+ else -> emptyList()
+ }
+ }
+
+ // Helper function to abstract away the version check for a single Parcelable.
+ private fun getParcelableUri(intent: Intent, key: String): Uri? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra(key, Uri::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ intent.getParcelableExtra(key)
+ }
+ }
+
+ // Helper function to abstract away the version check for a list of Parcelables.
+ private fun getParcelableUriList(intent: Intent, key: String): List? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableArrayListExtra(key, Uri::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ intent.getParcelableArrayListExtra(key)
}
}
diff --git a/app/src/main/kotlin/org/fossify/contacts/extensions/Activity.kt b/app/src/main/kotlin/org/fossify/contacts/extensions/Activity.kt
index 7c0af7236..48b28f7a8 100644
--- a/app/src/main/kotlin/org/fossify/contacts/extensions/Activity.kt
+++ b/app/src/main/kotlin/org/fossify/contacts/extensions/Activity.kt
@@ -3,6 +3,10 @@ package org.fossify.contacts.extensions
import android.app.Activity
import android.content.Intent
import android.net.Uri
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.fossify.commons.activities.BaseSimpleActivity
import org.fossify.commons.dialogs.RadioGroupDialog
import org.fossify.commons.extensions.getFileOutputStream
@@ -187,26 +191,42 @@ fun Activity.editContact(contact: Contact) {
}
fun SimpleActivity.tryImportContactsFromFile(uri: Uri, callback: (Boolean) -> Unit) {
- when (uri.scheme) {
- "file" -> showImportContactsDialog(uri.path!!, callback)
- "content" -> {
- val tempFile = getTempFile()
- if (tempFile == null) {
- toast(org.fossify.commons.R.string.unknown_error_occurred)
- return
+ lifecycleScope.launch {
+ try {
+ val contactFilePath = when (uri.scheme) {
+ "file" -> uri.path
+ "content" -> saveContentUriToTempFile(uri) // Process content URI in the background
+ else -> null // Unsupported scheme
}
- try {
- val inputStream = contentResolver.openInputStream(uri)
- val out = FileOutputStream(tempFile)
- inputStream!!.copyTo(out)
- showImportContactsDialog(tempFile.absolutePath, callback)
- } catch (e: Exception) {
- showErrorToast(e)
+ if (contactFilePath != null) {
+ // Switch back to the Main thread to show a dialog
+ showImportContactsDialog(contactFilePath, callback)
+ } else {
+ toast(org.fossify.commons.R.string.invalid_file_format)
}
+ } catch (e: Exception) {
+ showErrorToast(e)
}
+ }
+}
+
+private suspend fun SimpleActivity.saveContentUriToTempFile(uri: Uri): String? {
+ // Perform I/O operations on a background thread (Dispatchers.IO)
+ return withContext(Dispatchers.IO) {
+ val tempFile = getTempFile() ?: return@withContext null
- else -> toast(org.fossify.commons.R.string.invalid_file_format)
+ try {
+ // 'use' automatically closes the streams, even if an exception occurs
+ contentResolver.openInputStream(uri)?.use { inputStream ->
+ FileOutputStream(tempFile).use { outputStream ->
+ inputStream.copyTo(outputStream)
+ }
+ }
+ tempFile.absolutePath
+ } catch (_: Exception) {
+ null // Return null on failure
+ }
}
}