Skip to content

Commit

Permalink
feat: show search suggestions when dialing
Browse files Browse the repository at this point in the history
  • Loading branch information
SuhasDissa committed May 12, 2024
1 parent 86ff047 commit 8d39324
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 59 deletions.
5 changes: 4 additions & 1 deletion app/src/main/java/com/bnyro/contacts/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.bnyro.contacts.domain.repositories.DeviceContactsRepository
import com.bnyro.contacts.domain.repositories.DeviceSmsRepo
import com.bnyro.contacts.domain.repositories.LocalContactsRepository
import com.bnyro.contacts.domain.repositories.LocalSmsRepo
import com.bnyro.contacts.domain.repositories.PhoneLookupRepository
import com.bnyro.contacts.domain.repositories.SmsRepository
import com.bnyro.contacts.util.NotificationHelper
import com.bnyro.contacts.util.Preferences
Expand All @@ -23,7 +24,9 @@ class App : Application() {
val callLogRepository by lazy {
CallLogRepository(this)
}

val phoneLookupRepository by lazy {
PhoneLookupRepository(this)
}
lateinit var smsRepo: SmsRepository

fun initSmsRepo() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.bnyro.contacts.domain.model

import android.net.Uri

/**
* Data class containing contact name, number and thumbnail
* @property number phone number without any formatting
* @property name contact display name
* @property thumbnail contact thumbnail [Uri]
*/
data class BasicContactData(
val number: String,
val name: String? = null,
val thumbnail: Uri? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.bnyro.contacts.domain.repositories

import android.content.Context
import android.net.Uri
import android.provider.ContactsContract.CommonDataKinds.Phone
import android.provider.ContactsContract.PhoneLookup
import androidx.core.net.toUri
import com.bnyro.contacts.domain.model.BasicContactData
import com.bnyro.contacts.util.extension.stringValue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class PhoneLookupRepository(private val context: Context) {
/**
* Get all contacts with a number equal to or containing a given number.
* @param number The number to search for
* @return A list containing simple contact data
*/
suspend fun getContactsWithNumber(number: String): List<BasicContactData> =
withContext(Dispatchers.IO) {
val contacts = mutableListOf<BasicContactData>()
val uri = Phone.CONTENT_URI
val projection = arrayOf(
Phone.PHOTO_THUMBNAIL_URI,
Phone.DISPLAY_NAME,
Phone.NUMBER
)
val selection = "${Phone.NUMBER} LIKE ?"
val selectionArgs = arrayOf("%$number%")

val cursor =
context.contentResolver.query(uri, projection, selection, selectionArgs, null)

cursor?.use {
while (it.moveToNext()) {
val name = it.stringValue(Phone.DISPLAY_NAME)
val phoneNumber = it.stringValue(Phone.NUMBER)
val photoUri = it.stringValue(Phone.PHOTO_THUMBNAIL_URI)?.toUri()

if (phoneNumber != null) {
contacts.add(
BasicContactData(
number = phoneNumber,
name = name,
thumbnail = photoUri
)
)
}
}
}

contacts
}

/**
* Get all contacts with a name containing the given query
* @param nameQuery The name to search for
* @return A list containing simple contact data
*/
suspend fun getContactsWithName(nameQuery: String): List<BasicContactData> =
withContext(Dispatchers.IO) {
val contacts = mutableListOf<BasicContactData>()
val uri = Phone.CONTENT_URI
val projection = arrayOf(
Phone.PHOTO_THUMBNAIL_URI,
Phone.DISPLAY_NAME,
Phone.NUMBER
)
val selection = "${Phone.DISPLAY_NAME} LIKE ?"
val selectionArgs = arrayOf("%$nameQuery%")

val cursor =
context.contentResolver.query(uri, projection, selection, selectionArgs, null)

cursor?.use {
while (it.moveToNext()) {
val name = it.stringValue(Phone.DISPLAY_NAME)
val phoneNumber = it.stringValue(Phone.NUMBER)
val photoUri = it.stringValue(Phone.PHOTO_THUMBNAIL_URI)?.toUri()

if (phoneNumber != null) {
contacts.add(
BasicContactData(
number = phoneNumber,
name = name,
thumbnail = photoUri
)
)
}
}
}

contacts
}

/**Get caller id from a number
* @param phoneNumber The number of the caller
* @return A simple contact data containing name phone and thumbnail
*/
suspend fun getContactByNumber(phoneNumber: String): BasicContactData =
withContext(Dispatchers.IO) {
val uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber))
val query =
context.contentResolver.query(
uri,
arrayOf(PhoneLookup.DISPLAY_NAME, PhoneLookup.PHOTO_THUMBNAIL_URI),
null,
null,
null
)

query?.use { cursor ->
if (cursor.moveToFirst()) {
BasicContactData(
number = phoneNumber,
name = cursor.stringValue(
PhoneLookup.DISPLAY_NAME
),
thumbnail = cursor.stringValue(PhoneLookup.PHOTO_THUMBNAIL_URI)?.toUri()
)

}
}
BasicContactData(
number = phoneNumber
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.bnyro.contacts.R
import com.bnyro.contacts.presentation.components.NothingHere
import com.bnyro.contacts.presentation.components.NumberInput
import com.bnyro.contacts.presentation.components.PhoneNumberDisplay
import com.bnyro.contacts.presentation.features.ConfirmationDialog
import com.bnyro.contacts.presentation.screens.calllog.components.NumberInput
import com.bnyro.contacts.presentation.screens.calllog.components.PhoneNumberDisplay
import com.bnyro.contacts.presentation.screens.calllog.model.CallModel
import com.bnyro.contacts.presentation.screens.contacts.model.ContactsModel
import com.bnyro.contacts.presentation.screens.editor.components.ContactIconPlaceholder
Expand Down Expand Up @@ -135,7 +135,11 @@ fun CallLogsScreen(
.padding(horizontal = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
PhoneNumberDisplay(displayText = callModel.numberToCall)
PhoneNumberDisplay(
displayText = callModel.numberToCall,
contacts = callModel.contacts,
onClickContact = callModel::setPhoneNumberContact
)
NumberInput(
onNumberInput = callModel::onNumberInput,
onDelete = callModel::onBackSpace,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.bnyro.contacts.presentation.components
package com.bnyro.contacts.presentation.screens.calllog.components

import android.annotation.SuppressLint
import android.telephony.SubscriptionInfo
import android.view.SoundEffectConstants
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
Expand All @@ -16,17 +17,21 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Backspace
import androidx.compose.material.icons.rounded.Call
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
Expand All @@ -42,12 +47,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.bnyro.contacts.R
import com.bnyro.contacts.domain.model.BasicContactData

val keypadNumbers = arrayOf(
arrayOf("1", "2", "3"),
Expand Down Expand Up @@ -221,7 +229,11 @@ fun RowScope.NumpadButton(
}

@Composable
fun ColumnScope.PhoneNumberDisplay(displayText: String) {
fun ColumnScope.PhoneNumberDisplay(
displayText: String,
contacts: List<BasicContactData>,
onClickContact: (BasicContactData) -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
Expand All @@ -234,7 +246,9 @@ fun ColumnScope.PhoneNumberDisplay(displayText: String) {
.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.Bottom)
) {
// TODO: Contact Suggestions
items(contacts, key = { it.number }) { contact ->
ContactSuggestionItem(onClickContact, contact)
}
}
val scroll = rememberScrollState()
Row(
Expand All @@ -259,6 +273,45 @@ fun ColumnScope.PhoneNumberDisplay(displayText: String) {
}
}

@Composable
private fun ContactSuggestionItem(
onClickContact: (BasicContactData) -> Unit,
contact: BasicContactData
) {
ListItem(
modifier = Modifier.clickable { onClickContact.invoke(contact) },
headlineContent = { Text(text = contact.number) }, leadingContent = {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center
) {
if (contact.thumbnail != null) {
AsyncImage(
model = contact.thumbnail,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
alignment = Alignment.Center,
contentScale = ContentScale.Crop
)
} else {
Icon(
modifier = Modifier.size(48.dp),
imageVector = Icons.Rounded.Call,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}, supportingContent = {
if (contact.name != null) {
Text(text = contact.name)
}
})
}

@Composable
fun PhoneNumberOnlyDisplay(displayText: String) {
Column(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bnyro.contacts.App
import com.bnyro.contacts.domain.model.BasicContactData
import com.bnyro.contacts.domain.model.CallLogEntry
import com.bnyro.contacts.navigation.HomeRoutes
import com.bnyro.contacts.util.PermissionHelper
Expand All @@ -28,6 +29,7 @@ import kotlinx.coroutines.launch
class CallModel(private val application: Application, savedStateHandle: SavedStateHandle) :
AndroidViewModel(application) {
val callLogRepository = (application as App).callLogRepository
val phoneLookupRepository = (application as App).phoneLookupRepository

val initialPhoneNumber = savedStateHandle.get<String>(HomeRoutes.Phone.phoneNumber)

Expand All @@ -37,6 +39,9 @@ class CallModel(private val application: Application, savedStateHandle: SavedSta
var callLogs by mutableStateOf<List<CallLogEntry>>(emptyList(), policy = neverEqualPolicy())
private set

var contacts by mutableStateOf<List<BasicContactData>>(emptyList(), policy = neverEqualPolicy())
private set

private val toneGenerator = ToneGenerator(
AudioManager.STREAM_SYSTEM,
100
Expand All @@ -62,10 +67,21 @@ class CallModel(private val application: Application, savedStateHandle: SavedSta
fun onNumberInput(digit: String) {
numberToCall += digit
playToneForDigit(digit)
if (numberToCall.length > 3) {
searchPhoneNumber(numberToCall)
}
}

fun setPhoneNumberContact(contact: BasicContactData) {
numberToCall = contact.number
contacts = listOf(contact)
}

fun onBackSpace() {
numberToCall = numberToCall.removeLastChar()
if (numberToCall.length > 3) {
searchPhoneNumber(numberToCall)
}
}

fun callNumber(number: String = numberToCall) {
Expand Down Expand Up @@ -123,6 +139,12 @@ class CallModel(private val application: Application, savedStateHandle: SavedSta
toneGenerator.startTone(numericDigit, durationMs)
}

private fun searchPhoneNumber(number: String) {
viewModelScope.launch {
contacts = phoneLookupRepository.getContactsWithNumber(number)
}
}

companion object {
val phonePerms = arrayOf(
Manifest.permission.CALL_PHONE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.bnyro.contacts.R
import com.bnyro.contacts.presentation.components.DialerButton
import com.bnyro.contacts.presentation.components.NumberInput
import com.bnyro.contacts.presentation.components.PhoneNumberOnlyDisplay
import com.bnyro.contacts.presentation.screens.calllog.components.NumberInput
import com.bnyro.contacts.presentation.screens.calllog.components.PhoneNumberOnlyDisplay
import com.bnyro.contacts.presentation.screens.dialer.model.DialerModel
import com.bnyro.contacts.presentation.screens.dialer.model.state.CallState

Expand Down
Loading

0 comments on commit 8d39324

Please sign in to comment.