Skip to content
Binary file modified .gradle/8.13/checksums/checksums.lock
Binary file not shown.
Binary file modified .gradle/8.13/checksums/sha1-checksums.bin
Binary file not shown.
Binary file modified .gradle/8.13/executionHistory/executionHistory.lock
Binary file not shown.
Binary file modified .gradle/8.13/fileHashes/fileHashes.lock
Binary file not shown.
Binary file modified .gradle/buildOutputCleanup/buildOutputCleanup.lock
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -227,6 +224,95 @@ fun SearchTopAppBarText(
}


@Composable
fun FilePickerLauncher(
mmsValueChanged: ((Uri) -> Unit)?,
onFileSelected: (Uri) -> Unit
): ManagedActivityResultLauncher<String, Uri?> {
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<Void?, Uri?> {
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(
Expand All @@ -250,23 +336,21 @@ 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) {
messagingType = if(imageUri != null) "MMS" else "SMS"
}

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) {
Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -449,10 +532,8 @@ fun ChatCompose(
}
}
}

}
}

}

fun getSMSCount(
Expand Down