diff --git a/.gradle/8.13/checksums/checksums.lock b/.gradle/8.13/checksums/checksums.lock index ec79cb9..0d1800d 100644 Binary files a/.gradle/8.13/checksums/checksums.lock and b/.gradle/8.13/checksums/checksums.lock differ diff --git a/.gradle/8.13/checksums/sha1-checksums.bin b/.gradle/8.13/checksums/sha1-checksums.bin index 99e989e..163e60d 100644 Binary files a/.gradle/8.13/checksums/sha1-checksums.bin and b/.gradle/8.13/checksums/sha1-checksums.bin differ diff --git a/.gradle/8.13/executionHistory/executionHistory.lock b/.gradle/8.13/executionHistory/executionHistory.lock index 63e8cda..4206e67 100644 Binary files a/.gradle/8.13/executionHistory/executionHistory.lock and b/.gradle/8.13/executionHistory/executionHistory.lock differ diff --git a/.gradle/8.13/fileHashes/fileHashes.lock b/.gradle/8.13/fileHashes/fileHashes.lock index 8624150..fe9f7f7 100644 Binary files a/.gradle/8.13/fileHashes/fileHashes.lock and b/.gradle/8.13/fileHashes/fileHashes.lock differ diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index bbe6ed4..eeb3e27 100644 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/lib_smsmms_android/src/androidTest/java/com/afkanerd/smswithoutborders_libsmsmms/ConversationsComponentsTest.kt b/lib_smsmms_android/src/androidTest/java/com/afkanerd/smswithoutborders_libsmsmms/ConversationsComponentsTest.kt new file mode 100644 index 0000000..ff081d6 --- /dev/null +++ b/lib_smsmms_android/src/androidTest/java/com/afkanerd/smswithoutborders_libsmsmms/ConversationsComponentsTest.kt @@ -0,0 +1,93 @@ +package com.afkanerd.smswithoutborders_libsmsmms + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.afkanerd.smswithoutborders_libsmsmms.ui.components.FailedMessageOptionsModal +import com.afkanerd.smswithoutborders_libsmsmms.ui.components.SearchCounterCompose +import com.afkanerd.smswithoutborders_libsmsmms.ui.components.SearchTopAppBarText +import com.afkanerd.smswithoutborders_libsmsmms.ui.components.ShortCodeAlert +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ConversationsComponentsTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun searchCounter_displaysIndexAndTotal() { + composeTestRule.setContent { + SearchCounterCompose(index = "3", total = "10") + } + composeTestRule + .onNodeWithText("3/10", substring = true) + .assertIsDisplayed() + } + + + @Test + fun searchTopAppBar_displaysPlaceholder_whenEmpty() { + composeTestRule.setContent { + SearchTopAppBarText(searchQuery = "") + } + composeTestRule + .onNodeWithText("Text message", substring = true) + .assertIsDisplayed() + } + + + @Test + fun searchTopAppBar_closeButton_isDisplayed() { + composeTestRule.setContent { + SearchTopAppBarText(searchQuery = "hello") + } + composeTestRule + .onNodeWithContentDescription("cancel search", ignoreCase = true) + .assertIsDisplayed() + } + + @Test + fun failedMessageModal_resendOption_isDisplayed() { + composeTestRule.setContent { + FailedMessageOptionsModal( + retryCallback = {}, + deleteCallback = {}, + dismissCallback = {} + ) + } + composeTestRule + .onNodeWithText("Resend message", substring = true, ignoreCase = true) + .assertIsDisplayed() + } + + @Test + fun failedMessageModal_deleteOption_isDisplayed() { + composeTestRule.setContent { + FailedMessageOptionsModal( + retryCallback = {}, + deleteCallback = {}, + dismissCallback = {} + ) + } + composeTestRule + .onNodeWithText("Delete", substring = true, ignoreCase = true) + .assertIsDisplayed() + } + + @Test + fun shortCodeAlert_dismissButton_isDisplayed() { + composeTestRule.setContent { + ShortCodeAlert( + dismissCallback = {} + ) + } + composeTestRule + .onNodeWithText("OK", substring = true, ignoreCase = true) + .assertIsDisplayed() + } +} \ No newline at end of file diff --git a/lib_smsmms_android/src/androidTest/java/com/afkanerd/smswithoutborders_libsmsmms/TelephonyTest.kt b/lib_smsmms_android/src/androidTest/java/com/afkanerd/smswithoutborders_libsmsmms/TelephonyTest.kt new file mode 100644 index 0000000..91bb16d --- /dev/null +++ b/lib_smsmms_android/src/androidTest/java/com/afkanerd/smswithoutborders_libsmsmms/TelephonyTest.kt @@ -0,0 +1,33 @@ +package com.afkanerd.smswithoutborders_libsmsmms + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.makeE16PhoneNumber +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PhoneNumberInstrumentationTest { + + private lateinit var context: Context + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + } + + @Test + fun formatsInternationalNumber() { + val result = context.makeE16PhoneNumber("+237671234567") + assertEquals("+237671234567", result) + } + + @Test + fun removesSpacesAndDashes() { + val result = context.makeE16PhoneNumber("+237 671-234-567") + assertEquals("+237671234567", result) + } +} \ No newline at end of file diff --git a/lib_smsmms_android/src/main/java/com/afkanerd/smswithoutborders_libsmsmms/ui/ContactDetailsMain.kt b/lib_smsmms_android/src/main/java/com/afkanerd/smswithoutborders_libsmsmms/ui/ContactDetailsMain.kt index f8bfad7..c772099 100644 --- a/lib_smsmms_android/src/main/java/com/afkanerd/smswithoutborders_libsmsmms/ui/ContactDetailsMain.kt +++ b/lib_smsmms_android/src/main/java/com/afkanerd/smswithoutborders_libsmsmms/ui/ContactDetailsMain.kt @@ -79,7 +79,6 @@ import coil3.compose.AsyncImage import com.afkanerd.lib_smsmms_android.R import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.blockContact import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.copyItemToClipboard -import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.getDefaultSimSubscription import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.getSimCardInformation import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.isDefault import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.isDualSim diff --git a/lib_smsmms_android/src/main/java/com/afkanerd/smswithoutborders_libsmsmms/ui/components/ConversationsComponents.kt b/lib_smsmms_android/src/main/java/com/afkanerd/smswithoutborders_libsmsmms/ui/components/ConversationsComponents.kt index 2f062d1..f61f34d 100644 --- a/lib_smsmms_android/src/main/java/com/afkanerd/smswithoutborders_libsmsmms/ui/components/ConversationsComponents.kt +++ b/lib_smsmms_android/src/main/java/com/afkanerd/smswithoutborders_libsmsmms/ui/components/ConversationsComponents.kt @@ -19,14 +19,10 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize @@ -48,7 +44,10 @@ import androidx.compose.material.icons.filled.FilePresent import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material.icons.outlined.AddCircleOutline +import androidx.compose.material.icons.outlined.Description +import androidx.compose.material.icons.outlined.Image import androidx.compose.material.icons.outlined.SimCard import androidx.compose.material3.BottomAppBar import androidx.compose.material3.Card @@ -90,7 +89,6 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextDecoration @@ -109,7 +107,6 @@ import com.afkanerd.smswithoutborders_libsmsmms.data.entities.Conversations import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.copyItemToClipboard import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.getSimCardInformation import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.getSubscriptionBitmap -import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.getSubscriptionName import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.getUriForDrawable import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.isDualSim import com.afkanerd.smswithoutborders_libsmsmms.extensions.context.shareItem @@ -227,6 +224,95 @@ fun SearchTopAppBarText( } +@Composable +fun FilePickerLauncher( + mmsValueChanged: ((Uri) -> Unit)?, + onFileSelected: (Uri) -> Unit +): ManagedActivityResultLauncher { + val context = LocalContext.current + + return rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent() + ) { fileUri: Uri? -> + fileUri?.let { uri -> + val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION + context.contentResolver.takePersistableUriPermission(uri, flag) + mmsValueChanged?.invoke(uri) + onFileSelected(uri) + } + } +} + +@Composable +fun ContactPickerLauncher( + value: String, + valueChanged: ((String) -> Unit)? +): ManagedActivityResultLauncher { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + return rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickContact() + ) { contactUri: Uri? -> + contactUri?.let { uri -> + coroutineScope.launch(Dispatchers.IO) { + var displayName = "" + var phoneNumber = "" + + context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val nameIndex = cursor.getColumnIndex( + android.provider.ContactsContract.Contacts.DISPLAY_NAME) + val idIndex = cursor.getColumnIndex( + android.provider.ContactsContract.Contacts._ID) + + if (nameIndex >= 0) displayName = cursor.getString(nameIndex) + + val hasPhoneIndex = cursor.getColumnIndex( + android.provider.ContactsContract.Contacts.HAS_PHONE_NUMBER) + val hasPhone = if (hasPhoneIndex >= 0) + cursor.getString(hasPhoneIndex) else "0" + + if (hasPhone == "1" && idIndex >= 0) { + val contactId = cursor.getString(idIndex) + context.contentResolver.query( + android.provider.ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + null, + "${android.provider.ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?", + arrayOf(contactId), + null + )?.use { phoneCursor -> + if (phoneCursor.moveToFirst()) { + val numberIndex = phoneCursor.getColumnIndex( + android.provider.ContactsContract.CommonDataKinds.Phone.NUMBER) + if (numberIndex >= 0) { + phoneNumber = phoneCursor.getString(numberIndex) + } + } + } + } + } + } + + if (displayName.isNotEmpty() || phoneNumber.isNotEmpty()) { + val contactTextString = buildString { + if (displayName.isNotEmpty()) append("Name: $displayName") + if (phoneNumber.isNotEmpty()) { + if (displayName.isNotEmpty()) append("\n") + append("Phone: $phoneNumber") + } + } + launch(Dispatchers.Main) { + val updatedText = if (value.isEmpty()) contactTextString + else "$value\n$contactTextString" + valueChanged?.invoke(updatedText) + } + } + } + } + } +} + @OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable fun ChatCompose( @@ -250,13 +336,11 @@ fun ChatCompose( } var imageUri: Uri? by remember { mutableStateOf(imageUri) } - val imagePicker = mmsImagePicker { uri -> - val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION - context.contentResolver.takePersistableUriPermission(uri, flag) - mmsValueChanged?.invoke(uri) - imageUri = uri - } + val contactPickerLauncher = ContactPickerLauncher( + value = value, + valueChanged = valueChanged + ) var messagingType by remember { mutableStateOf("SMS") } LaunchedEffect(imageUri) { @@ -264,9 +348,9 @@ fun ChatCompose( } var length: String by remember{ - mutableStateOf( - if(inPreviewMode) "10/140" else - getSMSCount(context, value, subscriptionId) + mutableStateOf( + if(inPreviewMode) "10/140" else + getSMSCount(context, value, subscriptionId) ) } LaunchedEffect(value) { @@ -319,16 +403,15 @@ fun ChatCompose( .fillMaxWidth(), verticalAlignment = Alignment.Bottom, ) { + Column( - Modifier.padding(bottom=20.dp), + Modifier.padding(bottom = 20.dp), verticalArrangement = Arrangement.Bottom, ) { - IconButton(onClick = { - imagePicker.launch(arrayOf("image/png", "image/jpg", "image/jpeg")) - }) { + IconButton(onClick = { contactPickerLauncher.launch(null) }) { Icon( Icons.Outlined.AddCircleOutline, - stringResource(R.string.send_mms_photo), + contentDescription = "Attach Contact", tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(30.dp) ) @@ -449,10 +532,8 @@ fun ChatCompose( } } } - } } - } fun getSMSCount(