Added ip dialog and refactored socket connection

This commit is contained in:
Assasinnys 2022-12-06 14:20:39 +03:00
parent 266e5eb360
commit 85c164a89a
11 changed files with 210 additions and 24 deletions

View File

@ -23,6 +23,7 @@ class SocketRepositoryImpl @Inject constructor(
true
} catch (e: Exception) {
Log.e( javaClass.simpleName, "Socket connection error; address: $address, port: $port", e)
socketSource.close()
false
}
}

View File

@ -1,6 +1,10 @@
package com.dmitryzenevich.remoteaudiocontrol.data
import android.util.Log
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.Closeable
import java.net.InetSocketAddress
import java.net.Socket
import javax.inject.Inject
import javax.inject.Singleton
@ -9,6 +13,7 @@ import javax.inject.Singleton
class SocketSource @Inject constructor() : Closeable {
private var socket: Socket? = null
private val mutex = Mutex()
val writer
get() = socket().getOutputStream().writer()
@ -18,12 +23,16 @@ class SocketSource @Inject constructor() : Closeable {
private fun socket(): Socket = socket ?: throw IllegalStateException("Socket is not created")
fun openSocketConnection(address: String, port: Int) {
socket = Socket(address, port)
suspend fun openSocketConnection(address: String, port: Int) {
mutex.withLock {
close()
socket = Socket().apply { connect(InetSocketAddress(address, port), 5000) }
}
}
override fun close() {
Log.i(javaClass.simpleName, "Socket $socket is now closing")
socket?.close()
socket = null
}
}

View File

@ -1,13 +1,22 @@
package com.dmitryzenevich.remoteaudiocontrol.di
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class AppModule {
class AppModule {
@Singleton
@Provides
fun getSharedPreferences(@ApplicationContext appContext: Context): SharedPreferences {
return appContext.getSharedPreferences("AppPrefs", MODE_PRIVATE)
}
}

View File

@ -3,8 +3,20 @@ package com.dmitryzenevich.remoteaudiocontrol.presentation
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.dmitryzenevich.remoteaudiocontrol.presentation.theme.Dimens.TabHeight
import com.dmitryzenevich.remoteaudiocontrol.presentation.theme.RemoteAudioControlTheme
import dagger.hilt.android.AndroidEntryPoint
import androidx.lifecycle.viewmodel.compose.viewModel
import com.dmitryzenevich.remoteaudiocontrol.R
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@ -12,9 +24,95 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RemoteAudioControlTheme {
MainScreen()
RemoteAudioControlApp()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RemoteAudioControlApp(viewModel: MainViewModel = viewModel()) {
val uiState = viewModel.uiState.collectAsState()
val showDialog = viewModel.showAddressDialog.collectAsState()
val isLoading = viewModel.isLoading.collectAsState()
RemoteAudioControlTheme {
Scaffold(
topBar = { TopBar(viewModel::onAddressClick) }
) { padding ->
Box(Modifier.padding(padding)) {
MainScreen(
uiState = uiState.value,
onVolumeChanged = viewModel::onVolumeChanged,
onMuteClick = viewModel::onMuteClick
)
if (showDialog.value) {
IpAddressDialog(initIp = viewModel.getCurrentIp(), onConfirm = viewModel::onConfirmAddress)
}
if (isLoading.value) {
ConnectionProgressBar()
}
}
}
}
}
@Composable
fun ConnectionProgressBar() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
@Composable
fun TopBar(onAddressClick: () -> Unit) {
Surface(
modifier = Modifier
.height(TabHeight)
.fillMaxWidth(),
color = MaterialTheme.colorScheme.primary
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.app_name))
IconButton(onClick = onAddressClick) {
Icon(
imageVector = Icons.Filled.Build,
contentDescription = stringResource(R.string.accessibility_ip_address_button)
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun IpAddressDialog(initIp: String, onConfirm: (String) -> Unit) {
var ipText by remember { mutableStateOf(initIp) }
AlertDialog(
onDismissRequest = {},
title = {
Text(text = stringResource(R.string.ip_dialog_title))
},
text = {
TextField(value = ipText, onValueChange = { ipText = it })
},
confirmButton = {
Button(
onClick = { onConfirm(ipText) }
) {
Text(text = "Ok")
}
}
)
}

View File

@ -4,14 +4,14 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@Composable
fun MainScreen(viewModel: MainViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
val uiState by viewModel.uiState.collectAsState()
fun MainScreen(
uiState: MainScreenUiState,
onVolumeChanged: (VolumeItemState, Int) -> Unit,
onMuteClick: (VolumeItemState) -> Unit
) {
LazyRow(
modifier = Modifier.fillMaxSize()
) {
@ -22,9 +22,9 @@ fun MainScreen(viewModel: MainViewModel = androidx.lifecycle.viewmodel.compose.v
VolumeItem(
volumeItemState = itemState,
onValueChanged = { newValue: Float ->
viewModel.onVolumeChanged(itemState, newValue.toInt())
onVolumeChanged(itemState, newValue.toInt())
},
onMuteClick = { viewModel.onMuteClick(itemState) }
onMuteClick = { onMuteClick(itemState) }
)
}
}

View File

@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dmitryzenevich.remoteaudiocontrol.data.SocketRepositoryImpl
import com.dmitryzenevich.remoteaudiocontrol.data.model.*
import com.dmitryzenevich.remoteaudiocontrol.presentation.helper.PrefHelper
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@ -14,29 +15,42 @@ const val NOT_BLOCKED = -1L
@HiltViewModel
class MainViewModel @Inject constructor(
private val socketRepositoryImpl: SocketRepositoryImpl
private val socketRepositoryImpl: SocketRepositoryImpl,
private val prefHelper: PrefHelper
) : ViewModel() {
private val _uiState = MutableStateFlow(MainScreenUiState())
val uiState = _uiState.asStateFlow()
private val _showAddressDialog = MutableStateFlow(false)
val showAddressDialog = _showAddressDialog.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading = _isLoading.asStateFlow()
@Volatile
private var blockingPid: Long = NOT_BLOCKED
private var blockingJob: Job? = null
init {
connectToSocket()
}
private fun connectToSocket() {
viewModelScope.launch {
val isReady = socketRepositoryImpl.openSocketConnection(/*"10.0.2.2"*/"192.168.100.9", 54683)
_isLoading.value = true
val isReady = socketRepositoryImpl.openSocketConnection(prefHelper.getIpAddress(), 54683)
if (isReady) {
socketRepositoryImpl.bindSocketInput()
.filterNotNull()
.onEach {
Log.i(javaClass.simpleName, "received: $it")
Log.i(javaClass.simpleName, "Received: $it")
withContext(Dispatchers.Main) { proceedEvent(it) }
}
.catch { Log.e(javaClass.simpleName, "Fetch event error", it) }
.launchIn(CoroutineScope(Dispatchers.IO))
}
_isLoading.value = false
}
}
@ -110,6 +124,27 @@ class MainViewModel @Inject constructor(
_uiState.value.volumes.add(event.toVolumeItemState())
}
}
fun onAddressClick() {
_showAddressDialog.value = true
Log.i(javaClass.simpleName, "address clicked")
}
fun onConfirmAddress(ip: String) {
if (isValidIp(ip)) {
prefHelper.setIpAddress(ip)
_showAddressDialog.value = false
connectToSocket()
Log.i(javaClass.simpleName, "new ip: $ip")
}
}
fun getCurrentIp() = prefHelper.getIpAddress().also { Log.i(javaClass.simpleName, "Current ip: $it") }
private fun isValidIp(ip: String): Boolean = ip.matches(ipCheckRegex.toRegex())
.also { Log.i(javaClass.simpleName, "Regex matches: $it") }
}
const val ipCheckRegex = "^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}\$"
fun NewSessionEvent.toVolumeItemState() = VolumeItemState(pid = PID)

View File

@ -9,6 +9,7 @@ import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import com.dmitryzenevich.remoteaudiocontrol.R
@ -39,12 +40,12 @@ fun VolumeItem(
if (volumeItemState.isMuted.value)
Icon(
painter = painterResource(R.drawable.ic_volume_off),
contentDescription = "Muted"
contentDescription = stringResource(R.string.accessibility_muted)
)
else
Icon(
painter = painterResource(R.drawable.ic_volume_up),
contentDescription = "Resumed"
contentDescription = stringResource(R.string.accessibility_resumed)
)
}
}

View File

@ -0,0 +1,20 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation.helper
import android.content.SharedPreferences
import androidx.core.content.edit
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PrefHelper @Inject constructor(private val sPref: SharedPreferences) {
fun getIpAddress(): String {
return sPref.getString(KEY_IP, "192.168.100.9")!!
}
fun setIpAddress(ip: String) = sPref.edit { putString(KEY_IP, ip) }
private companion object {
const val KEY_IP = "key_ip"
}
}

View File

@ -2,10 +2,10 @@ package com.dmitryzenevich.remoteaudiocontrol.presentation.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple80 = Color(0xFF80CBC4)
val PurpleGrey80 = Color(0xFF4DB6AC)
val Pink80 = Color(0xFF81C784)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val Purple40 = Color(0xFF009688)
val PurpleGrey40 = Color(0xFF00897B)
val Pink40 = Color(0xFF80CBC4)

View File

@ -0,0 +1,7 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation.theme
import androidx.compose.ui.unit.dp
object Dimens {
val TabHeight = 56.dp
}

View File

@ -1,3 +1,9 @@
<resources>
<string name="app_name">Remote Audio Control</string>
<string name="ip_dialog_title">Enter a valid ip address</string>
<string name="accessibility_ip_address_button">Choose server ip address</string>
<string name="accessibility_muted">Muted</string>
<string name="accessibility_resumed">Resumed</string>
</resources>