diff --git a/android/build.gradle.kts b/android/build.gradle.kts
index 43414fb..7bb08ca 100644
--- a/android/build.gradle.kts
+++ b/android/build.gradle.kts
@@ -20,6 +20,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.compose)
+ // necessary for using T.serializer() on a data class
+ alias(libs.plugins.kotlin.serialization)
}
android {
@@ -96,6 +98,7 @@ dependencies {
implementation(libs.androidx.glance.material3)
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.android)
+ implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.activity.compose)
@@ -115,6 +118,10 @@ dependencies {
implementation(libs.androidx.compose.ui.viewbinding)
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)
androidTestImplementation(libs.junit)
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 676ce5b..6f87e86 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -9,6 +9,8 @@
+
+
insets }
+ var deviceDtoList: List = listOf()
+ viewModel.viewModelScope.launch {
+ val networkClient = NetworkClient()
+ deviceDtoList = networkClient.getDevices()
+ }
+
setContentView(
ComposeView(this).apply {
consumeWindowInsets = false
@@ -79,15 +90,18 @@ class NavActivity : AppCompatActivity() {
JetchatDrawer(
drawerState = drawerState,
selectedMenu = selectedMenu,
+ deviceDtoList = deviceDtoList,
onChatClicked = {
findNavController().popBackStack(R.id.nav_device, false)
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)
scope.launch {
drawerState.close()
}
- selectedMenu = it
+ selectedMenu = it.access_key
},
) {
AndroidViewBinding(ContentMainBinding::inflate)
diff --git a/android/src/main/kotlin/xyz/magicalbits/smsremote/components/JetchatDrawer.kt b/android/src/main/kotlin/xyz/magicalbits/smsremote/components/JetchatDrawer.kt
index d46e26a..490bfd9 100644
--- a/android/src/main/kotlin/xyz/magicalbits/smsremote/components/JetchatDrawer.kt
+++ b/android/src/main/kotlin/xyz/magicalbits/smsremote/components/JetchatDrawer.kt
@@ -56,11 +56,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import xyz.magicalbits.smsremote.R
+import xyz.magicalbits.smsremote.network.NetworkClient
import xyz.magicalbits.smsremote.theme.JetchatTheme
import xyz.magicalbits.smsremote.widget.WidgetReceiver
@Composable
-fun JetchatDrawerContent(onChatClicked: (String) -> Unit, selectedMenu: String = "iPhone XYZ") {
+fun JetchatDrawerContent(onChatClicked: (NetworkClient.DeviceDto) -> Unit, selectedMenu: String = "", deviceDtoList: List = listOf()) {
// Use windowInsetsTopHeight() to add a spacer which pushes the drawer content
// below the status bar (y-axis)
Column {
@@ -68,12 +69,13 @@ fun JetchatDrawerContent(onChatClicked: (String) -> Unit, selectedMenu: String =
DrawerHeader()
DividerItem()
DrawerItemHeader("Devices")
- DeviceItem("Samsung A14", selectedMenu == "Samsung A14") {
- onChatClicked("Samsung A14")
- }
- DeviceItem("iPhone XYZ", selectedMenu == "iPhone XYZ") {
- onChatClicked("iPhone XYZ")
+
+ for (deviceDto in deviceDtoList) {
+ DeviceItem(deviceDto.name, selectedMenu == deviceDto.access_key) {
+ onChatClicked(deviceDto)
+ }
}
+
// DividerItem(modifier = Modifier.padding(horizontal = 28.dp))
if (widgetAddingIsSupported(LocalContext.current)) {
DividerItem(modifier = Modifier.padding(horizontal = 28.dp))
diff --git a/android/src/main/kotlin/xyz/magicalbits/smsremote/components/JetchatScaffold.kt b/android/src/main/kotlin/xyz/magicalbits/smsremote/components/JetchatScaffold.kt
index 257ab78..7cf827f 100644
--- a/android/src/main/kotlin/xyz/magicalbits/smsremote/components/JetchatScaffold.kt
+++ b/android/src/main/kotlin/xyz/magicalbits/smsremote/components/JetchatScaffold.kt
@@ -23,13 +23,15 @@ import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
+import xyz.magicalbits.smsremote.network.NetworkClient
import xyz.magicalbits.smsremote.theme.JetchatTheme
@Composable
fun JetchatDrawer(
drawerState: DrawerState = rememberDrawerState(initialValue = Closed),
selectedMenu: String,
- onChatClicked: (String) -> Unit,
+ deviceDtoList: List,
+ onChatClicked: (NetworkClient.DeviceDto) -> Unit,
content: @Composable () -> Unit,
) {
JetchatTheme {
@@ -44,6 +46,7 @@ fun JetchatDrawer(
JetchatDrawerContent(
onChatClicked = onChatClicked,
selectedMenu = selectedMenu,
+ deviceDtoList = deviceDtoList,
)
}
},
diff --git a/android/src/main/kotlin/xyz/magicalbits/smsremote/data/FakeData.kt b/android/src/main/kotlin/xyz/magicalbits/smsremote/data/FakeData.kt
index defc460..d8a7d86 100644
--- a/android/src/main/kotlin/xyz/magicalbits/smsremote/data/FakeData.kt
+++ b/android/src/main/kotlin/xyz/magicalbits/smsremote/data/FakeData.kt
@@ -87,17 +87,3 @@ val meProfile =
timeZone = "In your timezone",
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")
- )
diff --git a/android/src/main/kotlin/xyz/magicalbits/smsremote/device/DeviceFragment.kt b/android/src/main/kotlin/xyz/magicalbits/smsremote/device/DeviceFragment.kt
index d1a6c5c..297d523 100644
--- a/android/src/main/kotlin/xyz/magicalbits/smsremote/device/DeviceFragment.kt
+++ b/android/src/main/kotlin/xyz/magicalbits/smsremote/device/DeviceFragment.kt
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
@@ -24,7 +23,6 @@ import xyz.magicalbits.smsremote.components.JetchatAppBar
import xyz.magicalbits.smsremote.theme.JetchatTheme
import kotlin.getValue
import androidx.navigation.findNavController
-import kotlinx.coroutines.launch
class DeviceFragment : Fragment() {
private val viewModel: DeviceViewModel by viewModels()
@@ -33,8 +31,10 @@ class DeviceFragment : Fragment() {
override fun onAttach(context: Context) {
super.onAttach(context)
// Consider using safe args plugin
- val deviceName = arguments?.getString("deviceName")
- viewModel.setDeviceId(deviceName)
+ val deviceId = arguments?.getString("deviceId")
+ val name = arguments?.getString("name")
+ val type = arguments?.getString("type")
+ viewModel.setDeviceData(deviceId, name, type)
}
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
@@ -65,6 +65,7 @@ class DeviceFragment : Fragment() {
JetchatTheme {
if (deviceData == null) {
+ println("calling device error")
DeviceError()
} else {
val navController: NavController = rootView.findNavController()
diff --git a/android/src/main/kotlin/xyz/magicalbits/smsremote/device/DeviceViewModel.kt b/android/src/main/kotlin/xyz/magicalbits/smsremote/device/DeviceViewModel.kt
index 179cada..0eb277e 100644
--- a/android/src/main/kotlin/xyz/magicalbits/smsremote/device/DeviceViewModel.kt
+++ b/android/src/main/kotlin/xyz/magicalbits/smsremote/device/DeviceViewModel.kt
@@ -4,26 +4,39 @@ import androidx.compose.runtime.Immutable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
-import xyz.magicalbits.smsremote.data.a14Device
-import xyz.magicalbits.smsremote.data.iPhoneDevice
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+import xyz.magicalbits.smsremote.network.NetworkClient
class DeviceViewModel : ViewModel() {
private var deviceId: String = ""
private val _deviceData = MutableLiveData()
val deviceData: LiveData = _deviceData
- fun setDeviceId(newDeviceId: String?) {
- if (newDeviceId != null) {
+ fun setDeviceData(newDeviceId: String?, name: String?, type: String?) {
+ if (newDeviceId != null && name != null) {
deviceId = newDeviceId
- }
- // placeholder since there's no API reading logic yet
- _deviceData.value =
- if (deviceId == "Samsung A14") {
- a14Device
- } else {
- iPhoneDevice
+ var simCardDtoList: List = 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
+ _deviceData.value =
+ DeviceScreenState(
+ deviceId = deviceId,
+ name = name,
+ phoneNumbers = simCardDtoList.map { it.phone_number },
+ )
+ println("sims live: ${_deviceData.value!!.phoneNumbers}")
}
+ }
}
}
diff --git a/android/src/main/kotlin/xyz/magicalbits/smsremote/network/NetworkClient.kt b/android/src/main/kotlin/xyz/magicalbits/smsremote/network/NetworkClient.kt
new file mode 100644
index 0000000..117f2a5
--- /dev/null
+++ b/android/src/main/kotlin/xyz/magicalbits/smsremote/network/NetworkClient.kt
@@ -0,0 +1,54 @@
+package xyz.magicalbits.smsremote.network
+
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.cio.CIO
+import io.ktor.client.plugins.auth.Auth
+import io.ktor.client.plugins.auth.providers.BearerTokens
+import io.ktor.client.plugins.auth.providers.bearer
+import io.ktor.client.request.get
+import io.ktor.client.statement.bodyAsText
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+
+class NetworkClient {
+ // TODO make apiBaseUrl configurable with fallback to my real domain
+ private val apiBaseUrl = "http://192.168.1.116:5000"
+
+ // TODO ask user for creds, call login endpoint, store tokens in local encrypted storage
+
+ private val networkClient = HttpClient(CIO) {
+ install(Auth) {
+ bearer {
+ // TODO configure refresh token so it refreshes the access token
+ loadTokens {
+ // TODO load from local encrypted storage
+ BearerTokens(
+ "test",
+ "test"
+ )
+ }
+ }
+ }
+ }
+
+ @Serializable()
+ data class DeviceDto(val access_key: String, val name: String, val type: String)
+
+ @Serializable
+ data class SimCardDto(val device_access_key: String, val phone_number: String)
+
+ // GET /api/v1/devices
+ suspend fun getDevices(): List {
+ // TODO handle non-200 status codes
+ // TODO handle '{"msg":"Token has expired"}' messages
+ val data = networkClient.get("$apiBaseUrl/api/v1/devices").bodyAsText()
+ return Json.decodeFromString(data)
+ }
+
+ // GET /api/v1/sim-cards
+ suspend fun getSimCardsByAccessKey(accessKey: String): List {
+ // TODO handle non-200 status codes
+ val data = networkClient.get("$apiBaseUrl/api/v1/sim-cards?access_key=$accessKey").bodyAsText()
+ return Json.decodeFromString(data)
+ }
+}
diff --git a/android/src/main/res/navigation/mobile_navigation.xml b/android/src/main/res/navigation/mobile_navigation.xml
index 84b650c..eac44a3 100644
--- a/android/src/main/res/navigation/mobile_navigation.xml
+++ b/android/src/main/res/navigation/mobile_navigation.xml
@@ -43,6 +43,12 @@
+
+