wip: wiring network client

This commit is contained in:
2026-05-29 00:33:12 +02:00
parent c68787cd01
commit 6b07424ebe
11 changed files with 78 additions and 40 deletions
+7
View File
@@ -20,6 +20,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.compose) alias(libs.plugins.compose)
// necessary for using T.serializer() on a data class
alias(libs.plugins.kotlin.serialization)
} }
android { android {
@@ -96,6 +98,7 @@ dependencies {
implementation(libs.androidx.glance.material3) implementation(libs.androidx.glance.material3)
implementation(libs.kotlin.stdlib) implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
@@ -115,6 +118,10 @@ dependencies {
implementation(libs.androidx.compose.ui.viewbinding) implementation(libs.androidx.compose.ui.viewbinding)
implementation(libs.androidx.compose.ui.googlefonts) implementation(libs.androidx.compose.ui.googlefonts)
implementation(libs.ktor.client.auth)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)
androidTestImplementation(libs.junit) androidTestImplementation(libs.junit)
+2
View File
@@ -9,6 +9,8 @@
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.provider.Telephony.SMS_RECEIVED" /> <uses-permission android:name="android.provider.Telephony.SMS_RECEIVED" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
@@ -32,19 +32,24 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.viewinterop.AndroidViewBinding import androidx.compose.ui.viewinterop.AndroidViewBinding
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import xyz.magicalbits.smsremote.components.JetchatDrawer import xyz.magicalbits.smsremote.components.JetchatDrawer
import xyz.magicalbits.smsremote.databinding.ContentMainBinding import xyz.magicalbits.smsremote.databinding.ContentMainBinding
import xyz.magicalbits.smsremote.network.NetworkClient
/** /**
* Main activity for the app. * Main activity for the app.
*/ */
class NavActivity : AppCompatActivity() { class NavActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels() private val viewModel: MainViewModel by viewModels()
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -53,6 +58,12 @@ class NavActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { _, insets -> insets } ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { _, insets -> insets }
var deviceDtoList: List<NetworkClient.DeviceDto> = listOf()
viewModel.viewModelScope.launch {
val networkClient = NetworkClient()
deviceDtoList = networkClient.getDevices()
}
setContentView( setContentView(
ComposeView(this).apply { ComposeView(this).apply {
consumeWindowInsets = false consumeWindowInsets = false
@@ -79,15 +90,18 @@ class NavActivity : AppCompatActivity() {
JetchatDrawer( JetchatDrawer(
drawerState = drawerState, drawerState = drawerState,
selectedMenu = selectedMenu, selectedMenu = selectedMenu,
deviceDtoList = deviceDtoList,
onChatClicked = { onChatClicked = {
findNavController().popBackStack(R.id.nav_device, false) findNavController().popBackStack(R.id.nav_device, false)
val args = Bundle(1) val args = Bundle(1)
args.putString("deviceName", it) args.putString("deviceId", it.access_key)
args.putString("name", it.name)
args.putString("type", it.type)
findNavController().navigate(R.id.nav_device, args) findNavController().navigate(R.id.nav_device, args)
scope.launch { scope.launch {
drawerState.close() drawerState.close()
} }
selectedMenu = it selectedMenu = it.access_key
}, },
) { ) {
AndroidViewBinding(ContentMainBinding::inflate) AndroidViewBinding(ContentMainBinding::inflate)
@@ -56,11 +56,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import xyz.magicalbits.smsremote.R import xyz.magicalbits.smsremote.R
import xyz.magicalbits.smsremote.network.NetworkClient
import xyz.magicalbits.smsremote.theme.JetchatTheme import xyz.magicalbits.smsremote.theme.JetchatTheme
import xyz.magicalbits.smsremote.widget.WidgetReceiver import xyz.magicalbits.smsremote.widget.WidgetReceiver
@Composable @Composable
fun JetchatDrawerContent(onChatClicked: (String) -> Unit, selectedMenu: String = "iPhone XYZ") { fun JetchatDrawerContent(onChatClicked: (NetworkClient.DeviceDto) -> Unit, selectedMenu: String = "", deviceDtoList: List<NetworkClient.DeviceDto> = listOf()) {
// Use windowInsetsTopHeight() to add a spacer which pushes the drawer content // Use windowInsetsTopHeight() to add a spacer which pushes the drawer content
// below the status bar (y-axis) // below the status bar (y-axis)
Column { Column {
@@ -68,12 +69,13 @@ fun JetchatDrawerContent(onChatClicked: (String) -> Unit, selectedMenu: String =
DrawerHeader() DrawerHeader()
DividerItem() DividerItem()
DrawerItemHeader("Devices") DrawerItemHeader("Devices")
DeviceItem("Samsung A14", selectedMenu == "Samsung A14") {
onChatClicked("Samsung A14") for (deviceDto in deviceDtoList) {
DeviceItem(deviceDto.name, selectedMenu == deviceDto.access_key) {
onChatClicked(deviceDto)
} }
DeviceItem("iPhone XYZ", selectedMenu == "iPhone XYZ") {
onChatClicked("iPhone XYZ")
} }
// DividerItem(modifier = Modifier.padding(horizontal = 28.dp)) // DividerItem(modifier = Modifier.padding(horizontal = 28.dp))
if (widgetAddingIsSupported(LocalContext.current)) { if (widgetAddingIsSupported(LocalContext.current)) {
DividerItem(modifier = Modifier.padding(horizontal = 28.dp)) DividerItem(modifier = Modifier.padding(horizontal = 28.dp))
@@ -23,13 +23,15 @@ import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import xyz.magicalbits.smsremote.network.NetworkClient
import xyz.magicalbits.smsremote.theme.JetchatTheme import xyz.magicalbits.smsremote.theme.JetchatTheme
@Composable @Composable
fun JetchatDrawer( fun JetchatDrawer(
drawerState: DrawerState = rememberDrawerState(initialValue = Closed), drawerState: DrawerState = rememberDrawerState(initialValue = Closed),
selectedMenu: String, selectedMenu: String,
onChatClicked: (String) -> Unit, deviceDtoList: List<NetworkClient.DeviceDto>,
onChatClicked: (NetworkClient.DeviceDto) -> Unit,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
JetchatTheme { JetchatTheme {
@@ -44,6 +46,7 @@ fun JetchatDrawer(
JetchatDrawerContent( JetchatDrawerContent(
onChatClicked = onChatClicked, onChatClicked = onChatClicked,
selectedMenu = selectedMenu, selectedMenu = selectedMenu,
deviceDtoList = deviceDtoList,
) )
} }
}, },
@@ -87,17 +87,3 @@ val meProfile =
timeZone = "In your timezone", timeZone = "In your timezone",
commonChannels = null, commonChannels = null,
) )
val a14Device =
DeviceScreenState(
deviceId = "012345",
name = "Samsung A14",
phoneNumbers = listOf("+420123456789", "+420777444111")
)
val iPhoneDevice =
DeviceScreenState(
deviceId = "012345",
name = "iPhone XYZ",
phoneNumbers = listOf("+15558881111")
)
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
@@ -24,7 +23,6 @@ import xyz.magicalbits.smsremote.components.JetchatAppBar
import xyz.magicalbits.smsremote.theme.JetchatTheme import xyz.magicalbits.smsremote.theme.JetchatTheme
import kotlin.getValue import kotlin.getValue
import androidx.navigation.findNavController import androidx.navigation.findNavController
import kotlinx.coroutines.launch
class DeviceFragment : Fragment() { class DeviceFragment : Fragment() {
private val viewModel: DeviceViewModel by viewModels() private val viewModel: DeviceViewModel by viewModels()
@@ -33,8 +31,10 @@ class DeviceFragment : Fragment() {
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
// Consider using safe args plugin // Consider using safe args plugin
val deviceName = arguments?.getString("deviceName") val deviceId = arguments?.getString("deviceId")
viewModel.setDeviceId(deviceName) val name = arguments?.getString("name")
val type = arguments?.getString("type")
viewModel.setDeviceData(deviceId, name, type)
} }
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
@@ -65,6 +65,7 @@ class DeviceFragment : Fragment() {
JetchatTheme { JetchatTheme {
if (deviceData == null) { if (deviceData == null) {
println("calling device error")
DeviceError() DeviceError()
} else { } else {
val navController: NavController = rootView.findNavController() val navController: NavController = rootView.findNavController()
@@ -4,25 +4,38 @@ import androidx.compose.runtime.Immutable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import xyz.magicalbits.smsremote.data.a14Device import androidx.lifecycle.viewModelScope
import xyz.magicalbits.smsremote.data.iPhoneDevice import kotlinx.coroutines.launch
import xyz.magicalbits.smsremote.network.NetworkClient
class DeviceViewModel : ViewModel() { class DeviceViewModel : ViewModel() {
private var deviceId: String = "" private var deviceId: String = ""
private val _deviceData = MutableLiveData<DeviceScreenState>() private val _deviceData = MutableLiveData<DeviceScreenState>()
val deviceData: LiveData<DeviceScreenState> = _deviceData val deviceData: LiveData<DeviceScreenState> = _deviceData
fun setDeviceId(newDeviceId: String?) { fun setDeviceData(newDeviceId: String?, name: String?, type: String?) {
if (newDeviceId != null) { if (newDeviceId != null && name != null) {
deviceId = newDeviceId deviceId = newDeviceId
}
var simCardDtoList: List<NetworkClient.SimCardDto> = listOf()
viewModelScope.launch {
val networkClient = NetworkClient()
simCardDtoList = networkClient.getSimCardsByAccessKey(deviceId)
println("sims: $simCardDtoList")
}.invokeOnCompletion {
// FIXME waiting for the response causes brief moment of DeviceError() before _deviceData is updated ...
// a solution: caching SIM phone numbers of all discovered devices locally on startup and updating them
// only on startup (implicit behavior) or with a pull-down refresh action (not done yet)
// placeholder since there's no API reading logic yet // placeholder since there's no API reading logic yet
_deviceData.value = _deviceData.value =
if (deviceId == "Samsung A14") { DeviceScreenState(
a14Device deviceId = deviceId,
} else { name = name,
iPhoneDevice phoneNumbers = simCardDtoList.map { it.phone_number },
)
println("sims live: ${_deviceData.value!!.phoneNumbers}")
}
} }
} }
} }
@@ -43,6 +43,12 @@
<argument <argument
android:name="deviceId" android:name="deviceId"
app:argType="string" /> app:argType="string" />
<argument
android:name="name"
app:argType="string" />
<argument
android:name="type"
app:argType="string" />
<action <action
android:id="@+id/action_device_to_conversation" android:id="@+id/action_device_to_conversation"
app:destination="@id/nav_conversation" app:destination="@id/nav_conversation"
+1 -1
View File
@@ -1,2 +1,2 @@
uv run gunicorn app:app \ uv run gunicorn -b 0.0.0.0:5000 app:app \
--reload --reload
+4
View File
@@ -37,6 +37,7 @@ gradle-versions = "0.54.0"
hilt = "2.59.2" hilt = "2.59.2"
hiltExt = "1.3.0" hiltExt = "1.3.0"
horologist = "0.7.15" horologist = "0.7.15"
ktor-client = "3.5.0"
jdkDesugar = "2.1.5" jdkDesugar = "2.1.5"
junit = "4.13.2" junit = "4.13.2"
kotlin = "2.3.21" kotlin = "2.3.21"
@@ -147,6 +148,9 @@ kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collec
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor-client"}
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-client"}
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor-client"}
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
okhttp3 = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp3 = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "play-services-wearable" } play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "play-services-wearable" }