diff --git a/app/build.gradle b/app/build.gradle
index b7cff6d..3af3186 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -50,6 +50,7 @@ android {
}
buildFeatures {
viewBinding true
+ compose true
}
namespace 'dev.alenajam.opendialer'
lint {
@@ -58,6 +59,9 @@ android {
compileOptions {
coreLibraryDesugaringEnabled true
}
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.2"
+ }
}
dependencies {
@@ -105,6 +109,17 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
+
+ def composeBom = platform('androidx.compose:compose-bom:2023.10.01')
+ implementation composeBom
+ androidTestImplementation composeBom
+ implementation 'androidx.compose.material3:material3'
+ implementation 'androidx.compose.ui:ui-tooling-preview'
+ debugImplementation 'androidx.compose.ui:ui-tooling'
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
+ debugImplementation 'androidx.compose.ui:ui-test-manifest'
+ implementation 'androidx.compose.material:material-icons-extended'
+ implementation "androidx.compose.ui:ui-viewbinding"
}
repositories {
diff --git a/app/src/main/java/dev/alenajam/opendialer/features/main/MainFragment.kt b/app/src/main/java/dev/alenajam/opendialer/features/main/MainFragment.kt
index d1fef9a..03b8ffe 100644
--- a/app/src/main/java/dev/alenajam/opendialer/features/main/MainFragment.kt
+++ b/app/src/main/java/dev/alenajam/opendialer/features/main/MainFragment.kt
@@ -6,6 +6,8 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.adapter.FragmentStateAdapter
@@ -22,6 +24,7 @@ import dev.alenajam.opendialer.feature.contacts.ContactsFragment
import dev.alenajam.opendialer.feature.contactsSearch.SearchContactsFragment
import dev.alenajam.opendialer.feature.settings.ProfileFragment
import dev.alenajam.opendialer.features.main.MainFragmentDirections.Companion.actionHomeFragmentToSearchContactsFragment
+import dev.alenajam.opendialer.ui.OpenDialerApp
class MainFragment :
Fragment(),
@@ -53,8 +56,21 @@ class MainFragment :
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- _binding = FragmentHomeBinding.inflate(inflater, container, false)
- return binding.root
+ /*_binding = FragmentHomeBinding.inflate(inflater, container, false)
+ return binding.root*/
+
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ OpenDialerApp(
+ openDialpad = {
+ val action =
+ actionHomeFragmentToSearchContactsFragment(SearchContactsFragment.InitiationType.DIALPAD)
+ findNavController().safeNavigate(action)
+ }
+ )
+ }
+ }
}
override fun onDestroyView() {
@@ -65,7 +81,7 @@ class MainFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- onStatusBarColorChange?.onColorChange(view.context.getColor(R.color.windowBackground))
+ /*onStatusBarColorChange?.onColorChange(view.context.getColor(R.color.windowBackground))
binding.bottomNavigation.setOnNavigationItemSelectedListener(this)
binding.bottomNavigation.itemIconTintList = null
@@ -77,7 +93,7 @@ class MainFragment :
adapter = viewPagerAdapter
isUserInputEnabled = false
registerOnPageChangeCallback(OnPageChange())
- }
+ }*/
}
private fun setPage(fragment: Tab) {
diff --git a/app/src/main/java/dev/alenajam/opendialer/ui/OpenDialerApp.kt b/app/src/main/java/dev/alenajam/opendialer/ui/OpenDialerApp.kt
new file mode 100644
index 0000000..8b3b154
--- /dev/null
+++ b/app/src/main/java/dev/alenajam/opendialer/ui/OpenDialerApp.kt
@@ -0,0 +1,96 @@
+package dev.alenajam.opendialer.ui
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccessTimeFilled
+import androidx.compose.material.icons.filled.People
+import androidx.compose.material.icons.outlined.AccessTime
+import androidx.compose.material.icons.outlined.Dialpad
+import androidx.compose.material.icons.outlined.People
+import androidx.compose.material.icons.outlined.Search
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SearchBar
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidViewBinding
+import dev.alenajam.opendialer.R
+import dev.alenajam.opendialer.databinding.AppFragmentCallsBinding
+import dev.alenajam.opendialer.databinding.AppFragmentContactsBinding
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun OpenDialerApp(
+ openDialpad: () -> Unit
+) {
+ var selectedNavigationItem by remember { mutableStateOf("CALLS") }
+ Scaffold(
+ topBar = {
+ SearchBar(
+ query = "",
+ active = false,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
+ placeholder = { Text(text = stringResource(id = R.string.searchContacts)) },
+ leadingIcon = { Icon(imageVector = Icons.Outlined.Search, contentDescription = null) },
+ onSearch = {},
+ onActiveChange = {},
+ onQueryChange = {},
+ ) {}
+ },
+ bottomBar = {
+ NavigationBar {
+ val isSelected = { item: String -> item == selectedNavigationItem }
+ NavigationBarItem(
+ selected = isSelected("CALLS"),
+ icon = {
+ Icon(
+ imageVector = if (isSelected("CALLS")) Icons.Filled.AccessTimeFilled else Icons.Outlined.AccessTime,
+ contentDescription = null
+ )
+ },
+ label = { Text("Recents") },
+ onClick = { selectedNavigationItem = "CALLS" },
+ )
+
+ NavigationBarItem(
+ selected = isSelected("CONTACTS"),
+ icon = {
+ Icon(
+ imageVector = if (isSelected("CONTACTS")) Icons.Filled.People else Icons.Outlined.People,
+ contentDescription = null
+ )
+ },
+ label = { Text("Contacts") },
+ onClick = { selectedNavigationItem = "CONTACTS" },
+ )
+ }
+ },
+ floatingActionButton = {
+ FloatingActionButton(onClick = openDialpad) {
+ Icon(imageVector = Icons.Outlined.Dialpad, contentDescription = null)
+ }
+ }
+ ) { innerPadding ->
+ Surface(modifier = Modifier.padding(innerPadding)) {
+ when (selectedNavigationItem) {
+ "CALLS" -> AndroidViewBinding(AppFragmentCallsBinding::inflate)
+ "CONTACTS" -> AndroidViewBinding(AppFragmentContactsBinding::inflate)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/app_fragment_calls.xml b/app/src/main/res/layout/app_fragment_calls.xml
new file mode 100644
index 0000000..d3f5b7f
--- /dev/null
+++ b/app/src/main/res/layout/app_fragment_calls.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/app_fragment_contacts.xml b/app/src/main/res/layout/app_fragment_contacts.xml
new file mode 100644
index 0000000..0b2761a
--- /dev/null
+++ b/app/src/main/res/layout/app_fragment_contacts.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts
index b4aa165..ecd201f 100644
--- a/core/common/build.gradle.kts
+++ b/core/common/build.gradle.kts
@@ -27,7 +27,6 @@ android {
}
dependencies {
-
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.10.0")
@@ -40,6 +39,18 @@ dependencies {
implementation("androidx.navigation:navigation-ui-ktx:2.7.5")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.google.code.gson:gson:2.9.0")
+
+ val composeBom = platform("androidx.compose:compose-bom:2023.10.01")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
+ implementation("androidx.compose.material:material-icons-extended")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
+ implementation("androidx.compose.runtime:runtime-livedata")
}
kotlin {
diff --git a/core/common/src/main/java/dev/alenajam/opendialer/core/common/CommonUtils.java b/core/common/src/main/java/dev/alenajam/opendialer/core/common/CommonUtils.java
index 2bed1d8..5c7da1d 100644
--- a/core/common/src/main/java/dev/alenajam/opendialer/core/common/CommonUtils.java
+++ b/core/common/src/main/java/dev/alenajam/opendialer/core/common/CommonUtils.java
@@ -107,6 +107,7 @@ public static void startInCallUI(Context context) throws ClassNotFoundException
public static void makeCall(Context context, String number) {
if (PermissionUtils.hasMakeCallPermission(context)) {
Intent intent = new Intent(Intent.ACTION_CALL, Uri.fromParts("tel", number, null));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
TelecomManager telecomManager = (TelecomManager) context.getSystemService(TELECOM_SERVICE);
if (telecomManager == null) return;
@@ -153,7 +154,9 @@ public static void makeCall(Context context, String number) {
}
public static void makeSms(Context context, String number) {
- context.startActivity(new Intent(Intent.ACTION_SENDTO, Uri.fromParts("smsto", number, null)));
+ Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("smsto", number, null));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
}
public static void copyToClipobard(Context context, String text) {
@@ -169,6 +172,7 @@ public static void showContactDetail(Context context, int contactId) {
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, String.valueOf(contactId));
intent.setData(uri);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
@@ -197,6 +201,7 @@ public static void addContactAsExisting(Context context, String number) {
Intent addExistingIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
addExistingIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
addExistingIntent.putExtra(ContactsContract.Intents.Insert.PHONE, number);
+ addExistingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(addExistingIntent);
}
diff --git a/core/common/src/main/java/dev/alenajam/opendialer/core/common/forwardingPainter.kt b/core/common/src/main/java/dev/alenajam/opendialer/core/common/forwardingPainter.kt
new file mode 100644
index 0000000..f5dd242
--- /dev/null
+++ b/core/common/src/main/java/dev/alenajam/opendialer/core/common/forwardingPainter.kt
@@ -0,0 +1,60 @@
+package dev.alenajam.opendialer.core.common
+
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.DefaultAlpha
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.painter.Painter
+
+/**
+ * Create and return a new [Painter] that wraps [painter] with its [alpha], [colorFilter], or [onDraw] overwritten.
+ */
+fun forwardingPainter(
+ painter: Painter,
+ alpha: Float = DefaultAlpha,
+ colorFilter: ColorFilter? = null,
+ onDraw: DrawScope.(ForwardingDrawInfo) -> Unit = DefaultOnDraw,
+): Painter = ForwardingPainter(painter, alpha, colorFilter, onDraw)
+
+data class ForwardingDrawInfo(
+ val painter: Painter,
+ val alpha: Float,
+ val colorFilter: ColorFilter?,
+)
+
+private class ForwardingPainter(
+ private val painter: Painter,
+ private var alpha: Float,
+ private var colorFilter: ColorFilter?,
+ private val onDraw: DrawScope.(ForwardingDrawInfo) -> Unit,
+) : Painter() {
+
+ private var info = newInfo()
+
+ override val intrinsicSize get() = painter.intrinsicSize
+
+ override fun applyAlpha(alpha: Float): Boolean {
+ if (alpha != DefaultAlpha) {
+ this.alpha = alpha
+ this.info = newInfo()
+ }
+ return true
+ }
+
+ override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
+ if (colorFilter != null) {
+ this.colorFilter = colorFilter
+ this.info = newInfo()
+ }
+ return true
+ }
+
+ override fun DrawScope.onDraw() = onDraw(info)
+
+ private fun newInfo() = ForwardingDrawInfo(painter, alpha, colorFilter)
+}
+
+private val DefaultOnDraw: DrawScope.(ForwardingDrawInfo) -> Unit = { info ->
+ with(info.painter) {
+ draw(size, info.alpha, info.colorFilter)
+ }
+}
\ No newline at end of file
diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerCall.kt b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerCall.kt
index b8c18c0..d87dfe1 100644
--- a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerCall.kt
+++ b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerCall.kt
@@ -6,7 +6,7 @@ import java.io.Serializable
import java.util.Date
@Keep
-class DialerCall(
+data class DialerCall(
val id: Int,
val number: String?,
val date: Date,
@@ -107,6 +107,7 @@ class DialerCall(
}
fun isAnonymous(): Boolean = contactInfo.number.isNullOrBlank()
+ fun isContactSaved(): Boolean = !contactInfo.name.isNullOrBlank()
}
fun equalNumbers(number1: String?, number2: String?): Boolean {
diff --git a/feature/callDetail/build.gradle.kts b/feature/callDetail/build.gradle.kts
index e54d78b..857dd2d 100644
--- a/feature/callDetail/build.gradle.kts
+++ b/feature/callDetail/build.gradle.kts
@@ -24,6 +24,10 @@ android {
}
buildFeatures {
viewBinding = true
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.2"
}
}
@@ -65,6 +69,20 @@ dependencies {
implementation("com.squareup.picasso:picasso:2.71828")
implementation("org.ocpsoft.prettytime:prettytime:4.0.1.Final")
+
+ val composeBom = platform("androidx.compose:compose-bom:2023.10.01")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
+ implementation("androidx.compose.material:material-icons-extended")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
+ implementation("androidx.compose.runtime:runtime-livedata")
+
+ implementation("io.coil-kt:coil-compose:2.5.0")
}
kotlin {
diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailFragment.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailFragment.kt
index bafe99c..11f435b 100644
--- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailFragment.kt
+++ b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailFragment.kt
@@ -8,11 +8,11 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
-import androidx.recyclerview.widget.LinearLayoutManager
-import com.squareup.picasso.Picasso
import com.squareup.picasso.Transformation
import dagger.hilt.android.AndroidEntryPoint
import dev.alenajam.opendialer.core.common.CALL_DETAIL_PARAM_CALL_IDS
@@ -23,29 +23,15 @@ import dev.alenajam.opendialer.core.common.SharedPreferenceHelper
import dev.alenajam.opendialer.core.common.ToolbarListener
import dev.alenajam.opendialer.data.calls.CallOption
import dev.alenajam.opendialer.data.calls.DialerCall
-import dev.alenajam.opendialer.feature.callDetail.databinding.FragmentCallDetailBinding
import kotlinx.coroutines.ExperimentalCoroutinesApi
-private val circleTransform: Transformation = CircleTransform()
-private val colorList = listOf(
- Color.parseColor("#4FAF44"),
- Color.parseColor("#F6D145"),
- Color.parseColor("#FF9526"),
- Color.parseColor("#EF4423"),
- Color.parseColor("#328AF0")
-)
-
@AndroidEntryPoint
class CallDetailFragment : Fragment(), View.OnClickListener {
private val viewModel: DialerViewModel by viewModels()
- lateinit var adapter: RecentsAdapter
private lateinit var callIds: List
private lateinit var call: DialerCall
- private var optionsAdapter: CallOptionsAdapter? = null
private var toolbarListener: ToolbarListener? = null
private var onStatusBarColorChange: OnStatusBarColorChange? = null
- private var _binding: FragmentCallDetailBinding? = null
- private val binding get() = _binding!!
private val requestMakeCallPermissions =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { data ->
/** Ensure that all permissions were allowed */
@@ -85,17 +71,19 @@ class CallDetailFragment : Fragment(), View.OnClickListener {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- _binding = FragmentCallDetailBinding.inflate(
+ /*_binding = FragmentCallDetailBinding.inflate(
inflater,
container,
false
)
- return binding.root
- }
+ return binding.root*/
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ CallDetailScreen()
+ }
+ }
}
@ExperimentalCoroutinesApi
@@ -105,7 +93,7 @@ class CallDetailFragment : Fragment(), View.OnClickListener {
onStatusBarColorChange?.onColorChange(view.context.getColor(R.color.colorPrimaryDark))
toolbarListener?.hideToolbar(false)
- binding.toolbarLayout.toolbar.setNavigationOnClickListener { goBack() }
+ /*binding.toolbarLayout.toolbar.setNavigationOnClickListener { goBack() }
context?.let { binding.toolbarLayout.toolbar.setTitle(R.string.call_details) }
viewModel.call.observe(viewLifecycleOwner) {
@@ -123,13 +111,13 @@ class CallDetailFragment : Fragment(), View.OnClickListener {
dev.alenajam.opendialer.core.common.functional.EventObserver { handleBlockedCaller(false) })
binding.callButton.setOnClickListener(this)
- binding.contactIcon.setOnClickListener(this)
+ binding.contactIcon.setOnClickListener(this)*/
viewModel.getCallByIds(callIds)
}
private fun renderCall() {
- context?.let { context ->
+ /*context?.let { context ->
viewModel.getDetailOptions(call)
Picasso.get()
@@ -163,11 +151,11 @@ class CallDetailFragment : Fragment(), View.OnClickListener {
optionsAdapter = CallOptionsAdapter { option ->
handleOptionClick(option)
}
- binding.recyclerViewCallDetailsOptions.adapter = optionsAdapter
+ binding.recyclerViewCallDetailsOptions.adapter = optionsAdapter*/
}
private fun handleOptions(options: List) {
- optionsAdapter?.setData(options)
+ // optionsAdapter?.setData(options)
}
private fun handleOptionClick(option: CallOption) {
@@ -208,11 +196,11 @@ class CallDetailFragment : Fragment(), View.OnClickListener {
}
override fun onClick(v: View?) {
- if (v?.id == binding.callButton.id) {
+ /*if (v?.id == binding.callButton.id) {
activity?.let { makeCall() }
} else if (v?.id == binding.contactIcon.id) {
activity?.let { viewModel.openContact(it, call) }
- }
+ }*/
}
private fun goBack() {
diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailScreen.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailScreen.kt
new file mode 100644
index 0000000..9f6dffd
--- /dev/null
+++ b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailScreen.kt
@@ -0,0 +1,277 @@
+package dev.alenajam.opendialer.feature.callDetail
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+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.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.outlined.ArrowBack
+import androidx.compose.material.icons.outlined.Block
+import androidx.compose.material.icons.outlined.Call
+import androidx.compose.material.icons.outlined.CallMade
+import androidx.compose.material.icons.outlined.CallMissed
+import androidx.compose.material.icons.outlined.CallReceived
+import androidx.compose.material.icons.outlined.ContentCopy
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.Edit
+import androidx.compose.material.icons.outlined.Message
+import androidx.compose.material.icons.outlined.Voicemail
+import androidx.compose.material3.BottomAppBar
+import androidx.compose.material3.BottomAppBarDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import dev.alenajam.opendialer.core.common.CommonUtils
+import dev.alenajam.opendialer.core.common.forwardingPainter
+import dev.alenajam.opendialer.data.calls.CallType
+import dev.alenajam.opendialer.data.calls.ContactInfo
+import dev.alenajam.opendialer.data.calls.DetailCall
+import dev.alenajam.opendialer.data.calls.DialerCall
+import org.ocpsoft.prettytime.PrettyTime
+import java.util.Date
+
+@Composable
+internal fun CallDetailScreen(
+ viewModel: DialerViewModel = viewModel()
+) {
+ val call = viewModel.call.observeAsState()
+ val isAnon = call.value?.isAnonymous() == true
+ val childCalls = call.value?.childCalls ?: emptyList()
+
+ Scaffold(
+ topBar = {
+ TopBar(
+ call = call.value
+ )
+ },
+ bottomBar = {
+ BottomBar(
+ isAnon = isAnon,
+ makeCall = { viewModel.makeCall(call.value!!.number!!) },
+ sendMessage = viewModel::sendMessage,
+ copyNumber = { viewModel.copyNumber(call.value!!) },
+ dialNumber = { viewModel.editNumberBeforeCall(call.value!!) },
+ deleteCalls = { viewModel.deleteCalls(call.value!!) }
+ )
+ }
+ ) { innerPadding ->
+ Column(
+ verticalArrangement = Arrangement.Bottom,
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(innerPadding)
+ ) {
+ LazyColumn {
+ items(childCalls) { call ->
+ CallRow(call = call)
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TopBar(
+ call: DialerCall?
+) {
+ TopAppBar(
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ titleContentColor = MaterialTheme.colorScheme.primary,
+ ),
+ title = {
+ if (call != null) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val placeholder = forwardingPainter(
+ painter = rememberVectorPainter(Icons.Filled.AccountCircle),
+ colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurfaceVariant)
+ )
+ AsyncImage(
+ model = call.contactInfo.photoUri,
+ contentDescription = null,
+ modifier = Modifier.size(50.dp),
+ placeholder = placeholder,
+ error = placeholder,
+ fallback = placeholder
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text(
+ text = if (call.isAnonymous()) stringResource(id = R.string.anonymous)
+ else if (!call.contactInfo.name.isNullOrBlank()) call.contactInfo.name!!
+ else call.contactInfo.number!!,
+ )
+ }
+ }
+ },
+ navigationIcon = {
+ Icon(imageVector = Icons.Outlined.ArrowBack, contentDescription = null)
+ }
+ )
+}
+
+@Composable
+private fun BottomBar(
+ isAnon: Boolean,
+ makeCall: () -> Unit,
+ sendMessage: () -> Unit,
+ copyNumber: () -> Unit,
+ dialNumber: () -> Unit,
+ deleteCalls: () -> Unit,
+) {
+ BottomAppBar(
+ actions = {
+ if (!isAnon) {
+ IconButton(onClick = sendMessage) {
+ Icon(Icons.Outlined.Message, contentDescription = "Localized description")
+ }
+ IconButton(onClick = copyNumber) {
+ Icon(Icons.Outlined.ContentCopy, contentDescription = "Localized description")
+ }
+ IconButton(onClick = dialNumber) {
+ Icon(Icons.Outlined.Edit, contentDescription = "Localized description")
+ }
+ }
+ IconButton(onClick = deleteCalls) {
+ Icon(Icons.Outlined.Delete, contentDescription = "Localized description")
+ }
+ },
+ floatingActionButton = {
+ if (!isAnon) {
+ FloatingActionButton(
+ onClick = makeCall,
+ containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+ elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
+ ) {
+ Icon(Icons.Outlined.Call, "Localized description")
+ }
+ }
+ }
+ )
+}
+
+@Composable
+private fun CallRow(
+ call: DetailCall
+) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp),
+ ) {
+ Icon(
+ imageVector = when (call.type) {
+ CallType.INCOMING, CallType.ANSWERED_EXTERNALLY -> Icons.Outlined.CallReceived
+ CallType.OUTGOING -> Icons.Outlined.CallMade
+ CallType.MISSED, CallType.REJECTED -> Icons.Outlined.CallMissed
+ CallType.VOICEMAIL -> Icons.Outlined.Voicemail
+ CallType.BLOCKED -> Icons.Outlined.Block
+ }, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = when (call.type) {
+ CallType.OUTGOING -> stringResource(id = R.string.outgoing_call)
+ CallType.INCOMING, CallType.ANSWERED_EXTERNALLY -> stringResource(id = R.string.incoming_call)
+ CallType.MISSED -> stringResource(id = R.string.missed_call)
+ CallType.VOICEMAIL -> stringResource(id = R.string.voicemail_call)
+ CallType.REJECTED -> stringResource(id = R.string.rejected_call)
+ CallType.BLOCKED -> stringResource(id = R.string.blocked_call)
+ },
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ ) {
+ Text(
+ text = PrettyTime().format(call.date),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ Text(
+ text = CommonUtils.getDurationTimeStringMinimal(call.duration * 1000),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+}
+
+private val incomingDetailCallMock = DetailCall(
+ id = 1,
+ date = Date(),
+ type = CallType.INCOMING,
+ duration = 500L,
+)
+
+private val callMock = DialerCall(
+ id = 1,
+ number = "333123456",
+ date = Date(),
+ type = CallType.OUTGOING,
+ options = emptyList(),
+ childCalls = listOf(
+ incomingDetailCallMock
+ ),
+ contactInfo = ContactInfo(
+ name = "John Doe",
+ number = "3331234567",
+ photoUri = null
+ )
+)
+
+@Preview(showBackground = true)
+@Composable
+private fun TopBarPreview() {
+ TopBar(call = callMock)
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BottomBarPreview() {
+ BottomBar(
+ isAnon = false,
+ makeCall = {},
+ sendMessage = {},
+ copyNumber = {},
+ dialNumber = {},
+ deleteCalls = {},
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun CallRowPreview() {
+ CallRow(call = incomingDetailCallMock)
+}
\ No newline at end of file
diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailsAdapter.java b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailsAdapter.java
index ac4c26e..baa2725 100644
--- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailsAdapter.java
+++ b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailsAdapter.java
@@ -50,7 +50,9 @@ public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
if (currentCall.getDuration() == 0) {
holder.duration.setVisibility(View.GONE);
} else {
- holder.duration.setText(CommonUtils.getDurationTimeStringMinimal(currentCall.getDuration() * 1000));
+ holder.duration.setText(
+ CommonUtils.getDurationTimeStringMinimal(currentCall.getDuration() * 1000)
+ );
}
// TODO refactor
diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/DialerViewModel.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/DialerViewModel.kt
index a2f0d7f..60c2e36 100644
--- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/DialerViewModel.kt
+++ b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/DialerViewModel.kt
@@ -49,6 +49,7 @@ class DialerViewModel
getDetailOptions(viewModelScope, call) { it.fold({}, ::handleDetailOptions) }
fun makeCall(activity: Activity, number: String) = CommonUtils.makeCall(activity, number)
+ fun makeCall(number: String) = CommonUtils.makeCall(app, number)
fun copyNumber(call: DialerCall) = CommonUtils.copyToClipobard(app, call.contactInfo.number)
fun openContact(activity: Activity, call: DialerCall) {
@@ -57,6 +58,10 @@ class DialerViewModel
}
}
+ fun sendMessage() {
+ call.value?.number?.let { CommonUtils.makeSms(app, it) }
+ }
+
fun editNumberBeforeCall(activity: Activity, call: DialerCall) {
val intent = Intent(Intent.ACTION_DIAL).apply {
data = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.number, null)
@@ -64,6 +69,15 @@ class DialerViewModel
activity.startActivity(intent)
}
+ fun editNumberBeforeCall(call: DialerCall) {
+ val intent = Intent(Intent.ACTION_DIAL).apply {
+ data = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.number, null)
+ }
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ app.startActivity(intent)
+ }
+
+
fun deleteCalls(call: DialerCall) = deleteCallsUseCase(viewModelScope, call.childCalls) {
it.fold(
{},
diff --git a/feature/calls/build.gradle.kts b/feature/calls/build.gradle.kts
index b3b9415..62d56c5 100644
--- a/feature/calls/build.gradle.kts
+++ b/feature/calls/build.gradle.kts
@@ -24,6 +24,10 @@ android {
}
buildFeatures {
viewBinding = true
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.2"
}
}
@@ -46,6 +50,7 @@ dependencies {
kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1")
testImplementation("com.google.dagger:hilt-android-testing:2.48.1")
kaptTest("com.google.dagger:hilt-compiler:2.48.1")
+ implementation("com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava")
implementation("com.squareup.picasso:picasso:2.71828")
implementation("org.ocpsoft.prettytime:prettytime:4.0.1.Final")
@@ -63,8 +68,23 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.6.2")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.5")
implementation("androidx.navigation:navigation-ui-ktx:2.7.5")
+ implementation("androidx.navigation:navigation-compose:2.7.5")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.recyclerview:recyclerview:1.3.2")
+
+ val composeBom = platform("androidx.compose:compose-bom:2023.10.01")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
+ implementation("androidx.compose.material:material-icons-extended")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
+ implementation("androidx.compose.runtime:runtime-livedata")
+
+ implementation("io.coil-kt:coil-compose:2.5.0")
}
kotlin {
diff --git a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/CallsScreen.kt b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/CallsScreen.kt
new file mode 100644
index 0000000..aedfd0b
--- /dev/null
+++ b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/CallsScreen.kt
@@ -0,0 +1,256 @@
+package dev.alenajam.opendialer.feature.calls
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxSize
+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.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.outlined.Block
+import androidx.compose.material.icons.outlined.CallMade
+import androidx.compose.material.icons.outlined.CallMissed
+import androidx.compose.material.icons.outlined.CallReceived
+import androidx.compose.material.icons.outlined.History
+import androidx.compose.material.icons.outlined.Message
+import androidx.compose.material.icons.outlined.PersonAddAlt
+import androidx.compose.material.icons.outlined.Phone
+import androidx.compose.material.icons.outlined.Voicemail
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+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.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import coil.compose.AsyncImage
+import dev.alenajam.opendialer.core.common.forwardingPainter
+import dev.alenajam.opendialer.data.calls.CallType
+import dev.alenajam.opendialer.data.calls.ContactInfo
+import dev.alenajam.opendialer.data.calls.DialerCall
+import org.ocpsoft.prettytime.PrettyTime
+import java.util.Date
+
+@Composable
+internal fun CallsScreen(
+ viewModel: DialerViewModel = viewModel(), navController: NavController
+) {
+ val calls = viewModel.calls.observeAsState(emptyList())
+ var openRowId by remember { mutableStateOf(null) }
+ Surface(modifier = Modifier.fillMaxSize()) {
+ LazyColumn {
+ items(calls.value) { call ->
+ val isOpen = openRowId == call.id
+ CallRow(call = call,
+ isOpen = isOpen,
+ onClick = { openRowId = if (isOpen) null else call.id },
+ makeCall = { viewModel.makeCall(call.contactInfo.number!!) },
+ sendMessage = { viewModel.sendMessage(call.contactInfo.number!!) },
+ addContact = { viewModel.addToContact(call.contactInfo.number!!) },
+ openHistory = { viewModel.callDetail(navController, call) })
+ }
+ }
+ }
+}
+
+@Composable
+private fun CallRow(
+ call: DialerCall,
+ isOpen: Boolean,
+ onClick: () -> Unit,
+ makeCall: () -> Unit,
+ sendMessage: () -> Unit,
+ addContact: () -> Unit,
+ openHistory: () -> Unit,
+) {
+ Surface(
+ onClick = onClick, tonalElevation = if (isOpen) 8.dp else 0.dp
+ ) {
+ Column {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp),
+ ) {
+ val placeholder = forwardingPainter(
+ painter = rememberVectorPainter(Icons.Filled.AccountCircle),
+ colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurfaceVariant)
+ )
+ AsyncImage(
+ model = call.contactInfo.photoUri,
+ contentDescription = null,
+ modifier = Modifier.size(50.dp),
+ placeholder = placeholder,
+ error = placeholder,
+ fallback = placeholder
+ )
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = if (call.isAnonymous()) stringResource(id = R.string.anonymous)
+ else if (!call.contactInfo.name.isNullOrBlank()) call.contactInfo.name!!
+ else call.contactInfo.number!!,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp),
+ ) {
+ Icon(
+ imageVector = when (call.type) {
+ CallType.INCOMING, CallType.ANSWERED_EXTERNALLY -> Icons.Outlined.CallReceived
+ CallType.OUTGOING -> Icons.Outlined.CallMade
+ CallType.MISSED, CallType.REJECTED -> Icons.Outlined.CallMissed
+ CallType.VOICEMAIL -> Icons.Outlined.Voicemail
+ CallType.BLOCKED -> Icons.Outlined.Block
+ },
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.size(14.dp)
+ )
+
+ Text(
+ text = PrettyTime().format(call.date),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ if (!call.isAnonymous()) {
+ IconButton(onClick = makeCall) {
+ Icon(
+ imageVector = Icons.Outlined.Phone,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+
+ AnimatedVisibility(visible = isOpen) {
+ Divider()
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (!call.isAnonymous()) {
+ if (!call.isContactSaved()) {
+ CallRowButton(
+ label = "Add contact", icon = Icons.Outlined.PersonAddAlt, onClick = addContact
+ )
+ }
+
+ CallRowButton(
+ label = "Message", icon = Icons.Outlined.Message, onClick = sendMessage
+ )
+ }
+
+ CallRowButton(
+ label = "History", icon = Icons.Outlined.History, onClick = openHistory
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun RowScope.CallRowButton(
+ label: String, icon: ImageVector, onClick: () -> Unit
+) {
+ Surface(
+ onClick = onClick, modifier = Modifier.weight(1f), color = Color.Transparent
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.padding(vertical = 16.dp)
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+}
+
+private val incomingCallMock = DialerCall(
+ id = 1,
+ number = "3331234567",
+ date = Date(),
+ type = CallType.INCOMING,
+ options = listOf(),
+ childCalls = listOf(),
+ contactInfo = ContactInfo(
+ number = "3331234567"
+ )
+)
+private val outgoingCallMock = incomingCallMock.copy(type = CallType.OUTGOING)
+private val anonymousCallMock = incomingCallMock.copy(
+ number = null, contactInfo = ContactInfo(number = null)
+)
+
+@Preview(showBackground = true)
+@Composable
+private fun IncomingCallPreview() {
+ CallRow(call = incomingCallMock,
+ isOpen = false,
+ onClick = {},
+ makeCall = {},
+ addContact = {},
+ sendMessage = {},
+ openHistory = {})
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun OutgoingCallPreview() {
+ CallRow(call = outgoingCallMock,
+ isOpen = false,
+ onClick = {},
+ makeCall = {},
+ addContact = {},
+ sendMessage = {},
+ openHistory = {})
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AnonymousCallPreview() {
+ CallRow(call = anonymousCallMock,
+ isOpen = false,
+ onClick = {},
+ makeCall = {},
+ addContact = {},
+ sendMessage = {},
+ openHistory = {})
+}
\ No newline at end of file
diff --git a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/DialerViewModel.kt b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/DialerViewModel.kt
index 9da5575..e2d4e6e 100644
--- a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/DialerViewModel.kt
+++ b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/DialerViewModel.kt
@@ -26,7 +26,7 @@ class DialerViewModel
@Inject constructor(
dialerRepository: DialerRepositoryImpl,
contactsRepository: dev.alenajam.opendialer.data.contacts.DialerRepositoryImpl,
- app: Application,
+ private val app: Application,
private val cacheRepository: CacheRepositoryImpl,
private val startCacheUseCase: StartCache
) : ViewModel() {
@@ -44,9 +44,11 @@ class DialerViewModel
fun sendMessage(activity: Activity, call: DialerCall) =
CommonUtils.makeSms(activity, call.contactInfo.number)
+ fun sendMessage(number: String) =
+ CommonUtils.makeSms(app, number)
- //
fun makeCall(activity: Activity, number: String) = CommonUtils.makeCall(activity, number)
+ fun makeCall(number: String) = CommonUtils.makeCall(app, number)
fun callDetail(navController: NavController, call: DialerCall) {
navController.navigateToCallDetail(call.childCalls.map { it.id })
@@ -57,6 +59,8 @@ class DialerViewModel
fun addToContact(activity: Activity, call: DialerCall) =
CommonUtils.addContactAsExisting(activity, call.contactInfo.number)
+ fun addToContact(number: String) =
+ CommonUtils.addContactAsExisting(app, number)
fun openContact(activity: Activity, call: DialerCall) {
ContactsHelper.getContactByPhoneNumber(activity, call.contactInfo.number)?.let {
diff --git a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsFragment.kt b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsFragment.kt
index 7e40a17..cd2a143 100644
--- a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsFragment.kt
+++ b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsFragment.kt
@@ -6,34 +6,29 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
-import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import dev.alenajam.opendialer.core.common.PermissionUtils
import dev.alenajam.opendialer.data.calls.CallOption
import dev.alenajam.opendialer.data.calls.DialerCall
-import dev.alenajam.opendialer.feature.calls.databinding.FragmentRecentsBinding
import kotlinx.coroutines.ExperimentalCoroutinesApi
@AndroidEntryPoint
class RecentsFragment : Fragment() {
private val viewModel: DialerViewModel by viewModels()
- lateinit var adapter: RecentsAdapter
private var notCalledNumber = ""
private var refreshNeeded = false
- private var _binding: FragmentRecentsBinding? = null
- private val binding get() = _binding!!
- @ExperimentalCoroutinesApi
private val requestPermissions =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { data ->
/** Ensure that all permissions were allowed */
if (PermissionUtils.recentsPermissions.all { data[it] == true }) {
/** Hide permission prompt */
- binding.permissionPrompt.visibility = View.GONE
+ // binding.permissionPrompt.visibility = View.GONE
observeCalls()
}
@@ -55,13 +50,17 @@ class RecentsFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- _binding = FragmentRecentsBinding.inflate(inflater, container, false)
- return binding.root
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
+ /*_binding = FragmentRecentsBinding.inflate(inflater, container, false)
+ return binding.root*/
+
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ CallsScreen(
+ navController = findNavController()
+ )
+ }
+ }
}
override fun onStart() {
@@ -74,11 +73,10 @@ class RecentsFragment : Fragment() {
viewModel.stopCache()
}
- @ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- context?.let {
+ /*context?.let {
adapter = RecentsAdapter(
it,
binding.recyclerViewCallLog,
@@ -95,7 +93,7 @@ class RecentsFragment : Fragment() {
if (PermissionUtils.hasRecentsPermission(context)) {
observeCalls()
} else {
- /** Show permission prompt */
+ *//** Show permission prompt *//*
binding.permissionPrompt.visibility = View.VISIBLE
binding.buttonPermission.setOnClickListener {
requestPermissions.launch(PermissionUtils.recentsPermissions)
@@ -104,7 +102,7 @@ class RecentsFragment : Fragment() {
if (PermissionUtils.hasContactsPermission(context)) {
observeContacts()
- }
+ }*/
}
override fun onResume() {
@@ -161,14 +159,14 @@ class RecentsFragment : Fragment() {
}
private fun handleCalls(calls: List) {
- adapter.setData(calls)
+ // adapter.setData(calls)
}
private fun refreshData() {
if (refreshNeeded) {
refreshNeeded = false
viewModel.invalidateCache()
- adapter.notifyDataSetChanged()
+ // adapter.notifyDataSetChanged()
}
}
}
\ No newline at end of file
diff --git a/feature/contacts/build.gradle.kts b/feature/contacts/build.gradle.kts
index ae3dddf..275d64e 100644
--- a/feature/contacts/build.gradle.kts
+++ b/feature/contacts/build.gradle.kts
@@ -24,6 +24,10 @@ android {
}
buildFeatures {
viewBinding = true
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.2"
}
}
@@ -63,6 +67,20 @@ dependencies {
implementation("androidx.navigation:navigation-ui-ktx:2.7.5")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.recyclerview:recyclerview:1.3.2")
+
+ val composeBom = platform("androidx.compose:compose-bom:2023.10.01")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
+ implementation("androidx.compose.material:material-icons-extended")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
+ implementation("androidx.compose.runtime:runtime-livedata")
+
+ implementation("io.coil-kt:coil-compose:2.5.0")
}
kotlin {
diff --git a/feature/contacts/src/main/java/dev/alenajam/opendialer/feature/contacts/ContactsFragment.kt b/feature/contacts/src/main/java/dev/alenajam/opendialer/feature/contacts/ContactsFragment.kt
index f9f16af..7b1216d 100644
--- a/feature/contacts/src/main/java/dev/alenajam/opendialer/feature/contacts/ContactsFragment.kt
+++ b/feature/contacts/src/main/java/dev/alenajam/opendialer/feature/contacts/ContactsFragment.kt
@@ -5,29 +5,23 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
-import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import dev.alenajam.opendialer.core.common.PermissionUtils
import dev.alenajam.opendialer.data.contacts.DialerContact
-import dev.alenajam.opendialer.feature.contacts.databinding.FragmentContactsBinding
-import kotlinx.coroutines.ExperimentalCoroutinesApi
@AndroidEntryPoint
class ContactsFragment : Fragment() {
private val viewModel: DialerViewModel by viewModels()
- lateinit var adapter: ContactAdapter
- private var _binding: FragmentContactsBinding? = null
- private val binding get() = _binding!!
-
- @ExperimentalCoroutinesApi
private val requestPermissions =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { data ->
/** Ensure that all permissions were allowed */
if (PermissionUtils.contactsPermissions.all { data[it] == true }) {
/** Hide permission promp */
- binding.permissionPrompt.visibility = View.GONE
+ //binding.permissionPrompt.visibility = View.GONE
observeContacts()
}
}
@@ -37,40 +31,41 @@ class ContactsFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- _binding = FragmentContactsBinding.inflate(inflater, container, false)
- return binding.root
- }
+ /*_binding = FragmentContactsBinding.inflate(inflater, container, false)
+ return binding.root*/
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ ContactsScreen()
+ }
+ }
}
- @ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- adapter = ContactAdapter(activity)
+ /*adapter = ContactAdapter(activity)
binding.recyclerViewContacts.adapter = adapter
binding.recyclerViewContacts.layoutManager = LinearLayoutManager(context)
if (PermissionUtils.hasContactsPermission(context)) {
observeContacts()
} else {
- /** Show permission prompt */
+ */
+ /** Show permission prompt *//*
binding.permissionPrompt.visibility = View.VISIBLE
binding.buttonPermission.setOnClickListener {
requestPermissions.launch(PermissionUtils.contactsPermissions)
}
- }
+ }*/
}
-
- @ExperimentalCoroutinesApi
+
private fun observeContacts() {
- viewModel.contacts.observe(viewLifecycleOwner) { handleContacts(it) }
+ // viewModel.contacts.observe(viewLifecycleOwner) { handleContacts(it) }
}
private fun handleContacts(list: List) {
- adapter.contacts = ArrayList(list)
+ // adapter.contacts = ArrayList(list)
}
}
\ No newline at end of file
diff --git a/feature/contacts/src/main/java/dev/alenajam/opendialer/feature/contacts/ContactsScreen.kt b/feature/contacts/src/main/java/dev/alenajam/opendialer/feature/contacts/ContactsScreen.kt
new file mode 100644
index 0000000..99b4292
--- /dev/null
+++ b/feature/contacts/src/main/java/dev/alenajam/opendialer/feature/contacts/ContactsScreen.kt
@@ -0,0 +1,96 @@
+package dev.alenajam.opendialer.feature.contacts
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+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.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.vector.RenderVectorGroup
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import dev.alenajam.opendialer.core.common.forwardingPainter
+import dev.alenajam.opendialer.data.contacts.DialerContact
+
+@Composable
+internal fun ContactsScreen(
+ viewModel: DialerViewModel = viewModel(),
+) {
+ val calls = viewModel.contacts.observeAsState(emptyList())
+ Surface(modifier = Modifier.fillMaxSize()) {
+ LazyColumn {
+ items(calls.value) { contact ->
+ ContactRow(
+ contact = contact,
+ onClick = { viewModel.openContact(contact.id) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ContactRow(
+ contact: DialerContact,
+ onClick: () -> Unit,
+) {
+ Surface(
+ onClick = onClick,
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp),
+ ) {
+ val placeholder = forwardingPainter(
+ painter = rememberVectorPainter(Icons.Filled.AccountCircle),
+ colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurfaceVariant)
+ )
+ AsyncImage(
+ model = contact.image,
+ contentDescription = null,
+ modifier = Modifier.size(50.dp),
+ placeholder = placeholder,
+ error = placeholder,
+ fallback = placeholder
+ )
+
+ Text(
+ text = contact.name,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+}
+
+val contactMock = DialerContact(
+ id = 1,
+ name = "John Doe",
+ starred = false,
+ image = null
+)
+
+@Preview(showBackground = true)
+@Composable
+private fun ContactRowPreview() {
+ ContactRow(
+ contact = contactMock,
+ onClick = {},
+ )
+}
diff --git a/feature/contacts/src/main/java/dev/alenajam/opendialer/feature/contacts/DialerViewModel.kt b/feature/contacts/src/main/java/dev/alenajam/opendialer/feature/contacts/DialerViewModel.kt
index d756091..521bdbc 100644
--- a/feature/contacts/src/main/java/dev/alenajam/opendialer/feature/contacts/DialerViewModel.kt
+++ b/feature/contacts/src/main/java/dev/alenajam/opendialer/feature/contacts/DialerViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import dagger.hilt.android.lifecycle.HiltViewModel
+import dev.alenajam.opendialer.core.common.CommonUtils
import dev.alenajam.opendialer.data.contacts.DialerContact
import dev.alenajam.opendialer.data.contacts.DialerRepositoryImpl
import kotlinx.coroutines.Dispatchers
@@ -13,14 +14,18 @@ import kotlinx.coroutines.flow.map
import javax.inject.Inject
@HiltViewModel
-class DialerViewModel
+internal class DialerViewModel
@Inject constructor(
dialerRepository: DialerRepositoryImpl,
- app: Application,
+ private val app: Application,
) : ViewModel() {
val contacts: LiveData> = dialerRepository
.getContacts(app.contentResolver)
.map { DialerContact.mapList(it) }
.flowOn(Dispatchers.IO)
.asLiveData()
+
+ fun openContact(contactId: Int) {
+ CommonUtils.showContactDetail(app, contactId)
+ }
}
\ No newline at end of file