From 0abf2da4d4c4dbb822f1fc5ff56795bff77d8fc9 Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Thu, 28 Aug 2025 21:03:00 -0400 Subject: [PATCH 01/24] fix crash --- .../com/shinjiindustrial/portmapper/UPnPManager.kt | 8 -------- .../shinjiindustrial/portmapper/domain/IGDDevice.kt | 13 +++++++------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index 3b3a2cf..7b9ac32 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -636,13 +636,6 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv // we enumerate port mappings later val device: IIGDDevice = getIGDDevice(externalIp) val timeTakenMillis = measureTimeMillis { - -// if(actionsMap.containsKey(UpnpManager.Companion.ACTION_NAMES.GetListOfPortMappings)) -// { -// OurLogger.log(Level.INFO, "Enumerating Port Listings using GetListOfPortMappings") -// var getPortMapping = actionsMap[UpnpManager.Companion.ACTION_NAMES.GetListOfPortMappings]!! -// getAllPortMappingsUsingListPortMappings(getPortMapping) -// } if(device.supportsAction(ACTION_NAMES.GetGenericPortMappingEntry)) { OurLogger.log(Level.INFO, "Enumerating Port Listings using GetGenericPortMappingEntry") @@ -652,7 +645,6 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv //TODO firebase integration OurLogger.log(Level.SEVERE, "device does not have GetGenericPortMappingEntry") } - } OurLogger.log(Level.INFO, "Time to enumerate ports: $timeTakenMillis ms") } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt index caf226e..10d33a9 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt @@ -18,22 +18,23 @@ interface IIGDDevice { // has state including rules. add update those rules. and do a upnp action. and sort. class IGDDevice constructor(private val rootDevice : RemoteDevice, private val wanIPService : RemoteService) : IIGDDevice { - // nullrefs warning: this is SSDP response, very possible for some fields to be null, DeviceDetails constructor allows it. // the best bet on ip is (rootDevice!!.identity.descriptorURL.host) imo. since that is what we use in RetreiveRemoteDescriptors class // which then calls remoteDeviceAdded (us). presentationURI can be and is null sometimes. private val displayName : String = this.rootDevice.displayString //i.e. Nokia IGD Version 2.00 - private val friendlyDetailsName : String = - this.rootDevice.details.friendlyName //i.e. Internet Home Gateway Device - private val manufacturer : String = - this.rootDevice.details.manufacturerDetails?.manufacturer ?: "" //i.e. Nokia private val ipAddress : String = rootDevice.identity.descriptorURL.host //i.e. 192.168.18.1 //this.rootDevice!!.details.presentationURI.host - private val upnpType : String = this.rootDevice.type.type //i.e. InternetGatewayDevice private val upnpTypeVersion : Int = this.rootDevice.type.version //i.e. 2 private val actionsMap : MutableMap> = mutableMapOf() override val udn = rootDevice.root.identity.udn.toString() + //private val upnpType : String = this.rootDevice.type.type //i.e. InternetGatewayDevice + // can cause crashes if friendlyName is null + //private val friendlyDetailsName : String = + // this.rootDevice.details.friendlyName //i.e. Internet Home Gateway Device +// private val manufacturer : String = +// this.rootDevice.details.manufacturerDetails?.manufacturer ?: "" //i.e. Nokia + override fun getUpnpVersion() : Int { return upnpTypeVersion } From fc28e2a728114ee48f2564d45122c715e9da044d Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Fri, 29 Aug 2025 00:03:06 -0400 Subject: [PATCH 02/24] bottom sheet update --- .../portmapper/MainActivity.kt | 49 ++++++++++++++++--- .../portmapper/ui/SortPanel.kt | 2 +- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt index 82f4fd5..1b55102 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt @@ -56,8 +56,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Checkbox import androidx.compose.material.CheckboxDefaults import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowBack @@ -85,6 +84,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarDuration @@ -93,8 +93,11 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TooltipBox import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberTooltipState import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -509,7 +512,9 @@ class MainActivity : ComponentActivity() { rememberCoroutineScope() val coroutineScope: CoroutineScope = rememberCoroutineScope() - val bottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) + var openBottomSheet by remember {mutableStateOf(false)} + val bottomSheetState = + rememberModalBottomSheetState(true) ModalBottomSheetLayout( sheetState = bottomSheetState, @@ -527,6 +532,30 @@ class MainActivity : ComponentActivity() { } } ) { + if (openBottomSheet) + { + ModalBottomSheet( + onDismissRequest = { openBottomSheet = false }, + sheetState = bottomSheetState, + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), + tonalElevation = 24.dp, + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp), + content = { + // This is what will be shown in the bottom sheet + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + BottomSheetSortBy(portViewModel) + } + } + ) +// +// LaunchedEffect(Unit) { +// bottomSheetState.expand() +// } + } Scaffold( @@ -597,6 +626,7 @@ class MainActivity : ComponentActivity() { { IconButton(onClick = { coroutineScope.launch { + openBottomSheet = true bottomSheetState.show() } }) @@ -1189,17 +1219,24 @@ fun ColumnScope.CreateRuleContents(hasSubmitted : MutableState, // }, // state = rememberTooltipState() // ) { - //TODO where is tooltipbox? + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip { Text("This is auto renew...") } + }, + state = rememberTooltipState() + ) { //MaterialTheme.colorScheme.outline Checkbox( autoRenew.value, - onCheckedChange = {autoRenew.value = it}, + onCheckedChange = { autoRenew.value = it }, // uncheckedColor == border color colors = CheckboxDefaults.colors(uncheckedColor = AdditionalColors.TextColorWeak) ) - Text("Auto Renew", color = AdditionalColors.TextColor) + Text("Auto Renew", color = AdditionalColors.TextColor) + } } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/SortPanel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/SortPanel.kt index 84693b1..2986378 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/SortPanel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/SortPanel.kt @@ -26,7 +26,7 @@ fun BottomSheetSortBy(portViewModel: PortViewModel) { Column( modifier = Modifier .fillMaxWidth() - .padding(12.dp) + .padding(start = 12.dp, end = 12.dp, bottom = 12.dp) ) { Text( text = "Sort By", From 6f113ff281cd828cb78ceb52d48c36936a872696 Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Fri, 29 Aug 2025 12:13:32 -0400 Subject: [PATCH 03/24] update pull refresh to material3 and remove material2 dependencies. --- app/build.gradle | 3 +- .../portmapper/LogViewActivity.kt | 3 +- .../portmapper/MainActivity.kt | 116 ++++++++---------- .../portmapper/SettingsActivity.kt | 3 +- .../portmapper/ui/DurationPicker.kt | 2 +- .../portmapper/ui/PortScreen.kt | 3 +- 6 files changed, 53 insertions(+), 77 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cc093c7..c739d49 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -110,6 +110,7 @@ def compose_version = "2.8.2" dependencies { implementation("androidx.room:room-runtime:$room_version") + implementation 'androidx.compose.material3:material3:1.3.2' // If this project uses any Kotlin source, use Kotlin Symbol Processing (KSP) // See Add the KSP plugin to your project @@ -127,8 +128,6 @@ dependencies { implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' - implementation 'androidx.compose.material3:material3' - implementation 'androidx.compose.material:material:1.9.0' implementation 'androidx.compose.material:material-icons-extended' //implementation 'androidx.appcompat:appcompat:1.4.1' implementation "androidx.datastore:datastore-preferences:1.0.0" diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt index cd3dbf2..1568fe5 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ContentCopy @@ -58,7 +57,7 @@ class LogViewActivity : ComponentActivity() { val settingsViewModel : SettingsViewModel by viewModels() @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") - @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt index 1b55102..97ba26c 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt @@ -1,5 +1,3 @@ -@file:OptIn(ExperimentalMaterialApi::class) - package com.shinjiindustrial.portmapper @@ -37,7 +35,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row @@ -53,9 +50,8 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Checkbox -import androidx.compose.material.CheckboxDefaults -import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.ModalBottomSheet import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add @@ -69,10 +65,6 @@ import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.Sort import androidx.compose.material.icons.filled.UnfoldLess import androidx.compose.material.icons.filled.UnfoldMore -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Divider @@ -94,8 +86,14 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.pullToRefreshIndicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberTooltipState import androidx.compose.material3.surfaceColorAtElevation @@ -105,10 +103,12 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment @@ -286,7 +286,7 @@ class MainActivity : ComponentActivity() { } @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") - @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -297,7 +297,7 @@ class MainActivity : ComponentActivity() { println("My Back Pressed Callback") if(IsMultiSelectMode()) { - MainActivity.MultiSelectItems!!.clear() + MultiSelectItems!!.clear() } else { @@ -410,7 +410,7 @@ class MainActivity : ComponentActivity() { } } - @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) + @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen(navController : NavHostController) { // A surface container using the 'background' color from the theme @@ -507,8 +507,8 @@ class MainActivity : ComponentActivity() { } - MainActivity.OurSnackbarHostState = remember { SnackbarHostState() } - MainActivity.MultiSelectItems = remember { mutableStateListOf() } + OurSnackbarHostState = remember { SnackbarHostState() } + MultiSelectItems = remember { mutableStateListOf() } rememberCoroutineScope() val coroutineScope: CoroutineScope = rememberCoroutineScope() @@ -516,22 +516,6 @@ class MainActivity : ComponentActivity() { val bottomSheetState = rememberModalBottomSheetState(true) - ModalBottomSheetLayout( - sheetState = bottomSheetState, - sheetShape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), - sheetElevation = 24.dp, - sheetBackgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp), - sheetContent = { - // This is what will be shown in the bottom sheet - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - BottomSheetSortBy(portViewModel) - } - } - ) { if (openBottomSheet) { ModalBottomSheet( @@ -560,7 +544,7 @@ class MainActivity : ComponentActivity() { Scaffold( snackbarHost = { - SnackbarHost(MainActivity.OurSnackbarHostState!!) { data -> + SnackbarHost(OurSnackbarHostState!!) { data -> // custom snackbar with the custom colors Snackbar( data, @@ -652,44 +636,45 @@ class MainActivity : ComponentActivity() { }, content = { it -> - val refreshScope = rememberCoroutineScope() + // TODO can revisit this - if we already know we have some kind of device + // then we can forget the main loading circle and show this until we + // finish enumerating our first device (or have a flow which is like + // combine (deviceNotFound, anyDeviceStillBeingSearched)) + // still the issue of if search finishes really fast then it can hang if + // you set isRefreshing to false too fast (can fix by having an arbitrary delay) - val refreshState = remember { mutableStateOf(false) } - var refreshing by refreshState + val state = rememberPullToRefreshState() + val scope = rememberCoroutineScope() + val isRefreshing = remember { mutableStateOf(false) } + fun refresh() = scope.launch { - val isRefreshing = remember { derivedStateOf { refreshing && searchStartedRecentlyAndNothingFoundYet } } - - val onlyShowMainProgressBar = true // looks slightly better in practice - - fun refresh() = refreshScope.launch { - refreshing = !onlyShowMainProgressBar + state.animateToHidden() portViewModel.fullRefresh() - - //TODO cancel if already done - - delay(6000) - println("finish refreshing $refreshing") - refreshing = false } - val state = rememberPullRefreshState(isRefreshing.value, ::refresh) - - BoxWithConstraints( - Modifier - .padding(it) - .pullRefresh(state) - .fillMaxHeight() - .fillMaxWidth() - ) - { - - val boxHeight = - with(LocalDensity.current) { constraints.maxHeight.toDp() } - - + PullToRefreshBox( + isRefreshing = isRefreshing.value, + onRefresh = ::refresh, + modifier = Modifier.padding(it), + state = state, + indicator = { + // dont show after initially pulling down + if (state.distanceFraction > 0f) + { + Indicator( + state = state, + isRefreshing = isRefreshing.value, + modifier = Modifier.align(Alignment.TopCenter) + ) + } + } + ) { + var heightPx by remember { mutableIntStateOf(0) } + val boxHeight = with(LocalDensity.current) { heightPx.toDp() } Column( Modifier + .onSizeChanged { heightPx = it.height } .fillMaxHeight() .fillMaxWidth() ) { @@ -790,17 +775,12 @@ class MainActivity : ComponentActivity() { } } - PullRefreshIndicator( - refreshing, - state, - Modifier.align(Alignment.TopCenter) - ) } } ) - } + } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt index 20f4f17..f171637 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.AlertDialog @@ -56,7 +55,7 @@ class SettingsActivity : ComponentActivity() { val settingsViewModel : SettingsViewModel by viewModels() @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") - @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/DurationPicker.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/DurationPicker.kt index d39468c..62d14a2 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/DurationPicker.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/DurationPicker.kt @@ -16,8 +16,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.LocalTextStyle import androidx.compose.material3.Divider +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt index 893d43f..92c7c7f 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.Card @@ -564,7 +563,7 @@ fun rememberTicker(periodMillis: Long): androidx.compose.runtime.State { } //lazy column IS recycler view basically. both recycle. -@OptIn(ExperimentalFoundationApi::class, ExperimentalUnitApi::class, ExperimentalMaterialApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalUnitApi::class) @Composable fun Conversation(messages: List) { From 60ac018d4ad93a37f9013a18a7f9813e1edb5ce5 Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Fri, 29 Aug 2025 13:13:11 -0400 Subject: [PATCH 04/24] update selectedids to be in view model, subscribe to port mappings, and no longer static. --- .../portmapper/MainActivity.kt | 105 +++++++++--------- .../portmapper/PortViewModel.kt | 56 +++++++++- .../portmapper/UPnPManager.kt | 25 +++-- .../portmapper/domain/PortMapping.kt | 6 +- .../portmapper/ui/PortScreen.kt | 32 +++--- 5 files changed, 143 insertions(+), 81 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt index 97ba26c..aa5a71f 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt @@ -282,7 +282,6 @@ class MainActivity : ComponentActivity() { } var OurSnackbarHostState: SnackbarHostState? = null - var MultiSelectItems : SnapshotStateList? = null } @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @@ -295,9 +294,9 @@ class MainActivity : ComponentActivity() { onBackPressedDispatcher.addCallback(this) { println("My Back Pressed Callback") - if(IsMultiSelectMode()) + if(portViewModel.inMultiSelectMode.value) { - MultiSelectItems!!.clear() + portViewModel.clearSelection() } else { @@ -420,6 +419,8 @@ class MainActivity : ComponentActivity() { val showAboutDialogState = remember { mutableStateOf(false) } val showMoreInfoDialogState = remember { mutableStateOf(false) } val showAboutDialog by showAboutDialogState //mutable state binds to UI (in sense if value changes, redraw). remember says when redrawing dont discard us. + val inMultiSelectMode by portViewModel.inMultiSelectMode.collectAsStateWithLifecycle() + val selectedIds by portViewModel.selectedIds.collectAsStateWithLifecycle() LaunchedEffect(Unit) { portViewModel.events.collect { ev -> @@ -508,7 +509,6 @@ class MainActivity : ComponentActivity() { OurSnackbarHostState = remember { SnackbarHostState() } - MultiSelectItems = remember { mutableStateListOf() } rememberCoroutineScope() val coroutineScope: CoroutineScope = rememberCoroutineScope() @@ -581,10 +581,10 @@ class MainActivity : ComponentActivity() { topBar = { TopAppBar( navigationIcon = { - if(IsMultiSelectMode()) + if(inMultiSelectMode) { IconButton(onClick = { - MultiSelectItems!!.clear() + portViewModel.clearSelection() }) { Icon(Icons.Filled.ArrowBack, contentDescription = "Back") @@ -597,7 +597,7 @@ class MainActivity : ComponentActivity() { ), //colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.secondary),// change the height here title = { - val title = if(MultiSelectItems!!.isEmpty()) "PortMapper" else "${MultiSelectItems!!.count()} Selected" + val title = if(inMultiSelectMode) "${selectedIds.size} Selected" else "PortMapper" Text( text = title, color = AdditionalColors.TextColorStrong, @@ -606,25 +606,32 @@ class MainActivity : ComponentActivity() { }, actions = { - if(MultiSelectItems!!.isEmpty()) + if(inMultiSelectMode) { IconButton(onClick = { - coroutineScope.launch { - openBottomSheet = true - bottomSheetState.show() + if (inMultiSelectMode) + { + portViewModel.deleteAll(selectedIds) + } + else + { + portViewModel.deleteAll() } }) { - Icon(Icons.Default.Sort, contentDescription = "Sort") + Icon(Icons.Default.Delete, contentDescription = "Delete") } } else { IconButton(onClick = { - portViewModel.deleteAll(MultiSelectItems) + coroutineScope.launch { + openBottomSheet = true + bottomSheetState.show() + } }) { - Icon(Icons.Default.Delete, contentDescription = "Sort") + Icon(Icons.Default.Sort, contentDescription = "Sort") } } @@ -771,7 +778,9 @@ class MainActivity : ComponentActivity() { } } else { val uiState by portViewModel.uiState.collectAsStateWithLifecycle() - PortMappingContent(uiState) + val selectedIds by portViewModel.selectedIds.collectAsStateWithLifecycle() + val isInMultiSelectMode by portViewModel.inMultiSelectMode.collectAsStateWithLifecycle() + PortMappingContent(uiState, isInMultiSelectMode, portViewModel::toggle, selectedIds) } } @@ -1617,8 +1626,8 @@ fun DeviceRow(content: @Composable RowScope.() -> Unit) { fun OverflowMenu(showAboutDialogState : MutableState, portViewModel : PortViewModel) { var expanded by remember { mutableStateOf(false) } - - + val isInMultiSelectMode by portViewModel.inMultiSelectMode.collectAsStateWithLifecycle() + val selectedIds by portViewModel.selectedIds.collectAsStateWithLifecycle() IconButton(onClick = { expanded = true }) { Icon(Icons.Default.MoreVert, contentDescription = "menu") @@ -1631,7 +1640,20 @@ fun OverflowMenu(showAboutDialogState : MutableState, portViewModel : P // this gets called on expanded, so I dont think we need to monitor additional state. val items : MutableList = mutableListOf() - if(MainActivity.MultiSelectItems!!.isEmpty()) + if(isInMultiSelectMode) + { + val anyEnabled = portViewModel.getSelectedItems(selectedIds).any { it -> it.portMapping.Enabled } + val anyDisabled = portViewModel.getSelectedItems(selectedIds).any { it -> !it.portMapping.Enabled } + if(anyEnabled) + { + items.add(R.string.disable_action) + } + if(anyDisabled) + { + items.add(R.string.enable_action) + } + } + else { items.add(R.string.refresh_action) if(portViewModel.isInitialized()) @@ -1655,21 +1677,6 @@ fun OverflowMenu(showAboutDialogState : MutableState, portViewModel : P items.add(R.string.settings) items.add(R.string.about) } - else - { - val anyEnabled = MainActivity.MultiSelectItems!!.any { it -> it.portMapping.Enabled } - val anyDisabled = MainActivity.MultiSelectItems!!.any { it -> !it.portMapping.Enabled } - if(anyEnabled) - { - items.add(R.string.disable_action) - } - if(anyDisabled) - { - items.add(R.string.enable_action) - } - } - - MainActivity.MultiSelectItems items.forEach { label -> DropdownMenuItem(text = { Text(stringResource(label)) }, onClick = { @@ -1691,11 +1698,21 @@ fun OverflowMenu(showAboutDialogState : MutableState, portViewModel : P } R.string.disable_action -> { - portViewModel.enableDisableAll(false, MainActivity.MultiSelectItems) + if (isInMultiSelectMode){ + portViewModel.enableDisableAll(false, selectedIds) + } + else{ + portViewModel.enableDisableAll(false) + } } R.string.enable_action -> { - portViewModel.enableDisableAll(true, MainActivity.MultiSelectItems) + if (isInMultiSelectMode){ + portViewModel.enableDisableAll(true, selectedIds) + } + else{ + portViewModel.enableDisableAll(true) + } } R.string.delete_all_action -> { @@ -1753,22 +1770,4 @@ class IGDDeviceHolder return "" } -} - -fun ToggleSelection(portMapping : PortMappingWithPref) -{ - val has = MainActivity.MultiSelectItems!!.contains(portMapping) - if(has) - { - MainActivity.MultiSelectItems!!.remove(portMapping) - } - else - { - MainActivity.MultiSelectItems!!.add(portMapping) - } -} - -fun IsMultiSelectMode() : Boolean -{ - return MainActivity.MultiSelectItems!!.isNotEmpty() } \ No newline at end of file diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index a1194d0..15aad68 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -3,6 +3,7 @@ package java.com.shinjiindustrial.portmapper import android.content.Context import android.util.Log import android.widget.Toast +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.shinjiindustrial.portmapper.PortForwardApplication @@ -14,6 +15,7 @@ import com.shinjiindustrial.portmapper.common.SortBy import com.shinjiindustrial.portmapper.common.SortInfo import com.shinjiindustrial.portmapper.domain.IIGDDevice import com.shinjiindustrial.portmapper.domain.NetworkInterfaceInfo +import com.shinjiindustrial.portmapper.domain.PortMappingKey import com.shinjiindustrial.portmapper.domain.PortMappingUserInput import com.shinjiindustrial.portmapper.domain.PortMappingWithPref import com.shinjiindustrial.portmapper.domain.UpnpViewRow @@ -30,10 +32,14 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.util.logging.Level @@ -48,7 +54,8 @@ data class PortUiState( @HiltViewModel class PortViewModel @Inject constructor( private val upnpRepository: UpnpManager, - private val preferencesRepository: PreferencesManager + private val preferencesRepository: PreferencesManager, + private val savedStateHandle: SavedStateHandle ) : ViewModel() { sealed interface UiEvent { @@ -64,6 +71,29 @@ class PortViewModel @Inject constructor( private val applicationScope : CoroutineScope = MainScope() + // we want to use key for selections. so if a rule renews while the user is in multi select + // mode, don't deselect that rule. but if we lose a rule (i.e. it gets deleted) then + // we still want to deselect. + private val _selectedIds = MutableStateFlow>( + savedStateHandle["selected_ids"] ?: emptySet() + ) + val selectedIds: StateFlow> = _selectedIds + + val inMultiSelectMode: StateFlow = + _selectedIds.map { it.isNotEmpty() } + .stateIn(viewModelScope, SharingStarted.Eagerly, false) + + fun toggle(id: PortMappingKey) { + _selectedIds.update { s -> if (id in s) s - id else s + id } + } + + fun getSelectedItems(selectedIds : Set) : List + { + return upnpRepository.portMappingsFromIds(selectedIds) + } + + fun clearSelection() { _selectedIds.value = emptySet() } + val anyDevices: StateFlow = upnpRepository.devices.map { devices -> devices.isNotEmpty() }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = false @@ -241,6 +271,11 @@ class PortViewModel @Inject constructor( } } + fun enableDisableAll(enable : Boolean, selectedIds : Set) + { + enableDisableAll(enable, upnpRepository.portMappingsFromIds(selectedIds)) + } + fun enableDisableAll(enable : Boolean, chosenRulesOnly : List? = null) = applicationScope.launch { @@ -323,6 +358,20 @@ class PortViewModel @Inject constructor( init { upnpRepository.SearchStarted += { o -> searchStarted(o) } + combine(_selectedIds, upnpRepository.portMappings) { selectedIds, currentMappings -> + val cur = currentMappings.keys + val sel = selectedIds + // map to the intersection of the two sets, in most cases this should be the same. + // if a rule got deleted then it will be different. in which case we update selectedIds + // to be the new pruned set. + sel intersect cur } + .distinctUntilChanged() + .onEach { filtered -> + println("filtered changed ...") + if (filtered != _selectedIds.value) _selectedIds.value = filtered + savedStateHandle["selected_ids"] = filtered + } + .launchIn(viewModelScope) } override fun onCleared() { @@ -343,6 +392,11 @@ class PortViewModel @Inject constructor( } } + fun deleteAll(selectedIds : Set) + { + deleteAll(upnpRepository.portMappingsFromIds(selectedIds)) + } + fun deleteAll(chosen: List? = null) = applicationScope.launch { try { // get all enabled. note: need to clone. diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index 7b9ac32..846ebbc 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -220,15 +220,9 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv private fun addOrUpdateMapping(pm: PortMappingWithPref) { - var existingRule : PortMappingWithPref? = null - val key = pm.getKey() _portMappings.update { old -> old + (pm.getKey() to pm) } - - if (existingRule != null && MainActivity.MultiSelectItems?.remove(existingRule) ?: false) { - MainActivity.MultiSelectItems!!.add(pm) - } } fun removeMapping(pm: PortMappingWithPref) @@ -236,7 +230,6 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv _portMappings.update { old -> old - pm.getKey() } - MainActivity.MultiSelectItems?.remove(pm) } // endregion @@ -775,6 +768,24 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv //endregion + fun portMappingsFromIds(selectedIds : Set) : List + { + val portMappingWithPrefList = mutableListOf() + for(id in selectedIds) + { + if (portMappings.value.containsKey(id)) + { + portMappingWithPrefList.add(portMappings.value[id]!!) + } + else + { + OurLogger.log(Level.SEVERE, "Cannot find port mapping with key $id"); + } + } + + return portMappingWithPrefList + } + } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt index 559859a..4fbcd2c 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt @@ -1,8 +1,10 @@ package com.shinjiindustrial.portmapper.domain +import android.os.Parcelable import android.os.SystemClock import com.shinjiindustrial.portmapper.PortForwardApplication import com.shinjiindustrial.portmapper.persistence.PortMappingEntity +import kotlinx.parcelize.Parcelize data class PortMappingWithPref(val portMapping: PortMapping, val portMappingPref: PortMappingPref? = null) { @@ -26,7 +28,9 @@ data class PortMappingPref(val autoRenew : Boolean, val desiredLeaseDuration: In } -data class PortMappingKey(val deviceIP: String, val externalPort: Int, val protocol: String) +@Parcelize +data class PortMappingKey(val deviceIP: String, val externalPort: Int, val protocol: String) : + Parcelable // this is what is on the router // with additional field for timeRead and psuedoSlot diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt index 92c7c7f..f14147c 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt @@ -34,7 +34,6 @@ 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.LaunchedEffect import androidx.compose.runtime.getValue @@ -54,13 +53,11 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import com.shinjiindustrial.portmapper.DayNightMode -import com.shinjiindustrial.portmapper.IsMultiSelectMode -import com.shinjiindustrial.portmapper.MainActivity import com.shinjiindustrial.portmapper.PortForwardApplication -import com.shinjiindustrial.portmapper.ToggleSelection import com.shinjiindustrial.portmapper._getDefaultPortMapping import com.shinjiindustrial.portmapper.domain.IIGDDevice import com.shinjiindustrial.portmapper.domain.PortMapping +import com.shinjiindustrial.portmapper.domain.PortMappingKey import com.shinjiindustrial.portmapper.domain.PortMappingWithPref import com.shinjiindustrial.portmapper.domain.UpnpViewRow import com.shinjiindustrial.portmapper.domain.Urgency @@ -75,7 +72,7 @@ import java.com.shinjiindustrial.portmapper.ThemeUiState ExperimentalFoundationApi::class ) @Composable -fun PortMappingCard(portMappingWithPref: PortMappingWithPref, now: Long = -1, additionalModifier : Modifier = Modifier.Companion) +fun PortMappingCard(portMappingWithPref: PortMappingWithPref, now: Long = -1, isInMultiSelectMode : Boolean = false, toggleSelection : (PortMappingKey) -> Unit = {}, selectedIds : Set, additionalModifier : Modifier = Modifier.Companion) { val portMapping = portMappingWithPref.portMapping println("external ip test ${portMapping.DeviceIP}") @@ -95,8 +92,8 @@ fun PortMappingCard(portMappingWithPref: PortMappingWithPref, now: Long = -1, ad .combinedClickable( onClick = { - if (IsMultiSelectMode()) { - ToggleSelection(portMappingWithPref) + if (isInMultiSelectMode) { + toggleSelection(portMappingWithPref.getKey()) } else { // TODO cleanup PortForwardApplication.Companion.showContextMenu.value = true @@ -106,7 +103,7 @@ fun PortMappingCard(portMappingWithPref: PortMappingWithPref, now: Long = -1, ad }, onLongClick = { - ToggleSelection(portMappingWithPref) + toggleSelection(portMappingWithPref.getKey()) } ), @@ -136,7 +133,6 @@ fun PortMappingCard(portMappingWithPref: PortMappingWithPref, now: Long = -1, ad ) { - val multiSelectMode = IsMultiSelectMode() //var padLeft = if(multiSelectMode) 5.dp else 13.dp //TODO need to do existing content slide to right and fade new element in @@ -171,18 +167,16 @@ fun PortMappingCard(portMappingWithPref: PortMappingWithPref, now: Long = -1, ad // offsetState.targetState = multiSelectMode AnimatedVisibility( - visible = multiSelectMode, + visible = isInMultiSelectMode, ) { CircleCheckbox( - MainActivity.Companion.MultiSelectItems!!.contains(portMappingWithPref), + selectedIds.contains(portMappingWithPref.getKey()), true, Modifier.Companion.padding(10.dp, 0.dp, 2.dp, 0.dp) ) { - - ToggleSelection(portMappingWithPref) - + toggleSelection(portMappingWithPref.getKey()) } } @@ -539,14 +533,14 @@ fun PreviewConversation() { { msgs.add(upnpViewEl) } - Conversation(msgs) + PortMappingsListContent(msgs, false, {}, emptySet()) } } @Composable -fun PortMappingContent(uiState : PortUiState) +fun PortMappingContent(uiState : PortUiState, isInMultiSelectMode : Boolean, onToggle : (PortMappingKey) -> Unit, selectedIds : Set) { - Conversation(uiState.items) + PortMappingsListContent(uiState.items, isInMultiSelectMode, onToggle, selectedIds) } @Composable @@ -565,7 +559,7 @@ fun rememberTicker(periodMillis: Long): androidx.compose.runtime.State { //lazy column IS recycler view basically. both recycle. @OptIn(ExperimentalFoundationApi::class, ExperimentalUnitApi::class) @Composable -fun Conversation(messages: List) { +fun PortMappingsListContent(messages: List, isInMultiSelectMode : Boolean, onToggle : (PortMappingKey) -> Unit, selectedIds : Set) { val now by rememberTicker(8_000) @@ -590,7 +584,7 @@ fun Conversation(messages: List) { NoMappingsCard(message.device) } is UpnpViewRow.PortViewRow -> { - PortMappingCard(message.portMapping, now, Modifier.animateItem()) + PortMappingCard(message.portMapping, now, isInMultiSelectMode, onToggle,selectedIds, Modifier.animateItem()) } } } From 9ec9a386d06373c773409550db4471185bac3acb Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Fri, 29 Aug 2025 13:16:53 -0400 Subject: [PATCH 05/24] reformat code --- app/src/main/AndroidManifest.xml | 18 +- .../portmapper/LogViewActivity.kt | 56 +- .../portmapper/MainActivity.kt | 1059 +++++++++-------- .../shinjiindustrial/portmapper/Modules.kt | 7 - .../portmapper/PortForwardApplication.kt | 23 +- .../portmapper/PortMappingSort.kt | 61 +- .../portmapper/PortViewModel.kt | 260 ++-- .../portmapper/PreferencesManager.kt | 17 +- .../portmapper/SettingsActivity.kt | 13 +- .../portmapper/SharedPreferences.kt | 16 +- .../portmapper/UPnPManager.kt | 1002 ++++++++-------- .../portmapper/client/IUpnpClient.kt | 42 +- .../portmapper/common/CommonUtil.kt | 10 +- .../portmapper/common/PortSorting.kt | 22 +- .../portmapper/common/Validators.kt | 46 +- .../domain/AndroidUpnpServiceConfiguration.kt | 26 +- .../portmapper/domain/IGDDevice.kt | 43 +- .../portmapper/domain/LocalNetworkInfo.kt | 53 +- .../portmapper/domain/PortMapping.kt | 146 +-- .../portmapper/domain/PortMappingUserInput.kt | 62 +- .../portmapper/domain/UpnpViewRow.kt | 26 +- .../persistence/PortMapperPersistence.kt | 24 +- .../portmapper/ui/CommonUI.kt | 36 +- .../portmapper/ui/DurationPicker.kt | 120 +- .../portmapper/ui/MoreInfoDialog.kt | 9 +- .../portmapper/ui/PortScreen.kt | 243 ++-- .../portmapper/ui/PreviewUtil.kt | 3 +- .../portmapper/ui/RuleCreationDialog.kt | 175 +-- .../portmapper/ui/SortPanel.kt | 4 +- .../portmapper/ui/theme/Theme.kt | 55 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 4 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 4 +- .../portmapper/FirebaseConditional.kt | 2 +- .../portmapper/UPnPManagerTests.kt | 1 - .../portmapper/common/ValidatorsTest.kt | 33 +- 35 files changed, 1944 insertions(+), 1777 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index daa4229..e02fd98 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,11 +1,13 @@ - - - - - + + + + + + + - + android:parentActivityName="com.shinjiindustrial.portmapper.MainActivity"> - + android:parentActivityName="com.shinjiindustrial.portmapper.MainActivity"> diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt index 1568fe5..c1e4434 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt @@ -1,10 +1,8 @@ package com.shinjiindustrial.portmapper import android.annotation.SuppressLint -import android.app.Activity import android.content.ClipData import android.content.ClipboardManager -import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity @@ -34,7 +32,6 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -44,22 +41,18 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import com.shinjiindustrial.portmapper.ui.theme.MyApplicationTheme import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import java.com.shinjiindustrial.portmapper.SettingsViewModel import java.com.shinjiindustrial.portmapper.ThemeUiState -import javax.inject.Inject @AndroidEntryPoint class LogViewActivity : ComponentActivity() { - val settingsViewModel : SettingsViewModel by viewModels() + val settingsViewModel: SettingsViewModel by viewModels() @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState); + super.onCreate(savedInstanceState) PortForwardApplication.CurrentActivity = this @@ -70,8 +63,7 @@ class LogViewActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable - fun LogViewInner(themeUiState: ThemeUiState, logLines : List, scrollToBottom : Boolean) - { + fun LogViewInner(themeUiState: ThemeUiState, logLines: List, scrollToBottom: Boolean) { MyApplicationTheme(themeUiState) { Scaffold( topBar = { @@ -100,8 +92,17 @@ class LogViewActivity : ComponentActivity() { Icon(Icons.Default.DeleteSweep, contentDescription = "Clear Logs") } - IconButton(onClick = { CopyTextToClipboard(PortForwardApplication.Logs.joinToString("\n")) }) { - Icon(Icons.Default.ContentCopy, contentDescription = "Copy to Clipboard") + IconButton(onClick = { + CopyTextToClipboard( + PortForwardApplication.Logs.joinToString( + "\n" + ) + ) + }) { + Icon( + Icons.Default.ContentCopy, + contentDescription = "Copy to Clipboard" + ) } IconButton(onClick = { logsSaveAs() }) { @@ -113,10 +114,9 @@ class LogViewActivity : ComponentActivity() { content = { it -> val listState = rememberLazyListState() - val coroutineScope = rememberCoroutineScope() + rememberCoroutineScope() - if(scrollToBottom) - { + if (scrollToBottom) { //scrollToBottom = true // not necessary? // launched effect - action to take when composable is first launched / relaunched // passing in Unit ensures its only done the first time. @@ -126,7 +126,8 @@ class LogViewActivity : ComponentActivity() { } - LazyColumn( state = listState, + LazyColumn( + state = listState, //modifier = Modifier.background(MaterialTheme.colorScheme.background), modifier = Modifier .background(MaterialTheme.colorScheme.background) @@ -138,8 +139,7 @@ class LogViewActivity : ComponentActivity() { ) { itemsIndexed(logLines) { index, message -> - val color = when - { + val color = when { message.startsWith("W: ") -> AdditionalColors.LogWarningText message.startsWith("E: ") -> AdditionalColors.LogErrorText else -> AdditionalColors.TextColor @@ -156,9 +156,9 @@ class LogViewActivity : ComponentActivity() { } @Composable - fun LogViewContent() - { - val scrollToBottom = this.intent.getBooleanExtra(PortForwardApplication.ScrollToBottom, false); + fun LogViewContent() { + val scrollToBottom = + this.intent.getBooleanExtra(PortForwardApplication.ScrollToBottom, false) val logLines = PortForwardApplication.Logs val themeState by settingsViewModel.uiState.collectAsStateWithLifecycle() LogViewInner(themeState, logLines, scrollToBottom) @@ -177,8 +177,7 @@ class LogViewActivity : ComponentActivity() { override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { super.onActivityResult(requestCode, resultCode, resultData) - if (requestCode == CREATE_LOG_FILE && resultCode == Activity.RESULT_OK) - { + if (requestCode == CREATE_LOG_FILE && resultCode == RESULT_OK) { resultData?.data?.also { uri -> val contentResolver = applicationContext.contentResolver contentResolver.openOutputStream(uri)?.bufferedWriter().use { out -> @@ -189,11 +188,10 @@ class LogViewActivity : ComponentActivity() { } - fun CopyTextToClipboard(txt : String) - { - var clipboardManager = this.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager; - var clipData = ClipData.newPlainText("simple text", txt); - clipboardManager.setPrimaryClip(clipData); + fun CopyTextToClipboard(txt: String) { + var clipboardManager = this.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + var clipData = ClipData.newPlainText("simple text", txt) + clipboardManager.setPrimaryClip(clipData) } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt index aa5a71f..9aa50b9 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt @@ -50,9 +50,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowBack @@ -67,6 +64,8 @@ import androidx.compose.material.icons.filled.UnfoldLess import androidx.compose.material.icons.filled.UnfoldMore import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -75,6 +74,7 @@ import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Scaffold @@ -90,9 +90,7 @@ import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator -import androidx.compose.material3.pulltorefresh.pullToRefreshIndicator import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberTooltipState @@ -101,14 +99,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment @@ -157,10 +152,10 @@ import com.shinjiindustrial.portmapper.domain.PortMapping import com.shinjiindustrial.portmapper.domain.PortMappingUserInput import com.shinjiindustrial.portmapper.domain.PortMappingWithPref import com.shinjiindustrial.portmapper.ui.BottomSheetSortBy -import com.shinjiindustrial.portmapper.ui.PortMappingContent import com.shinjiindustrial.portmapper.ui.DurationPickerDialog import com.shinjiindustrial.portmapper.ui.LoadingIcon import com.shinjiindustrial.portmapper.ui.MoreInfoDialog +import com.shinjiindustrial.portmapper.ui.PortMappingContent import com.shinjiindustrial.portmapper.ui.RuleCreationDialog import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import com.shinjiindustrial.portmapper.ui.theme.MyApplicationTheme @@ -168,7 +163,6 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.fourthline.cling.model.meta.RemoteDevice import java.com.shinjiindustrial.portmapper.PortViewModel @@ -179,44 +173,44 @@ import java.nio.ByteOrder import java.util.logging.Level import java.util.logging.LogRecord -var PseudoSlotCounter : Int = MAX_PORT // as starting slot -fun GetPsuedoSlot() : Int -{ +var PseudoSlotCounter: Int = MAX_PORT // as starting slot +fun GetPsuedoSlot(): Int { PseudoSlotCounter += 1 return PseudoSlotCounter } -class StringBuilderHandler(private val stringBuilder: SnapshotStateList) : java.util.logging.Handler() { - - override fun publish(record: LogRecord?) { - record?.let { - val prefix = when (it.level) - { - Level.INFO -> "I: " - Level.WARNING -> "W: " - Level.SEVERE -> "E: " - else -> return // i.e. do not log - } - stringBuilder.add(prefix + it.message) - } - } -// - override fun flush() { - // Nothing to do here since StringBuilder doesn't need to be flushed - } +class StringBuilderHandler(private val stringBuilder: SnapshotStateList) : + java.util.logging.Handler() { - override fun close() { - // Nothing to do here since StringBuilder doesn't need to be closed - } + override fun publish(record: LogRecord?) { + record?.let { + val prefix = when (it.level) { + Level.INFO -> "I: " + Level.WARNING -> "W: " + Level.SEVERE -> "E: " + else -> return // i.e. do not log + } + stringBuilder.add(prefix + it.message) + } + } + + // + override fun flush() { + // Nothing to do here since StringBuilder doesn't need to be flushed + } + + override fun close() { + // Nothing to do here since StringBuilder doesn't need to be closed + } } enum class Protocol(val protocol: String) { TCP("TCP"), UDP("UDP"), BOTH("BOTH"); - fun str() : String - { + + fun str(): String { return protocol } } @@ -255,15 +249,12 @@ class MainActivity : ComponentActivity() { duration: SnackbarDuration, onAction: () -> Unit = { } ) { - if (OurSnackbarHostState == null) - { + if (OurSnackbarHostState == null) { PortForwardApplication.ShowToast( message, if ((duration == SnackbarDuration.Long || duration == SnackbarDuration.Indefinite)) Toast.LENGTH_LONG else Toast.LENGTH_SHORT ) - } - else - { + } else { GlobalScope.launch(Dispatchers.Main) { val snackbarResult = OurSnackbarHostState!!.showSnackbar( @@ -290,16 +281,13 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) PortForwardApplication.CurrentActivity = this - + onBackPressedDispatcher.addCallback(this) { println("My Back Pressed Callback") - if(portViewModel.inMultiSelectMode.value) - { + if (portViewModel.inMultiSelectMode.value) { portViewModel.clearSelection() - } - else - { + } else { isEnabled = false onBackPressedDispatcher.onBackPressed() } @@ -314,8 +302,7 @@ class MainActivity : ComponentActivity() { } @Composable - fun MainContent() - { + fun MainContent() { val themeState by settingsViewModel.uiState.collectAsStateWithLifecycle() MyApplicationTheme(themeState) { Surface( @@ -334,26 +321,32 @@ class MainActivity : ComponentActivity() { val navController = rememberNavController() NavHost(navController = navController, startDestination = "main_screen") { - composable("main_screen", + composable( + "main_screen", exitTransition = { fadeOut(animationSpec = tween(300)) }, popEnterTransition = { //this is whats used when going back from "create rule" - slideInVertically(initialOffsetY = { it / 2 }, animationSpec = tween( - durationMillis = 200, easing = FastOutSlowInEasing - )) + fadeIn(animationSpec = tween(400)) + slideInVertically( + initialOffsetY = { it / 2 }, animationSpec = tween( + durationMillis = 200, easing = FastOutSlowInEasing + ) + ) + fadeIn(animationSpec = tween(400)) }, popExitTransition = { - fadeOut(animationSpec = tween(300)) + fadeOut(animationSpec = tween(300)) }, ) { MainScreen(navController = navController) } - composable("full_screen_dialog?description={description}&internalIp={internalIp}&internalRange={internalRange}&externalIp={externalIp}&externalRange={externalRange}&protocol={protocol}&leaseDuration={leaseDuration}&enabled={enabled}&autorenew={autorenew}", + composable( + "full_screen_dialog?description={description}&internalIp={internalIp}&internalRange={internalRange}&externalIp={externalIp}&externalRange={externalRange}&protocol={protocol}&leaseDuration={leaseDuration}&enabled={enabled}&autorenew={autorenew}", arguments = listOf( - navArgument("description") { nullable = true; type = NavType.StringType }, // only if nullable (or default) are they optional + navArgument("description") { + nullable = true; type = NavType.StringType + }, // only if nullable (or default) are they optional navArgument("internalIp") { nullable = true; type = NavType.StringType }, navArgument("internalRange") { nullable = true; type = NavType.StringType }, navArgument("externalIp") { nullable = true; type = NavType.StringType }, @@ -363,30 +356,33 @@ class MainActivity : ComponentActivity() { navArgument("enabled") { type = NavType.BoolType; defaultValue = false }, navArgument("autorenew") { type = NavType.BoolType; defaultValue = false }, ), - popExitTransition = { + popExitTransition = { - slideOutVertically( - targetOffsetY = { it / 2 }, animationSpec = tween( - durationMillis = 200, easing = FastOutSlowInEasing - )) + fadeOut(animationSpec = tween(100)) + slideOutVertically( + targetOffsetY = { it / 2 }, animationSpec = tween( + durationMillis = 200, easing = FastOutSlowInEasing + ) + ) + fadeOut(animationSpec = tween(100)) }, exitTransition = { - slideOutVertically( - targetOffsetY = { it }, animationSpec = tween( - durationMillis = 5000, easing = FastOutSlowInEasing - )) - }, + slideOutVertically( + targetOffsetY = { it }, animationSpec = tween( + durationMillis = 5000, easing = FastOutSlowInEasing + ) + ) + }, enterTransition = { - slideInVertically(initialOffsetY = { it }, animationSpec = tween( + slideInVertically( + initialOffsetY = { it }, animationSpec = tween( durationMillis = 200, easing = FastOutSlowInEasing - )) + fadeIn(animationSpec = tween(400)) + ) + ) + fadeIn(animationSpec = tween(400)) }, popEnterTransition = { fadeIn(animationSpec = tween(150)) } ) - { - backStackEntry -> + { backStackEntry -> val arguments = backStackEntry.arguments val desc = arguments?.getString("description") val internalIp = arguments?.getString("internalIp") @@ -398,20 +394,33 @@ class MainActivity : ComponentActivity() { val enabled = arguments?.getBoolean("enabled") val autorenew = arguments?.getBoolean("autorenew") - var portMappingUserInputToEdit : PortMappingUserInput? = null - if(desc != null) - { - portMappingUserInputToEdit = PortMappingUserInput(desc, internalIp!!, internalRange!!, externalIp!!, externalRange!!, protocol!!, leaseDuration!!, enabled!!, autorenew!!) + var portMappingUserInputToEdit: PortMappingUserInput? = null + if (desc != null) { + portMappingUserInputToEdit = PortMappingUserInput( + desc, + internalIp!!, + internalRange!!, + externalIp!!, + externalRange!!, + protocol!!, + leaseDuration!!, + enabled!!, + autorenew!! + ) } - RuleCreationDialog(navController = navController, portViewModel, ruleToEdit = portMappingUserInputToEdit) + RuleCreationDialog( + navController = navController, + portViewModel, + ruleToEdit = portMappingUserInputToEdit + ) } } } @OptIn(ExperimentalMaterial3Api::class) @Composable - fun MainScreen(navController : NavHostController) { + fun MainScreen(navController: NavHostController) { // A surface container using the 'background' color from the theme val searchStartedRecentlyAndNothingFoundYet by portViewModel.searchStartedRecentlyAndNothingFoundYet.collectAsStateWithLifecycle() val anyDevices by portViewModel.anyDevices.collectAsStateWithLifecycle() @@ -424,16 +433,13 @@ class MainActivity : ComponentActivity() { LaunchedEffect(Unit) { portViewModel.events.collect { ev -> - if (ev is PortViewModel.UiEvent.ToastEvent) - { + if (ev is PortViewModel.UiEvent.ToastEvent) { Toast.makeText( PortForwardApplication.appContext, ev.msg, ev.duration ).show() - } - else if (ev is PortViewModel.UiEvent.SnackBarViewLogEvent) - { + } else if (ev is PortViewModel.UiEvent.SnackBarViewLogEvent) { showSnackBarViewLog(ev.msg) // emit these } } @@ -512,14 +518,13 @@ class MainActivity : ComponentActivity() { rememberCoroutineScope() val coroutineScope: CoroutineScope = rememberCoroutineScope() - var openBottomSheet by remember {mutableStateOf(false)} + var openBottomSheet by remember { mutableStateOf(false) } val bottomSheetState = rememberModalBottomSheetState(true) - if (openBottomSheet) - { + if (openBottomSheet) { ModalBottomSheet( - onDismissRequest = { openBottomSheet = false }, + onDismissRequest = { openBottomSheet = false }, sheetState = bottomSheetState, shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), tonalElevation = 24.dp, @@ -541,273 +546,266 @@ class MainActivity : ComponentActivity() { // } } - Scaffold( + Scaffold( - snackbarHost = { - SnackbarHost(OurSnackbarHostState!!) { data -> - // custom snackbar with the custom colors - Snackbar( - data, - actionColor = AdditionalColors.PrimaryDarkerBlue - // according to https://m2.material.io/design/color/dark-theme.html - // light snackbar in darkmode is good. + snackbarHost = { + SnackbarHost(OurSnackbarHostState!!) { data -> + // custom snackbar with the custom colors + Snackbar( + data, + actionColor = AdditionalColors.PrimaryDarkerBlue + // according to https://m2.material.io/design/color/dark-theme.html + // light snackbar in darkmode is good. // containerColor = AdditionalColors.CardContainerColor, // contentColor = AdditionalColors.TextColor // //contentColor = ..., - ) - } - }, - floatingActionButton = { + ) + } + }, + floatingActionButton = { - if (anyDevices) { - FloatingActionButton( - // uses MaterialTheme.colorScheme.secondaryContainer - containerColor = MaterialTheme.colorScheme.secondaryContainer, //todo revert to secondar - onClick = { - //showAddRuleDialogState.value = true - navController.navigate("full_screen_dialog") + if (anyDevices) { + FloatingActionButton( + // uses MaterialTheme.colorScheme.secondaryContainer + containerColor = MaterialTheme.colorScheme.secondaryContainer, //todo revert to secondar + onClick = { + //showAddRuleDialogState.value = true + navController.navigate("full_screen_dialog") - //this works + //this works - }) { - Icon( - Icons.Default.Add, - contentDescription = "Localized description", - tint = AdditionalColors.TextColor, - ) - } + }) { + Icon( + Icons.Default.Add, + contentDescription = "Localized description", + tint = AdditionalColors.TextColor, + ) } - }, - topBar = { - TopAppBar( - navigationIcon = { - if(inMultiSelectMode) + } + }, + topBar = { + TopAppBar( + navigationIcon = { + if (inMultiSelectMode) { + IconButton(onClick = { + portViewModel.clearSelection() + }) { - IconButton(onClick = { - portViewModel.clearSelection() - }) - { - Icon(Icons.Filled.ArrowBack, contentDescription = "Back") - } + Icon(Icons.Filled.ArrowBack, contentDescription = "Back") } - }, + } + }, // modifier = Modifier.height(40.dp), - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = AdditionalColors.TopAppBarColor - ), - //colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.secondary),// change the height here - title = { - val title = if(inMultiSelectMode) "${selectedIds.size} Selected" else "PortMapper" - Text( - text = title, - color = AdditionalColors.TextColorStrong, - fontWeight = FontWeight.Normal - ) - }, - actions = { - - if(inMultiSelectMode) - { - IconButton(onClick = { - if (inMultiSelectMode) - { - portViewModel.deleteAll(selectedIds) - } - else - { - portViewModel.deleteAll() - } - }) - { - Icon(Icons.Default.Delete, contentDescription = "Delete") + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = AdditionalColors.TopAppBarColor + ), + //colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.secondary),// change the height here + title = { + val title = + if (inMultiSelectMode) "${selectedIds.size} Selected" else "PortMapper" + Text( + text = title, + color = AdditionalColors.TextColorStrong, + fontWeight = FontWeight.Normal + ) + }, + actions = { + + if (inMultiSelectMode) { + IconButton(onClick = { + if (inMultiSelectMode) { + portViewModel.deleteAll(selectedIds) + } else { + portViewModel.deleteAll() } - } - else + }) { - IconButton(onClick = { - coroutineScope.launch { - openBottomSheet = true - bottomSheetState.show() - } - }) - { - Icon(Icons.Default.Sort, contentDescription = "Sort") + Icon(Icons.Default.Delete, contentDescription = "Delete") + } + } else { + IconButton(onClick = { + coroutineScope.launch { + openBottomSheet = true + bottomSheetState.show() } + }) + { + Icon(Icons.Default.Sort, contentDescription = "Sort") } - - OverflowMenu( - showAboutDialogState, portViewModel - ) } - ) - }, - content = { it -> - - // TODO can revisit this - if we already know we have some kind of device - // then we can forget the main loading circle and show this until we - // finish enumerating our first device (or have a flow which is like - // combine (deviceNotFound, anyDeviceStillBeingSearched)) - // still the issue of if search finishes really fast then it can hang if - // you set isRefreshing to false too fast (can fix by having an arbitrary delay) - val state = rememberPullToRefreshState() - val scope = rememberCoroutineScope() - val isRefreshing = remember { mutableStateOf(false) } - fun refresh() = scope.launch { - - state.animateToHidden() - portViewModel.fullRefresh() + OverflowMenu( + showAboutDialogState, portViewModel + ) } + ) + }, + content = { it -> + + // TODO can revisit this - if we already know we have some kind of device + // then we can forget the main loading circle and show this until we + // finish enumerating our first device (or have a flow which is like + // combine (deviceNotFound, anyDeviceStillBeingSearched)) + // still the issue of if search finishes really fast then it can hang if + // you set isRefreshing to false too fast (can fix by having an arbitrary delay) + + val state = rememberPullToRefreshState() + val scope = rememberCoroutineScope() + val isRefreshing = remember { mutableStateOf(false) } + fun refresh() = scope.launch { + + state.animateToHidden() + portViewModel.fullRefresh() + } - PullToRefreshBox( - isRefreshing = isRefreshing.value, - onRefresh = ::refresh, - modifier = Modifier.padding(it), - state = state, - indicator = { - // dont show after initially pulling down - if (state.distanceFraction > 0f) - { - Indicator( - state = state, - isRefreshing = isRefreshing.value, - modifier = Modifier.align(Alignment.TopCenter) - ) + PullToRefreshBox( + isRefreshing = isRefreshing.value, + onRefresh = ::refresh, + modifier = Modifier.padding(it), + state = state, + indicator = { + // dont show after initially pulling down + if (state.distanceFraction > 0f) { + Indicator( + state = state, + isRefreshing = isRefreshing.value, + modifier = Modifier.align(Alignment.TopCenter) + ) - } } + } + ) { + var heightPx by remember { mutableIntStateOf(0) } + val boxHeight = with(LocalDensity.current) { heightPx.toDp() } + Column( + Modifier + .onSizeChanged { heightPx = it.height } + .fillMaxHeight() + .fillMaxWidth() ) { - var heightPx by remember { mutableIntStateOf(0) } - val boxHeight = with(LocalDensity.current) { heightPx.toDp() } - Column( - Modifier - .onSizeChanged { heightPx = it.height } - .fillMaxHeight() - .fillMaxWidth() - ) { - if (searchStartedRecentlyAndNothingFoundYet) { - val offset = boxHeight * 0.28f - LoadingIcon( - "Searching for devices", - Modifier.offset(y = offset) + if (searchStartedRecentlyAndNothingFoundYet) { + val offset = boxHeight * 0.28f + LoadingIcon( + "Searching for devices", + Modifier.offset(y = offset) + ) + } else if (!anyDevices) { + val offset = boxHeight * 0.28f + Column(modifier = Modifier.offset(y = offset)) + { + Text( + "No UPnP enabled internet gateway devices found", + modifier = Modifier + .padding(0.dp, 10.dp) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center ) - } else if (!anyDevices) { - val offset = boxHeight * 0.28f - Column(modifier = Modifier.offset(y = offset)) - { + + // these can go through vm -> repo -> client + val interfacesUsedInSearch = + portViewModel.getInterfacesUsedInSearch() + val anyInterfaces = + portViewModel.isInitialized() + if (!anyInterfaces) { Text( - "No UPnP enabled internet gateway devices found", + "No valid interfaces", modifier = Modifier .padding(0.dp, 10.dp) .align(Alignment.CenterHorizontally), textAlign = TextAlign.Center ) + } else { - // these can go through vm -> repo -> client - val interfacesUsedInSearch = - portViewModel.getInterfacesUsedInSearch() - val anyInterfaces = - portViewModel.isInitialized() - if (!anyInterfaces) { - Text( - "No valid interfaces", - modifier = Modifier - .padding(0.dp, 10.dp) - .align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center - ) - } else { - - Text( - "Interfaces Searched:", - modifier = Modifier - .padding(0.dp, 10.dp, 0.dp, 0.dp) - .align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center - ) - + Text( + "Interfaces Searched:", + modifier = Modifier + .padding(0.dp, 10.dp, 0.dp, 0.dp) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center + ) - for (interfaceUsed in interfacesUsedInSearch!!) { - Text( - "${interfaceUsed.networkInterface.displayName} (${interfaceUsed.networkType.networkTypeString.lowercase()})", - modifier = Modifier - .padding(0.dp, 0.dp) - .align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center - ) - } - } - // if no wifi - if (!interfacesUsedInSearch.any { it.networkType == NetworkType.WIFI } - ) { + for (interfaceUsed in interfacesUsedInSearch!!) { Text( - "Enable WiFi and retry", + "${interfaceUsed.networkInterface.displayName} (${interfaceUsed.networkType.networkTypeString.lowercase()})", modifier = Modifier - .padding(0.dp, 20.dp, 0.dp, 10.dp) - .align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center - ) - } else { - Text( - "Ensure a valid UPnP enabled internet gateway device is on the network and retry", - modifier = Modifier - .padding(0.dp, 20.dp, 0.dp, 10.dp) + .padding(0.dp, 0.dp) .align(Alignment.CenterHorizontally), textAlign = TextAlign.Center ) } - Button( - onClick = { - portViewModel.initialize( - PortForwardApplication.appContext, - true - ) - portViewModel.fullRefresh() - }, + } + + // if no wifi + if (!interfacesUsedInSearch.any { it.networkType == NetworkType.WIFI } + ) { + Text( + "Enable WiFi and retry", modifier = Modifier - .padding(0.dp, 10.dp) + .padding(0.dp, 20.dp, 0.dp, 10.dp) .align(Alignment.CenterHorizontally), - shape = RoundedCornerShape(4), - ) { - Text( - "Retry", - color = AdditionalColors.TextColorStrong + textAlign = TextAlign.Center + ) + } else { + Text( + "Ensure a valid UPnP enabled internet gateway device is on the network and retry", + modifier = Modifier + .padding(0.dp, 20.dp, 0.dp, 10.dp) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center + ) + } + Button( + onClick = { + portViewModel.initialize( + PortForwardApplication.appContext, + true ) - } + portViewModel.fullRefresh() + }, + modifier = Modifier + .padding(0.dp, 10.dp) + .align(Alignment.CenterHorizontally), + shape = RoundedCornerShape(4), + ) { + Text( + "Retry", + color = AdditionalColors.TextColorStrong + ) } - } else { - val uiState by portViewModel.uiState.collectAsStateWithLifecycle() - val selectedIds by portViewModel.selectedIds.collectAsStateWithLifecycle() - val isInMultiSelectMode by portViewModel.inMultiSelectMode.collectAsStateWithLifecycle() - PortMappingContent(uiState, isInMultiSelectMode, portViewModel::toggle, selectedIds) } + } else { + val uiState by portViewModel.uiState.collectAsStateWithLifecycle() + val selectedIds by portViewModel.selectedIds.collectAsStateWithLifecycle() + val isInMultiSelectMode by portViewModel.inMultiSelectMode.collectAsStateWithLifecycle() + PortMappingContent( + uiState, + isInMultiSelectMode, + portViewModel::toggle, + selectedIds + ) } - } } - ) + + } + ) } } -fun fallbackRecursiveSearch(rootDevice : RemoteDevice) -{ +fun fallbackRecursiveSearch(rootDevice: RemoteDevice) { // recursively look through devices val deviceList = mutableListOf() //.toMutableList() deviceList.add(rootDevice) - while(deviceList.isNotEmpty()) - { + while (deviceList.isNotEmpty()) { val deviceInQuestion = deviceList.removeAt(0) - for (service in deviceInQuestion.services) - { - for (action in service.actions) - { - if (ActionNames.contains(action.name)) - { + for (service in deviceInQuestion.services) { + for (action in service.actions) { + if (ActionNames.contains(action.name)) { println("Service ${service.serviceType} contains relevant action ${action}") } } @@ -818,10 +816,14 @@ fun fallbackRecursiveSearch(rootDevice : RemoteDevice) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun EnterContextMenu(singleSelectedItem : MutableState, showMoreInfoDialog : MutableState, navController : NavHostController, portViewModel : PortViewModel, settingsViewModel : SettingsViewModel) -{ - if(singleSelectedItem.value == null) - { +fun EnterContextMenu( + singleSelectedItem: MutableState, + showMoreInfoDialog: MutableState, + navController: NavHostController, + portViewModel: PortViewModel, + settingsViewModel: SettingsViewModel +) { + if (singleSelectedItem.value == null) { return } @@ -839,7 +841,8 @@ fun EnterContextMenu(singleSelectedItem : MutableState, sh properties = prop, ) { Surface( - shape = RoundedCornerShape(size = 6.dp)) { + shape = RoundedCornerShape(size = 6.dp) + ) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier @@ -851,7 +854,8 @@ fun EnterContextMenu(singleSelectedItem : MutableState, sh val menuItems: MutableList Unit>> = mutableListOf() - val portMappingWithPref = singleSelectedItem.value as PortMappingWithPref // TODO remove cast + val portMappingWithPref = + singleSelectedItem.value as PortMappingWithPref // TODO remove cast val portMapping = portMappingWithPref.portMapping menuItems.add( Pair Unit>( @@ -862,14 +866,30 @@ fun EnterContextMenu(singleSelectedItem : MutableState, sh .path("full_screen_dialog") .appendQueryParameter("description", portMapping.Description) .appendQueryParameter("internalIp", portMapping.InternalIP) - .appendQueryParameter("internalRange", portMapping.InternalPort.toString()) - .appendQueryParameter("externalIp", portMapping.DeviceIP) // this is actual external IP as we only use it to delete the old rule... - .appendQueryParameter("externalRange", portMapping.ExternalPort.toString()) + .appendQueryParameter( + "internalRange", + portMapping.InternalPort.toString() + ) + .appendQueryParameter( + "externalIp", + portMapping.DeviceIP + ) // this is actual external IP as we only use it to delete the old rule... + .appendQueryParameter( + "externalRange", + portMapping.ExternalPort.toString() + ) .appendQueryParameter("protocol", portMapping.Protocol) - .appendQueryParameter("leaseDuration", portMappingWithPref.getDesiredLeaseDurationOrDefault().toString()) + .appendQueryParameter( + "leaseDuration", + portMappingWithPref.getDesiredLeaseDurationOrDefault() + .toString() + ) .appendQueryParameter("enabled", portMapping.Enabled.toString()) - .appendQueryParameter("autorenew", portMappingWithPref.getAutoRenewOrDefault().toString()) - val uri = uriBuilder.build() + .appendQueryParameter( + "autorenew", + portMappingWithPref.getAutoRenewOrDefault().toString() + ) + val uri = uriBuilder.build() navController.navigate(uri.toString()) } ) @@ -877,7 +897,10 @@ fun EnterContextMenu(singleSelectedItem : MutableState, sh Pair Unit>( if (portMapping.Enabled) "Disable" else "Enable" ) { - portViewModel.enableDisable(portMappingWithPref, !portMapping.Enabled) + portViewModel.enableDisable( + portMappingWithPref, + !portMapping.Enabled + ) } ) menuItems.add( @@ -895,7 +918,8 @@ fun EnterContextMenu(singleSelectedItem : MutableState, sh } ) menuItems.add( - Pair Unit>("More Info" + Pair Unit>( + "More Info" ) { showMoreInfoDialog.value = true } @@ -940,39 +964,40 @@ fun EnterContextMenu(singleSelectedItem : MutableState, sh @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ColumnScope.CreateRuleContents(hasSubmitted : MutableState, - internalPortText: MutableState, - internalPortTextEnd: MutableState, - externalPortText: MutableState, - externalPortTextEnd: MutableState, - leaseDuration: MutableState, - description: MutableState, - descriptionHasError: MutableState, - startInternalHasError: MutableState, - endInternalHasError: MutableState, - selectedProtocolMutable: MutableState, - startExternalHasError: MutableState, - endExternalHasError: MutableState, - internalIp: MutableState, - internalIpHasError: MutableState, - gatewayIps : MutableList, - externalDeviceText: MutableState, - expandedInternal: MutableState, - expandedExternal: MutableState, - wanIpIsV1: State, - autoRenew: MutableState -) -{ +fun ColumnScope.CreateRuleContents( + hasSubmitted: MutableState, + internalPortText: MutableState, + internalPortTextEnd: MutableState, + externalPortText: MutableState, + externalPortTextEnd: MutableState, + leaseDuration: MutableState, + description: MutableState, + descriptionHasError: MutableState, + startInternalHasError: MutableState, + endInternalHasError: MutableState, + selectedProtocolMutable: MutableState, + startExternalHasError: MutableState, + endExternalHasError: MutableState, + internalIp: MutableState, + internalIpHasError: MutableState, + gatewayIps: MutableList, + externalDeviceText: MutableState, + expandedInternal: MutableState, + expandedExternal: MutableState, + wanIpIsV1: State, + autoRenew: MutableState +) { val showLeaseDialog = remember { mutableStateOf(false) } - if(showLeaseDialog.value) - { + if (showLeaseDialog.value) { DurationPickerDialog(showLeaseDialog, leaseDuration, wanIpIsV1.value) } - val descriptionErrorString = remember { mutableStateOf(validateDescription(description.value).validationError) } - val interalIpErrorString = remember { mutableStateOf(validateInternalIp(internalIp.value).validationError) } + val descriptionErrorString = + remember { mutableStateOf(validateDescription(description.value).validationError) } + val interalIpErrorString = + remember { mutableStateOf(validateInternalIp(internalIp.value).validationError) } Row( modifier = Modifier @@ -986,21 +1011,25 @@ fun ColumnScope.CreateRuleContents(hasSubmitted : MutableState, onValueChange = { description.value = it descriptionHasError.value = validateDescription(description.value).hasError - descriptionErrorString.value = validateDescription(description.value).validationError + descriptionErrorString.value = + validateDescription(description.value).validationError }, label = { Text("Description") }, singleLine = true, modifier = Modifier - .weight(0.4f, true) - ,//.height(60.dp), + .weight(0.4f, true),//.height(60.dp), isError = hasSubmitted.value && descriptionHasError.value, trailingIcon = { if (hasSubmitted.value && descriptionHasError.value) { - Icon(Icons.Filled.Error, descriptionErrorString.value.toMessage(), tint = MaterialTheme.colorScheme.error) + Icon( + Icons.Filled.Error, + descriptionErrorString.value.toMessage(), + tint = MaterialTheme.colorScheme.error + ) } }, supportingText = { - if(hasSubmitted.value && descriptionHasError.value) { + if (hasSubmitted.value && descriptionHasError.value) { Text( descriptionErrorString.value.toMessage(), color = MaterialTheme.colorScheme.error @@ -1031,7 +1060,7 @@ fun ColumnScope.CreateRuleContents(hasSubmitted : MutableState, }, isError = hasSubmitted.value && internalIpHasError.value, supportingText = { - if(hasSubmitted.value && internalIpHasError.value) { + if (hasSubmitted.value && internalIpHasError.value) { Text( interalIpErrorString.value.toMessage(), color = MaterialTheme.colorScheme.error @@ -1040,7 +1069,11 @@ fun ColumnScope.CreateRuleContents(hasSubmitted : MutableState, }, trailingIcon = { if (hasSubmitted.value && internalIpHasError.value) { - Icon(Icons.Filled.Error, interalIpErrorString.value.toMessage(), tint = MaterialTheme.colorScheme.error) + Icon( + Icons.Filled.Error, + interalIpErrorString.value.toMessage(), + tint = MaterialTheme.colorScheme.error + ) } }, singleLine = true, @@ -1068,15 +1101,23 @@ fun ColumnScope.CreateRuleContents(hasSubmitted : MutableState, Spacer(modifier = Modifier.width(8.dp)) //pass in expanded, pass in startHasErrorString, pass in internalPortText, pass in StartExternalHasError - // port start - val startHasErrorString = remember { mutableStateOf(validateStartPort(internalPortText.value).validationError) } - StartPortExpandable(internalPortText, startInternalHasError, startHasErrorString, hasSubmitted, expandedInternal, portStartSize) + // port start + val startHasErrorString = + remember { mutableStateOf(validateStartPort(internalPortText.value).validationError) } + StartPortExpandable( + internalPortText, + startInternalHasError, + startHasErrorString, + hasSubmitted, + expandedInternal, + portStartSize + ) } AnimatedVisibility( visible = expandedInternal.value ) { - //if(expandedInternal.value) { + //if(expandedInternal.value) { PortRangeRow( internalPortText, internalPortTextEnd, @@ -1106,8 +1147,16 @@ fun ColumnScope.CreateRuleContents(hasSubmitted : MutableState, // .height(with(LocalDensity.current) { textfieldSize.height.toDp() }) DropDownOutline(defaultModifier, externalDeviceText, gatewayIps, "External Device") Spacer(modifier = Modifier.width(8.dp)) - val startHasErrorString = remember { mutableStateOf(validateStartPort(externalPortText.value).validationError) } - StartPortExpandable(externalPortText, startExternalHasError, startHasErrorString, hasSubmitted, expandedExternal, portStartSize) + val startHasErrorString = + remember { mutableStateOf(validateStartPort(externalPortText.value).validationError) } + StartPortExpandable( + externalPortText, + startExternalHasError, + startHasErrorString, + hasSubmitted, + expandedExternal, + portStartSize + ) } @@ -1134,10 +1183,12 @@ fun ColumnScope.CreateRuleContents(hasSubmitted : MutableState, .weight(0.5f, true) //.height(60.dp) //.height(with(LocalDensity.current) { textfieldSize.height.toDp() }) - DropDownOutline(defaultModifier,//Size(500f, textfieldSize.height), + DropDownOutline( + defaultModifier,//Size(500f, textfieldSize.height), selectedText = selectedProtocolMutable, suggestions = listOf(Protocol.TCP.str(), Protocol.UDP.str(), Protocol.BOTH.str()), - "Protocol") + "Protocol" + ) Spacer(modifier = Modifier.width(8.dp)) @@ -1151,14 +1202,14 @@ fun ColumnScope.CreateRuleContents(hasSubmitted : MutableState, if (digitsOnly.isBlank()) { leaseDuration.value = digitsOnly - } else{ + } else { leaseDuration.value = capLeaseDur(digitsOnly, wanIpIsV1.value) } - }, + }, label = { Text("Lease") }, trailingIcon = { - IconButton(onClick = { showLeaseDialog.value = true }) + IconButton(onClick = { showLeaseDialog.value = true }) { Icon( Icons.Filled.Schedule, @@ -1172,8 +1223,7 @@ fun ColumnScope.CreateRuleContents(hasSubmitted : MutableState, // one is allowed to temporarily leave blank. // but on leaving it must be valid - if(!it.isFocused && leaseDuration.value.isBlank()) - { + if (!it.isFocused && leaseDuration.value.isBlank()) { leaseDuration.value = "0" } @@ -1231,8 +1281,14 @@ fun ColumnScope.CreateRuleContents(hasSubmitted : MutableState, @OptIn(ExperimentalMaterial3Api::class) @Composable -fun StartPortExpandable(portText : MutableState, hasError : MutableState, errorString : MutableState, hasSubmitted: MutableState, expanded : MutableState, startPortSize : MutableState) -{ +fun StartPortExpandable( + portText: MutableState, + hasError: MutableState, + errorString: MutableState, + hasSubmitted: MutableState, + expanded: MutableState, + startPortSize: MutableState +) { OutlinedTextField( keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), value = portText.value, @@ -1246,23 +1302,23 @@ fun StartPortExpandable(portText : MutableState, hasError : MutableState hasError.value = validateStartPort(portText.value).hasError errorString.value = validateStartPort(portText.value).validationError }, - label = { if(expanded.value) Text("Port Start") else Text("Port") }, + label = { if (expanded.value) Text("Port Start") else Text("Port") }, //modifier = Modifier.then(modifier), //.height(60.dp), isError = hasSubmitted.value && hasError.value, supportingText = { - if(hasSubmitted.value && hasError.value) - { + if (hasSubmitted.value && hasError.value) { Text(errorString.value.toMessage(), color = MaterialTheme.colorScheme.error) } }, trailingIcon = { - if(hasSubmitted.value && hasError.value) - { - Icon(Icons.Filled.Error, contentDescription = errorString.value.toMessage(), tint = MaterialTheme.colorScheme.error) //TODO set error on main theme if not already. - } - else - { + if (hasSubmitted.value && hasError.value) { + Icon( + Icons.Filled.Error, + contentDescription = errorString.value.toMessage(), + tint = MaterialTheme.colorScheme.error + ) //TODO set error on main theme if not already. + } else { IconButton(onClick = { expanded.value = !expanded.value }) { if (expanded.value) { @@ -1287,9 +1343,7 @@ val createNewRuleRowWidth = 1.0f fun String.toIntOrMaxValue(): Int { return try { this.toInt() - } - catch(e : Exception) - { + } catch (e: Exception) { Int.MAX_VALUE } } @@ -1297,8 +1351,7 @@ fun String.toIntOrMaxValue(): Int { @OptIn(ExperimentalMaterial3Api::class) @Composable @Preview -fun outlineTextWithPicker() -{ +fun outlineTextWithPicker() { Column() { val expanded = remember { mutableStateOf(false) } @@ -1315,7 +1368,8 @@ fun outlineTextWithPicker() Spacer(modifier = Modifier.width(8.dp)) - OutlinedTextField("Port", + OutlinedTextField( + "Port", onValueChange = { }, @@ -1341,55 +1395,56 @@ fun outlineTextWithPicker() ) } - val expandAnimation by animateDpAsState( - targetValue = if (expanded.value) 60.dp else 0.dp, - animationSpec = tween( - durationMillis = 2000, // Duration of the animation - easing = LinearEasing // Animation easing + val expandAnimation by animateDpAsState( + targetValue = if (expanded.value) 60.dp else 0.dp, + animationSpec = tween( + durationMillis = 2000, // Duration of the animation + easing = LinearEasing // Animation easing + ) ) - ) - + // AnimatedVisibility( // visible = expanded.value, // enter = fadeIn(), // exit = fadeOut() // ) { - if (expanded.value) { - Row(modifier = Modifier.height(expandAnimation)) - { - Spacer( - modifier = Modifier.weight(.5f), - ) + if (expanded.value) { + Row(modifier = Modifier.height(expandAnimation)) + { + Spacer( + modifier = Modifier.weight(.5f), + ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(8.dp)) - OutlinedTextField("Port", - onValueChange = { + OutlinedTextField( + "Port", + onValueChange = { - }, - modifier = Modifier.weight(.5f), - trailingIcon = { - IconButton(onClick = { expanded.value = !expanded.value }) - { - if (expanded.value) { - Icon( - Icons.Filled.UnfoldLess, - contentDescription = "" - ) //TODO set error on main theme if not already. - } else { - Icon( - Icons.Filled.UnfoldMore, - contentDescription = "" - ) //TODO set error on main theme if not already. - } + }, + modifier = Modifier.weight(.5f), + trailingIcon = { + IconButton(onClick = { expanded.value = !expanded.value }) + { + if (expanded.value) { + Icon( + Icons.Filled.UnfoldLess, + contentDescription = "" + ) //TODO set error on main theme if not already. + } else { + Icon( + Icons.Filled.UnfoldMore, + contentDescription = "" + ) //TODO set error on main theme if not already. + } + } } - } - ) + ) + } } - } //} Row() @@ -1405,13 +1460,14 @@ fun outlineTextWithPicker() Spacer(modifier = Modifier.width(8.dp)) - OutlinedTextField("Port", + OutlinedTextField( + "Port", onValueChange = { }, modifier = Modifier.weight(.5f), trailingIcon = { - IconButton(onClick = { }) + IconButton(onClick = { }) { Icon( @@ -1420,7 +1476,6 @@ fun outlineTextWithPicker() ) //TODO set error on main theme if not already. - } } ) @@ -1430,12 +1485,26 @@ fun outlineTextWithPicker() @OptIn(ExperimentalMaterial3Api::class) @Composable -fun PortRangeRow(startPortText : MutableState, endPortText : MutableState, startHasError : MutableState, endHasError : MutableState, hasSubmitted : MutableState, modifier : Modifier, portSize : IntSize) -{ +fun PortRangeRow( + startPortText: MutableState, + endPortText: MutableState, + startHasError: MutableState, + endHasError: MutableState, + hasSubmitted: MutableState, + modifier: Modifier, + portSize: IntSize +) { DeviceRow() { - val endHasErrorString = remember { mutableStateOf(validateEndPort(startPortText.value, endPortText.value).validationError) } + val endHasErrorString = remember { + mutableStateOf( + validateEndPort( + startPortText.value, + endPortText.value + ).validationError + ) + } Spacer(modifier = Modifier.weight(0.5f, true)) @@ -1445,7 +1514,7 @@ fun PortRangeRow(startPortText : MutableState, endPortText : MutableStat Spacer(modifier = Modifier.width(8.dp)) - + OutlinedTextField( keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), @@ -1453,15 +1522,18 @@ fun PortRangeRow(startPortText : MutableState, endPortText : MutableStat onValueChange = { endPortText.value = it.filter { charIt -> charIt.isDigit() } endHasError.value = validateEndPort(startPortText.value, endPortText.value).hasError - endHasErrorString.value = validateEndPort(startPortText.value, endPortText.value).validationError - }, + endHasErrorString.value = + validateEndPort(startPortText.value, endPortText.value).validationError + }, label = { Text("Port End") }, modifier = Modifier.width(with(LocalDensity.current) { portSize.width.toDp() }), isError = hasSubmitted.value && endHasError.value, supportingText = { - if(hasSubmitted.value && endHasError.value) - { - Text(endHasErrorString.value.toMessage(), color = MaterialTheme.colorScheme.error) + if (hasSubmitted.value && endHasError.value) { + Text( + endHasErrorString.value.toMessage(), + color = MaterialTheme.colorScheme.error + ) } }, @@ -1469,9 +1541,12 @@ fun PortRangeRow(startPortText : MutableState, endPortText : MutableStat trailingIcon = if (hasSubmitted.value && endHasError.value) { @Composable { - if(hasSubmitted.value && endHasError.value) - { - Icon(Icons.Filled.Error, contentDescription = endHasErrorString.value.toMessage(), tint = MaterialTheme.colorScheme.error) //TODO set error on main theme if not already. + if (hasSubmitted.value && endHasError.value) { + Icon( + Icons.Filled.Error, + contentDescription = endHasErrorString.value.toMessage(), + tint = MaterialTheme.colorScheme.error + ) //TODO set error on main theme if not already. } } } else { @@ -1483,8 +1558,7 @@ fun PortRangeRow(startPortText : MutableState, endPortText : MutableStat } -fun RunUIThread(runnable: Runnable) -{ +fun RunUIThread(runnable: Runnable) { val mainLooper = Looper.getMainLooper() // Create a Handler to run some code on the UI thread @@ -1494,8 +1568,12 @@ fun RunUIThread(runnable: Runnable) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun DropDownOutline(defaultModifier : Modifier, selectedText : MutableState, suggestions : List, outlineLabel : String) -{ +fun DropDownOutline( + defaultModifier: Modifier, + selectedText: MutableState, + suggestions: List, + outlineLabel: String +) { // var defaultModifier = Modifier.then(Modifier) // if(textfieldSize != null) // { @@ -1519,7 +1597,8 @@ fun DropDownOutline(defaultModifier : Modifier, selectedText : MutableState Unit) { } @Composable -fun OverflowMenu(showAboutDialogState : MutableState, portViewModel : PortViewModel) { +fun OverflowMenu(showAboutDialogState: MutableState, portViewModel: PortViewModel) { var expanded by remember { mutableStateOf(false) } val isInMultiSelectMode by portViewModel.inMultiSelectMode.collectAsStateWithLifecycle() @@ -1639,36 +1719,30 @@ fun OverflowMenu(showAboutDialogState : MutableState, portViewModel : P ) { // this gets called on expanded, so I dont think we need to monitor additional state. - val items : MutableList = mutableListOf() - if(isInMultiSelectMode) - { - val anyEnabled = portViewModel.getSelectedItems(selectedIds).any { it -> it.portMapping.Enabled } - val anyDisabled = portViewModel.getSelectedItems(selectedIds).any { it -> !it.portMapping.Enabled } - if(anyEnabled) - { + val items: MutableList = mutableListOf() + if (isInMultiSelectMode) { + val anyEnabled = + portViewModel.getSelectedItems(selectedIds).any { it -> it.portMapping.Enabled } + val anyDisabled = + portViewModel.getSelectedItems(selectedIds).any { it -> !it.portMapping.Enabled } + if (anyEnabled) { items.add(R.string.disable_action) } - if(anyDisabled) - { + if (anyDisabled) { items.add(R.string.enable_action) } - } - else - { + } else { items.add(R.string.refresh_action) - if(portViewModel.isInitialized()) - { + if (portViewModel.isInitialized()) { val (anyEnabled, anyDisabled) = portViewModel.getExistingRuleInfos() - if(anyEnabled) // also get info i.e. any enabled, any disabled + if (anyEnabled) // also get info i.e. any enabled, any disabled { items.add(R.string.disable_all_action) } - if (anyDisabled) - { + if (anyDisabled) { items.add(R.string.enable_all_action) } - if(anyDisabled || anyEnabled) - { + if (anyDisabled || anyEnabled) { items.add(R.string.delete_all_action) items.add(R.string.renew_all_action) } @@ -1684,58 +1758,61 @@ fun OverflowMenu(showAboutDialogState : MutableState, portViewModel : P expanded = false when (label) { - R.string.refresh_action -> - { + R.string.refresh_action -> { portViewModel.fullRefresh() } - R.string.disable_all_action -> - { + + R.string.disable_all_action -> { portViewModel.enableDisableAll(false) } - R.string.enable_all_action -> - { + + R.string.enable_all_action -> { portViewModel.enableDisableAll(true) } - R.string.disable_action -> - { - if (isInMultiSelectMode){ + + R.string.disable_action -> { + if (isInMultiSelectMode) { portViewModel.enableDisableAll(false, selectedIds) - } - else{ + } else { portViewModel.enableDisableAll(false) } } - R.string.enable_action -> - { - if (isInMultiSelectMode){ + + R.string.enable_action -> { + if (isInMultiSelectMode) { portViewModel.enableDisableAll(true, selectedIds) - } - else{ + } else { portViewModel.enableDisableAll(true) } } - R.string.delete_all_action -> - { + + R.string.delete_all_action -> { portViewModel.deleteAll() } - R.string.renew_all_action -> - { + + R.string.renew_all_action -> { portViewModel.renewAll() } - R.string.view_log_action -> - { + + R.string.view_log_action -> { val intent = - Intent(PortForwardApplication.CurrentActivity, LogViewActivity::class.java) + Intent( + PortForwardApplication.CurrentActivity, + LogViewActivity::class.java + ) PortForwardApplication.CurrentActivity?.startActivity(intent) } - R.string.settings -> - { + + R.string.settings -> { val intent = - Intent(PortForwardApplication.CurrentActivity, SettingsActivity::class.java) + Intent( + PortForwardApplication.CurrentActivity, + SettingsActivity::class.java + ) PortForwardApplication.CurrentActivity?.startActivity(intent) } - R.string.about -> - { + + R.string.about -> { showAboutDialogState.value = true } } @@ -1744,8 +1821,7 @@ fun OverflowMenu(showAboutDialogState : MutableState, portViewModel : P } } -fun formatIpv4(ipAddr : Int) : String -{ +fun formatIpv4(ipAddr: Int): String { val byteBuffer = ByteBuffer.allocate(4) byteBuffer.order(ByteOrder.LITTLE_ENDIAN) byteBuffer.putInt(ipAddr) @@ -1753,20 +1829,31 @@ fun formatIpv4(ipAddr : Int) : String return inetAddress.hostAddress } -data class Message(val name : String, val msg : String) +data class Message(val name: String, val msg: String) -fun _getDefaultPortMapping() : PortMappingWithPref -{ +fun _getDefaultPortMapping(): PortMappingWithPref { return PortMappingWithPref( - PortMapping("Web Server", "","192.168.18.13",80,80, "UDP", true, 0, "192.168.18.1", SystemClock.elapsedRealtime(), 0)) + PortMapping( + "Web Server", + "", + "192.168.18.13", + 80, + 80, + "UDP", + true, + 0, + "192.168.18.1", + SystemClock.elapsedRealtime(), + 0 + ) + ) } // TODO: for mock purposes -class IGDDeviceHolder -{ +class IGDDeviceHolder { var displayName: String = "Nokia" - get(){ + get() { return "" } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/Modules.kt b/app/src/main/java/com/shinjiindustrial/portmapper/Modules.kt index a3876bb..5c25ba7 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/Modules.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/Modules.kt @@ -1,15 +1,8 @@ package java.com.shinjiindustrial.portmapper -import android.content.Context -import com.shinjiindustrial.portmapper.domain.AndroidUpnpServiceConfigurationImpl import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import org.fourthline.cling.UpnpService -import org.fourthline.cling.UpnpServiceImpl -import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortForwardApplication.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortForwardApplication.kt index a167f6f..54101aa 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortForwardApplication.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortForwardApplication.kt @@ -9,17 +9,11 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.unit.dp -import androidx.datastore.dataStore -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.preferencesDataStore -import com.shinjiindustrial.portmapper.common.SortBy import com.shinjiindustrial.portmapper.domain.PortMappingWithPref import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.util.logging.Logger import javax.inject.Inject @@ -50,26 +44,25 @@ class PortForwardApplication : Application() { } - companion object { lateinit var appContext: Context lateinit var instance: PortForwardApplication var CurrentActivity: ComponentActivity? = null + // lateinit var showPopup : MutableState - lateinit var currentSingleSelectedObject : MutableState - var showContextMenu : MutableState = mutableStateOf(false) + lateinit var currentSingleSelectedObject: MutableState + var showContextMenu: MutableState = mutableStateOf(false) var PaddingBetweenCreateNewRuleRows = 4.dp - var Logs : SnapshotStateList = mutableStateListOf() - var OurLogger : Logger = Logger.getLogger("PortMapper") + var Logs: SnapshotStateList = mutableStateListOf() + var OurLogger: Logger = Logger.getLogger("PortMapper") val ScrollToBottom = "ScrollToBottom" var crashlyticsEnabled: Boolean = false - val RENEW_RULE_WITHIN_X_SECONDS_OF_EXPIRING = 30L; - val RENEW_BATCH_WITHIN_X_SECONDS = 5L; + val RENEW_RULE_WITHIN_X_SECONDS_OF_EXPIRING = 30L + val RENEW_BATCH_WITHIN_X_SECONDS = 5L - fun ShowToast( msg : String, toastLength : Int) - { + fun ShowToast(msg: String, toastLength: Int) { GlobalScope.launch(Dispatchers.Main) { CurrentActivity?.runOnUiThread { Toast.makeText( diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortMappingSort.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortMappingSort.kt index 6406b47..e65c356 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortMappingSort.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortMappingSort.kt @@ -3,62 +3,56 @@ package com.shinjiindustrial.portmapper import com.shinjiindustrial.portmapper.domain.PortMapping import com.shinjiindustrial.portmapper.domain.PortMappingWithPref -open class PortMapperComparatorBase(ascending : Boolean) : Comparator { +open class PortMapperComparatorBase(ascending: Boolean) : Comparator { - val orderBySwitch = if(ascending) 1 else -1 + val orderBySwitch = if (ascending) 1 else -1 override fun compare(p1: PortMapping, p2: PortMapping): Int { var cmp = p1.ExternalPort.compareTo(p2.ExternalPort) - if(cmp != 0) - { + if (cmp != 0) { return orderBySwitch * cmp } cmp = p1.Protocol.compareTo(p2.Protocol) - if(cmp != 0) - { + if (cmp != 0) { return orderBySwitch * cmp } //maybe 100% step before here cmp = p1.InternalPort.compareTo(p2.InternalPort) - if(cmp != 0) - { + if (cmp != 0) { return orderBySwitch * cmp } cmp = p1.DeviceIP.compareTo(p2.DeviceIP) - if(cmp != 0) - { + if (cmp != 0) { return orderBySwitch * cmp } cmp = p1.Description.compareTo(p2.Description) - if(cmp != 0) - { + if (cmp != 0) { return orderBySwitch * cmp } cmp = p1.InternalIP.compareTo(p2.InternalIP) - if(cmp != 0) - { + if (cmp != 0) { return orderBySwitch * cmp } return 0 } } -open class PortMapperComparatorExternalPort(ascending : Boolean) : PortMapperComparatorBase(ascending) { +open class PortMapperComparatorExternalPort(ascending: Boolean) : + PortMapperComparatorBase(ascending) { override fun compare(p1: PortMapping, p2: PortMapping): Int { val cmp = p1.ExternalPort.compareTo(p2.ExternalPort) - if(cmp != 0) - { + if (cmp != 0) { return orderBySwitch * cmp } return super.compare(p1, p2) } } -open class PortMapperComparatorInternalPort(ascending : Boolean) : PortMapperComparatorBase(ascending) { +open class PortMapperComparatorInternalPort(ascending: Boolean) : + PortMapperComparatorBase(ascending) { override fun compare(p1: PortMapping, p2: PortMapping): Int { val cmp = p1.InternalPort.compareTo(p2.InternalPort) - if(cmp != 0) - { + if (cmp != 0) { return orderBySwitch * cmp } return super.compare(p1, p2) @@ -78,57 +72,54 @@ open class PortMapperComparatorInternalPort(ascending : Boolean) : PortMapperCom // } -open class PortMapperComparatorDevice(ascending : Boolean) : PortMapperComparatorBase(ascending) { +open class PortMapperComparatorDevice(ascending: Boolean) : PortMapperComparatorBase(ascending) { override fun compare(p1: PortMapping, p2: PortMapping): Int { val cmp = p1.InternalIP.compareTo(p2.InternalIP) - if(cmp != 0) - { + if (cmp != 0) { return orderBySwitch * cmp } return super.compare(p1, p2) } } -open class PortMapperComparatorExpiration(ascending : Boolean) : PortMapperComparatorBase(ascending) { +open class PortMapperComparatorExpiration(ascending: Boolean) : + PortMapperComparatorBase(ascending) { override fun compare(p1: PortMapping, p2: PortMapping): Int { val cmp = p1.LeaseDuration.compareTo(p2.LeaseDuration) - if(cmp != 0) - { + if (cmp != 0) { return orderBySwitch * cmp } return super.compare(p1, p2) } } -open class PortMapperComparatorDescription(ascending : Boolean) : PortMapperComparatorBase(ascending) { +open class PortMapperComparatorDescription(ascending: Boolean) : + PortMapperComparatorBase(ascending) { override fun compare(p1: PortMapping, p2: PortMapping): Int { val cmp = p1.Description.compareTo(p2.Description) - if(cmp != 0) - { + if (cmp != 0) { return orderBySwitch * cmp } return super.compare(p1, p2) } } -open class PortMapperComparerSlot(ascending : Boolean) : PortMapperComparatorBase(ascending) { +open class PortMapperComparerSlot(ascending: Boolean) : PortMapperComparatorBase(ascending) { override fun compare(p1: PortMapping, p2: PortMapping): Int { val cmp = p1.Slot.compareTo(p2.Slot) - if(cmp != 0) - { + if (cmp != 0) { return orderBySwitch * cmp } return super.compare(p1, p2) } } -class ComparerWrapper(val comparator : Comparator) : Comparator { +class ComparerWrapper(val comparator: Comparator) : Comparator { override fun compare(p1: PortMappingWithPref, p2: PortMappingWithPref): Int { val pm1 = p1.portMapping val pm2 = p2.portMapping val cmp = pm1.DeviceIP.compareTo(pm2.DeviceIP) - if(cmp != 0) - { + if (cmp != 0) { return cmp } return comparator.compare(pm1, pm2) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index 15aad68..6a2ec3a 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -69,7 +69,7 @@ class PortViewModel @Inject constructor( val searchStartedRecently: MutableStateFlow = MutableStateFlow(false) - private val applicationScope : CoroutineScope = MainScope() + private val applicationScope: CoroutineScope = MainScope() // we want to use key for selections. so if a rule renews while the user is in multi select // mode, don't deselect that rule. but if we lose a rule (i.e. it gets deleted) then @@ -87,29 +87,32 @@ class PortViewModel @Inject constructor( _selectedIds.update { s -> if (id in s) s - id else s + id } } - fun getSelectedItems(selectedIds : Set) : List - { + fun getSelectedItems(selectedIds: Set): List { return upnpRepository.portMappingsFromIds(selectedIds) } - fun clearSelection() { _selectedIds.value = emptySet() } + fun clearSelection() { + _selectedIds.value = emptySet() + } - val anyDevices: StateFlow = upnpRepository.devices.map { devices -> devices.isNotEmpty() }.stateIn( scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = false - ) + val anyDevices: StateFlow = + upnpRepository.devices.map { devices -> devices.isNotEmpty() }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = false + ) - val searchStartedRecentlyAndNothingFoundYet: StateFlow = combine(upnpRepository.devices, searchStartedRecently) - { - devices, searchStartedRecently -> - devices.isEmpty() && searchStartedRecently - }.stateIn( + val searchStartedRecentlyAndNothingFoundYet: StateFlow = + combine(upnpRepository.devices, searchStartedRecently) + { devices, searchStartedRecently -> + devices.isEmpty() && searchStartedRecently + }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = false ) - val sortInfo : StateFlow = preferencesRepository.sortInfo.stateIn( + val sortInfo: StateFlow = preferencesRepository.sortInfo.stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), SortInfo(SortBy.ExternalPort, false) @@ -124,28 +127,22 @@ class PortViewModel @Inject constructor( Log.i("Port", "UI STATE FLOW is running") val upnpElements = mutableListOf() - if (devices.isEmpty()) - { - } - else - { + if (devices.isEmpty()) { + } else { var index = 0 - val curDevice : IIGDDevice? = devices.elementAt(0) - val portMappingsList = portMappings.values.sortedWith(sortInfo.sortBy.getComparer(!sortInfo.sortDesc)) - for (curDevice in devices) - { + val curDevice: IIGDDevice? = devices.elementAt(0) + val portMappingsList = + portMappings.values.sortedWith(sortInfo.sortBy.getComparer(!sortInfo.sortDesc)) + for (curDevice in devices) { // do we have any port mappings upnpElements.add(UpnpViewRow.DeviceHeaderViewRow(curDevice)) - if (index >= portMappingsList.size || portMappingsList.elementAt(index).portMapping.DeviceIP != curDevice.getIpAddress()) - { + if (index >= portMappingsList.size || portMappingsList.elementAt(index).portMapping.DeviceIP != curDevice.getIpAddress()) { // emit empty and continue upnpElements.add(UpnpViewRow.DeviceEmptyViewRow(curDevice)) continue } - while (index < portMappingsList.size) - { - if (portMappingsList.elementAt(index).portMapping.DeviceIP == curDevice.getIpAddress()) - { + while (index < portMappingsList.size) { + if (portMappingsList.elementAt(index).portMapping.DeviceIP == curDevice.getIpAddress()) { upnpElements.add(UpnpViewRow.PortViewRow(portMappingsList.elementAt(index))) } index++ @@ -161,8 +158,7 @@ class PortViewModel @Inject constructor( initialValue = PortUiState(isLoading = true) ) - fun initialize(context : Context, force : Boolean) - { + fun initialize(context: Context, force: Boolean) { upnpRepository.Initialize(context, force) } @@ -178,12 +174,12 @@ class PortViewModel @Inject constructor( return upnpRepository.GetUPnPClient().isInitialized() } - fun updateSortingDesc(sortDesc : Boolean) = viewModelScope.launch { + fun updateSortingDesc(sortDesc: Boolean) = viewModelScope.launch { preferencesRepository.updateSortDesc(sortDesc) } - fun updateSortingSortBy(sortBy : SortBy) = viewModelScope.launch { - preferencesRepository.updateSortBy(sortBy) + fun updateSortingSortBy(sortBy: SortBy) = viewModelScope.launch { + preferencesRepository.updateSortBy(sortBy) } fun getIGDDevice(ipAddress: String): IIGDDevice { @@ -194,8 +190,7 @@ class PortViewModel @Inject constructor( return upnpRepository.GetGatewayIpsWithDefault(deviceGateway) } - fun fullRefresh() - { + fun fullRefresh() { upnpRepository.FullRefresh() } @@ -204,19 +199,17 @@ class PortViewModel @Inject constructor( } fun renew(portMapping: PortMappingWithPref) = applicationScope.launch { - try { - val res = upnpRepository.renewRule(portMapping) - if (res is UPnPCreateMappingWrapperResult.Success) { - _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) - } - else - { - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).reason}")) - } - } catch (e: Exception) { - _events.emit(UiEvent.SnackBarViewLogEvent("Renew Port Mapping Failed")) + try { + val res = upnpRepository.renewRule(portMapping) + if (res is UPnPCreateMappingWrapperResult.Success) { + _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) + } else { + _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).reason}")) } + } catch (e: Exception) { + _events.emit(UiEvent.SnackBarViewLogEvent("Renew Port Mapping Failed")) } + } fun renewAll(chosen: List? = null) = applicationScope.launch { try { @@ -239,12 +232,10 @@ class PortViewModel @Inject constructor( val anyFailed = result.any { it is UPnPCreateMappingWrapperResult.Failure } - if(anyFailed) { + if (anyFailed) { val res = result.first { it is UPnPCreateMappingWrapperResult.Failure } _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).reason}")) - } - else - { + } else { _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } } catch (e: Exception) { @@ -254,37 +245,37 @@ class PortViewModel @Inject constructor( fun enableDisable( portMapping: PortMappingWithPref, - enable: Boolean) = + enable: Boolean + ) = applicationScope.launch { try { val res = upnpRepository.disableEnablePortMappingEntry(portMapping, enable) if (res is UPnPCreateMappingWrapperResult.Success) { _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) - } - else - { + } else { _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).reason}")) } } catch (e: Exception) { - val enableDisableString = if(enable) "Enable" else "Disable" + val enableDisableString = if (enable) "Enable" else "Disable" _events.emit(UiEvent.SnackBarViewLogEvent("$enableDisableString Port Mapping Failed")) } } - fun enableDisableAll(enable : Boolean, selectedIds : Set) - { + fun enableDisableAll(enable: Boolean, selectedIds: Set) { enableDisableAll(enable, upnpRepository.portMappingsFromIds(selectedIds)) } - fun enableDisableAll(enable : Boolean, chosenRulesOnly : List? = null) = + fun enableDisableAll(enable: Boolean, chosenRulesOnly: List? = null) = applicationScope.launch { try { val result = when { chosenRulesOnly != null -> { - val rules = chosenRulesOnly.filter { it -> it.portMapping.Enabled != enable } + val rules = + chosenRulesOnly.filter { it -> it.portMapping.Enabled != enable } upnpRepository.disableEnablePortMappingEntries(rules, enable) } + else -> { val rules = upnpRepository.GetEnabledDisabledRules(!enable) upnpRepository.disableEnablePortMappingEntries(rules, enable) @@ -308,25 +299,20 @@ class PortViewModel @Inject constructor( val anyFailed = result.any { it is UPnPCreateMappingWrapperResult.Failure } - if(anyFailed) { + if (anyFailed) { val res = result.first { it is UPnPCreateMappingWrapperResult.Failure } _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).reason}")) - } - else - { + } else { _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } - } catch(e : Exception) - { - val enableDisableString = if(enable) "Enable" else "Disable" + } catch (e: Exception) { + val enableDisableString = if (enable) "Enable" else "Disable" _events.emit(UiEvent.SnackBarViewLogEvent("$enableDisableString Port Mappings Failed")) } - } - + } - fun start() - { + fun start() { if (upnpRepository.FailedToInitialize) { searchStartedRecently.value = false } else { @@ -364,7 +350,8 @@ class PortViewModel @Inject constructor( // map to the intersection of the two sets, in most cases this should be the same. // if a rule got deleted then it will be different. in which case we update selectedIds // to be the new pruned set. - sel intersect cur } + sel intersect cur + } .distinctUntilChanged() .onEach { filtered -> println("filtered changed ...") @@ -373,7 +360,7 @@ class PortViewModel @Inject constructor( } .launchIn(viewModelScope) } - + override fun onCleared() { upnpRepository.SearchStarted -= { o -> searchStarted(o) } super.onCleared() @@ -392,8 +379,7 @@ class PortViewModel @Inject constructor( } } - fun deleteAll(selectedIds : Set) - { + fun deleteAll(selectedIds: Set) { deleteAll(upnpRepository.portMappingsFromIds(selectedIds)) } @@ -417,17 +403,15 @@ class PortViewModel @Inject constructor( } } - val anyFailed = result.any { it is UPnPResult.Failure } + val anyFailed = result.any { it is UPnPResult.Failure } - if(anyFailed) { - val res = result.first { it is UPnPResult.Failure } - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPResult.Failure).reason}")) - } - else - { - _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) - } - } catch (e : Exception) { + if (anyFailed) { + val res = result.first { it is UPnPResult.Failure } + _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPResult.Failure).reason}")) + } else { + _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) + } + } catch (e: Exception) { _events.emit(UiEvent.SnackBarViewLogEvent("Delete Port Mappings Failed")) } } @@ -440,77 +424,79 @@ class PortViewModel @Inject constructor( } else { _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPResult.Failure).reason}")) } - } catch (e : Exception) { - _events.emit(UiEvent.SnackBarViewLogEvent("Delete Port Mapping Failed")) - } + } catch (e: Exception) { + _events.emit(UiEvent.SnackBarViewLogEvent("Delete Port Mapping Failed")) + } } - fun editRule(oldRule : PortMappingWithPref, portMappingRequestInput : PortMappingUserInput) = applicationScope.launch { - try { - val res = upnpRepository.deletePortMappingEntry(oldRule) - if (res is UPnPResult.Failure) { + fun editRule(oldRule: PortMappingWithPref, portMappingRequestInput: PortMappingUserInput) = + applicationScope.launch { + try { + val res = upnpRepository.deletePortMappingEntry(oldRule) + if (res is UPnPResult.Failure) { + _events.emit(UiEvent.SnackBarViewLogEvent("Failed to modify entry.")) + return@launch + } + } catch (exception: Exception) { + PortForwardApplication.OurLogger.log( + Level.SEVERE, + "Delete Original Port Mappings Failed: " + exception.message + exception.stackTraceToString() + ) _events.emit(UiEvent.SnackBarViewLogEvent("Failed to modify entry.")) return@launch } - } catch (exception: Exception) { - PortForwardApplication.OurLogger.log( - Level.SEVERE, - "Delete Original Port Mappings Failed: " + exception.message + exception.stackTraceToString() - ) - _events.emit(UiEvent.SnackBarViewLogEvent("Failed to modify entry.")) - return@launch + // delete was successful, create new rules + createRules(portMappingRequestInput, true) } - // delete was successful, create new rules - createRules(portMappingRequestInput, true) - } - fun createRules(portMappingUserInput: PortMappingUserInput, modifyCase : Boolean = false) = applicationScope.launch { - try { - val result = upnpRepository.createPortMappingRulesEntry(portMappingUserInput) - result.forEach { res -> - when (res) { - is UPnPCreateMappingWrapperResult.Success -> { - print("success") - print(res.requestInfo.Description) - } + fun createRules(portMappingUserInput: PortMappingUserInput, modifyCase: Boolean = false) = + applicationScope.launch { + try { + val result = upnpRepository.createPortMappingRulesEntry(portMappingUserInput) + result.forEach { res -> + when (res) { + is UPnPCreateMappingWrapperResult.Success -> { + print("success") + print(res.requestInfo.Description) + } - is UPnPCreateMappingWrapperResult.Failure -> { - print("failure") - print(res.reason) - print(res.response) + is UPnPCreateMappingWrapperResult.Failure -> { + print("failure") + print(res.reason) + print(res.response) + } } - } - } + } - val numFailed = result.count { it is UPnPCreateMappingWrapperResult.Failure } + val numFailed = result.count { it is UPnPCreateMappingWrapperResult.Failure } - val anyFailed = numFailed > 0 + val anyFailed = numFailed > 0 - if (anyFailed) { + if (anyFailed) { - val verbString = if(modifyCase) "modify" else "create" + val verbString = if (modifyCase) "modify" else "create" - // all failed - if (numFailed == result.size) { - if (result.size == 1) { - _events.emit(UiEvent.SnackBarViewLogEvent("Failed to $verbString rule.")) + // all failed + if (numFailed == result.size) { + if (result.size == 1) { + _events.emit(UiEvent.SnackBarViewLogEvent("Failed to $verbString rule.")) + } else { + _events.emit(UiEvent.SnackBarViewLogEvent("Failed to $verbString rules.")) + } } else { - _events.emit(UiEvent.SnackBarViewLogEvent("Failed to $verbString rules.")) + _events.emit(UiEvent.SnackBarViewLogEvent("Failed to $verbString some rules.")) } } else { - _events.emit(UiEvent.SnackBarViewLogEvent("Failed to $verbString some rules.")) + _events.emit(UiEvent.SnackBarViewShortNoEvent("Success")) } - } else { - _events.emit(UiEvent.SnackBarViewShortNoEvent("Success")) + } catch (exception: Exception) { + PortForwardApplication.OurLogger.log( + Level.SEVERE, + "Delete Original Port Mappings Failed: " + exception.message + exception.stackTraceToString() + ) + _events.emit(UiEvent.SnackBarViewLogEvent("Failed to modify entry.")) + return@launch } - } catch (exception: Exception) { - PortForwardApplication.OurLogger.log( - Level.SEVERE, - "Delete Original Port Mappings Failed: " + exception.message + exception.stackTraceToString() - ) - _events.emit(UiEvent.SnackBarViewLogEvent("Failed to modify entry.")) - return@launch } - } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PreferencesManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PreferencesManager.kt index f6780f0..a56cea4 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PreferencesManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PreferencesManager.kt @@ -5,21 +5,13 @@ import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.preferencesDataStore -import com.shinjiindustrial.portmapper.DayNightMode -import com.shinjiindustrial.portmapper.SharedPrefKeys -import com.shinjiindustrial.portmapper.SharedPrefValues +import com.shinjiindustrial.portmapper.PreferencesManager.Keys.SORT_DESC_KEY import com.shinjiindustrial.portmapper.common.SortBy import com.shinjiindustrial.portmapper.common.SortInfo import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import com.shinjiindustrial.portmapper.PreferencesManager.Keys.SORT_DESC_KEY import javax.inject.Inject import javax.inject.Singleton @@ -52,10 +44,11 @@ class PreferencesManager @Inject constructor( .map { preferences -> SortInfo( SortBy.from(preferences[Keys.SORT_BY_KEY] ?: SortBy.ExternalPort.sortByValue), - preferences[SORT_DESC_KEY] ?: false) + preferences[SORT_DESC_KEY] ?: false + ) } .distinctUntilChanged() - + suspend fun updateDayNight(dayNightMode: DayNightMode) { context.dataStore.edit { preferences -> preferences[Keys.DAY_NIGHT_KEY] = dayNightMode.intVal @@ -76,7 +69,7 @@ class PreferencesManager @Inject constructor( suspend fun updateSortDesc(sortDesc: Boolean) { context.dataStore.edit { preferences -> - preferences[Keys.SORT_DESC_KEY] = sortDesc + preferences[SORT_DESC_KEY] = sortDesc } } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt index f171637..9b54441 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt @@ -45,19 +45,18 @@ import com.shinjiindustrial.portmapper.ui.SetupPreview import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import com.shinjiindustrial.portmapper.ui.theme.MyApplicationTheme import dagger.hilt.android.AndroidEntryPoint -import java.com.shinjiindustrial.portmapper.ThemeUiState import java.com.shinjiindustrial.portmapper.SettingsViewModel -import javax.inject.Inject +import java.com.shinjiindustrial.portmapper.ThemeUiState @AndroidEntryPoint class SettingsActivity : ComponentActivity() { - val settingsViewModel : SettingsViewModel by viewModels() + val settingsViewModel: SettingsViewModel by viewModels() @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState); + super.onCreate(savedInstanceState) PortForwardApplication.CurrentActivity = this setContent { @@ -248,7 +247,7 @@ class SettingsActivity : ComponentActivity() { fun RadioGroupPreviewTheme() { val themeState = ThemeUiState(DayNightMode.FORCE_NIGHT, false) MyApplicationTheme(themeState) { - ThemeRadioGroup(themeState, {_ -> Unit}) + ThemeRadioGroup(themeState, { _ -> }) } } @@ -257,7 +256,7 @@ class SettingsActivity : ComponentActivity() { val dark = "Dark" @Composable - fun ThemeRadioGroup(themeState : ThemeUiState, setDayNightMode : (DayNightMode) -> Unit) { + fun ThemeRadioGroup(themeState: ThemeUiState, setDayNightMode: (DayNightMode) -> Unit) { val options = listOf(followSystem, light, dark) val selectedOption = options[themeState.dayNightMode.intVal] @@ -287,7 +286,7 @@ class SettingsActivity : ComponentActivity() { fun RadioGroupPreview() { val themeState = ThemeUiState(DayNightMode.FORCE_NIGHT, false) SetupPreview() - ThemeRadioGroup(themeState,{_->Unit}) + ThemeRadioGroup(themeState, { _ -> }) } @Composable diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/SharedPreferences.kt b/app/src/main/java/com/shinjiindustrial/portmapper/SharedPreferences.kt index 50c6f98..a8afa62 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/SharedPreferences.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/SharedPreferences.kt @@ -1,23 +1,19 @@ package com.shinjiindustrial.portmapper -import com.shinjiindustrial.portmapper.common.SortBy - object SharedPrefKeys -{ -} -object SharedPrefValues -{ - var DayNightPref : DayNightMode = DayNightMode.FOLLOW_SYSTEM - var MaterialYouTheme : Boolean = false +object SharedPrefValues { + var DayNightPref: DayNightMode = DayNightMode.FOLLOW_SYSTEM + var MaterialYouTheme: Boolean = false } -enum class DayNightMode(val intVal : Int) { +enum class DayNightMode(val intVal: Int) { FOLLOW_SYSTEM(0), FORCE_DAY(1), FORCE_NIGHT(2); companion object { - fun from(findValue: Int): DayNightMode = DayNightMode.values().first { it.intVal == findValue } + fun from(findValue: Int): DayNightMode = + DayNightMode.values().first { it.intVal == findValue } } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index 846ebbc..96acf36 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -41,26 +41,23 @@ import kotlinx.coroutines.withContext import java.util.logging.Level import javax.inject.Inject import javax.inject.Singleton -import kotlin.collections.filter -import kotlin.collections.forEach -import kotlin.collections.map -import kotlin.collections.plus import kotlin.system.measureTimeMillis @Singleton -class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, private val portMappingDao : PortMappingDao) { +class UpnpManager @Inject constructor( + private val upnpClient: IUpnpClient, + private val portMappingDao: PortMappingDao +) { init { upnpClient.deviceFoundEvent += { device -> // this is on the cling thread - if (_devices.value.any { it.getIpAddress() == device.getIpAddress() }) - { + if (_devices.value.any { it.getIpAddress() == device.getIpAddress() }) { OurLogger.log( Level.WARNING, - "device ${device.udn} came in again at ${device.getIpAddress()}. ignoring.") - } - else - { + "device ${device.udn} came in again at ${device.getIpAddress()}. ignoring." + ) + } else { addDevice(device)//TODO test with suspend runBlocking { enumeratePortMappings(device.getIpAddress()) @@ -72,17 +69,21 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv subscribeForAutoRenewCancelTimerNotWork() } - fun subscribeForAutoRenewCancelTimerNotWork() - { + fun subscribeForAutoRenewCancelTimerNotWork() { // too early for app scope MainScope().launch { - portMappings.mapNotNull { - it -> it.filter { it.value.getAutoRenewOrDefault() }.minByOrNull { it.value.portMapping.getExpiresTimeMillis() } - }.onEach{ + portMappings.mapNotNull { it -> + it.filter { it.value.getAutoRenewOrDefault() } + .minByOrNull { it.value.portMapping.getExpiresTimeMillis() } + }.onEach { // null check - it -> println("running with ${it.value.portMapping.shortName()}") + it -> + println("running with ${it.value.portMapping.shortName()}") }.collectLatest { - delayUntilExpiryBuffer((it.value.portMapping.getExpiresTimeMillis()), PortForwardApplication.RENEW_RULE_WITHIN_X_SECONDS_OF_EXPIRING) + delayUntilExpiryBuffer( + (it.value.portMapping.getExpiresTimeMillis()), + PortForwardApplication.RENEW_RULE_WITHIN_X_SECONDS_OF_EXPIRING + ) println("delay is over launching") withContext(NonCancellable) { @@ -100,9 +101,9 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv val snapshot = portMappings.first() val now = SystemClock.elapsedRealtime() - val expiring = snapshot.filter { it.value.portMapping.getExpiresTimeMillis() - now <= 1000*(PortForwardApplication.RENEW_RULE_WITHIN_X_SECONDS_OF_EXPIRING + PortForwardApplication.RENEW_BATCH_WITHIN_X_SECONDS) } - if (expiring.isEmpty()) - { + val expiring = + snapshot.filter { it.value.portMapping.getExpiresTimeMillis() - now <= 1000 * (PortForwardApplication.RENEW_RULE_WITHIN_X_SECONDS_OF_EXPIRING + PortForwardApplication.RENEW_BATCH_WITHIN_X_SECONDS) } + if (expiring.isEmpty()) { println("batch is empty") return } @@ -115,9 +116,7 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv println("renewed ${it.value.portMapping.shortName()}") try { renewRule(it.value) - } - catch (exception : Exception) - { + } catch (exception: Exception) { // we already logged just continue for now (TODO) } println("updated ${it.value.portMapping.shortName()}") @@ -126,9 +125,14 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv println("end renewing all rules") } - private suspend fun delayUntilExpiryBuffer(expirationTime: Long, renewWithinXSecondsOfExpiring: Long) - { - val delayMilliseconds = (expirationTime - renewWithinXSecondsOfExpiring * 1000 - SystemClock.elapsedRealtime()).coerceAtLeast(0L) + private suspend fun delayUntilExpiryBuffer( + expirationTime: Long, + renewWithinXSecondsOfExpiring: Long + ) { + val delayMilliseconds = + (expirationTime - renewWithinXSecondsOfExpiring * 1000 - SystemClock.elapsedRealtime()).coerceAtLeast( + 0L + ) println("wait for $delayMilliseconds ms") delay(delayMilliseconds) } @@ -172,241 +176,213 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv // }.filter(it -> it != null) // } - // region data + // region data - private var _initialized: Boolean = false - var HasSearched: Boolean = false - var FailedToInitialize: Boolean = false + private var _initialized: Boolean = false + var HasSearched: Boolean = false + var FailedToInitialize: Boolean = false - private val _devices = MutableStateFlow(listOf()) // TreeSet - val devices : StateFlow> = - _devices//.map { it..sortedBy { d -> d.name } } - //.stateIn(MainScope(), SharingStarted.Eagerly, emptyList()) - val anyDevices : Flow = _devices.map { !it.isEmpty() } + private val _devices = MutableStateFlow(listOf()) // TreeSet + val devices: StateFlow> = + _devices//.map { it..sortedBy { d -> d.name } } -// private val cmp = compareBy({ it.ActualExternalIP }, { it.InternalPort }) - private var _portMappings : MutableStateFlow> = MutableStateFlow(mapOf()) + //.stateIn(MainScope(), SharingStarted.Eagerly, emptyList()) + val anyDevices: Flow = _devices.map { !it.isEmpty() } - // I think this should be a local var inside mutable state flow - val portMappings : StateFlow> = _portMappings -// + // private val cmp = compareBy({ it.ActualExternalIP }, { it.InternalPort }) + private var _portMappings: MutableStateFlow> = + MutableStateFlow(mapOf()) + + // I think this should be a local var inside mutable state flow + val portMappings: StateFlow> = _portMappings + + // // fun update(pm: PortMapping) = _setFlow.update { old -> // TreeSet(old).apply { add(pm) } // } - fun add(device: IIGDDevice) - { - // list is sorted - if (_devices.value.any { it.getIpAddress() == device.getIpAddress() }) - { - return - } - _devices.update { curList -> - buildList { - addAll(curList) - var index = 0 - for (i in 0..size-1) - { - if (curList[i].getIpAddress() > device.getIpAddress()) - { - break - } - index++ + fun add(device: IIGDDevice) { + // list is sorted + if (_devices.value.any { it.getIpAddress() == device.getIpAddress() }) { + return + } + _devices.update { curList -> + buildList { + addAll(curList) + var index = 0 + for (i in 0..size - 1) { + if (curList[i].getIpAddress() > device.getIpAddress()) { + break } - add(index, device) + index++ } + add(index, device) } } - + } - private fun addOrUpdateMapping(pm: PortMappingWithPref) - { - _portMappings.update { old -> - old + (pm.getKey() to pm) - } + + private fun addOrUpdateMapping(pm: PortMappingWithPref) { + _portMappings.update { old -> + old + (pm.getKey() to pm) } + } - fun removeMapping(pm: PortMappingWithPref) - { - _portMappings.update { old -> - old - pm.getKey() - } + fun removeMapping(pm: PortMappingWithPref) { + _portMappings.update { old -> + old - pm.getKey() } + } - // endregion + // endregion - // region datagetters + // region datagetters - fun getIGDDevice(ipAddr: String): IIGDDevice { - return devices.value.first { it.getIpAddress() == ipAddr } - } + fun getIGDDevice(ipAddr: String): IIGDDevice { + return devices.value.first { it.getIpAddress() == ipAddr } + } - // returns anyEnabled, anyDisabled - fun GetEnabledDisabledRules(enabledRules: Boolean): MutableList { - return portMappings.value.filter { it.value.portMapping.Enabled == enabledRules }.values.toMutableList() - } + // returns anyEnabled, anyDisabled + fun GetEnabledDisabledRules(enabledRules: Boolean): MutableList { + return portMappings.value.filter { it.value.portMapping.Enabled == enabledRules }.values.toMutableList() + } - // TODO: we need a flow for just the IGD , port mappings that are available - // we need a flow for the sorting method - // autorenew can subscribe to the 1st - // UI can subscribe to both + // TODO: we need a flow for just the IGD , port mappings that are available + // we need a flow for the sorting method + // autorenew can subscribe to the 1st + // UI can subscribe to both - fun GetAllRules(): MutableList { - return portMappings.value.values.toMutableList() - } + fun GetAllRules(): MutableList { + return portMappings.value.values.toMutableList() + } - fun GetGatewayIpsWithDefault(deviceGateway: String): Pair, String> { - val gatewayIps: MutableList = mutableListOf() - var defaultGatewayIp = "" - synchronized(lockIgdDevices) - { - for (device in devices.value) { - gatewayIps.add(device.getIpAddress()) - if (device.getIpAddress() == deviceGateway) { - defaultGatewayIp = device.getIpAddress() - } + fun GetGatewayIpsWithDefault(deviceGateway: String): Pair, String> { + val gatewayIps: MutableList = mutableListOf() + var defaultGatewayIp = "" + synchronized(lockIgdDevices) + { + for (device in devices.value) { + gatewayIps.add(device.getIpAddress()) + if (device.getIpAddress() == deviceGateway) { + defaultGatewayIp = device.getIpAddress() } } - - if (defaultGatewayIp == "" && !gatewayIps.isEmpty()) { - defaultGatewayIp = gatewayIps[0] - } - return Pair, String>(gatewayIps, defaultGatewayIp) } - var lockIgdDevices = Any() - - // returns anyEnabled, anyDisabled - fun GetExistingRuleInfos(): Pair { - val anyEnabled: Boolean = portMappings.value.any { pm -> pm.value.portMapping.Enabled } - val anyDisabled: Boolean = portMappings.value.any { pm -> !pm.value.portMapping.Enabled } - return Pair(anyEnabled, anyDisabled) + if (defaultGatewayIp == "" && !gatewayIps.isEmpty()) { + defaultGatewayIp = gatewayIps[0] } + return Pair, String>(gatewayIps, defaultGatewayIp) + } - // endregion + var lockIgdDevices = Any() + // returns anyEnabled, anyDisabled + fun GetExistingRuleInfos(): Pair { + val anyEnabled: Boolean = portMappings.value.any { pm -> pm.value.portMapping.Enabled } + val anyDisabled: Boolean = portMappings.value.any { pm -> !pm.value.portMapping.Enabled } + return Pair(anyEnabled, anyDisabled) + } - // region uievents + // endregion - var DeviceFoundEvent = Event() - var PortAddedEvent = Event() - var PortInitialFoundEvent = Event() - // used if we finish and there are no ports to show the "no devices" card - var FinishedListingPortsEvent = Event() - var UpdateUIFromData = Event() - var SearchStarted = Event() + // region uievents - // endregion + var DeviceFoundEvent = Event() + var PortAddedEvent = Event() + var PortInitialFoundEvent = Event() - // region search + // used if we finish and there are no ports to show the "no devices" card + var FinishedListingPortsEvent = Event() + var UpdateUIFromData = Event() + var SearchStarted = Event() - fun Search(onlyIfNotYetSearched: Boolean): Boolean { - if (onlyIfNotYetSearched && HasSearched) { - return false - } - SearchStarted.invoke(null) - clearOldData() - //NetworkInfoAtTimeOfSearch = OurNetworkInfo.GetNetworkInfo(PortForwardApplication.appContext, true) - HasSearched = true - // can do urn:schemas-upnp-org:device:{deviceType}:{ver} - // 0-1 second response time intentional delay from devices - upnpClient.search(1) - //launchMockUPnPSearch(this, upnpElementsViewModel) - return true - } + // endregion - // endregion + // region search - fun GetUPnPClient(): IUpnpClient { - return upnpClient + fun Search(onlyIfNotYetSearched: Boolean): Boolean { + if (onlyIfNotYetSearched && HasSearched) { + return false } + SearchStarted.invoke(null) + clearOldData() + //NetworkInfoAtTimeOfSearch = OurNetworkInfo.GetNetworkInfo(PortForwardApplication.appContext, true) + HasSearched = true + // can do urn:schemas-upnp-org:device:{deviceType}:{ver} + // 0-1 second response time intentional delay from devices + upnpClient.search(1) + //launchMockUPnPSearch(this, upnpElementsViewModel) + return true + } + // endregion - fun FullRefresh() { - Initialize(PortForwardApplication.appContext, true) - Search(false) - } - - fun Initialize(context: Context, force: Boolean): Boolean { - if (_initialized && !force) { - return true - } + fun GetUPnPClient(): IUpnpClient { + return upnpClient + } - upnpClient.instantiateAndBindUpnpService() - // initialization failed. no point in trying as even if we later get service, we - // do not re-intialize automatically - FailedToInitialize = !upnpClient.isInitialized() - if (FailedToInitialize) { - return false - } + fun FullRefresh() { + Initialize(PortForwardApplication.appContext, true) + Search(false) + } - _initialized = true + fun Initialize(context: Context, force: Boolean): Boolean { + if (_initialized && !force) { return true } - //region actions - - // create new rule with enabled set, and update data - suspend fun disableEnablePortMappingEntry( - portMapping: PortMappingWithPref, - enable: Boolean, - ): UPnPCreateMappingWrapperResult { - // AddPortMapping - // This action creates a new port mapping or overwrites an existing mapping with the same internal client. If - // the ExternalPort and PortMappingProtocol pair is already mapped to another internal client, an error is - // returned. (On my Nokia, I can modify other devices rules no problem). - // However, deleting a mapping it is only a recommendation that they be the same.. - // so Edit = Delete and Add is more powerful? - try - { - val portMappingRequest = PortMappingRequest.from(portMapping).copy(enabled = enable) - val res = createPortMappingRuleWrapper( - portMappingRequest, - false, - getEnabledDisabledString(enable).lowercase(), - ) - addOrUpdateForEnabledDisabled(res, portMapping) - // this is to cause an update - return res - } - catch (exception: Exception) { - val enableDisableString = if(enable) "Enable" else "Disable" - OurLogger.log( - Level.SEVERE, - "$enableDisableString Port Mapping Failed: " + exception.message + exception.stackTraceToString() - ) - throw exception - } + upnpClient.instantiateAndBindUpnpService() + + // initialization failed. no point in trying as even if we later get service, we + // do not re-intialize automatically + FailedToInitialize = !upnpClient.isInitialized() + if (FailedToInitialize) { + return false } - suspend fun renewRules(portMappings : List) - : List - { - return portMappings.map { portMapping -> - - try { - val portMappingRequest = PortMappingRequest.from(portMapping) - val res = createPortMappingRuleWrapper( - portMappingRequest, - false, - "renewed", - ) - addOrUpdateForRenew(res, portMapping) - res - } catch (exception: Exception) { - OurLogger.log( - Level.SEVERE, - "Renew Port Mappings Failed: " + exception.message + exception.stackTraceToString() - ) - throw exception - } - } + _initialized = true + return true + } + + //region actions + + // create new rule with enabled set, and update data + suspend fun disableEnablePortMappingEntry( + portMapping: PortMappingWithPref, + enable: Boolean, + ): UPnPCreateMappingWrapperResult { + // AddPortMapping + // This action creates a new port mapping or overwrites an existing mapping with the same internal client. If + // the ExternalPort and PortMappingProtocol pair is already mapped to another internal client, an error is + // returned. (On my Nokia, I can modify other devices rules no problem). + // However, deleting a mapping it is only a recommendation that they be the same.. + // so Edit = Delete and Add is more powerful? + try { + val portMappingRequest = PortMappingRequest.from(portMapping).copy(enabled = enable) + val res = createPortMappingRuleWrapper( + portMappingRequest, + false, + getEnabledDisabledString(enable).lowercase(), + ) + addOrUpdateForEnabledDisabled(res, portMapping) + // this is to cause an update + return res + } catch (exception: Exception) { + val enableDisableString = if (enable) "Enable" else "Disable" + OurLogger.log( + Level.SEVERE, + "$enableDisableString Port Mapping Failed: " + exception.message + exception.stackTraceToString() + ) + throw exception } + } + + suspend fun renewRules(portMappings: List) + : List { + return portMappings.map { portMapping -> - suspend fun renewRule( - portMapping: PortMappingWithPref, - ): UPnPCreateMappingWrapperResult { try { val portMappingRequest = PortMappingRequest.from(portMapping) val res = createPortMappingRuleWrapper( @@ -414,9 +390,8 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv false, "renewed", ) - // this is so it "refreshes" addOrUpdateForRenew(res, portMapping) - return res + res } catch (exception: Exception) { OurLogger.log( Level.SEVERE, @@ -425,20 +400,71 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv throw exception } } + } + + suspend fun renewRule( + portMapping: PortMappingWithPref, + ): UPnPCreateMappingWrapperResult { + try { + val portMappingRequest = PortMappingRequest.from(portMapping) + val res = createPortMappingRuleWrapper( + portMappingRequest, + false, + "renewed", + ) + // this is so it "refreshes" + addOrUpdateForRenew(res, portMapping) + return res + } catch (exception: Exception) { + OurLogger.log( + Level.SEVERE, + "Renew Port Mappings Failed: " + exception.message + exception.stackTraceToString() + ) + throw exception + } + } - // DeletePortMappingRange is only available in v2.0 (also it can only delete - // a contiguous range) - suspend fun deletePortMappingEntry(portMappingWithPref: PortMappingWithPref) : UPnPResult { + // DeletePortMappingRange is only available in v2.0 (also it can only delete + // a contiguous range) + suspend fun deletePortMappingEntry(portMappingWithPref: PortMappingWithPref): UPnPResult { + + try { + val pm = portMappingWithPref.portMapping + println("Requesting Delete: ${pm.shortName()}") + val device: IIGDDevice = getIGDDevice(pm.DeviceIP) + portMappingDao.deleteByKey(pm.DeviceIP, device.udn, pm.Protocol, pm.ExternalPort) + val result = upnpClient.deletePortMapping(device, pm) + removeForDelete(result, portMappingWithPref) + return result + } catch (exception: Exception) { + OurLogger.log( + Level.SEVERE, + "Delete Port Mappings Failed: " + exception.message + exception.stackTraceToString() + ) + throw exception + } + } + + suspend fun deletePortMappingsEntry( + portMappings: List, + ): List { + return portMappings.map { portMappingWithPref -> try { - val pm = portMappingWithPref.portMapping - println("Requesting Delete: ${pm.shortName()}") - val device: IIGDDevice = getIGDDevice(pm.DeviceIP) - portMappingDao.deleteByKey(pm.DeviceIP, device.udn, pm.Protocol, pm.ExternalPort) - val result = upnpClient.deletePortMapping(device, pm) + val portMapping = portMappingWithPref.portMapping + println("Requesting Delete: ${portMapping.shortName()}") + val device = getIGDDevice(portMapping.DeviceIP) + portMappingDao.deleteByKey( + portMapping.DeviceIP, + device.udn, + portMapping.Protocol, + portMapping.ExternalPort + ) + val result = upnpClient.deletePortMapping(device, portMapping) removeForDelete(result, portMappingWithPref) - return result + result + } catch (exception: Exception) { OurLogger.log( Level.SEVERE, @@ -447,33 +473,12 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv throw exception } } + } - suspend fun deletePortMappingsEntry( - portMappings: List, - ): List { - return portMappings.map { portMappingWithPref -> - - try { - val portMapping = portMappingWithPref.portMapping - println("Requesting Delete: ${portMapping.shortName()}") - val device = getIGDDevice(portMapping.DeviceIP) - portMappingDao.deleteByKey(portMapping.DeviceIP, device.udn, portMapping.Protocol, portMapping.ExternalPort) - val result = upnpClient.deletePortMapping(device,portMapping) - removeForDelete(result, portMappingWithPref) - result - - } catch (exception: Exception) { - OurLogger.log( - Level.SEVERE, - "Delete Port Mappings Failed: " + exception.message + exception.stackTraceToString() - ) - throw exception - } - } - } - - private fun createPortMappingDaoEntity(portMapping: PortMapping, pref: PortMappingPref) : PortMappingEntity - { + private fun createPortMappingDaoEntity( + portMapping: PortMapping, + pref: PortMappingPref + ): PortMappingEntity { val igdDevice = getIGDDevice(portMapping.DeviceIP) return PortMappingEntity( igdDevice.getIpAddress(), @@ -485,72 +490,74 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv portMapping.InternalPort, pref.autoRenew, pref.desiredLeaseDuration, - portMapping.Enabled) // TODO enabled + portMapping.Enabled + ) // TODO enabled } - suspend fun createPortMappingRulesEntry( - portMappingUserInput: PortMappingUserInput, - ): List { + suspend fun createPortMappingRulesEntry( + portMappingUserInput: PortMappingUserInput, + ): List { - try { - val portMappingRequestRules = portMappingUserInput.splitIntoRules() - - return portMappingRequestRules.map { portMappingRequestRule -> - - val result = createPortMappingRuleWrapper( - portMappingRequestRule, - false, - "created") - // this is what the user input - println("default adding rule callback") - if (result is UPnPCreateMappingWrapperResult.Success) - { - val portMapping = result.resultingMapping - val pref = PortMappingPref(portMappingUserInput.autoRenew, portMappingUserInput.leaseDuration.toIntOrMaxValue()) - val entity = createPortMappingDaoEntity(portMapping, pref) - portMappingDao.upsert(entity) - addOrUpdateMapping(PortMappingWithPref(portMapping, pref)) - } - result - } - } - catch(exception : Exception) - { - OurLogger.log( - Level.SEVERE, - "Create Rule Failed: " + exception.message + exception.stackTraceToString() + try { + val portMappingRequestRules = portMappingUserInput.splitIntoRules() + + return portMappingRequestRules.map { portMappingRequestRule -> + + val result = createPortMappingRuleWrapper( + portMappingRequestRule, + false, + "created" ) - throw exception + // this is what the user input + println("default adding rule callback") + if (result is UPnPCreateMappingWrapperResult.Success) { + val portMapping = result.resultingMapping + val pref = PortMappingPref( + portMappingUserInput.autoRenew, + portMappingUserInput.leaseDuration.toIntOrMaxValue() + ) + val entity = createPortMappingDaoEntity(portMapping, pref) + portMappingDao.upsert(entity) + addOrUpdateMapping(PortMappingWithPref(portMapping, pref)) + } + result } + } catch (exception: Exception) { + OurLogger.log( + Level.SEVERE, + "Create Rule Failed: " + exception.message + exception.stackTraceToString() + ) + throw exception } + } - suspend fun disableEnablePortMappingEntries( - portMappings: List, - enable: Boolean, - ): List { + suspend fun disableEnablePortMappingEntries( + portMappings: List, + enable: Boolean, + ): List { - return portMappings.map { portMapping -> + return portMappings.map { portMapping -> - try { - val portMappingRequest = PortMappingRequest.from(portMapping).copy(enabled = enable) - val res = createPortMappingRuleWrapper( - portMappingRequest, - false, - getEnabledDisabledString(enable).lowercase(), - ) - addOrUpdateForEnabledDisabled(res, portMapping) - res - } catch (exception: Exception) { - - val enableDisableString = if (enable) "Enable" else "Disable" - OurLogger.log( - Level.SEVERE, - "$enableDisableString Port Mappings Failed: " + exception.message + exception.stackTraceToString() - ) - throw exception - } + try { + val portMappingRequest = PortMappingRequest.from(portMapping).copy(enabled = enable) + val res = createPortMappingRuleWrapper( + portMappingRequest, + false, + getEnabledDisabledString(enable).lowercase(), + ) + addOrUpdateForEnabledDisabled(res, portMapping) + res + } catch (exception: Exception) { + + val enableDisableString = if (enable) "Enable" else "Disable" + OurLogger.log( + Level.SEVERE, + "$enableDisableString Port Mappings Failed: " + exception.message + exception.stackTraceToString() + ) + throw exception } } + } private fun addOrUpdateForEnabledDisabled( res: UPnPCreateMappingWrapperResult, @@ -574,212 +581,218 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv ) { // this is so it "refreshes" if (res is UPnPCreateMappingWrapperResult.Success) { - addOrUpdateMapping(PortMappingWithPref(res.resultingMapping, portMapping.portMappingPref)) + addOrUpdateMapping( + PortMappingWithPref( + res.resultingMapping, + portMapping.portMappingPref + ) + ) } } //TODO: if List all port mappings exists, that should be used instead of getgeneric. - // this method creates a rule, then grabs it again to verify it. - private suspend fun createPortMappingRuleWrapper( - portMappingRequest: PortMappingRequest, - skipReadingBack: Boolean, - createContext: String, - ) : UPnPCreateMappingWrapperResult { - //var completeableFuture = CompletableFuture() - - val externalIp = portMappingRequest.externalIp - val device: IIGDDevice = getIGDDevice(externalIp) - val createMappingResult = upnpClient.createPortMappingRule(device, portMappingRequest) - when (createMappingResult) - { - is UPnPCreateMappingResult.Success -> - { - OurLogger.log( - Level.INFO, - "Successfully $createContext rule (${ - portMappingRequest.realize().shortName() - })." + // this method creates a rule, then grabs it again to verify it. + private suspend fun createPortMappingRuleWrapper( + portMappingRequest: PortMappingRequest, + skipReadingBack: Boolean, + createContext: String, + ): UPnPCreateMappingWrapperResult { + //var completeableFuture = CompletableFuture() + + val externalIp = portMappingRequest.externalIp + val device: IIGDDevice = getIGDDevice(externalIp) + val createMappingResult = upnpClient.createPortMappingRule(device, portMappingRequest) + when (createMappingResult) { + is UPnPCreateMappingResult.Success -> { + OurLogger.log( + Level.INFO, + "Successfully $createContext rule (${ + portMappingRequest.realize().shortName() + })." + ) + println("Successfully added, now reading back") + if (skipReadingBack) { + val result = UPnPCreateMappingWrapperResult.Success( + portMappingRequest.realize(), + portMappingRequest.realize(), + false + ) + return result + } else { + val result = upnpClient.getSpecificPortMappingRule( + device, + portMappingRequest.remoteHost, + portMappingRequest.externalPort, + portMappingRequest.protocol, ) - println("Successfully added, now reading back") - if (skipReadingBack) { - val result = UPnPCreateMappingWrapperResult.Success(portMappingRequest.realize(), portMappingRequest.realize(), false) - return result - } else { - val result = upnpClient.getSpecificPortMappingRule( - device, - portMappingRequest.remoteHost, - portMappingRequest.externalPort, - portMappingRequest.protocol, - ) - return result - } - } - is UPnPCreateMappingResult.Failure -> - { - val result = UPnPCreateMappingWrapperResult.Failure(createMappingResult.reason, createMappingResult.response) return result } } + + is UPnPCreateMappingResult.Failure -> { + val result = UPnPCreateMappingWrapperResult.Failure( + createMappingResult.reason, + createMappingResult.response + ) + return result + } } + } - suspend fun enumeratePortMappings(externalIp : String) - { - // we enumerate port mappings later - val device: IIGDDevice = getIGDDevice(externalIp) - val timeTakenMillis = measureTimeMillis { - if(device.supportsAction(ACTION_NAMES.GetGenericPortMappingEntry)) - { - OurLogger.log(Level.INFO, "Enumerating Port Listings using GetGenericPortMappingEntry") - getAllPortMappingsUsingGenericPortMappingEntry(device) - } - else{ - //TODO firebase integration - OurLogger.log(Level.SEVERE, "device does not have GetGenericPortMappingEntry") - } + suspend fun enumeratePortMappings(externalIp: String) { + // we enumerate port mappings later + val device: IIGDDevice = getIGDDevice(externalIp) + val timeTakenMillis = measureTimeMillis { + if (device.supportsAction(ACTION_NAMES.GetGenericPortMappingEntry)) { + OurLogger.log( + Level.INFO, + "Enumerating Port Listings using GetGenericPortMappingEntry" + ) + getAllPortMappingsUsingGenericPortMappingEntry(device) + } else { + //TODO firebase integration + OurLogger.log(Level.SEVERE, "device does not have GetGenericPortMappingEntry") } - OurLogger.log(Level.INFO, "Time to enumerate ports: $timeTakenMillis ms") } + OurLogger.log(Level.INFO, "Time to enumerate ports: $timeTakenMillis ms") + } - private fun isRuleOurs(databaseEntity : PortMappingEntity?, device: IIGDDevice, portMapping: PortMapping) : Boolean - { - if (databaseEntity == null) - { + private fun isRuleOurs( + databaseEntity: PortMappingEntity?, + device: IIGDDevice, + portMapping: PortMapping + ): Boolean { + if (databaseEntity == null) { return false - } - else - { - if (databaseEntity.description == portMapping.Description && - databaseEntity.internalIp == portMapping.InternalIP && - databaseEntity.internalPort == portMapping.InternalPort) - { - return true - } + } else { + return databaseEntity.description == portMapping.Description && + databaseEntity.internalIp == portMapping.InternalIP && + databaseEntity.internalPort == portMapping.InternalPort // rule has been created / changed on the router side and no longer matches // what we had before. it now belongs to the router, do not take control of it. - return false } } - // Had previously tried GetListOfPortMappings but it would encounter error more than 100 ports - private suspend fun getAllPortMappingsUsingGenericPortMappingEntry(device : IIGDDevice) { - var slotIndex : Int = 0; - var retryCount : Int = 0 - while(true) - { - var shouldRetry : Boolean = false - var success : Boolean = false; - try { - //future.get() // SYNCHRONOUS (note this can, and does, throw) - val result = upnpClient.getGenericPortMappingRule(device, slotIndex) - when (result) { - is UPnPGetSpecificMappingResult.Success -> { - val portMapping = result.resultingMapping - val entity = portMappingDao.getByPrimaryKey(device.getIpAddress(), device.udn, portMapping.Protocol, portMapping.ExternalPort) - var pref : PortMappingPref? = null - if(isRuleOurs(entity, device, portMapping)) - { - OurLogger.log(Level.INFO, "The rule is ours: ${portMapping.shortName()}") - pref = entity!!.getPrefs() - } - else - { - OurLogger.log(Level.INFO, "The rule is not ours: ${portMapping.shortName()}") - } - addOrUpdateMapping(PortMappingWithPref(portMapping, pref)) //!! - success = true - OurLogger.log(Level.INFO, portMapping.toStringFull()) - retryCount = 0 - - } - is UPnPGetSpecificMappingResult.Failure -> { - OurLogger.log(Level.INFO, "GetGenericPortMapping failed for entry $slotIndex: ${result.reason}. NOTE: This is normal.") + // Had previously tried GetListOfPortMappings but it would encounter error more than 100 ports + private suspend fun getAllPortMappingsUsingGenericPortMappingEntry(device: IIGDDevice) { + var slotIndex: Int = 0 + var retryCount: Int = 0 + while (true) { + var shouldRetry: Boolean = false + var success: Boolean = false + try { + //future.get() // SYNCHRONOUS (note this can, and does, throw) + val result = upnpClient.getGenericPortMappingRule(device, slotIndex) + when (result) { + is UPnPGetSpecificMappingResult.Success -> { + val portMapping = result.resultingMapping + val entity = portMappingDao.getByPrimaryKey( + device.getIpAddress(), + device.udn, + portMapping.Protocol, + portMapping.ExternalPort + ) + var pref: PortMappingPref? = null + if (isRuleOurs(entity, device, portMapping)) { + OurLogger.log( + Level.INFO, + "The rule is ours: ${portMapping.shortName()}" + ) + pref = entity!!.getPrefs() + } else { + OurLogger.log( + Level.INFO, + "The rule is not ours: ${portMapping.shortName()}" + ) } + addOrUpdateMapping(PortMappingWithPref(portMapping, pref)) //!! + success = true + OurLogger.log(Level.INFO, portMapping.toStringFull()) + retryCount = 0 + } - } - catch(e : Exception) - { - if(retryCount == 0) // if two exceptions in a row then stop. - { - shouldRetry = true // network issue.. retrying does work. - retryCount++ + + is UPnPGetSpecificMappingResult.Failure -> { + OurLogger.log( + Level.INFO, + "GetGenericPortMapping failed for entry $slotIndex: ${result.reason}. NOTE: This is normal." + ) } - OurLogger.log(Level.SEVERE, "GetGenericPortMapping threw ${e.message}") } - - if(shouldRetry) + } catch (e: Exception) { + if (retryCount == 0) // if two exceptions in a row then stop. { - OurLogger.log(Level.INFO, "Retrying for entry $slotIndex") - continue + shouldRetry = true // network issue.. retrying does work. + retryCount++ } + OurLogger.log(Level.SEVERE, "GetGenericPortMapping threw ${e.message}") + } + if (shouldRetry) { + OurLogger.log(Level.INFO, "Retrying for entry $slotIndex") + continue + } - if (!success) - { - break - } - slotIndex++ + if (!success) { + break + } - if (slotIndex > 65535) - { - OurLogger.log(Level.SEVERE, "CRITICAL ERROR ENUMERATING PORTS, made it past 65535") - } + slotIndex++ + + if (slotIndex > 65535) { + OurLogger.log(Level.SEVERE, "CRITICAL ERROR ENUMERATING PORTS, made it past 65535") } } + } - // endregion + // endregion - //fun List + //fun List - private fun getEnabledDisabledString(enabled: Boolean): String { - return if (enabled) "enabled" else "disabled" - } + private fun getEnabledDisabledString(enabled: Boolean): String { + return if (enabled) "enabled" else "disabled" + } - //region DataUpdate + //region DataUpdate - private fun clearOldData() { - upnpClient.clearOldDevices() - _devices.update { listOf() } - //_portMappings.update {TreeSet(SharedPrefValues.SortByPortMapping.getComparer(SharedPrefValues.Ascending))} - _portMappings.update { mapOf() } - } + private fun clearOldData() { + upnpClient.clearOldDevices() + _devices.update { listOf() } + //_portMappings.update {TreeSet(SharedPrefValues.SortByPortMapping.getComparer(SharedPrefValues.Ascending))} + _portMappings.update { mapOf() } + } - private fun removeForDelete(result : UPnPResult, portMappingWithPref: PortMappingWithPref) { + private fun removeForDelete(result: UPnPResult, portMappingWithPref: PortMappingWithPref) { - println("default adding rule callback") - if (result is UPnPResult.Success) - { - removeMapping(portMappingWithPref) - } + println("default adding rule callback") + if (result is UPnPResult.Success) { + removeMapping(portMappingWithPref) } + } - private fun addDevice(igdDevice: IIGDDevice) { - add(igdDevice) - Log.i("portmapperUI", "IGD device added") - OurLogger.log( - Level.INFO, - "Added Device ${igdDevice.getDisplayName()} at ${igdDevice.getIpAddress()}." - ) - } + private fun addDevice(igdDevice: IIGDDevice) { + add(igdDevice) + Log.i("portmapperUI", "IGD device added") + OurLogger.log( + Level.INFO, + "Added Device ${igdDevice.getDisplayName()} at ${igdDevice.getIpAddress()}." + ) + } - //endregion + //endregion - fun portMappingsFromIds(selectedIds : Set) : List - { + fun portMappingsFromIds(selectedIds: Set): List { val portMappingWithPrefList = mutableListOf() - for(id in selectedIds) - { - if (portMappings.value.containsKey(id)) - { + for (id in selectedIds) { + if (portMappings.value.containsKey(id)) { portMappingWithPrefList.add(portMappings.value[id]!!) - } - else - { - OurLogger.log(Level.SEVERE, "Cannot find port mapping with key $id"); + } else { + OurLogger.log(Level.SEVERE, "Cannot find port mapping with key $id") } } @@ -790,24 +803,63 @@ class UpnpManager @Inject constructor(private val upnpClient : IUpnpClient, priv // this is the request we give the router. basically a port mapping minus time read and psuedo slot -data class PortMappingRequest(val description : String, val internalIp : String, val internalPort : String, val externalIp : String, val externalPort : String, val protocol : String, val leaseDuration : String, val enabled : Boolean, val remoteHost : String) -{ - fun realize() : PortMapping - { - return PortMapping(description, remoteHost, internalIp, externalPort.toInt(), internalPort.toInt(), protocol, enabled, leaseDuration.toInt(), externalIp, SystemClock.elapsedRealtime(), GetPsuedoSlot()) +data class PortMappingRequest( + val description: String, + val internalIp: String, + val internalPort: String, + val externalIp: String, + val externalPort: String, + val protocol: String, + val leaseDuration: String, + val enabled: Boolean, + val remoteHost: String +) { + fun realize(): PortMapping { + return PortMapping( + description, + remoteHost, + internalIp, + externalPort.toInt(), + internalPort.toInt(), + protocol, + enabled, + leaseDuration.toInt(), + externalIp, + SystemClock.elapsedRealtime(), + GetPsuedoSlot() + ) } companion object { - fun from(portMapping: PortMapping, desiredLeaseDuration: Int) : PortMappingRequest - { - return PortMappingRequest(portMapping.Description, portMapping.InternalIP, portMapping.InternalPort.toString(), portMapping.DeviceIP, portMapping.ExternalPort.toString(), portMapping.Protocol, desiredLeaseDuration.toString(), portMapping.Enabled, portMapping.RemoteHost) + fun from(portMapping: PortMapping, desiredLeaseDuration: Int): PortMappingRequest { + return PortMappingRequest( + portMapping.Description, + portMapping.InternalIP, + portMapping.InternalPort.toString(), + portMapping.DeviceIP, + portMapping.ExternalPort.toString(), + portMapping.Protocol, + desiredLeaseDuration.toString(), + portMapping.Enabled, + portMapping.RemoteHost + ) } - fun from(portMapping: PortMapping) : PortMappingRequest - { - return PortMappingRequest(portMapping.Description, portMapping.InternalIP, portMapping.InternalPort.toString(), portMapping.DeviceIP, portMapping.ExternalPort.toString(), portMapping.Protocol, portMapping.LeaseDuration.toString(), portMapping.Enabled, portMapping.RemoteHost) + + fun from(portMapping: PortMapping): PortMappingRequest { + return PortMappingRequest( + portMapping.Description, + portMapping.InternalIP, + portMapping.InternalPort.toString(), + portMapping.DeviceIP, + portMapping.ExternalPort.toString(), + portMapping.Protocol, + portMapping.LeaseDuration.toString(), + portMapping.Enabled, + portMapping.RemoteHost + ) } - fun from(portMapping: PortMappingWithPref) : PortMappingRequest - { + + fun from(portMapping: PortMappingWithPref): PortMappingRequest { return from(portMapping.portMapping, portMapping.getDesiredLeaseDurationOrDefault()) } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt b/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt index f331a3e..3f90e5a 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt @@ -2,12 +2,10 @@ package com.shinjiindustrial.portmapper.client import com.shinjiindustrial.portmapper.PortMappingRequest import com.shinjiindustrial.portmapper.common.Event -import com.shinjiindustrial.portmapper.domain.IGDDevice import com.shinjiindustrial.portmapper.domain.IIGDDevice import com.shinjiindustrial.portmapper.domain.NetworkInterfaceInfo import com.shinjiindustrial.portmapper.domain.PortMapping import org.fourthline.cling.model.message.UpnpResponse -import java.util.concurrent.Future // Wrapper around cling provided upnp service // allows us to better mock interactions @@ -18,7 +16,7 @@ interface IUpnpClient { suspend fun createPortMappingRule( device: IIGDDevice, portMappingRequest: PortMappingRequest, - ) : UPnPCreateMappingResult + ): UPnPCreateMappingResult suspend fun deletePortMapping( device: IIGDDevice, @@ -30,11 +28,12 @@ interface IUpnpClient { remoteHost: String, remotePort: String, protocol: String, - ) : UPnPCreateMappingWrapperResult + ): UPnPCreateMappingWrapperResult - suspend fun getGenericPortMappingRule(device : IIGDDevice, - slotIndex : Int, - ) : UPnPGetSpecificMappingResult + suspend fun getGenericPortMappingRule( + device: IIGDDevice, + slotIndex: Int, + ): UPnPGetSpecificMappingResult fun search(maxSeconds: Int) @@ -49,26 +48,31 @@ interface IUpnpClient { val deviceFoundEvent: Event } -sealed class UPnPCreateMappingWrapperResult -{ - data class Success(val requestInfo: PortMapping, val resultingMapping: PortMapping, val wasReadBack: Boolean) : UPnPCreateMappingWrapperResult() - data class Failure(val reason: String, val response: UpnpResponse) : UPnPCreateMappingWrapperResult() +sealed class UPnPCreateMappingWrapperResult { + data class Success( + val requestInfo: PortMapping, + val resultingMapping: PortMapping, + val wasReadBack: Boolean + ) : UPnPCreateMappingWrapperResult() + + data class Failure(val reason: String, val response: UpnpResponse) : + UPnPCreateMappingWrapperResult() } -sealed class UPnPCreateMappingResult -{ +sealed class UPnPCreateMappingResult { data class Success(val requestInfo: PortMapping) : UPnPCreateMappingResult() data class Failure(val reason: String, val response: UpnpResponse) : UPnPCreateMappingResult() } -sealed class UPnPGetSpecificMappingResult -{ - data class Success(val requestInfo: PortMapping, val resultingMapping: PortMapping) : UPnPGetSpecificMappingResult() - data class Failure(val reason: String, val response: UpnpResponse) : UPnPGetSpecificMappingResult() +sealed class UPnPGetSpecificMappingResult { + data class Success(val requestInfo: PortMapping, val resultingMapping: PortMapping) : + UPnPGetSpecificMappingResult() + + data class Failure(val reason: String, val response: UpnpResponse) : + UPnPGetSpecificMappingResult() } -sealed class UPnPResult -{ +sealed class UPnPResult { data class Success(val requestInfo: PortMapping) : UPnPResult() data class Failure(val reason: String, val response: UpnpResponse) : UPnPResult() } \ No newline at end of file diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/common/CommonUtil.kt b/app/src/main/java/com/shinjiindustrial/portmapper/common/CommonUtil.kt index f6ae4a6..26645e6 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/common/CommonUtil.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/common/CommonUtil.kt @@ -5,14 +5,10 @@ import com.shinjiindustrial.portmapper.toIntOrMaxValue // v1 - max is ui4 maxvalue (which is 100+ years, so just cap at signed int4 max) // v2 - max is 1 week (604800) -fun capLeaseDur(leaseDurString : String, v1: Boolean) : String -{ - return if(v1) - { +fun capLeaseDur(leaseDurString: String, v1: Boolean): String { + return if (v1) { leaseDurString.toIntOrMaxValue().toString() - } - else - { + } else { val leaseInt = leaseDurString.toIntOrMaxValue() minOf(leaseInt, 604800).toString() } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/common/PortSorting.kt b/app/src/main/java/com/shinjiindustrial/portmapper/common/PortSorting.kt index fd0d127..e4659a1 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/common/PortSorting.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/common/PortSorting.kt @@ -1,21 +1,19 @@ package com.shinjiindustrial.portmapper.common import com.shinjiindustrial.portmapper.ComparerWrapper -import com.shinjiindustrial.portmapper.PortMapperComparatorBase import com.shinjiindustrial.portmapper.PortMapperComparatorDescription import com.shinjiindustrial.portmapper.PortMapperComparatorDevice import com.shinjiindustrial.portmapper.PortMapperComparatorExpiration import com.shinjiindustrial.portmapper.PortMapperComparatorExternalPort import com.shinjiindustrial.portmapper.PortMapperComparatorInternalPort import com.shinjiindustrial.portmapper.PortMapperComparerSlot -import com.shinjiindustrial.portmapper.domain.PortMapping import com.shinjiindustrial.portmapper.domain.PortMappingWithPref // TODO organize re other portmappingsort file -data class SortInfo(val sortBy : SortBy, val sortDesc : Boolean) +data class SortInfo(val sortBy: SortBy, val sortDesc: Boolean) -enum class SortBy(val sortByValue : Int) { +enum class SortBy(val sortByValue: Int) { Slot(0), Description(1), InternalPort(2), @@ -27,29 +25,27 @@ enum class SortBy(val sortByValue : Int) { fun from(findValue: Int): SortBy = SortBy.values().first { it.sortByValue == findValue } } - fun getName() : String - { - return if(this == InternalPort) { + fun getName(): String { + return if (this == InternalPort) { "Internal Port" - } else if(this == ExternalPort) { + } else if (this == ExternalPort) { "External Port" } else { name } } - fun getShortName() : String - { - return if(this == InternalPort) { + fun getShortName(): String { + return if (this == InternalPort) { "Int. Port" - } else if(this == ExternalPort) { + } else if (this == ExternalPort) { "Ext. Port" } else { name } } - fun getComparer(ascending : Boolean): Comparator { + fun getComparer(ascending: Boolean): Comparator { return ComparerWrapper( when (this) { Slot -> PortMapperComparerSlot(ascending) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/common/Validators.kt b/app/src/main/java/com/shinjiindustrial/portmapper/common/Validators.kt index 880912c..0825a68 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/common/Validators.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/common/Validators.kt @@ -1,6 +1,5 @@ package com.shinjiindustrial.portmapper.common -import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.example.myapplication.R @@ -8,8 +7,8 @@ import com.shinjiindustrial.portmapper.toIntOrMaxValue data class ValidationResult( val hasError: Boolean, - val validationError: ValidationError) -{ + val validationError: ValidationError +) { companion object { val ok = ValidationResult(false, ValidationError.NONE) fun error(error: ValidationError) = ValidationResult(true, error) @@ -23,6 +22,7 @@ fun ValidationError.toMessage(): String { ValidationError.EMPTY_PORT -> stringResource(R.string.error_empty) ValidationError.INVALID_PORT_RANGE -> stringResource(R.string.error_invalid_port_range, MIN_PORT, MAX_PORT) + ValidationError.END_BEFORE_START -> stringResource(R.string.error_end_before_start) ValidationError.NONE -> "" ValidationError.INVALID_INTERNAL_IP -> stringResource(R.string.error_invalid_ip_address) @@ -38,38 +38,30 @@ enum class ValidationError { INVALID_INTERNAL_IP } -fun validateDescription(description : String) : ValidationResult -{ - if (description.isEmpty()) - { +fun validateDescription(description: String): ValidationResult { + if (description.isEmpty()) { return ValidationResult.error(ValidationError.EMPTY_DESCRIPTION) } return ValidationResult.ok } -fun validateStartPort(startPort : String) : ValidationResult -{ - if (startPort.isEmpty()) - { +fun validateStartPort(startPort: String): ValidationResult { + if (startPort.isEmpty()) { return ValidationResult.error(ValidationError.EMPTY_PORT) } val portInt = startPort.toIntOrMaxValue() - if (portInt < MIN_PORT || portInt > MAX_PORT) - { + if (portInt < MIN_PORT || portInt > MAX_PORT) { return ValidationResult.error(ValidationError.INVALID_PORT_RANGE) } return ValidationResult.ok } -fun validateEndPort(startPort : String, endPort : String) : ValidationResult -{ - if (startPort.isEmpty()) - { +fun validateEndPort(startPort: String, endPort: String): ValidationResult { + if (startPort.isEmpty()) { // let them deal with that error first return ValidationResult.ok } - if (endPort.isEmpty()) - { + if (endPort.isEmpty()) { //valid return ValidationResult.ok } @@ -77,22 +69,22 @@ fun validateEndPort(startPort : String, endPort : String) : ValidationResult val startPortInt = startPort.toIntOrMaxValue() val endPortInt = endPort.toIntOrMaxValue() - if (endPortInt < MIN_PORT || endPortInt > MAX_PORT) - { + if (endPortInt < MIN_PORT || endPortInt > MAX_PORT) { return ValidationResult.error(ValidationError.INVALID_PORT_RANGE) } - if (startPortInt > endPortInt) - { + if (startPortInt > endPortInt) { return ValidationResult.error(ValidationError.END_BEFORE_START) } return ValidationResult.ok } -fun validateInternalIp(ip : String) : ValidationResult -{ - val regexIPv4 = """^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$""".toRegex() - return if(regexIPv4.matches(ip)) ValidationResult.ok else ValidationResult.error(ValidationError.INVALID_INTERNAL_IP) +fun validateInternalIp(ip: String): ValidationResult { + val regexIPv4 = + """^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$""".toRegex() + return if (regexIPv4.matches(ip)) ValidationResult.ok else ValidationResult.error( + ValidationError.INVALID_INTERNAL_IP + ) } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/AndroidUpnpServiceConfiguration.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/AndroidUpnpServiceConfiguration.kt index 44926dd..3213dfc 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/AndroidUpnpServiceConfiguration.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/AndroidUpnpServiceConfiguration.kt @@ -8,9 +8,10 @@ import org.fourthline.cling.binding.xml.ServiceDescriptorBinder import org.fourthline.cling.binding.xml.UDA10ServiceDescriptorBinderImpl import org.fourthline.cling.transport.spi.NetworkAddressFactory -class AndroidUpnpServiceConfigurationImpl(context : Context) : AndroidUpnpServiceConfiguration() { +class AndroidUpnpServiceConfigurationImpl(context: Context) : AndroidUpnpServiceConfiguration() { + + var Context: Context - var Context : Context init { Context = context } @@ -40,17 +41,24 @@ class AndroidUpnpServiceConfigurationImpl(context : Context) : AndroidUpnpServic NetworkMappings = OurNetworkInfo.GetNameTypeMappings(Context) NetworkInterfacesUsedInfos = mutableListOf() - for (netInterface in networkInterfaces) - { - NetworkInterfacesUsedInfos.add(NetworkInterfaceInfo(netInterface, OurNetworkInfo.GetTypeFromInterfaceName(NetworkMappings, netInterface.name))) + for (netInterface in networkInterfaces) { + NetworkInterfacesUsedInfos.add( + NetworkInterfaceInfo( + netInterface, + OurNetworkInfo.GetTypeFromInterfaceName(NetworkMappings, netInterface.name) + ) + ) } return addressFactory } - private var NetworkInterfacesUsed : MutableList? = null - private var NetworkMappings : MutableMap? = null - var NetworkInterfacesUsedInfos : MutableList = mutableListOf() + private var NetworkInterfacesUsed: MutableList? = null + private var NetworkMappings: MutableMap? = null + var NetworkInterfacesUsedInfos: MutableList = mutableListOf() } -data class NetworkInterfaceInfo(val networkInterface : java.net.NetworkInterface, val networkType : NetworkType) \ No newline at end of file +data class NetworkInterfaceInfo( + val networkInterface: java.net.NetworkInterface, + val networkType: NetworkType +) \ No newline at end of file diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt index 10d33a9..2cbf6b1 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt @@ -6,26 +6,27 @@ import org.fourthline.cling.model.meta.RemoteDevice import org.fourthline.cling.model.meta.RemoteService interface IIGDDevice { - fun getDisplayName() : String - fun getIpAddress() : String - fun getUpnpVersion() : Int - fun supportsAction(actionName : String) : Boolean - fun getActionInvocation(actionName : String) : ActionInvocation<*> - fun getDeviceSignature() : String - val udn : String + fun getDisplayName(): String + fun getIpAddress(): String + fun getUpnpVersion(): Int + fun supportsAction(actionName: String): Boolean + fun getActionInvocation(actionName: String): ActionInvocation<*> + fun getDeviceSignature(): String + val udn: String } // has state including rules. add update those rules. and do a upnp action. and sort. -class IGDDevice constructor(private val rootDevice : RemoteDevice, private val wanIPService : RemoteService) : IIGDDevice -{ +class IGDDevice(private val rootDevice: RemoteDevice, private val wanIPService: RemoteService) : + IIGDDevice { // nullrefs warning: this is SSDP response, very possible for some fields to be null, DeviceDetails constructor allows it. // the best bet on ip is (rootDevice!!.identity.descriptorURL.host) imo. since that is what we use in RetreiveRemoteDescriptors class // which then calls remoteDeviceAdded (us). presentationURI can be and is null sometimes. - private val displayName : String = this.rootDevice.displayString //i.e. Nokia IGD Version 2.00 - private val ipAddress : String = rootDevice.identity.descriptorURL.host //i.e. 192.168.18.1 + private val displayName: String = this.rootDevice.displayString //i.e. Nokia IGD Version 2.00 + private val ipAddress: String = rootDevice.identity.descriptorURL.host //i.e. 192.168.18.1 + //this.rootDevice!!.details.presentationURI.host - private val upnpTypeVersion : Int = this.rootDevice.type.version //i.e. 2 - private val actionsMap : MutableMap> = mutableMapOf() + private val upnpTypeVersion: Int = this.rootDevice.type.version //i.e. 2 + private val actionsMap: MutableMap> = mutableMapOf() override val udn = rootDevice.root.identity.udn.toString() //private val upnpType : String = this.rootDevice.type.type //i.e. InternetGatewayDevice @@ -35,19 +36,19 @@ class IGDDevice constructor(private val rootDevice : RemoteDevice, private val w // private val manufacturer : String = // this.rootDevice.details.manufacturerDetails?.manufacturer ?: "" //i.e. Nokia - override fun getUpnpVersion() : Int { + override fun getUpnpVersion(): Int { return upnpTypeVersion } - override fun getDisplayName() : String { + override fun getDisplayName(): String { return displayName } - override fun getIpAddress() : String { + override fun getIpAddress(): String { return ipAddress } - override fun supportsAction(actionName : String) : Boolean { + override fun supportsAction(actionName: String): Boolean { return this.actionsMap.containsKey(actionName) } @@ -57,15 +58,13 @@ class IGDDevice constructor(private val rootDevice : RemoteDevice, private val w } override fun getDeviceSignature(): String { - val action = this.actionsMap[""] + this.actionsMap[""] return "" } init { - for (action in wanIPService.actions) - { - if(ActionNames.contains(action.name)) - { + for (action in wanIPService.actions) { + if (ActionNames.contains(action.name)) { // are easy to call and parse output actionsMap[action.name] = action } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/LocalNetworkInfo.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/LocalNetworkInfo.kt index 2678f3c..0686ee6 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/LocalNetworkInfo.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/LocalNetworkInfo.kt @@ -11,7 +11,11 @@ import com.shinjiindustrial.portmapper.PortForwardApplication import com.shinjiindustrial.portmapper.common.NetworkType import com.shinjiindustrial.portmapper.formatIpv4 -data class OurNetworkInfoBundle(val networkType: NetworkType, val ourIp: String?, val gatewayIp : String?) +data class OurNetworkInfoBundle( + val networkType: NetworkType, + val ourIp: String?, + val gatewayIp: String? +) class OurNetworkInfo { companion object { //singleton @@ -83,14 +87,13 @@ class OurNetworkInfo { // return null } - fun GetNameTypeMappings(context: Context) : MutableMap { + fun GetNameTypeMappings(context: Context): MutableMap { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val mappings : MutableMap = mutableMapOf() - for (net1 in cm.getAllNetworks()) { + val mappings: MutableMap = mutableMapOf() + for (net1 in cm.allNetworks) { val netInfo = cm.getNetworkInfo(net1) val name = cm.getLinkProperties(net1)?.interfaceName - if(name == null) - { + if (name == null) { continue } val type = getNetworkType(cm, net1, netInfo) @@ -99,25 +102,22 @@ class OurNetworkInfo { return mappings } - fun GetTypeFromInterfaceName(_mappings : MutableMap?, interfaceName : String) : NetworkType - { + fun GetTypeFromInterfaceName( + _mappings: MutableMap?, + interfaceName: String + ): NetworkType { var mappings = _mappings - if (mappings == null) - { - mappings = GetNameTypeMappings(PortForwardApplication.appContext) //TODO: dont call this expensive call everytime + if (mappings == null) { + mappings = + GetNameTypeMappings(PortForwardApplication.appContext) //TODO: dont call this expensive call everytime } mappings - if (mappings.containsKey(interfaceName)) - { + if (mappings.containsKey(interfaceName)) { return mappings[interfaceName] ?: NetworkType.NONE - } - else { - if (interfaceName.contains("wlan")) - { + } else { + if (interfaceName.contains("wlan")) { return NetworkType.WIFI - } - else if(interfaceName.contains("rmnet") || interfaceName.contains("data")) - { + } else if (interfaceName.contains("rmnet") || interfaceName.contains("data")) { return NetworkType.DATA } } @@ -135,18 +135,19 @@ class OurNetworkInfo { //get type - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { networkType = getNetworkType(cm, cm.activeNetwork, cm.activeNetworkInfo) - } - else - { + } else { networkType = getNetworkType(cm, null, cm.activeNetworkInfo) } return networkType as NetworkType } - fun getNetworkType(cm: ConnectivityManager, network : Network?, networkInfo : NetworkInfo?) : NetworkType { + fun getNetworkType( + cm: ConnectivityManager, + network: Network?, + networkInfo: NetworkInfo? + ): NetworkType { var result = NetworkType.NONE if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { cm.getNetworkCapabilities(network)?.run { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt index 4fbcd2c..8931eaf 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt @@ -6,27 +6,24 @@ import com.shinjiindustrial.portmapper.PortForwardApplication import com.shinjiindustrial.portmapper.persistence.PortMappingEntity import kotlinx.parcelize.Parcelize -data class PortMappingWithPref(val portMapping: PortMapping, val portMappingPref: PortMappingPref? = null) -{ - fun getKey() : PortMappingKey - { +data class PortMappingWithPref( + val portMapping: PortMapping, + val portMappingPref: PortMappingPref? = null +) { + fun getKey(): PortMappingKey { return portMapping.getKey() } - fun getAutoRenewOrDefault() : Boolean - { + fun getAutoRenewOrDefault(): Boolean { return portMappingPref?.autoRenew ?: false } - fun getDesiredLeaseDurationOrDefault() : Int - { + fun getDesiredLeaseDurationOrDefault(): Int { return portMappingPref?.desiredLeaseDuration ?: portMapping.LeaseDuration } } -data class PortMappingPref(val autoRenew : Boolean, val desiredLeaseDuration: Int) { - -} +data class PortMappingPref(val autoRenew: Boolean, val desiredLeaseDuration: Int) @Parcelize data class PortMappingKey(val deviceIP: String, val externalPort: Int, val protocol: String) : @@ -35,18 +32,18 @@ data class PortMappingKey(val deviceIP: String, val externalPort: Int, val proto // this is what is on the router // with additional field for timeRead and psuedoSlot data class PortMapping( - val Description : String, - val RemoteHost : String, - val InternalIP : String, - val ExternalPort : Int, - val InternalPort : Int, - val Protocol : String, - val Enabled : Boolean, - val LeaseDuration : Int, - val DeviceIP : String, - val TimeReadLeaseDurationMs : Long, - val Slot : Int) -{ + val Description: String, + val RemoteHost: String, + val InternalIP: String, + val ExternalPort: Int, + val InternalPort: Int, + val Protocol: String, + val Enabled: Boolean, + val LeaseDuration: Int, + val DeviceIP: String, + val TimeReadLeaseDurationMs: Long, + val Slot: Int +) { // the returned ip from get port mapping // https://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v2-Service.pdf page 17 // (this is an optional filter for another device on the network that is trying to reach you.) @@ -76,66 +73,53 @@ data class PortMapping( fun getRemoteHostNormalizedForDelete(): String = this.RemoteHost.ifBlank { "*" } - fun getKey() : PortMappingKey - { + fun getKey(): PortMappingKey { return PortMappingKey(this.DeviceIP, this.ExternalPort, this.Protocol) } - fun shortName() : String - { - return formatShortName(Protocol,DeviceIP,ExternalPort.toString()) + fun shortName(): String { + return formatShortName(Protocol, DeviceIP, ExternalPort.toString()) } - fun getRemainingLeaseTime(now : Long = -1) : Int - { - val secondsPassed = ((if(now == -1L) SystemClock.elapsedRealtime() else now) - TimeReadLeaseDurationMs)/1000L + fun getRemainingLeaseTime(now: Long = -1): Int { + val secondsPassed = + ((if (now == -1L) SystemClock.elapsedRealtime() else now) - TimeReadLeaseDurationMs) / 1000L val timeToExpiration = (LeaseDuration.toLong() - secondsPassed) return timeToExpiration.toInt() } - fun getExpiresTimeMillis() : Long { - return TimeReadLeaseDurationMs + LeaseDuration.toLong() * 1000; + fun getExpiresTimeMillis(): Long { + return TimeReadLeaseDurationMs + LeaseDuration.toLong() * 1000 } - fun getRemainingLeaseTimeRoughString(autoRenew : Boolean, now: Long = -1) : String - { - if (this.LeaseDuration == 0) - { + fun getRemainingLeaseTimeRoughString(autoRenew: Boolean, now: Long = -1): String { + if (this.LeaseDuration == 0) { return "Expires Never" } val totalSecs = getRemainingLeaseTime(now) val dhms = getDHMS(totalSecs) - val renewsExpiresString = if(autoRenew) "Renews in" else "Expires in" + val renewsExpiresString = if (autoRenew) "Renews in" else "Expires in" val renewStringTime = roundOneUnit(dhms) - if (totalSecs < 60) - { + if (totalSecs < 60) { if (autoRenew) { - if (totalSecs < PortForwardApplication.RENEW_RULE_WITHIN_X_SECONDS_OF_EXPIRING + 10) - { + if (totalSecs < PortForwardApplication.RENEW_RULE_WITHIN_X_SECONDS_OF_EXPIRING + 10) { return "Renewing now" - } - else - { + } else { return "Renewing <1 minute" } - } - else { + } else { return "$renewsExpiresString <1 minute" } - } - else - { + } else { return "$renewsExpiresString $renewStringTime" } } - fun getRemainingLeaseTimeString(now: Long = -1) : String - { - if (this.LeaseDuration == 0) - { + fun getRemainingLeaseTimeString(now: Long = -1): String { + if (this.LeaseDuration == 0) { return "Never" } // show only 2 units (i.e. days and hours. or hours and minutes. or minutes and seconds. or just seconds) @@ -150,27 +134,21 @@ data class PortMapping( } } - fun getUrgency(autoRenew : Boolean, now : Long = -1) : Urgency - { - if (autoRenew) - { + fun getUrgency(autoRenew: Boolean, now: Long = -1): Urgency { + if (autoRenew) { return Urgency.Normal } val totalSecs = getRemainingLeaseTime(now) val totalMinutes = totalSecs / 60 - if (totalMinutes <= 1) - { + if (totalMinutes <= 1) { return Urgency.Error - } - else if (totalMinutes <= 5) - { + } else if (totalMinutes <= 5) { return Urgency.Warn } return Urgency.Normal } - fun toStringFull() : String - { + fun toStringFull(): String { return "PortMapping(RemoteHost=$RemoteHost, ActualExternalIP=$DeviceIP, InternalIP=$InternalIP, ExternalPort=$ExternalPort, InternalPort=$InternalPort, Protocol=$Protocol, Enabled=$Enabled, LeaseDuration=$LeaseDuration, Description=$Description, TimeReadLeaseDurationMs=$TimeReadLeaseDurationMs, Slot=$Slot)" } } @@ -181,25 +159,21 @@ enum class Urgency { Error, } -data class DayHourMinSec(val days : Int, val hours : Int, val minutes : Int, val seconds : Int) -{ - fun totalSeconds() : Int - { +data class DayHourMinSec(val days: Int, val hours: Int, val minutes: Int, val seconds: Int) { + fun totalSeconds(): Int { return days * 3600 * 24 + hours * 3600 + minutes * 60 + seconds } } -fun getDHMS(totalSeconds : Int) : DayHourMinSec -{ - val days = totalSeconds / (24*3600) - val hours = (totalSeconds % (24*3600)) / 3600 - val mins = (totalSeconds % (3600)) / 60 +fun getDHMS(totalSeconds: Int): DayHourMinSec { + val days = totalSeconds / (24 * 3600) + val hours = (totalSeconds % (24 * 3600)) / 3600 + val mins = (totalSeconds % (3600)) / 60 val secs = (totalSeconds % (60)) return DayHourMinSec(days, hours, mins, secs) } -fun getPlural(value : Int) : String -{ +fun getPlural(value: Int): String { return if (value > 1) "s" else "" } @@ -216,6 +190,7 @@ fun roundOneUnit(time: DayHourMinSec): String { } "$days day${getPlural(days)}" } + hours > 0 -> { val remainderSec = minutes * 60 + seconds if (remainderSec >= 1800) { @@ -226,6 +201,7 @@ fun roundOneUnit(time: DayHourMinSec): String { } return "$hours hour${getPlural(hours)}" } + minutes > 0 -> { if (seconds >= 30) { minutes += 1 @@ -236,6 +212,7 @@ fun roundOneUnit(time: DayHourMinSec): String { return "$minutes minute${getPlural(minutes)}" } + else -> "$seconds second${getPlural(seconds)}" } } @@ -250,27 +227,34 @@ fun roundTwoUnits(time: DayHourMinSec): String { val remainderSec = minutes * 60 + seconds if (remainderSec >= 30 * 60) { hours += 1 - if (hours == 24) { days += 1; hours = 0 } + if (hours == 24) { + days += 1; hours = 0 + } } "$days day${getPlural(days)}, $hours hour${getPlural(hours)}" } + hours > 0 -> { if (seconds >= 30) { minutes += 1 - if (minutes == 60) { hours += 1; minutes = 0 } + if (minutes == 60) { + hours += 1; minutes = 0 + } } return "$hours hour${getPlural(hours)}, $minutes minute${getPlural(minutes)}" } + minutes > 0 -> { return "$minutes minute${getPlural(minutes)}, $seconds second${getPlural(seconds)}" } + else -> "$seconds second${getPlural(seconds)}" } } -fun formatShortName(protocol: String, externalIp: String, externalPort: String) : String -{ +fun formatShortName(protocol: String, externalIp: String, externalPort: String): String { return "$protocol rule at $externalIp:$externalPort" } -fun PortMappingEntity.getPrefs(): PortMappingPref = PortMappingPref(this.autoRenew, this.desiredLeaseDuration) \ No newline at end of file +fun PortMappingEntity.getPrefs(): PortMappingPref = + PortMappingPref(this.autoRenew, this.desiredLeaseDuration) \ No newline at end of file diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMappingUserInput.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMappingUserInput.kt index 94d9e62..0ccd454 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMappingUserInput.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMappingUserInput.kt @@ -5,15 +5,36 @@ import com.shinjiindustrial.portmapper.Protocol import com.shinjiindustrial.portmapper.toIntOrMaxValue // this is the information the user gives us to create a rule. i.e. what the router needs + any preference info (autorenew) -data class PortMappingUserInput(val description : String, val internalIp : String, val internalRange : String, val externalIp : String, val externalRange : String, val protocol : String, val leaseDuration : String, val enabled : Boolean, val autoRenew : Boolean) -{ - fun requestWith(internalPortSpecified : String, externalPortSpecified : String, portocolSpecified : String) : PortMappingRequest - { - return PortMappingRequest(description, internalIp, internalPortSpecified, externalIp, externalPortSpecified, portocolSpecified, leaseDuration, enabled, "") +data class PortMappingUserInput( + val description: String, + val internalIp: String, + val internalRange: String, + val externalIp: String, + val externalRange: String, + val protocol: String, + val leaseDuration: String, + val enabled: Boolean, + val autoRenew: Boolean +) { + fun requestWith( + internalPortSpecified: String, + externalPortSpecified: String, + portocolSpecified: String + ): PortMappingRequest { + return PortMappingRequest( + description, + internalIp, + internalPortSpecified, + externalIp, + externalPortSpecified, + portocolSpecified, + leaseDuration, + enabled, + "" + ) } - fun validateRange() : String - { + fun validateRange(): String { // many 1 to 1 makes sense // many different external to 1 internal // 1 external to many internal - this isnt a thing and it causes a contradiction with upnp port retrieval api @@ -24,32 +45,24 @@ data class PortMappingUserInput(val description : String, val internalIp : Strin val inSize = inEnd - inStart + 1 // inclusive val outSize = outEnd - outStart + 1 val manyToOne = (inSize == 1) // many out to 1 in - if (inSize != outSize && !manyToOne) - { + if (inSize != outSize && !manyToOne) { return "Internal and External Ranges do not match up." - } - else - { + } else { return "" } } - fun getRange(internal : Boolean) : Pair - { - val rangeInQuestion = if(internal) internalRange else externalRange - if(rangeInQuestion.contains('-')) - { + fun getRange(internal: Boolean): Pair { + val rangeInQuestion = if (internal) internalRange else externalRange + if (rangeInQuestion.contains('-')) { val inRange = rangeInQuestion.split('-') return Pair(inRange[0].toInt(), inRange[1].toInt()) - } - else - { + } else { return Pair(rangeInQuestion.toInt(), rangeInQuestion.toInt()) } } - fun getProtocols() : List - { + fun getProtocols(): List { return when (protocol) { Protocol.BOTH.str() -> listOf("TCP", "UDP") else -> listOf(protocol) @@ -87,9 +100,8 @@ data class PortMappingUserInput(val description : String, val internalIp : Strin return portMappingRequests } - fun validateAutoRenew() : String { - if (this.autoRenew && this.leaseDuration.toIntOrMaxValue() != 0 && this.leaseDuration.toIntOrMaxValue() < 90) - { + fun validateAutoRenew(): String { + if (this.autoRenew && this.leaseDuration.toIntOrMaxValue() != 0 && this.leaseDuration.toIntOrMaxValue() < 90) { return "If auto renew is enabled, lease duration must at least be 90 seconds" } return "" diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/UpnpViewRow.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/UpnpViewRow.kt index 7c4a7fc..f6dad74 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/UpnpViewRow.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/UpnpViewRow.kt @@ -7,22 +7,32 @@ import kotlinx.parcelize.Parcelize sealed class ViewKey : Parcelable { // TODO include slot? what does it mean for 2 ports to be the same since we can edit them? @Parcelize - data class PortViewKey(val externalPort: Int, val protocol: String, val deviceIp : String) : ViewKey() + data class PortViewKey(val externalPort: Int, val protocol: String, val deviceIp: String) : + ViewKey() + @Parcelize - data class DeviceHeaderKey(val deviceIp : String) : ViewKey() + data class DeviceHeaderKey(val deviceIp: String) : ViewKey() + @Parcelize - data class DeviceEmptyKey(val deviceIp : String) : ViewKey() + data class DeviceEmptyKey(val deviceIp: String) : ViewKey() } sealed class UpnpViewRow { - abstract val key : ViewKey - data class PortViewRow constructor(val portMapping: PortMappingWithPref) : UpnpViewRow() { - override val key = ViewKey.PortViewKey(portMapping.portMapping.ExternalPort, portMapping.portMapping.Protocol, portMapping.portMapping.DeviceIP) + abstract val key: ViewKey + + data class PortViewRow(val portMapping: PortMappingWithPref) : UpnpViewRow() { + override val key = ViewKey.PortViewKey( + portMapping.portMapping.ExternalPort, + portMapping.portMapping.Protocol, + portMapping.portMapping.DeviceIP + ) } - data class DeviceHeaderViewRow constructor(val device: IIGDDevice) : UpnpViewRow() { + + data class DeviceHeaderViewRow(val device: IIGDDevice) : UpnpViewRow() { override val key = ViewKey.DeviceHeaderKey(device.getIpAddress()) } - data class DeviceEmptyViewRow constructor(val device: IIGDDevice) : UpnpViewRow() { + + data class DeviceEmptyViewRow(val device: IIGDDevice) : UpnpViewRow() { override val key = ViewKey.DeviceEmptyKey(device.getIpAddress()) } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/persistence/PortMapperPersistence.kt b/app/src/main/java/com/shinjiindustrial/portmapper/persistence/PortMapperPersistence.kt index 55c1310..06ecf31 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/persistence/PortMapperPersistence.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/persistence/PortMapperPersistence.kt @@ -16,7 +16,10 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton -@Entity(tableName = "port_mappings", primaryKeys = ["deviceIp", "deviceSignature", "protocol", "externalPort"]) +@Entity( + tableName = "port_mappings", + primaryKeys = ["deviceIp", "deviceSignature", "protocol", "externalPort"] +) data class PortMappingEntity( // uniquely identifies a rule @@ -44,14 +47,16 @@ interface PortMappingDao { @Query("SELECT * FROM port_mappings") suspend fun getAll(): List - @Query(""" + @Query( + """ SELECT * FROM port_mappings WHERE deviceIp = :deviceIp AND deviceSignature = :deviceSignature AND protocol = :protocol AND externalPort = :externalPort LIMIT 1 - """) + """ + ) suspend fun getByPrimaryKey( deviceIp: String, deviceSignature: String, @@ -62,14 +67,21 @@ interface PortMappingDao { @Upsert suspend fun upsert(entity: PortMappingEntity) - @Query(""" + @Query( + """ DELETE FROM port_mappings WHERE deviceIp = :deviceIp AND deviceSignature = :deviceSignature AND externalPort = :externalPort AND protocol = :protocol - """) - suspend fun deleteByKey(deviceIp: String, deviceSignature: String, protocol: String, externalPort: Int): Int + """ + ) + suspend fun deleteByKey( + deviceIp: String, + deviceSignature: String, + protocol: String, + externalPort: Int + ): Int } @Database(entities = [PortMappingEntity::class], version = 1, exportSchema = true) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/CommonUI.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/CommonUI.kt index 9d328cf..1d82a04 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/CommonUI.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/CommonUI.kt @@ -31,33 +31,44 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable -fun CircleCheckbox(selected: Boolean, enabled: Boolean = true, modifier : Modifier = Modifier, onChecked: () -> Unit) { +fun CircleCheckbox( + selected: Boolean, + enabled: Boolean = true, + modifier: Modifier = Modifier, + onChecked: () -> Unit +) { val color = MaterialTheme.colorScheme val imageVector = if (selected) Icons.Filled.CheckCircle else Icons.Outlined.Circle - val tint = if (selected) color.primary.copy(alpha = 0.8f) else Color.White.copy(alpha = 0.8f) // colorScheme.primary.copy(alpha=.8f) + val tint = + if (selected) color.primary.copy(alpha = 0.8f) else Color.White.copy(alpha = 0.8f) // colorScheme.primary.copy(alpha=.8f) val background = if (selected) Color.White else Color.Transparent - IconButton(onClick = { }, - enabled = enabled, modifier = modifier.then(Modifier.size(28.dp))) { + IconButton( + onClick = { }, + enabled = enabled, modifier = modifier.then(Modifier.size(28.dp)) + ) { - Icon(imageVector = imageVector, tint = tint, - modifier = Modifier.background(background, shape = CircleShape).size(28.dp), - contentDescription = "checkbox") + Icon( + imageVector = imageVector, tint = tint, + modifier = Modifier + .background(background, shape = CircleShape) + .size(28.dp), + contentDescription = "checkbox" + ) } } @Preview @Composable -fun CircleCheckboxPreview() -{ - val selected = remember { mutableStateOf(false)} +fun CircleCheckboxPreview() { + val selected = remember { mutableStateOf(false) } CircleCheckbox(selected.value, true) { selected.value = !selected.value } } @ExperimentalMaterial3Api @Composable -fun SortSelectButton(modifier : Modifier, text: String, isSelected: Boolean, onClick: () -> Unit) { +fun SortSelectButton(modifier: Modifier, text: String, isSelected: Boolean, onClick: () -> Unit) { val buttonColor: Color val textColor: Color if (isSelected) { @@ -72,7 +83,8 @@ fun SortSelectButton(modifier : Modifier, text: String, isSelected: Boolean, onC modifier = modifier.then( Modifier .height(60.dp) - .padding(6.dp)), + .padding(6.dp) + ), colors = CardDefaults.cardColors(containerColor = buttonColor), shape = RoundedCornerShape(14.dp), onClick = onClick diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/DurationPicker.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/DurationPicker.kt index 62d14a2..bc98851 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/DurationPicker.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/DurationPicker.kt @@ -60,8 +60,7 @@ import java.com.shinjiindustrial.portmapper.ThemeUiState @Composable @Preview -fun DurationPickerDialogPreview() -{ +fun DurationPickerDialogPreview() { SetupPreview() MyApplicationTheme(ThemeUiState(DayNightMode.FORCE_NIGHT, false)) { val showDialog = remember { mutableStateOf(true) } @@ -71,33 +70,50 @@ fun DurationPickerDialogPreview() } @Composable -fun DurationPickerDialog(showDialog : MutableState, leaseDurationValueSeconds : MutableState, wanIpConnectionV1 : Boolean) -{ +fun DurationPickerDialog( + showDialog: MutableState, + leaseDurationValueSeconds: MutableState, + wanIpConnectionV1: Boolean +) { val chosenValue = remember { mutableStateOf("") } - Dialog(onDismissRequest = { showDialog.value = false } ) { + Dialog(onDismissRequest = { showDialog.value = false }) { // Use Surface to apply elevation Surface(shape = MaterialTheme.shapes.medium, shadowElevation = 24.dp) { Column(modifier = Modifier.padding(0.dp)) { - Text("Duration", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(18.dp, 12.dp)) - - Divider(modifier = Modifier - .height(2.dp) - .fillMaxWidth()) + Text( + "Duration", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(18.dp, 12.dp) + ) + + Divider( + modifier = Modifier + .height(2.dp) + .fillMaxWidth() + ) Spacer(modifier = Modifier.height(10.dp)) PickerRow(leaseDurationValueSeconds.value, chosenValue, wanIpConnectionV1) - Divider(modifier = Modifier - .height(2.dp) - .fillMaxWidth()) + Divider( + modifier = Modifier + .height(2.dp) + .fillMaxWidth() + ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End ) { TextButton(onClick = { showDialog.value = false }) { - Text("Cancel", fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(4.dp, 9.dp, 4.dp, 9.dp)) + Text( + "Cancel", + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(4.dp, 9.dp, 4.dp, 9.dp) + ) } TextButton(onClick = { @@ -105,7 +121,12 @@ fun DurationPickerDialog(showDialog : MutableState, leaseDurationValueS leaseDurationValueSeconds.value = capLeaseDur(chosenValue.value, wanIpConnectionV1) }) { - Text("Set", fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(4.dp, 9.dp, 4.dp, 9.dp)) + Text( + "Set", + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(4.dp, 9.dp, 4.dp, 9.dp) + ) } Spacer(modifier = Modifier.width(8.dp)) @@ -116,25 +137,29 @@ fun DurationPickerDialog(showDialog : MutableState, leaseDurationValueS } @Composable -fun rememberPickerState(initialValue : Int) : PickerState -{ +fun rememberPickerState(initialValue: Int): PickerState { return remember { PickerState(initialValue) } } @Composable -fun PickerRow(initialSeconds : String, chosenValue : MutableState, wanIpConnectionV1 : Boolean) { +fun PickerRow( + initialSeconds: String, + chosenValue: MutableState, + wanIpConnectionV1: Boolean +) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - val initSeconds = if(initialSeconds.isBlank()) 0 else initialSeconds.replace(" (max)", "").toInt() + val initSeconds = + if (initialSeconds.isBlank()) 0 else initialSeconds.replace(" (max)", "").toInt() val dhms = getDHMS(initSeconds) val infiniteScroll = true - val maxDays = if(wanIpConnectionV1) 365 else 7 + val maxDays = if (wanIpConnectionV1) 365 else 7 val dayValues = remember { getStringRange(0, maxDays, infiniteScroll) } val dayValuesPickerState = rememberPickerState(dhms.days) @@ -145,16 +170,18 @@ fun PickerRow(initialSeconds : String, chosenValue : MutableState, wanIp val minsValues = remember { getStringRange(0, 59, infiniteScroll) } val minsValuesPickerState = rememberPickerState(dhms.minutes) - val secsValues = remember { getStringRange(0, 59, infiniteScroll) } - val secsValuesPickerState = rememberPickerState(dhms.seconds) + remember { getStringRange(0, 59, infiniteScroll) } + rememberPickerState(dhms.seconds) - Row(modifier = Modifier - .fillMaxWidth() - .height(240.dp)) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(240.dp) + ) { Picker( header = "Days", infinite = infiniteScroll, @@ -199,7 +226,7 @@ fun PickerRow(initialSeconds : String, chosenValue : MutableState, wanIp } } -class PickerState(initialValue : Int) { +class PickerState(initialValue: Int) { var selectedItem by mutableStateOf(initialValue.toString()) } @@ -218,19 +245,17 @@ fun RowScope.Picker( dividerColor: Color = AdditionalColors.TextColorWeak, ) { val visibleItemsMiddle = visibleItemsCount / 2 - val listScrollCount = remember { if(infinite) Integer.MAX_VALUE else (items.count() - 4) }//Integer.MAX_VALUE + val listScrollCount = + remember { if (infinite) Integer.MAX_VALUE else (items.count() - 4) }//Integer.MAX_VALUE val listScrollMiddle = listScrollCount / 2 - val listStartIndex = listScrollMiddle - listScrollMiddle % items.size - visibleItemsMiddle + startIndex + 1 + (if(infinite) -1 else 0) + val listStartIndex = + listScrollMiddle - listScrollMiddle % items.size - visibleItemsMiddle + startIndex + 1 + (if (infinite) -1 else 0) - fun getItem(index: Int) : String - { - if(infinite) - { + fun getItem(index: Int): String { + if (infinite) { return items[index % items.size] - } - else - { - return if(index < -1) items[0] else items[index % items.size] + } else { + return if (index < -1) items[0] else items[index % items.size] } } @@ -257,10 +282,12 @@ fun RowScope.Picker( Column(modifier.weight(1f, true)) { - Text(header, + Text( + header, modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.SemiBold) + fontWeight = FontWeight.SemiBold + ) Spacer(Modifier.height(6.dp)) @@ -276,11 +303,10 @@ fun RowScope.Picker( .fadingEdge(fadingEdgeGradient) ) { items(listScrollCount) { index -> - Log.e("",index.toString()) + Log.e("", index.toString()) var style = textStyle //var color = Color.Black - if(state.selectedItem == getItem(index)) - { + if (state.selectedItem == getItem(index)) { //color = Color.Blue } Text( @@ -330,14 +356,10 @@ private fun Modifier.fadingEdge(brush: Brush) = this @Composable private fun pixelsToDp(pixels: Int) = with(LocalDensity.current) { pixels.toDp() } -fun getStringRange(low : Int, high : Int, infinite: Boolean) : List -{ - if(infinite) - { +fun getStringRange(low: Int, high: Int, infinite: Boolean): List { + if (infinite) { return (low..high).map { it.toString() } - } - else - { - return ((low-1)..(high+5)).map { if(it < low || it > high) "" else it.toString() } + } else { + return ((low - 1)..(high + 5)).map { if (it < low || it > high) "" else it.toString() } } } \ No newline at end of file diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/MoreInfoDialog.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/MoreInfoDialog.kt index b9cddbe..66df2b4 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/MoreInfoDialog.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/MoreInfoDialog.kt @@ -16,23 +16,20 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.shinjiindustrial.portmapper._getDefaultPortMapping -import com.shinjiindustrial.portmapper.domain.PortMapping import com.shinjiindustrial.portmapper.domain.PortMappingWithPref import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors @Preview @Composable -fun MoreInfoDialog() -{ +fun MoreInfoDialog() { SetupPreview() MoreInfoDialog(_getDefaultPortMapping(), remember { mutableStateOf(true) }) } @Composable -fun MoreInfoDialog(portMappingWithPref : PortMappingWithPref, showDialog : MutableState) -{ +fun MoreInfoDialog(portMappingWithPref: PortMappingWithPref, showDialog: MutableState) { val portMapping = portMappingWithPref.portMapping - if(showDialog.value) { + if (showDialog.value) { AlertDialog( onDismissRequest = { showDialog.value = false }, title = { Text("Info") }, diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt index f14147c..9f21fa2 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt @@ -72,40 +72,46 @@ import java.com.shinjiindustrial.portmapper.ThemeUiState ExperimentalFoundationApi::class ) @Composable -fun PortMappingCard(portMappingWithPref: PortMappingWithPref, now: Long = -1, isInMultiSelectMode : Boolean = false, toggleSelection : (PortMappingKey) -> Unit = {}, selectedIds : Set, additionalModifier : Modifier = Modifier.Companion) -{ +fun PortMappingCard( + portMappingWithPref: PortMappingWithPref, + now: Long = -1, + isInMultiSelectMode: Boolean = false, + toggleSelection: (PortMappingKey) -> Unit = {}, + selectedIds: Set, + additionalModifier: Modifier = Modifier.Companion +) { val portMapping = portMappingWithPref.portMapping println("external ip test ${portMapping.DeviceIP}") - Card( + Card( // onClick = { // if(PortForwardApplication.showPopup != null) // { // PortForwardApplication.showPopup.value = true // } // }, - modifier = additionalModifier - .fillMaxWidth() - //.clip(RoundedCornerShape(borderRadius)) - .padding(4.dp, 4.dp) + modifier = additionalModifier + .fillMaxWidth() + //.clip(RoundedCornerShape(borderRadius)) + .padding(4.dp, 4.dp) // .background(MaterialTheme.colorScheme.secondaryContainer) - .combinedClickable( - onClick = { - - if (isInMultiSelectMode) { - toggleSelection(portMappingWithPref.getKey()) - } else { - // TODO cleanup - PortForwardApplication.Companion.showContextMenu.value = true - PortForwardApplication.Companion.currentSingleSelectedObject.value = - portMappingWithPref - } - - }, - onLongClick = { + .combinedClickable( + onClick = { + + if (isInMultiSelectMode) { toggleSelection(portMappingWithPref.getKey()) + } else { + // TODO cleanup + PortForwardApplication.Companion.showContextMenu.value = true + PortForwardApplication.Companion.currentSingleSelectedObject.value = + portMappingWithPref } - ), + + }, + onLongClick = { + toggleSelection(portMappingWithPref.getKey()) + } + ), // Snackbar @@ -115,30 +121,30 @@ fun PortMappingCard(portMappingWithPref: PortMappingWithPref, now: Long = -1, is // }) // .setActionTextColor(getResources().getColor(R.color.holo_red_light)) // .show() - //isRound = !isRound + //isRound = !isRound - elevation = CardDefaults.cardElevation(), - border = BorderStroke(1.dp, AdditionalColors.SubtleBorder), - colors = CardDefaults.cardColors( - containerColor = AdditionalColors.CardContainerColor, - ), - ) { + elevation = CardDefaults.cardElevation(), + border = BorderStroke(1.dp, AdditionalColors.SubtleBorder), + colors = CardDefaults.cardColors( + containerColor = AdditionalColors.CardContainerColor, + ), + ) { - Row( - modifier = Modifier.Companion - .padding(2.dp, 6.dp, 15.dp, 6.dp),//.background(Color(0xffc5dceb)), - //.background(MaterialTheme.colorScheme.secondaryContainer), - verticalAlignment = Alignment.Companion.CenterVertically + Row( + modifier = Modifier.Companion + .padding(2.dp, 6.dp, 15.dp, 6.dp),//.background(Color(0xffc5dceb)), + //.background(MaterialTheme.colorScheme.secondaryContainer), + verticalAlignment = Alignment.Companion.CenterVertically - ) { + ) { - //var padLeft = if(multiSelectMode) 5.dp else 13.dp + //var padLeft = if(multiSelectMode) 5.dp else 13.dp - //TODO need to do existing content slide to right and fade new element in + //TODO need to do existing content slide to right and fade new element in - val padLeft = 13.dp - //val transition = updateTransition(multiSelectMode, label = "transition") + val padLeft = 13.dp + //val transition = updateTransition(multiSelectMode, label = "transition") // val offsetState = remember { MutableTransitionState(initialState = false) } // val visState = remember { MutableTransitionState(initialState = false) } @@ -166,59 +172,72 @@ fun PortMappingCard(portMappingWithPref: PortMappingWithPref, now: Long = -1, is // // offsetState.targetState = multiSelectMode - AnimatedVisibility( - visible = isInMultiSelectMode, - ) { + AnimatedVisibility( + visible = isInMultiSelectMode, + ) { - CircleCheckbox( - selectedIds.contains(portMappingWithPref.getKey()), - true, - Modifier.Companion.padding(10.dp, 0.dp, 2.dp, 0.dp) - ) { - toggleSelection(portMappingWithPref.getKey()) - } + CircleCheckbox( + selectedIds.contains(portMappingWithPref.getKey()), + true, + Modifier.Companion.padding(10.dp, 0.dp, 2.dp, 0.dp) + ) { + toggleSelection(portMappingWithPref.getKey()) } + } - Column( - modifier = Modifier.Companion.weight(1f).padding(padLeft, 0.dp, 0.dp, 0.dp) - ) { - Text( - portMapping.Description, - fontSize = TextUnit(20f, TextUnitType.Companion.Sp), - fontWeight = FontWeight.Companion.SemiBold, - color = AdditionalColors.TextColor - ) - Text(portMapping.InternalIP, color = AdditionalColors.TextColor) - - val text = buildAnnotatedString { - withStyle(style = SpanStyle(color = if (portMapping.Enabled) AdditionalColors.Enabled_Green else AdditionalColors.Disabled_Red)) { - append("⬤") - } - withStyle(style = SpanStyle()) { - append(if (portMapping.Enabled) " Enabled" else " Disabled") - } + Column( + modifier = Modifier.Companion + .weight(1f) + .padding(padLeft, 0.dp, 0.dp, 0.dp) + ) { + Text( + portMapping.Description, + fontSize = TextUnit(20f, TextUnitType.Companion.Sp), + fontWeight = FontWeight.Companion.SemiBold, + color = AdditionalColors.TextColor + ) + Text(portMapping.InternalIP, color = AdditionalColors.TextColor) + + val text = buildAnnotatedString { + withStyle(style = SpanStyle(color = if (portMapping.Enabled) AdditionalColors.Enabled_Green else AdditionalColors.Disabled_Red)) { + append("⬤") } + withStyle(style = SpanStyle()) { + append(if (portMapping.Enabled) " Enabled" else " Disabled") + } + } - Text(text, color = AdditionalColors.TextColor) - val urgency = portMapping.getUrgency(portMappingWithPref.getAutoRenewOrDefault(), now) - val color by urgencyColor(urgency, AdditionalColors.TextColor, AdditionalColors.LogWarningText, AdditionalColors.LogErrorText) - Text(portMapping.getRemainingLeaseTimeRoughString(portMappingWithPref.getAutoRenewOrDefault(), now), color = color) - } + Text(text, color = AdditionalColors.TextColor) + val urgency = + portMapping.getUrgency(portMappingWithPref.getAutoRenewOrDefault(), now) + val color by urgencyColor( + urgency, + AdditionalColors.TextColor, + AdditionalColors.LogWarningText, + AdditionalColors.LogErrorText + ) + Text( + portMapping.getRemainingLeaseTimeRoughString( + portMappingWithPref.getAutoRenewOrDefault(), + now + ), color = color + ) + } - Column(horizontalAlignment = Alignment.Companion.CenterHorizontally) { - Text( - "${portMapping.ExternalPort} ➝ ${portMapping.InternalPort}", - fontSize = TextUnit(20f, TextUnitType.Companion.Sp), - fontWeight = FontWeight.Companion.SemiBold, - color = AdditionalColors.TextColor - ) - Text("${portMapping.Protocol}", color = AdditionalColors.TextColor) + Column(horizontalAlignment = Alignment.Companion.CenterHorizontally) { + Text( + "${portMapping.ExternalPort} ➝ ${portMapping.InternalPort}", + fontSize = TextUnit(20f, TextUnitType.Companion.Sp), + fontWeight = FontWeight.Companion.SemiBold, + color = AdditionalColors.TextColor + ) + Text("${portMapping.Protocol}", color = AdditionalColors.TextColor) - } } } + } } @Composable @@ -230,9 +249,9 @@ fun urgencyColor( expired: Color = Color.Gray ) = animateColorAsState( when (urgency) { - Urgency.Normal -> normal - Urgency.Warn -> warn - Urgency.Error -> error + Urgency.Normal -> normal + Urgency.Warn -> warn + Urgency.Error -> error } ) @@ -287,8 +306,7 @@ fun PortMappingCardAltPreview() // TODO: rename? @OptIn(ExperimentalUnitApi::class) @Composable -fun PortMappingCardAlt(portMapping: PortMapping) -{ +fun PortMappingCardAlt(portMapping: PortMapping) { Card( modifier = Modifier.Companion .fillMaxWidth() @@ -344,16 +362,14 @@ fun PortMappingCardAlt(portMapping: PortMapping) @Preview @Composable -fun PreviewDeviceHeader() -{ +fun PreviewDeviceHeader() { //TODO //DeviceHeader(IGDDevice(null, null)) } @OptIn(ExperimentalUnitApi::class) @Composable -fun DeviceHeader(device : IIGDDevice) -{ +fun DeviceHeader(device: IIGDDevice) { Spacer(modifier = Modifier.Companion.padding(2.dp)) Column( modifier = Modifier.Companion @@ -380,14 +396,12 @@ fun DeviceHeader(device : IIGDDevice) @Preview @Composable -fun LoadingIcon() -{ +fun LoadingIcon() { LoadingIcon("Searching for devices", Modifier.Companion) } @Composable -fun LoadingIcon(label : String, modifier : Modifier) -{ +fun LoadingIcon(label: String, modifier: Modifier) { Column(modifier = modifier.fillMaxWidth()) { CircularProgressIndicator( modifier = Modifier.Companion @@ -404,14 +418,13 @@ fun LoadingIcon(label : String, modifier : Modifier) } @Composable -fun NoMappingsCard(remoteDevice : IIGDDevice) { +fun NoMappingsCard(remoteDevice: IIGDDevice) { NoMappingsCard() } @Preview @Composable -fun NoMappingsCard() -{ +fun NoMappingsCard() { MyApplicationTheme(ThemeUiState(DayNightMode.FORCE_NIGHT, false)) { Card( // onClick = { @@ -521,6 +534,7 @@ fun ScaffoldDemo() { }, ) } + @Preview @Composable fun PreviewConversation() { @@ -529,8 +543,7 @@ fun PreviewConversation() { val msgs = mutableListOf() val pm = _getDefaultPortMapping() val upnpViewEl = UpnpViewRow.PortViewRow(pm) - for (i in 0..20) - { + for (i in 0..20) { msgs.add(upnpViewEl) } PortMappingsListContent(msgs, false, {}, emptySet()) @@ -538,8 +551,12 @@ fun PreviewConversation() { } @Composable -fun PortMappingContent(uiState : PortUiState, isInMultiSelectMode : Boolean, onToggle : (PortMappingKey) -> Unit, selectedIds : Set) -{ +fun PortMappingContent( + uiState: PortUiState, + isInMultiSelectMode: Boolean, + onToggle: (PortMappingKey) -> Unit, + selectedIds: Set +) { PortMappingsListContent(uiState.items, isInMultiSelectMode, onToggle, selectedIds) } @@ -559,7 +576,12 @@ fun rememberTicker(periodMillis: Long): androidx.compose.runtime.State { //lazy column IS recycler view basically. both recycle. @OptIn(ExperimentalFoundationApi::class, ExperimentalUnitApi::class) @Composable -fun PortMappingsListContent(messages: List, isInMultiSelectMode : Boolean, onToggle : (PortMappingKey) -> Unit, selectedIds : Set) { +fun PortMappingsListContent( + messages: List, + isInMultiSelectMode: Boolean, + onToggle: (PortMappingKey) -> Unit, + selectedIds: Set +) { val now by rememberTicker(8_000) @@ -574,17 +596,28 @@ fun PortMappingsListContent(messages: List, isInMultiSelectMode : B ) { - itemsIndexed(messages, key = { index, message -> message.key }) { index, message -> //, key = {indexIt, keyIt -> keyIt.hashCode() } + itemsIndexed( + messages, + key = { index, message -> message.key }) { index, message -> //, key = {indexIt, keyIt -> keyIt.hashCode() } - when(message) { + when (message) { is UpnpViewRow.DeviceHeaderViewRow -> { DeviceHeader(message.device) } + is UpnpViewRow.DeviceEmptyViewRow -> { NoMappingsCard(message.device) } + is UpnpViewRow.PortViewRow -> { - PortMappingCard(message.portMapping, now, isInMultiSelectMode, onToggle,selectedIds, Modifier.animateItem()) + PortMappingCard( + message.portMapping, + now, + isInMultiSelectMode, + onToggle, + selectedIds, + Modifier.animateItem() + ) } } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PreviewUtil.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PreviewUtil.kt index dc7aca7..da2f674 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PreviewUtil.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PreviewUtil.kt @@ -10,8 +10,7 @@ import com.shinjiindustrial.portmapper.SharedPrefValues @Composable @Preview -fun SetupPreview() -{ +fun SetupPreview() { SharedPrefValues.DayNightPref = DayNightMode.FORCE_NIGHT SharedPrefValues.MaterialYouTheme = false PortForwardApplication.appContext = LocalContext.current diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt index ad9c6b9..ec28874 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt @@ -26,15 +26,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController import com.shinjiindustrial.portmapper.CreateRuleContents import com.shinjiindustrial.portmapper.MainActivity import com.shinjiindustrial.portmapper.MainActivity.Companion.OurSnackbarHostState import com.shinjiindustrial.portmapper.PortForwardApplication import com.shinjiindustrial.portmapper.Protocol -import com.shinjiindustrial.portmapper.UpnpManager import com.shinjiindustrial.portmapper.common.validateDescription import com.shinjiindustrial.portmapper.common.validateEndPort import com.shinjiindustrial.portmapper.common.validateInternalIp @@ -52,8 +49,7 @@ import java.util.logging.Level @Composable @Preview -fun RadioGroupPreview2() -{ +fun RadioGroupPreview2() { SetupPreview() Text("test") } @@ -67,7 +63,11 @@ fun PreviewRuleCreationDialog() { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun RuleCreationDialog(navController : NavHostController, portViewModel : PortViewModel, ruleToEdit : PortMappingUserInput? = null) { +fun RuleCreationDialog( + navController: NavHostController, + portViewModel: PortViewModel, + ruleToEdit: PortMappingUserInput? = null +) { // TODO vm.collect() @@ -75,18 +75,36 @@ fun RuleCreationDialog(navController : NavHostController, portViewModel : PortVi val isPreview = false val hasSubmitted = remember { mutableStateOf(false) } - val internalPortText = rememberSaveable { mutableStateOf(ruleToEdit?.internalRange ?: "") } - val internalPortTextEnd = rememberSaveable { mutableStateOf("") } - val externalPortText = rememberSaveable { mutableStateOf(ruleToEdit?.externalRange ?: "") } - val externalPortTextEnd = rememberSaveable { mutableStateOf("") } + val internalPortText = rememberSaveable { mutableStateOf(ruleToEdit?.internalRange ?: "") } + val internalPortTextEnd = rememberSaveable { mutableStateOf("") } + val externalPortText = rememberSaveable { mutableStateOf(ruleToEdit?.externalRange ?: "") } + val externalPortTextEnd = rememberSaveable { mutableStateOf("") } val leaseDuration = rememberSaveable { mutableStateOf(ruleToEdit?.leaseDuration ?: "0 (max)") } val description = rememberSaveable { mutableStateOf(ruleToEdit?.description ?: "") } - val descriptionHasError = remember { mutableStateOf(validateDescription(description.value).hasError) } - val startInternalHasError = remember { mutableStateOf(validateStartPort(internalPortText.value).hasError) } - val endInternalHasError = remember { mutableStateOf(validateEndPort(internalPortText.value,internalPortTextEnd.value).hasError) } - val selectedProtocolMutable = remember { mutableStateOf(ruleToEdit?.protocol ?: Protocol.TCP.str()) } - val startExternalHasError = remember { mutableStateOf(validateStartPort(externalPortText.value).hasError) } - val endExternalHasError = remember { mutableStateOf(validateEndPort(externalPortText.value,externalPortTextEnd.value).hasError) } + val descriptionHasError = + remember { mutableStateOf(validateDescription(description.value).hasError) } + val startInternalHasError = + remember { mutableStateOf(validateStartPort(internalPortText.value).hasError) } + val endInternalHasError = remember { + mutableStateOf( + validateEndPort( + internalPortText.value, + internalPortTextEnd.value + ).hasError + ) + } + val selectedProtocolMutable = + remember { mutableStateOf(ruleToEdit?.protocol ?: Protocol.TCP.str()) } + val startExternalHasError = + remember { mutableStateOf(validateStartPort(externalPortText.value).hasError) } + val endExternalHasError = remember { + mutableStateOf( + validateEndPort( + externalPortText.value, + externalPortTextEnd.value + ).hasError + ) + } val autoRenew = remember { mutableStateOf(ruleToEdit?.autoRenew ?: false) } val (ourIp, ourGatewayIp) = remember { if (isPreview) Pair( @@ -98,15 +116,22 @@ fun RuleCreationDialog(navController : NavHostController, portViewModel : PortVi ) } val internalIp = remember { mutableStateOf(ruleToEdit?.internalIp ?: ourIp!!) } - val internalIpHasError = remember { mutableStateOf(validateInternalIp(internalIp.value).hasError) } - val (gatewayIps, defaultGatewayIp) = remember { portViewModel.GetGatewayIpsWithDefault(ourGatewayIp!!) } + val internalIpHasError = + remember { mutableStateOf(validateInternalIp(internalIp.value).hasError) } + val (gatewayIps, defaultGatewayIp) = remember { + portViewModel.GetGatewayIpsWithDefault( + ourGatewayIp!! + ) + } val externalDeviceText = remember { mutableStateOf(defaultGatewayIp) } val expandedInternal = remember { mutableStateOf(false) } val expandedExternal = remember { mutableStateOf(false) } - val wanIpVersionOfGatewayIsVersion1 = remember { derivedStateOf { - val version = portViewModel.getIGDDevice(defaultGatewayIp).getUpnpVersion() - version == 1 - } } + val wanIpVersionOfGatewayIsVersion1 = remember { + derivedStateOf { + val version = portViewModel.getIGDDevice(defaultGatewayIp).getUpnpVersion() + version == 1 + } + } // TODO @@ -119,15 +144,14 @@ fun RuleCreationDialog(navController : NavHostController, portViewModel : PortVi //END // this can be null as its possible to start at this navhost - if(OurSnackbarHostState == null) - { - MainActivity.OurSnackbarHostState = remember { SnackbarHostState() } + if (OurSnackbarHostState == null) { + OurSnackbarHostState = remember { SnackbarHostState() } } Scaffold( snackbarHost = { - SnackbarHost(MainActivity.OurSnackbarHostState!!) { data -> + SnackbarHost(OurSnackbarHostState!!) { data -> // custom snackbar with the custom colors Snackbar( data, @@ -156,7 +180,7 @@ fun RuleCreationDialog(navController : NavHostController, portViewModel : PortVi //colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.secondary),// change the height here title = { Text( - text = if(ruleToEdit == null) "New Rule" else "Edit Rule", + text = if (ruleToEdit == null) "New Rule" else "Edit Rule", color = AdditionalColors.TextColorStrong, fontWeight = FontWeight.Normal ) @@ -165,11 +189,15 @@ fun RuleCreationDialog(navController : NavHostController, portViewModel : PortVi TextButton( onClick = { - val actualEndInternalError = expandedInternal.value && endInternalHasError.value - val actualEndExternalError = expandedExternal.value && endExternalHasError.value + val actualEndInternalError = + expandedInternal.value && endInternalHasError.value + val actualEndExternalError = + expandedExternal.value && endExternalHasError.value - val usingInternalRange = expandedInternal.value && internalPortTextEnd.value.isNotEmpty() - val usingExternalRange = expandedExternal.value && externalPortTextEnd.value.isNotEmpty() + val usingInternalRange = + expandedInternal.value && internalPortTextEnd.value.isNotEmpty() + val usingExternalRange = + expandedExternal.value && externalPortTextEnd.value.isNotEmpty() hasSubmitted.value = true if (descriptionHasError.value || @@ -182,28 +210,22 @@ fun RuleCreationDialog(navController : NavHostController, portViewModel : PortVi // show toast and return // Invalid Description, ExternalPort. val invalidFields = mutableListOf() - if(descriptionHasError.value) - { + if (descriptionHasError.value) { invalidFields.add("Description") } - if(startInternalHasError.value) - { - invalidFields.add(if(usingInternalRange) "Internal Port Start" else "Internal Port") + if (startInternalHasError.value) { + invalidFields.add(if (usingInternalRange) "Internal Port Start" else "Internal Port") } - if(startExternalHasError.value) - { - invalidFields.add(if(usingExternalRange) "External Port Start" else "External Port") + if (startExternalHasError.value) { + invalidFields.add(if (usingExternalRange) "External Port Start" else "External Port") } - if(actualEndInternalError) - { + if (actualEndInternalError) { invalidFields.add("Internal Port End") } - if(actualEndExternalError) - { + if (actualEndExternalError) { invalidFields.add("External Port End") } - if(internalIpHasError.value) - { + if (internalIpHasError.value) { invalidFields.add("Internal IP") } @@ -213,8 +235,10 @@ fun RuleCreationDialog(navController : NavHostController, portViewModel : PortVi return@TextButton } - val internalRangeStr = if(usingInternalRange) internalPortText.value + "-" + internalPortTextEnd.value else internalPortText.value - val externalRangeStr = if(usingExternalRange) externalPortText.value + "-" + externalPortTextEnd.value else externalPortText.value + val internalRangeStr = + if (usingInternalRange) internalPortText.value + "-" + internalPortTextEnd.value else internalPortText.value + val externalRangeStr = + if (usingExternalRange) externalPortText.value + "-" + externalPortTextEnd.value else externalPortText.value val portMappingRequestInput = PortMappingUserInput( description.value, @@ -223,21 +247,19 @@ fun RuleCreationDialog(navController : NavHostController, portViewModel : PortVi externalDeviceText.value, externalRangeStr, selectedProtocolMutable.value, - leaseDuration.value.replace(" (max)",""), + leaseDuration.value.replace(" (max)", ""), true, autoRenew.value ) var errorString = portMappingRequestInput.validateAutoRenew() - if(errorString.isNotEmpty()) - { + if (errorString.isNotEmpty()) { MainActivity.showSnackBarLongNoAction(errorString) return@TextButton } errorString = portMappingRequestInput.validateRange() - if(errorString.isNotEmpty()) - { + if (errorString.isNotEmpty()) { MainActivity.showSnackBarLongNoAction(errorString) return@TextButton } @@ -247,36 +269,33 @@ fun RuleCreationDialog(navController : NavHostController, portViewModel : PortVi GlobalScope.launch(Dispatchers.Main) { - if(ruleToEdit != null) - { - if(ruleToEdit == portMappingRequestInput) - { - PortForwardApplication.OurLogger.log( - Level.INFO, - "Rule has not changed. Nothing to do." - ) - navController.popBackStack() - } - else - { - val oldRulesToDelete = ruleToEdit.splitIntoRules() - portViewModel.editRule( - PortMappingWithPref( oldRulesToDelete[0].realize(), null), + if (ruleToEdit != null) { + if (ruleToEdit == portMappingRequestInput) { + PortForwardApplication.OurLogger.log( + Level.INFO, + "Rule has not changed. Nothing to do." + ) + navController.popBackStack() + } else { + val oldRulesToDelete = ruleToEdit.splitIntoRules() + portViewModel.editRule( + PortMappingWithPref( + oldRulesToDelete[0].realize(), + null + ), portMappingRequestInput ) + navController.popBackStack() + } + } else { + portViewModel.createRules(portMappingRequestInput) navController.popBackStack() } } - else - { - portViewModel.createRules(portMappingRequestInput) - navController.popBackStack() - } - } } ) { Text( - text = if(ruleToEdit == null) "CREATE" else "APPLY", + text = if (ruleToEdit == null) "CREATE" else "APPLY", color = AdditionalColors.TextColorStrong, style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold) ) @@ -286,9 +305,11 @@ fun RuleCreationDialog(navController : NavHostController, portViewModel : PortVi }, content = { it -> - Column(modifier = Modifier - .padding(it) - .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) + Column( + modifier = Modifier + .padding(it) + .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally + ) { CreateRuleContents( hasSubmitted, diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/SortPanel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/SortPanel.kt index 2986378..bc8445a 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/SortPanel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/SortPanel.kt @@ -14,8 +14,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.shinjiindustrial.portmapper.PortForwardApplication -import com.shinjiindustrial.portmapper.SharedPrefValues import com.shinjiindustrial.portmapper.common.SortBy import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import java.com.shinjiindustrial.portmapper.PortViewModel @@ -73,7 +71,7 @@ fun BottomSheetSortBy(portViewModel: PortViewModel) { val ascendingButton = j == 0 SortSelectButton( modifier = Modifier.weight(1.0f), - text = if(ascendingButton) "Ascending" else "Descending",//FilterField.from(i).name, + text = if (ascendingButton) "Ascending" else "Descending",//FilterField.from(i).name, isSelected = ascendingButton == !desc, onClick = { portViewModel.updateSortingDesc(!ascendingButton) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/theme/Theme.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/theme/Theme.kt index 8d6da37..f62cdc7 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/theme/Theme.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/theme/Theme.kt @@ -12,23 +12,19 @@ import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp - import androidx.core.view.WindowCompat import com.shinjiindustrial.portmapper.DayNightMode -import com.shinjiindustrial.portmapper.SharedPrefValues import java.com.shinjiindustrial.portmapper.ThemeUiState -fun getBlend(first : Color, second : Color, ratio : Double) : Color -{ - val newRed = first.red.toDouble() * (1f-ratio) + second.red.toDouble() * (ratio) - val newGreen = first.green.toDouble() * (1f-ratio) + second.green.toDouble() * (ratio) - val newBlue = first.blue.toDouble() * (1f-ratio) + second.blue.toDouble() * (ratio) +fun getBlend(first: Color, second: Color, ratio: Double): Color { + val newRed = first.red.toDouble() * (1f - ratio) + second.red.toDouble() * (ratio) + val newGreen = first.green.toDouble() * (1f - ratio) + second.green.toDouble() * (ratio) + val newBlue = first.blue.toDouble() * (1f - ratio) + second.blue.toDouble() * (ratio) return Color(newRed.toFloat(), newGreen.toFloat(), newBlue.toFloat(), 1f) } @@ -48,10 +44,10 @@ private val DarkColorScheme = darkColorScheme( secondaryContainer = Blue40, background = Color(0xff181C1F), -) + ) private val LightColorScheme = lightColorScheme( - primary = Blue80_Darker, //AAdditionalColors.PrimaryDarkerBlue, + primary = Blue80_Darker, //AAdditionalColors.PrimaryDarkerBlue, secondary = Blue80, //AdditionalColors.PrimaryBlue, tertiary = Pink40, // used in places where the background color isnt explicitly changed @@ -88,7 +84,6 @@ object AdditionalColors { var LogWarningText = AdditionalColorsLight.LogWarningText - interface IAdditionalColors { val Background: Color val SubtleBorder: Color @@ -100,8 +95,7 @@ object AdditionalColors { val LogWarningText: Color } - object AdditionalColorsLight : IAdditionalColors - { + object AdditionalColorsLight : IAdditionalColors { override var Background = Color(0xffF0F0F2) override var SubtleBorder = Color(0xffE2E2E4) override var CardContainerColor = Color(0xffFCFCFE) @@ -111,13 +105,12 @@ object AdditionalColors { override var TextColor = Color(0xff303037) override var TextColorWeak = Color(0xff45464F) - override var LogErrorText = Color(0xffCD0000) - override var LogWarningText = Color(0xFFAD9727) + override var LogErrorText = Color(0xffCD0000) + override var LogWarningText = Color(0xFFAD9727) } - object AdditionalColorsDark : IAdditionalColors - { + object AdditionalColorsDark : IAdditionalColors { override var Background = Color(0xff181C1F) override var SubtleBorder = Color(0xff3A3E41) override var CardContainerColor = Color(0xff2D3134) @@ -126,15 +119,13 @@ object AdditionalColors { override var TextColor = Color(0xffd8dad9) override var TextColorWeak = Color(0xffa3a8a6) - override var LogErrorText = Color(0xffCF5B56) - override var LogWarningText = Color(0xffBBB529) + override var LogErrorText = Color(0xffCF5B56) + override var LogWarningText = Color(0xffBBB529) } } - - @Composable fun MyApplicationTheme( themeState: ThemeUiState, @@ -147,14 +138,7 @@ fun MyApplicationTheme( var dayNightMode = themeState.dayNightMode var useDark = darkTheme - if(dayNightMode == DayNightMode.FORCE_DAY) - { - useDark = false - } - else - { - useDark = true - } + useDark = dayNightMode != DayNightMode.FORCE_DAY var useMaterialYou = themeState.materialYou key(useMaterialYou, dayNightMode) @@ -194,7 +178,8 @@ fun MyApplicationTheme( 4.dp ) - AdditionalColors.SubtleBorder = getBlend(colorSchemeToUse.surface, colorSchemeToUse.onSurface, 0.2) + AdditionalColors.SubtleBorder = + getBlend(colorSchemeToUse.surface, colorSchemeToUse.onSurface, 0.2) } //val colorScheme = if(colorSchemeToUse) DarkColorScheme else LightColorScheme @@ -221,11 +206,11 @@ fun MyApplicationTheme( // both clauses are the same. this is a hack to get // theme to recompose. //if (AdditionalColors.ThemeSetting.value == 0) { - MaterialTheme( - colorScheme = colorSchemeToUse, - typography = Typography, - content = content - ) + MaterialTheme( + colorScheme = colorSchemeToUse, + typography = Typography, + content = content + ) // } else { // MaterialTheme( // colorScheme = colorSchemeToUse, diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 036d09b..c9ad5f9 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 036d09b..c9ad5f9 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/no_gps/java/com/shinjiindustrial/portmapper/FirebaseConditional.kt b/app/src/no_gps/java/com/shinjiindustrial/portmapper/FirebaseConditional.kt index f1f8237..6c910fd 100644 --- a/app/src/no_gps/java/com/shinjiindustrial/portmapper/FirebaseConditional.kt +++ b/app/src/no_gps/java/com/shinjiindustrial/portmapper/FirebaseConditional.kt @@ -6,7 +6,7 @@ class FirebaseConditional { companion object { fun Initialize(context : Context) { - PortForwardApplication.crashlyticsEnabled = false; + PortForwardApplication.crashlyticsEnabled = false } } } \ No newline at end of file diff --git a/app/src/testMock/java/com/shinjiindustrial/portmapper/UPnPManagerTests.kt b/app/src/testMock/java/com/shinjiindustrial/portmapper/UPnPManagerTests.kt index b8bef56..bf1ceed 100644 --- a/app/src/testMock/java/com/shinjiindustrial/portmapper/UPnPManagerTests.kt +++ b/app/src/testMock/java/com/shinjiindustrial/portmapper/UPnPManagerTests.kt @@ -54,7 +54,6 @@ class UPnPManagerTests { fun groupRules() { // if compare returns equals then they are considered the same and will be replaced in the TreeSet - val groupByProtocol = false var groupByRange = true val portMappingsToAdd : MutableList = mutableListOf() diff --git a/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt b/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt index c40f8aa..8d4ed23 100644 --- a/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt +++ b/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt @@ -40,27 +40,27 @@ import kotlin.test.assertEquals class ValidatorsTest { @Test fun `test description validation`() { - assertFalse(validateDescription("Description is required").hasError); - assertTrue(validateDescription("").hasError); + assertFalse(validateDescription("Description is required").hasError) + assertTrue(validateDescription("").hasError) } @Test fun `test start port validation`() { - assertTrue(validateStartPort("").hasError); - assertTrue(validateStartPort("70000").hasError); - assertTrue(validateStartPort("-100").hasError); - assertFalse(validateDescription("4000").hasError); + assertTrue(validateStartPort("").hasError) + assertTrue(validateStartPort("70000").hasError) + assertTrue(validateStartPort("-100").hasError) + assertFalse(validateDescription("4000").hasError) } @Test fun `test end port validation`() { - assertFalse(validateEndPort("", "").hasError); - assertFalse(validateEndPort("1000", "").hasError); - assertFalse(validateEndPort("1000", "").hasError); - assertTrue(validateEndPort("1000", "70000").hasError); - assertTrue(validateEndPort("1000", "-100").hasError); - assertTrue(validateEndPort("1000", "100").hasError); - assertFalse(validateEndPort("4000", "4040").hasError); + assertFalse(validateEndPort("", "").hasError) + assertFalse(validateEndPort("1000", "").hasError) + assertFalse(validateEndPort("1000", "").hasError) + assertTrue(validateEndPort("1000", "70000").hasError) + assertTrue(validateEndPort("1000", "-100").hasError) + assertTrue(validateEndPort("1000", "100").hasError) + assertFalse(validateEndPort("4000", "4040").hasError) } @Test @@ -74,8 +74,7 @@ class ValidatorsTest { fun `flow map and collect`() = runBlocking { val _devices = MutableStateFlow(listOf()) // TreeSet - val devices: StateFlow> = - _devices//.map { it..sortedBy { d -> d.name } } + _devices//.map { it..sortedBy { d -> d.name } } _devices.update { it + 0 } val size_flow = _devices.map { it.size }.onEach { println("size is " + it) } //.also { println("size is " + it) } @@ -133,7 +132,7 @@ class ValidatorsTest { val job = launch { intermediateFlow.collect() //{ it -> println("final " + it) } } - val job2 = launch { + launch { intermediateFlow.collect() //{ it -> println("final " + it) } } @@ -185,7 +184,7 @@ class ValidatorsTest { repo.Search(false) } delay(60000) - val allRules = repo.GetAllRules() + repo.GetAllRules() } From 71ad11de9b407818760f608d207652d051f67aa7 Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Sat, 30 Aug 2025 13:43:12 -0400 Subject: [PATCH 06/24] reorg and hoist view model --- .../portmapper/MainActivity.kt | 490 +----------------- .../portmapper/PortMapperMainScreen.kt | 450 ++++++++++++++++ .../portmapper/PortMapperNavGraph.kt | 126 +++++ 3 files changed, 582 insertions(+), 484 deletions(-) create mode 100644 app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt create mode 100644 app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt index 9aa50b9..04da7de 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt @@ -165,8 +165,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.fourthline.cling.model.meta.RemoteDevice +import java.com.shinjiindustrial.portmapper.PortMapperNavGraph import java.com.shinjiindustrial.portmapper.PortViewModel import java.com.shinjiindustrial.portmapper.SettingsViewModel +import java.com.shinjiindustrial.portmapper.ThemeUiState import java.net.InetAddress import java.nio.ByteBuffer import java.nio.ByteOrder @@ -297,503 +299,24 @@ class MainActivity : ComponentActivity() { portViewModel.start() setContent { - MainContent() + PortMapperMainContent(portViewModel) } } @Composable - fun MainContent() { + fun PortMapperMainContent(portViewModel : PortViewModel) { val themeState by settingsViewModel.uiState.collectAsStateWithLifecycle() MyApplicationTheme(themeState) { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - AppNavigator() + PortMapperNavGraph(portViewModel, themeState) } } } - @OptIn(ExperimentalAnimationApi::class) - @Composable - fun AppNavigator() { - val navController = rememberNavController() - - NavHost(navController = navController, startDestination = "main_screen") { - composable( - "main_screen", - exitTransition = { - fadeOut(animationSpec = tween(300)) - }, - popEnterTransition = { - //this is whats used when going back from "create rule" - slideInVertically( - initialOffsetY = { it / 2 }, animationSpec = tween( - durationMillis = 200, easing = FastOutSlowInEasing - ) - ) + fadeIn(animationSpec = tween(400)) - }, - popExitTransition = { - fadeOut(animationSpec = tween(300)) - }, - ) - { - MainScreen(navController = navController) - } - composable( - "full_screen_dialog?description={description}&internalIp={internalIp}&internalRange={internalRange}&externalIp={externalIp}&externalRange={externalRange}&protocol={protocol}&leaseDuration={leaseDuration}&enabled={enabled}&autorenew={autorenew}", - arguments = listOf( - navArgument("description") { - nullable = true; type = NavType.StringType - }, // only if nullable (or default) are they optional - navArgument("internalIp") { nullable = true; type = NavType.StringType }, - navArgument("internalRange") { nullable = true; type = NavType.StringType }, - navArgument("externalIp") { nullable = true; type = NavType.StringType }, - navArgument("externalRange") { nullable = true; type = NavType.StringType }, - navArgument("protocol") { nullable = true; type = NavType.StringType }, - navArgument("leaseDuration") { nullable = true; type = NavType.StringType }, - navArgument("enabled") { type = NavType.BoolType; defaultValue = false }, - navArgument("autorenew") { type = NavType.BoolType; defaultValue = false }, - ), - popExitTransition = { - - slideOutVertically( - targetOffsetY = { it / 2 }, animationSpec = tween( - durationMillis = 200, easing = FastOutSlowInEasing - ) - ) + fadeOut(animationSpec = tween(100)) - }, - exitTransition = { - slideOutVertically( - targetOffsetY = { it }, animationSpec = tween( - durationMillis = 5000, easing = FastOutSlowInEasing - ) - ) - }, - enterTransition = { - slideInVertically( - initialOffsetY = { it }, animationSpec = tween( - durationMillis = 200, easing = FastOutSlowInEasing - ) - ) + fadeIn(animationSpec = tween(400)) - }, - popEnterTransition = { - fadeIn(animationSpec = tween(150)) - } - ) - { backStackEntry -> - val arguments = backStackEntry.arguments - val desc = arguments?.getString("description") - val internalIp = arguments?.getString("internalIp") - val internalRange = arguments?.getString("internalRange") - val externalIp = arguments?.getString("externalIp") - val externalRange = arguments?.getString("externalRange") - val protocol = arguments?.getString("protocol") - val leaseDuration = arguments?.getString("leaseDuration") - val enabled = arguments?.getBoolean("enabled") - val autorenew = arguments?.getBoolean("autorenew") - - var portMappingUserInputToEdit: PortMappingUserInput? = null - if (desc != null) { - portMappingUserInputToEdit = PortMappingUserInput( - desc, - internalIp!!, - internalRange!!, - externalIp!!, - externalRange!!, - protocol!!, - leaseDuration!!, - enabled!!, - autorenew!! - ) - } - - RuleCreationDialog( - navController = navController, - portViewModel, - ruleToEdit = portMappingUserInputToEdit - ) - } - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun MainScreen(navController: NavHostController) { - // A surface container using the 'background' color from the theme - val searchStartedRecentlyAndNothingFoundYet by portViewModel.searchStartedRecentlyAndNothingFoundYet.collectAsStateWithLifecycle() - val anyDevices by portViewModel.anyDevices.collectAsStateWithLifecycle() - rememberScrollState() - val showAboutDialogState = remember { mutableStateOf(false) } - val showMoreInfoDialogState = remember { mutableStateOf(false) } - val showAboutDialog by showAboutDialogState //mutable state binds to UI (in sense if value changes, redraw). remember says when redrawing dont discard us. - val inMultiSelectMode by portViewModel.inMultiSelectMode.collectAsStateWithLifecycle() - val selectedIds by portViewModel.selectedIds.collectAsStateWithLifecycle() - - LaunchedEffect(Unit) { - portViewModel.events.collect { ev -> - if (ev is PortViewModel.UiEvent.ToastEvent) { - Toast.makeText( - PortForwardApplication.appContext, - ev.msg, - ev.duration - ).show() - } else if (ev is PortViewModel.UiEvent.SnackBarViewLogEvent) { - showSnackBarViewLog(ev.msg) // emit these - } - } - } - - PortForwardApplication.currentSingleSelectedObject = - remember { mutableStateOf(null) } - - if (PortForwardApplication.showContextMenu.value && PortForwardApplication.currentSingleSelectedObject.value != null) { - EnterContextMenu( - PortForwardApplication.currentSingleSelectedObject, // why is this not typed?? - showMoreInfoDialogState, - navController, - portViewModel, - settingsViewModel - ) - } - - if (showMoreInfoDialogState.value) { - MoreInfoDialog( - portMappingWithPref = PortForwardApplication.currentSingleSelectedObject.value as PortMappingWithPref, - showDialog = showMoreInfoDialogState - ) - } - - if (showAboutDialog) { - AlertDialog( - onDismissRequest = { showAboutDialogState.value = false }, - title = { Text("About") }, - text = { - - Column() - { - var aboutString: String = - PortForwardApplication.appContext.getString(R.string.about_body) - val packageInfo = - PortForwardApplication.appContext.packageManager.getPackageInfo( - PortForwardApplication.appContext.packageName, - 0 - ) - - aboutString = String.format(aboutString, packageInfo.versionName) - var spannedString: Spanned? = null - if (Build.VERSION.SDK_INT >= 24) { - spannedString = - Html.fromHtml(aboutString, Html.FROM_HTML_MODE_LEGACY) - } else { - spannedString = Html.fromHtml(aboutString) - } - - // compose doesn't allow spannable nor link movement method - AndroidView(factory = { context -> - TextView(context).apply { - setTextColor(AdditionalColors.TextColor.toArgb()) - textSize = 16f - text = spannedString - movementMethod = LinkMovementMethod.getInstance() - // you can apply other TextView properties here - } - }) - - //Text(aboutString) - } - - - }, - confirmButton = { - Button(onClick = { showAboutDialogState.value = false }) { - Text("OK") - } - }) - } - - - OurSnackbarHostState = remember { SnackbarHostState() } - rememberCoroutineScope() - val coroutineScope: CoroutineScope = rememberCoroutineScope() - - var openBottomSheet by remember { mutableStateOf(false) } - val bottomSheetState = - rememberModalBottomSheetState(true) - - if (openBottomSheet) { - ModalBottomSheet( - onDismissRequest = { openBottomSheet = false }, - sheetState = bottomSheetState, - shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), - tonalElevation = 24.dp, - containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp), - content = { - // This is what will be shown in the bottom sheet - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - BottomSheetSortBy(portViewModel) - } - } - ) -// -// LaunchedEffect(Unit) { -// bottomSheetState.expand() -// } - } - - Scaffold( - - snackbarHost = { - SnackbarHost(OurSnackbarHostState!!) { data -> - // custom snackbar with the custom colors - Snackbar( - data, - actionColor = AdditionalColors.PrimaryDarkerBlue - // according to https://m2.material.io/design/color/dark-theme.html - // light snackbar in darkmode is good. -// containerColor = AdditionalColors.CardContainerColor, -// contentColor = AdditionalColors.TextColor -// //contentColor = ..., - ) - } - }, - floatingActionButton = { - - if (anyDevices) { - FloatingActionButton( - // uses MaterialTheme.colorScheme.secondaryContainer - containerColor = MaterialTheme.colorScheme.secondaryContainer, //todo revert to secondar - onClick = { - //showAddRuleDialogState.value = true - navController.navigate("full_screen_dialog") - - //this works - - }) { - Icon( - Icons.Default.Add, - contentDescription = "Localized description", - tint = AdditionalColors.TextColor, - ) - } - } - }, - topBar = { - TopAppBar( - navigationIcon = { - if (inMultiSelectMode) { - IconButton(onClick = { - portViewModel.clearSelection() - }) - { - Icon(Icons.Filled.ArrowBack, contentDescription = "Back") - } - } - }, -// modifier = Modifier.height(40.dp), - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = AdditionalColors.TopAppBarColor - ), - //colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.secondary),// change the height here - title = { - val title = - if (inMultiSelectMode) "${selectedIds.size} Selected" else "PortMapper" - Text( - text = title, - color = AdditionalColors.TextColorStrong, - fontWeight = FontWeight.Normal - ) - }, - actions = { - - if (inMultiSelectMode) { - IconButton(onClick = { - if (inMultiSelectMode) { - portViewModel.deleteAll(selectedIds) - } else { - portViewModel.deleteAll() - } - }) - { - Icon(Icons.Default.Delete, contentDescription = "Delete") - } - } else { - IconButton(onClick = { - coroutineScope.launch { - openBottomSheet = true - bottomSheetState.show() - } - }) - { - Icon(Icons.Default.Sort, contentDescription = "Sort") - } - } - - OverflowMenu( - showAboutDialogState, portViewModel - ) - } - ) - }, - content = { it -> - - // TODO can revisit this - if we already know we have some kind of device - // then we can forget the main loading circle and show this until we - // finish enumerating our first device (or have a flow which is like - // combine (deviceNotFound, anyDeviceStillBeingSearched)) - // still the issue of if search finishes really fast then it can hang if - // you set isRefreshing to false too fast (can fix by having an arbitrary delay) - - val state = rememberPullToRefreshState() - val scope = rememberCoroutineScope() - val isRefreshing = remember { mutableStateOf(false) } - fun refresh() = scope.launch { - - state.animateToHidden() - portViewModel.fullRefresh() - } - - PullToRefreshBox( - isRefreshing = isRefreshing.value, - onRefresh = ::refresh, - modifier = Modifier.padding(it), - state = state, - indicator = { - // dont show after initially pulling down - if (state.distanceFraction > 0f) { - Indicator( - state = state, - isRefreshing = isRefreshing.value, - modifier = Modifier.align(Alignment.TopCenter) - ) - - } - } - ) { - var heightPx by remember { mutableIntStateOf(0) } - val boxHeight = with(LocalDensity.current) { heightPx.toDp() } - Column( - Modifier - .onSizeChanged { heightPx = it.height } - .fillMaxHeight() - .fillMaxWidth() - ) { - if (searchStartedRecentlyAndNothingFoundYet) { - val offset = boxHeight * 0.28f - LoadingIcon( - "Searching for devices", - Modifier.offset(y = offset) - ) - } else if (!anyDevices) { - val offset = boxHeight * 0.28f - Column(modifier = Modifier.offset(y = offset)) - { - Text( - "No UPnP enabled internet gateway devices found", - modifier = Modifier - .padding(0.dp, 10.dp) - .align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center - ) - - // these can go through vm -> repo -> client - val interfacesUsedInSearch = - portViewModel.getInterfacesUsedInSearch() - val anyInterfaces = - portViewModel.isInitialized() - if (!anyInterfaces) { - Text( - "No valid interfaces", - modifier = Modifier - .padding(0.dp, 10.dp) - .align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center - ) - } else { - - Text( - "Interfaces Searched:", - modifier = Modifier - .padding(0.dp, 10.dp, 0.dp, 0.dp) - .align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center - ) - - - for (interfaceUsed in interfacesUsedInSearch!!) { - Text( - "${interfaceUsed.networkInterface.displayName} (${interfaceUsed.networkType.networkTypeString.lowercase()})", - modifier = Modifier - .padding(0.dp, 0.dp) - .align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center - ) - } - } - - // if no wifi - if (!interfacesUsedInSearch.any { it.networkType == NetworkType.WIFI } - ) { - Text( - "Enable WiFi and retry", - modifier = Modifier - .padding(0.dp, 20.dp, 0.dp, 10.dp) - .align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center - ) - } else { - Text( - "Ensure a valid UPnP enabled internet gateway device is on the network and retry", - modifier = Modifier - .padding(0.dp, 20.dp, 0.dp, 10.dp) - .align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center - ) - } - Button( - onClick = { - portViewModel.initialize( - PortForwardApplication.appContext, - true - ) - portViewModel.fullRefresh() - }, - modifier = Modifier - .padding(0.dp, 10.dp) - .align(Alignment.CenterHorizontally), - shape = RoundedCornerShape(4), - ) { - Text( - "Retry", - color = AdditionalColors.TextColorStrong - ) - } - } - } else { - val uiState by portViewModel.uiState.collectAsStateWithLifecycle() - val selectedIds by portViewModel.selectedIds.collectAsStateWithLifecycle() - val isInMultiSelectMode by portViewModel.inMultiSelectMode.collectAsStateWithLifecycle() - PortMappingContent( - uiState, - isInMultiSelectMode, - portViewModel::toggle, - selectedIds - ) - } - } - - } - - } - ) - - - } } @@ -821,7 +344,7 @@ fun EnterContextMenu( showMoreInfoDialog: MutableState, navController: NavHostController, portViewModel: PortViewModel, - settingsViewModel: SettingsViewModel + themeState: ThemeUiState ) { if (singleSelectedItem.value == null) { return @@ -834,7 +357,6 @@ fun EnterContextMenu( usePlatformDefaultWidth = false, decorFitsSystemWindows = true, ) - val themeState by settingsViewModel.uiState.collectAsStateWithLifecycle() MyApplicationTheme(themeState) { Dialog( onDismissRequest = { PortForwardApplication.showContextMenu.value = false }, diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt new file mode 100644 index 0000000..c7b4671 --- /dev/null +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt @@ -0,0 +1,450 @@ +package java.com.shinjiindustrial.portmapper + +import android.os.Build +import android.text.Html +import android.text.Spanned +import android.text.method.LinkMovementMethod +import android.widget.TextView +import android.widget.Toast +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Sort +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import com.example.myapplication.R +import com.shinjiindustrial.portmapper.EnterContextMenu +import com.shinjiindustrial.portmapper.MainActivity.Companion.OurSnackbarHostState +import com.shinjiindustrial.portmapper.MainActivity.Companion.showSnackBarViewLog +import com.shinjiindustrial.portmapper.OverflowMenu +import com.shinjiindustrial.portmapper.PortForwardApplication +import com.shinjiindustrial.portmapper.common.NetworkType +import com.shinjiindustrial.portmapper.domain.PortMappingWithPref +import com.shinjiindustrial.portmapper.ui.BottomSheetSortBy +import com.shinjiindustrial.portmapper.ui.LoadingIcon +import com.shinjiindustrial.portmapper.ui.MoreInfoDialog +import com.shinjiindustrial.portmapper.ui.PortMappingContent +import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState, navController: NavHostController) { + // A surface container using the 'background' color from the theme + val searchStartedRecentlyAndNothingFoundYet by portViewModel.searchStartedRecentlyAndNothingFoundYet.collectAsStateWithLifecycle() + val anyDevices by portViewModel.anyDevices.collectAsStateWithLifecycle() + rememberScrollState() + val showAboutDialogState = remember { mutableStateOf(false) } + val showMoreInfoDialogState = remember { mutableStateOf(false) } + val showAboutDialog by showAboutDialogState //mutable state binds to UI (in sense if value changes, redraw). remember says when redrawing dont discard us. + val inMultiSelectMode by portViewModel.inMultiSelectMode.collectAsStateWithLifecycle() + val selectedIds by portViewModel.selectedIds.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + portViewModel.events.collect { ev -> + if (ev is PortViewModel.UiEvent.ToastEvent) { + Toast.makeText( + PortForwardApplication.appContext, + ev.msg, + ev.duration + ).show() + } else if (ev is PortViewModel.UiEvent.SnackBarViewLogEvent) { + showSnackBarViewLog(ev.msg) // emit these + } + } + } + + PortForwardApplication.currentSingleSelectedObject = + remember { mutableStateOf(null) } + + if (PortForwardApplication.showContextMenu.value && PortForwardApplication.currentSingleSelectedObject.value != null) { + EnterContextMenu( + PortForwardApplication.currentSingleSelectedObject, // why is this not typed?? + showMoreInfoDialogState, + navController, + portViewModel, + themeState + ) + } + + if (showMoreInfoDialogState.value) { + MoreInfoDialog( + portMappingWithPref = PortForwardApplication.currentSingleSelectedObject.value as PortMappingWithPref, + showDialog = showMoreInfoDialogState + ) + } + + if (showAboutDialog) { + AlertDialog( + onDismissRequest = { showAboutDialogState.value = false }, + title = { Text("About") }, + text = { + + Column() + { + var aboutString: String = + PortForwardApplication.appContext.getString(R.string.about_body) + val packageInfo = + PortForwardApplication.appContext.packageManager.getPackageInfo( + PortForwardApplication.appContext.packageName, + 0 + ) + + aboutString = String.format(aboutString, packageInfo.versionName) + var spannedString: Spanned? = null + if (Build.VERSION.SDK_INT >= 24) { + spannedString = + Html.fromHtml(aboutString, Html.FROM_HTML_MODE_LEGACY) + } else { + spannedString = Html.fromHtml(aboutString) + } + + // compose doesn't allow spannable nor link movement method + AndroidView(factory = { context -> + TextView(context).apply { + setTextColor(AdditionalColors.TextColor.toArgb()) + textSize = 16f + text = spannedString + movementMethod = LinkMovementMethod.getInstance() + // you can apply other TextView properties here + } + }) + + //Text(aboutString) + } + + + }, + confirmButton = { + Button(onClick = { showAboutDialogState.value = false }) { + Text("OK") + } + }) + } + + + OurSnackbarHostState = remember { SnackbarHostState() } + rememberCoroutineScope() + val coroutineScope: CoroutineScope = rememberCoroutineScope() + + var openBottomSheet by remember { mutableStateOf(false) } + val bottomSheetState = + rememberModalBottomSheetState(true) + + if (openBottomSheet) { + ModalBottomSheet( + onDismissRequest = { openBottomSheet = false }, + sheetState = bottomSheetState, + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), + tonalElevation = 24.dp, + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp), + content = { + // This is what will be shown in the bottom sheet + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + BottomSheetSortBy(portViewModel) + } + } + ) +// +// LaunchedEffect(Unit) { +// bottomSheetState.expand() +// } + } + + Scaffold( + + snackbarHost = { + SnackbarHost(OurSnackbarHostState!!) { data -> + // custom snackbar with the custom colors + Snackbar( + data, + actionColor = AdditionalColors.PrimaryDarkerBlue + // according to https://m2.material.io/design/color/dark-theme.html + // light snackbar in darkmode is good. +// containerColor = AdditionalColors.CardContainerColor, +// contentColor = AdditionalColors.TextColor +// //contentColor = ..., + ) + } + }, + floatingActionButton = { + + if (anyDevices) { + FloatingActionButton( + // uses MaterialTheme.colorScheme.secondaryContainer + containerColor = MaterialTheme.colorScheme.secondaryContainer, //todo revert to secondar + onClick = { + //showAddRuleDialogState.value = true + navController.navigate("full_screen_dialog") + + //this works + + }) { + Icon( + Icons.Default.Add, + contentDescription = "Localized description", + tint = AdditionalColors.TextColor, + ) + } + } + }, + topBar = { + TopAppBar( + navigationIcon = { + if (inMultiSelectMode) { + IconButton(onClick = { + portViewModel.clearSelection() + }) + { + Icon(Icons.Filled.ArrowBack, contentDescription = "Back") + } + } + }, +// modifier = Modifier.height(40.dp), + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = AdditionalColors.TopAppBarColor + ), + //colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = MaterialTheme.colorScheme.secondary),// change the height here + title = { + val title = + if (inMultiSelectMode) "${selectedIds.size} Selected" else "PortMapper" + Text( + text = title, + color = AdditionalColors.TextColorStrong, + fontWeight = FontWeight.Normal + ) + }, + actions = { + + if (inMultiSelectMode) { + IconButton(onClick = { + if (inMultiSelectMode) { + portViewModel.deleteAll(selectedIds) + } else { + portViewModel.deleteAll() + } + }) + { + Icon(Icons.Default.Delete, contentDescription = "Delete") + } + } else { + IconButton(onClick = { + coroutineScope.launch { + openBottomSheet = true + bottomSheetState.show() + } + }) + { + Icon(Icons.Default.Sort, contentDescription = "Sort") + } + } + + OverflowMenu( + showAboutDialogState, portViewModel + ) + } + ) + }, + content = { it -> + + // TODO can revisit this - if we already know we have some kind of device + // then we can forget the main loading circle and show this until we + // finish enumerating our first device (or have a flow which is like + // combine (deviceNotFound, anyDeviceStillBeingSearched)) + // still the issue of if search finishes really fast then it can hang if + // you set isRefreshing to false too fast (can fix by having an arbitrary delay) + + val state = rememberPullToRefreshState() + val scope = rememberCoroutineScope() + val isRefreshing = remember { mutableStateOf(false) } + fun refresh() = scope.launch { + + state.animateToHidden() + portViewModel.fullRefresh() + } + + PullToRefreshBox( + isRefreshing = isRefreshing.value, + onRefresh = ::refresh, + modifier = Modifier.padding(it), + state = state, + indicator = { + // dont show after initially pulling down + if (state.distanceFraction > 0f) { + Indicator( + state = state, + isRefreshing = isRefreshing.value, + modifier = Modifier.align(Alignment.TopCenter) + ) + + } + } + ) { + var heightPx by remember { mutableIntStateOf(0) } + val boxHeight = with(LocalDensity.current) { heightPx.toDp() } + Column( + Modifier + .onSizeChanged { heightPx = it.height } + .fillMaxHeight() + .fillMaxWidth() + ) { + if (searchStartedRecentlyAndNothingFoundYet) { + val offset = boxHeight * 0.28f + LoadingIcon( + "Searching for devices", + Modifier.offset(y = offset) + ) + } else if (!anyDevices) { + val offset = boxHeight * 0.28f + Column(modifier = Modifier.offset(y = offset)) + { + Text( + "No UPnP enabled internet gateway devices found", + modifier = Modifier + .padding(0.dp, 10.dp) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center + ) + + // these can go through vm -> repo -> client + val interfacesUsedInSearch = + portViewModel.getInterfacesUsedInSearch() + val anyInterfaces = + portViewModel.isInitialized() + if (!anyInterfaces) { + Text( + "No valid interfaces", + modifier = Modifier + .padding(0.dp, 10.dp) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center + ) + } else { + + Text( + "Interfaces Searched:", + modifier = Modifier + .padding(0.dp, 10.dp, 0.dp, 0.dp) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center + ) + + + for (interfaceUsed in interfacesUsedInSearch!!) { + Text( + "${interfaceUsed.networkInterface.displayName} (${interfaceUsed.networkType.networkTypeString.lowercase()})", + modifier = Modifier + .padding(0.dp, 0.dp) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center + ) + } + } + + // if no wifi + if (!interfacesUsedInSearch.any { it.networkType == NetworkType.WIFI } + ) { + Text( + "Enable WiFi and retry", + modifier = Modifier + .padding(0.dp, 20.dp, 0.dp, 10.dp) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center + ) + } else { + Text( + "Ensure a valid UPnP enabled internet gateway device is on the network and retry", + modifier = Modifier + .padding(0.dp, 20.dp, 0.dp, 10.dp) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center + ) + } + Button( + onClick = { + portViewModel.initialize( + PortForwardApplication.appContext, + true + ) + portViewModel.fullRefresh() + }, + modifier = Modifier + .padding(0.dp, 10.dp) + .align(Alignment.CenterHorizontally), + shape = RoundedCornerShape(4), + ) { + Text( + "Retry", + color = AdditionalColors.TextColorStrong + ) + } + } + } else { + val uiState by portViewModel.uiState.collectAsStateWithLifecycle() + val selectedIds by portViewModel.selectedIds.collectAsStateWithLifecycle() + val isInMultiSelectMode by portViewModel.inMultiSelectMode.collectAsStateWithLifecycle() + PortMappingContent( + uiState, + isInMultiSelectMode, + portViewModel::toggle, + selectedIds + ) + } + } + + } + + } + ) +} diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt new file mode 100644 index 0000000..6b9c2ce --- /dev/null +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt @@ -0,0 +1,126 @@ +package java.com.shinjiindustrial.portmapper + +import androidx.activity.viewModels +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.shinjiindustrial.portmapper.domain.PortMappingUserInput +import com.shinjiindustrial.portmapper.ui.RuleCreationDialog +import kotlin.getValue + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun PortMapperNavGraph(portViewModel : PortViewModel, themeState : ThemeUiState) { + + val navController = rememberNavController() + + NavHost(navController = navController, startDestination = "main_screen") { + composable( + "main_screen", + exitTransition = { + fadeOut(animationSpec = tween(300)) + }, + popEnterTransition = { + //this is whats used when going back from "create rule" + slideInVertically( + initialOffsetY = { it / 2 }, animationSpec = tween( + durationMillis = 200, easing = FastOutSlowInEasing + ) + ) + fadeIn(animationSpec = tween(400)) + }, + popExitTransition = { + fadeOut(animationSpec = tween(300)) + }, + ) + { + PortMapperMainScreen(portViewModel, themeState, navController = navController) + } + composable( + "full_screen_dialog?description={description}&internalIp={internalIp}&internalRange={internalRange}&externalIp={externalIp}&externalRange={externalRange}&protocol={protocol}&leaseDuration={leaseDuration}&enabled={enabled}&autorenew={autorenew}", + arguments = listOf( + navArgument("description") { + nullable = true; type = NavType.StringType + }, // only if nullable (or default) are they optional + navArgument("internalIp") { nullable = true; type = NavType.StringType }, + navArgument("internalRange") { nullable = true; type = NavType.StringType }, + navArgument("externalIp") { nullable = true; type = NavType.StringType }, + navArgument("externalRange") { nullable = true; type = NavType.StringType }, + navArgument("protocol") { nullable = true; type = NavType.StringType }, + navArgument("leaseDuration") { nullable = true; type = NavType.StringType }, + navArgument("enabled") { type = NavType.BoolType; defaultValue = false }, + navArgument("autorenew") { type = NavType.BoolType; defaultValue = false }, + ), + popExitTransition = { + + slideOutVertically( + targetOffsetY = { it / 2 }, animationSpec = tween( + durationMillis = 200, easing = FastOutSlowInEasing + ) + ) + fadeOut(animationSpec = tween(100)) + }, + exitTransition = { + slideOutVertically( + targetOffsetY = { it }, animationSpec = tween( + durationMillis = 5000, easing = FastOutSlowInEasing + ) + ) + }, + enterTransition = { + slideInVertically( + initialOffsetY = { it }, animationSpec = tween( + durationMillis = 200, easing = FastOutSlowInEasing + ) + ) + fadeIn(animationSpec = tween(400)) + }, + popEnterTransition = { + fadeIn(animationSpec = tween(150)) + } + ) + { backStackEntry -> + val arguments = backStackEntry.arguments + val desc = arguments?.getString("description") + val internalIp = arguments?.getString("internalIp") + val internalRange = arguments?.getString("internalRange") + val externalIp = arguments?.getString("externalIp") + val externalRange = arguments?.getString("externalRange") + val protocol = arguments?.getString("protocol") + val leaseDuration = arguments?.getString("leaseDuration") + val enabled = arguments?.getBoolean("enabled") + val autorenew = arguments?.getBoolean("autorenew") + + var portMappingUserInputToEdit: PortMappingUserInput? = null + if (desc != null) { + portMappingUserInputToEdit = PortMappingUserInput( + desc, + internalIp!!, + internalRange!!, + externalIp!!, + externalRange!!, + protocol!!, + leaseDuration!!, + enabled!!, + autorenew!! + ) + } + + RuleCreationDialog( + navController = navController, + portViewModel, + ruleToEdit = portMappingUserInputToEdit + ) + } + } +} From f176ed61f9f67e848fa48bf182f6dffa8f99c33f Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Sat, 30 Aug 2025 14:48:56 -0400 Subject: [PATCH 07/24] fix source sets so the default package is no longer java.com --- app/build.gradle | 8 +-- .../portmapper/LogViewActivity.kt | 2 - .../portmapper/MainActivity.kt | 57 +------------------ .../shinjiindustrial/portmapper/Modules.kt | 2 +- .../portmapper/PortMapperMainScreen.kt | 6 +- .../portmapper/PortMapperNavGraph.kt | 7 +-- .../portmapper/PortViewModel.kt | 48 +++++++--------- .../portmapper/SettingsActivity.kt | 2 - .../portmapper/SettingsViewModel.kt | 4 +- .../portmapper/UPnPManager.kt | 2 +- .../portmapper/ui/DurationPicker.kt | 2 +- .../portmapper/ui/PortScreen.kt | 8 +-- .../portmapper/ui/RuleCreationDialog.kt | 4 +- .../portmapper/ui/SortPanel.kt | 2 +- .../portmapper/ui/theme/Theme.kt | 2 +- .../portmapper/common/ValidatorsTest.kt | 13 +---- 16 files changed, 41 insertions(+), 128 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c739d49..cfcff5a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -53,16 +53,16 @@ android { } sourceSets { main { - java.srcDirs = ['src/main'] + java.srcDirs = ['src/main/java'] } release { - java.srcDirs = ['src/release'] + java.srcDirs = ['src/release/java'] } release_no_gps { - java.srcDirs = ['src/no_gps'] + java.srcDirs = ['src/no_gps/java'] } debug { - java.srcDirs = ['src/no_gps'] + java.srcDirs = ['src/no_gps/java'] } } compileOptions { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt index c1e4434..451b13c 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt @@ -41,8 +41,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import com.shinjiindustrial.portmapper.ui.theme.MyApplicationTheme import dagger.hilt.android.AndroidEntryPoint -import java.com.shinjiindustrial.portmapper.SettingsViewModel -import java.com.shinjiindustrial.portmapper.ThemeUiState @AndroidEntryPoint class LogViewActivity : ComponentActivity() { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt index 04da7de..d0f5ac8 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt @@ -5,31 +5,20 @@ import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper import android.os.SystemClock -import android.text.Html -import android.text.Spanned -import android.text.method.LinkMovementMethod import android.util.DisplayMetrics -import android.widget.TextView import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.addCallback import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -40,70 +29,46 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.ArrowDropUp -import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Schedule -import androidx.compose.material.icons.filled.Sort import androidx.compose.material.icons.filled.UnfoldLess import androidx.compose.material.icons.filled.UnfoldMore -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.PlainTooltip -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipDefaults -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberTooltipState -import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment @@ -113,12 +78,10 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -126,20 +89,13 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.toSize -import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.SecureFlagPolicy import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController -import androidx.navigation.NavType -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument import com.example.myapplication.R import com.shinjiindustrial.portmapper.common.MAX_PORT -import com.shinjiindustrial.portmapper.common.NetworkType import com.shinjiindustrial.portmapper.common.ValidationError import com.shinjiindustrial.portmapper.common.capLeaseDur import com.shinjiindustrial.portmapper.common.toMessage @@ -149,26 +105,15 @@ import com.shinjiindustrial.portmapper.common.validateInternalIp import com.shinjiindustrial.portmapper.common.validateStartPort import com.shinjiindustrial.portmapper.domain.ActionNames import com.shinjiindustrial.portmapper.domain.PortMapping -import com.shinjiindustrial.portmapper.domain.PortMappingUserInput import com.shinjiindustrial.portmapper.domain.PortMappingWithPref -import com.shinjiindustrial.portmapper.ui.BottomSheetSortBy import com.shinjiindustrial.portmapper.ui.DurationPickerDialog -import com.shinjiindustrial.portmapper.ui.LoadingIcon -import com.shinjiindustrial.portmapper.ui.MoreInfoDialog -import com.shinjiindustrial.portmapper.ui.PortMappingContent -import com.shinjiindustrial.portmapper.ui.RuleCreationDialog import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import com.shinjiindustrial.portmapper.ui.theme.MyApplicationTheme import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.fourthline.cling.model.meta.RemoteDevice -import java.com.shinjiindustrial.portmapper.PortMapperNavGraph -import java.com.shinjiindustrial.portmapper.PortViewModel -import java.com.shinjiindustrial.portmapper.SettingsViewModel -import java.com.shinjiindustrial.portmapper.ThemeUiState import java.net.InetAddress import java.nio.ByteBuffer import java.nio.ByteOrder @@ -783,7 +728,7 @@ fun ColumnScope.CreateRuleContents( TooltipBox( positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), tooltip = { - PlainTooltip { Text("This is auto renew...") } + PlainTooltip { Text("When checked, port mapper will automatically renew rules before expiration.") } }, state = rememberTooltipState() ) { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/Modules.kt b/app/src/main/java/com/shinjiindustrial/portmapper/Modules.kt index 5c25ba7..2b571ca 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/Modules.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/Modules.kt @@ -1,4 +1,4 @@ -package java.com.shinjiindustrial.portmapper +package com.shinjiindustrial.portmapper import dagger.Module import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt index c7b4671..eab7a66 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt @@ -1,4 +1,4 @@ -package java.com.shinjiindustrial.portmapper +package com.shinjiindustrial.portmapper import android.os.Build import android.text.Html @@ -55,15 +55,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import com.example.myapplication.R -import com.shinjiindustrial.portmapper.EnterContextMenu import com.shinjiindustrial.portmapper.MainActivity.Companion.OurSnackbarHostState import com.shinjiindustrial.portmapper.MainActivity.Companion.showSnackBarViewLog -import com.shinjiindustrial.portmapper.OverflowMenu -import com.shinjiindustrial.portmapper.PortForwardApplication import com.shinjiindustrial.portmapper.common.NetworkType import com.shinjiindustrial.portmapper.domain.PortMappingWithPref import com.shinjiindustrial.portmapper.ui.BottomSheetSortBy diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt index 6b9c2ce..e9e6e0c 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt @@ -1,6 +1,5 @@ -package java.com.shinjiindustrial.portmapper +package com.shinjiindustrial.portmapper -import androidx.activity.viewModels import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween @@ -9,9 +8,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -19,7 +15,6 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.shinjiindustrial.portmapper.domain.PortMappingUserInput import com.shinjiindustrial.portmapper.ui.RuleCreationDialog -import kotlin.getValue @OptIn(ExperimentalAnimationApi::class) @Composable diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index 6a2ec3a..1f4f113 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -1,14 +1,10 @@ -package java.com.shinjiindustrial.portmapper +package com.shinjiindustrial.portmapper import android.content.Context -import android.util.Log import android.widget.Toast import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.shinjiindustrial.portmapper.PortForwardApplication -import com.shinjiindustrial.portmapper.PreferencesManager -import com.shinjiindustrial.portmapper.UpnpManager import com.shinjiindustrial.portmapper.client.UPnPCreateMappingWrapperResult import com.shinjiindustrial.portmapper.client.UPnPResult import com.shinjiindustrial.portmapper.common.SortBy @@ -124,28 +120,24 @@ class PortViewModel @Inject constructor( upnpRepository.portMappings ) { sortInfo, devices, portMappings -> - Log.i("Port", "UI STATE FLOW is running") - val upnpElements = mutableListOf() - if (devices.isEmpty()) { - } else { + if (!devices.isEmpty()) { var index = 0 - val curDevice: IIGDDevice? = devices.elementAt(0) val portMappingsList = - portMappings.values.sortedWith(sortInfo.sortBy.getComparer(!sortInfo.sortDesc)) + portMappings.values.sortedWith(sortInfo.sortBy.getComparer(ascending = !sortInfo.sortDesc)) for (curDevice in devices) { - // do we have any port mappings upnpElements.add(UpnpViewRow.DeviceHeaderViewRow(curDevice)) - if (index >= portMappingsList.size || portMappingsList.elementAt(index).portMapping.DeviceIP != curDevice.getIpAddress()) { - // emit empty and continue - upnpElements.add(UpnpViewRow.DeviceEmptyViewRow(curDevice)) - continue - } - while (index < portMappingsList.size) { - if (portMappingsList.elementAt(index).portMapping.DeviceIP == curDevice.getIpAddress()) { - upnpElements.add(UpnpViewRow.PortViewRow(portMappingsList.elementAt(index))) + var anyFound = false + for (portMapping in portMappingsList) { + if (curDevice.getIpAddress() == portMapping.portMapping.DeviceIP) + { + upnpElements.add(UpnpViewRow.PortViewRow(portMapping)) + anyFound = true } - index++ + } + if (!anyFound) + { + upnpElements.add(UpnpViewRow.DeviceEmptyViewRow(curDevice)) } } } @@ -186,7 +178,7 @@ class PortViewModel @Inject constructor( return upnpRepository.getIGDDevice(ipAddress) } - fun GetGatewayIpsWithDefault(deviceGateway: String): Pair, String> { + fun getGatewayIpsWithDefault(deviceGateway: String): Pair, String> { return upnpRepository.GetGatewayIpsWithDefault(deviceGateway) } @@ -213,7 +205,7 @@ class PortViewModel @Inject constructor( fun renewAll(chosen: List? = null) = applicationScope.launch { try { - val portMappings = chosen?.toList() ?: upnpRepository.GetAllRules() + val portMappings = chosen?.toList() ?: upnpRepository.getAllRules() val result = upnpRepository.renewRules(portMappings) result.forEach { res -> when (res) { @@ -342,8 +334,10 @@ class PortViewModel @Inject constructor( initialValue = Unit ) + private val onSearchStarted: (Any?) -> Unit = { o -> searchStarted(o) } + init { - upnpRepository.SearchStarted += { o -> searchStarted(o) } + upnpRepository.SearchStarted += onSearchStarted combine(_selectedIds, upnpRepository.portMappings) { selectedIds, currentMappings -> val cur = currentMappings.keys val sel = selectedIds @@ -362,11 +356,11 @@ class PortViewModel @Inject constructor( } override fun onCleared() { - upnpRepository.SearchStarted -= { o -> searchStarted(o) } + upnpRepository.SearchStarted -= onSearchStarted super.onCleared() } - // move to viewmodel + var searchInProgressJob: Job? = null fun searchStarted(o: Any?) { searchStartedRecently.value = true // controls when loading bar is there @@ -386,7 +380,7 @@ class PortViewModel @Inject constructor( fun deleteAll(chosen: List? = null) = applicationScope.launch { try { // get all enabled. note: need to clone. - val rules = chosen?.toList() ?: upnpRepository.GetAllRules() + val rules = chosen?.toList() ?: upnpRepository.getAllRules() val result = upnpRepository.deletePortMappingsEntry(rules) result.forEach { res -> when (res) { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt index 9b54441..e75f5ec 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt @@ -45,8 +45,6 @@ import com.shinjiindustrial.portmapper.ui.SetupPreview import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import com.shinjiindustrial.portmapper.ui.theme.MyApplicationTheme import dagger.hilt.android.AndroidEntryPoint -import java.com.shinjiindustrial.portmapper.SettingsViewModel -import java.com.shinjiindustrial.portmapper.ThemeUiState @AndroidEntryPoint class SettingsActivity : ComponentActivity() { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/SettingsViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/SettingsViewModel.kt index e2900ae..817c54f 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/SettingsViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/SettingsViewModel.kt @@ -1,9 +1,7 @@ -package java.com.shinjiindustrial.portmapper +package com.shinjiindustrial.portmapper import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.shinjiindustrial.portmapper.DayNightMode -import com.shinjiindustrial.portmapper.PreferencesManager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index 96acf36..ef347dd 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -251,7 +251,7 @@ class UpnpManager @Inject constructor( // autorenew can subscribe to the 1st // UI can subscribe to both - fun GetAllRules(): MutableList { + fun getAllRules(): MutableList { return portMappings.value.values.toMutableList() } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/DurationPicker.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/DurationPicker.kt index bc98851..272a3d1 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/DurationPicker.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/DurationPicker.kt @@ -55,7 +55,7 @@ import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import com.shinjiindustrial.portmapper.ui.theme.MyApplicationTheme import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import java.com.shinjiindustrial.portmapper.ThemeUiState +import com.shinjiindustrial.portmapper.ThemeUiState @Composable diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt index 9f21fa2..e44579e 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -64,8 +65,8 @@ import com.shinjiindustrial.portmapper.domain.Urgency import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import com.shinjiindustrial.portmapper.ui.theme.MyApplicationTheme import kotlinx.coroutines.delay -import java.com.shinjiindustrial.portmapper.PortUiState -import java.com.shinjiindustrial.portmapper.ThemeUiState +import com.shinjiindustrial.portmapper.PortUiState +import com.shinjiindustrial.portmapper.ThemeUiState @OptIn( ExperimentalUnitApi::class, ExperimentalMaterial3Api::class, @@ -562,8 +563,7 @@ fun PortMappingContent( @Composable fun rememberTicker(periodMillis: Long): androidx.compose.runtime.State { - Log.i("rememberTicker", "called") - val now = remember { mutableStateOf(SystemClock.elapsedRealtime()) } + val now = remember { mutableLongStateOf(SystemClock.elapsedRealtime()) } LaunchedEffect(Unit) { while (true) { delay(periodMillis) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt index ec28874..115f670 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt @@ -43,7 +43,7 @@ import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import java.com.shinjiindustrial.portmapper.PortViewModel +import com.shinjiindustrial.portmapper.PortViewModel import java.util.logging.Level @@ -119,7 +119,7 @@ fun RuleCreationDialog( val internalIpHasError = remember { mutableStateOf(validateInternalIp(internalIp.value).hasError) } val (gatewayIps, defaultGatewayIp) = remember { - portViewModel.GetGatewayIpsWithDefault( + portViewModel.getGatewayIpsWithDefault( ourGatewayIp!! ) } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/SortPanel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/SortPanel.kt index bc8445a..a505eaf 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/SortPanel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/SortPanel.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.shinjiindustrial.portmapper.common.SortBy import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors -import java.com.shinjiindustrial.portmapper.PortViewModel +import com.shinjiindustrial.portmapper.PortViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/theme/Theme.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/theme/Theme.kt index f62cdc7..b6d2976 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/theme/Theme.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/theme/Theme.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import com.shinjiindustrial.portmapper.DayNightMode -import java.com.shinjiindustrial.portmapper.ThemeUiState +import com.shinjiindustrial.portmapper.ThemeUiState fun getBlend(first: Color, second: Color, ratio: Double): Color { val newRed = first.red.toDouble() * (1f - ratio) + second.red.toDouble() * (ratio) diff --git a/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt b/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt index 8d4ed23..d99678e 100644 --- a/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt +++ b/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt @@ -1,41 +1,30 @@ package com.shinjiindustrial.portmapper.common -import android.os.SystemClock import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.junit.Assert.assertEquals import android.util.Log -import androidx.compose.runtime.LaunchedEffect import com.shinjiindustrial.portmapper.UpnpManager import com.shinjiindustrial.portmapper.client.MockUpnpClient import com.shinjiindustrial.portmapper.client.MockUpnpClientConfig import com.shinjiindustrial.portmapper.client.Speed -import com.shinjiindustrial.portmapper.domain.IGDDevice import io.mockk.every import io.mockk.mockkStatic import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runTest import org.junit.Test -import kotlin.test.assertEquals class ValidatorsTest { @Test @@ -184,7 +173,7 @@ class ValidatorsTest { repo.Search(false) } delay(60000) - repo.GetAllRules() + repo.getAllRules() } From 105e135cc6c9f1ef8162503e59a96f4159ab0371 Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Sat, 30 Aug 2025 15:36:25 -0400 Subject: [PATCH 08/24] use io dispatcher for work. inject application scope (which is supervisorjob + io dispatcher). --- .../portmapper/MainActivity.kt | 2 +- .../shinjiindustrial/portmapper/Modules.kt | 36 +++++++-- .../portmapper/PortMapperMainScreen.kt | 1 - .../portmapper/PortViewModel.kt | 28 ++++--- .../portmapper/UPnPManager.kt | 73 +++++++++---------- .../portmapper/common/ValidatorsTest.kt | 2 +- 6 files changed, 79 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt index d0f5ac8..4082645 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt @@ -240,7 +240,7 @@ class MainActivity : ComponentActivity() { } } - portViewModel.initialize(this, false) + portViewModel.initialize(false) portViewModel.start() setContent { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/Modules.kt b/app/src/main/java/com/shinjiindustrial/portmapper/Modules.kt index 2b571ca..69473e3 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/Modules.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/Modules.kt @@ -1,15 +1,37 @@ package com.shinjiindustrial.portmapper import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import javax.inject.Qualifier +import javax.inject.Singleton + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ApplicationScope + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class IODispatcher @Module @InstallIn(SingletonComponent::class) -object ClingModule { -// @Singleton -// @Provides -// fun providesUpnpService(@ApplicationContext context: Context): UpnpService { -// return UpnpServiceImpl(AndroidUpnpServiceConfigurationImpl(context)) -// } -} +object CoroutinesModule { + + @Provides + @Singleton + @IODispatcher + fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO + + @Provides @Singleton @ApplicationScope + fun provideApplicationScope( + @IODispatcher io: CoroutineDispatcher + ): CoroutineScope { + return CoroutineScope(SupervisorJob() + io) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt index eab7a66..4d35025 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt @@ -410,7 +410,6 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState Button( onClick = { portViewModel.initialize( - PortForwardApplication.appContext, true ) portViewModel.fullRefresh() diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index 1f4f113..47b8e6a 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -1,6 +1,5 @@ package com.shinjiindustrial.portmapper -import android.content.Context import android.widget.Toast import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -17,8 +16,8 @@ import com.shinjiindustrial.portmapper.domain.PortMappingWithPref import com.shinjiindustrial.portmapper.domain.UpnpViewRow import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.MainScope import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -51,7 +51,8 @@ data class PortUiState( class PortViewModel @Inject constructor( private val upnpRepository: UpnpManager, private val preferencesRepository: PreferencesManager, - private val savedStateHandle: SavedStateHandle + private val savedStateHandle: SavedStateHandle, + @ApplicationScope private val applicationScope: CoroutineScope, ) : ViewModel() { sealed interface UiEvent { @@ -65,8 +66,6 @@ class PortViewModel @Inject constructor( val searchStartedRecently: MutableStateFlow = MutableStateFlow(false) - private val applicationScope: CoroutineScope = MainScope() - // we want to use key for selections. so if a rule renews while the user is in multi select // mode, don't deselect that rule. but if we lose a rule (i.e. it gets deleted) then // we still want to deselect. @@ -122,7 +121,6 @@ class PortViewModel @Inject constructor( val upnpElements = mutableListOf() if (!devices.isEmpty()) { - var index = 0 val portMappingsList = portMappings.values.sortedWith(sortInfo.sortBy.getComparer(ascending = !sortInfo.sortDesc)) for (curDevice in devices) { @@ -143,15 +141,15 @@ class PortViewModel @Inject constructor( } PortUiState(upnpElements) - } + }.flowOn(Dispatchers.Default) .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = PortUiState(isLoading = true) ) - fun initialize(context: Context, force: Boolean) { - upnpRepository.Initialize(context, force) + fun initialize(force: Boolean) { + upnpRepository.initialize(force) } fun getExistingRuleInfos(): Pair { @@ -179,11 +177,11 @@ class PortViewModel @Inject constructor( } fun getGatewayIpsWithDefault(deviceGateway: String): Pair, String> { - return upnpRepository.GetGatewayIpsWithDefault(deviceGateway) + return upnpRepository.getGatewayIpsWithDefault(deviceGateway) } fun fullRefresh() { - upnpRepository.FullRefresh() + upnpRepository.fullRefresh() } fun devices(): StateFlow> { @@ -269,7 +267,7 @@ class PortViewModel @Inject constructor( } else -> { - val rules = upnpRepository.GetEnabledDisabledRules(!enable) + val rules = upnpRepository.getEnabledDisabledRules(!enable) upnpRepository.disableEnablePortMappingEntries(rules, enable) } } @@ -305,11 +303,11 @@ class PortViewModel @Inject constructor( fun start() { - if (upnpRepository.FailedToInitialize) { + if (upnpRepository.failedToInitialize) { searchStartedRecently.value = false } else { - searchStartedRecently.value = !upnpRepository.HasSearched - upnpRepository.Search(true) // by default STAll + searchStartedRecently.value = !upnpRepository.hasSearched + upnpRepository.search(true) // by default STAll } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index ef347dd..6a9e006 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -1,6 +1,5 @@ package com.shinjiindustrial.portmapper -import android.content.Context import android.os.SystemClock import android.util.Log import com.shinjiindustrial.portmapper.PortForwardApplication.Companion.OurLogger @@ -21,7 +20,7 @@ import com.shinjiindustrial.portmapper.domain.PortMappingWithPref import com.shinjiindustrial.portmapper.domain.getPrefs import com.shinjiindustrial.portmapper.persistence.PortMappingDao import com.shinjiindustrial.portmapper.persistence.PortMappingEntity -import kotlinx.coroutines.MainScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -46,8 +45,28 @@ import kotlin.system.measureTimeMillis @Singleton class UpnpManager @Inject constructor( private val upnpClient: IUpnpClient, - private val portMappingDao: PortMappingDao + private val portMappingDao: PortMappingDao, + @ApplicationScope private val applicationContext: CoroutineScope ) { + private val renewMutex = Mutex() + private var counter = 1 + + private var _initialized: Boolean = false + var hasSearched: Boolean = false + var failedToInitialize: Boolean = false + + private val _devices = MutableStateFlow(listOf()) // TreeSet + val devices: StateFlow> = + _devices//.map { it..sortedBy { d -> d.name } } + + val anyDevices: Flow = _devices.map { !it.isEmpty() } + + // private val cmp = compareBy({ it.ActualExternalIP }, { it.InternalPort }) + private var _portMappings: MutableStateFlow> = + MutableStateFlow(mapOf()) + + // I think this should be a local var inside mutable state flow + val portMappings: StateFlow> = _portMappings init { upnpClient.deviceFoundEvent += { device -> @@ -71,7 +90,7 @@ class UpnpManager @Inject constructor( fun subscribeForAutoRenewCancelTimerNotWork() { // too early for app scope - MainScope().launch { + applicationContext.launch { portMappings.mapNotNull { it -> it.filter { it.value.getAutoRenewOrDefault() } .minByOrNull { it.value.portMapping.getExpiresTimeMillis() } @@ -93,9 +112,6 @@ class UpnpManager @Inject constructor( } } - private val renewMutex = Mutex() - private var counter = 1 - private suspend fun performBatchRenew() = renewMutex.withLock { println("renewing all rules") val snapshot = portMappings.first() @@ -178,23 +194,6 @@ class UpnpManager @Inject constructor( // region data - private var _initialized: Boolean = false - var HasSearched: Boolean = false - var FailedToInitialize: Boolean = false - - private val _devices = MutableStateFlow(listOf()) // TreeSet - val devices: StateFlow> = - _devices//.map { it..sortedBy { d -> d.name } } - - //.stateIn(MainScope(), SharingStarted.Eagerly, emptyList()) - val anyDevices: Flow = _devices.map { !it.isEmpty() } - - // private val cmp = compareBy({ it.ActualExternalIP }, { it.InternalPort }) - private var _portMappings: MutableStateFlow> = - MutableStateFlow(mapOf()) - - // I think this should be a local var inside mutable state flow - val portMappings: StateFlow> = _portMappings // // fun update(pm: PortMapping) = _setFlow.update { old -> @@ -242,7 +241,7 @@ class UpnpManager @Inject constructor( } // returns anyEnabled, anyDisabled - fun GetEnabledDisabledRules(enabledRules: Boolean): MutableList { + fun getEnabledDisabledRules(enabledRules: Boolean): MutableList { return portMappings.value.filter { it.value.portMapping.Enabled == enabledRules }.values.toMutableList() } @@ -255,7 +254,7 @@ class UpnpManager @Inject constructor( return portMappings.value.values.toMutableList() } - fun GetGatewayIpsWithDefault(deviceGateway: String): Pair, String> { + fun getGatewayIpsWithDefault(deviceGateway: String): Pair, String> { val gatewayIps: MutableList = mutableListOf() var defaultGatewayIp = "" synchronized(lockIgdDevices) @@ -288,11 +287,9 @@ class UpnpManager @Inject constructor( // region uievents - var DeviceFoundEvent = Event() - var PortAddedEvent = Event() - var PortInitialFoundEvent = Event() // used if we finish and there are no ports to show the "no devices" card + var PortInitialFoundEvent = Event() var FinishedListingPortsEvent = Event() var UpdateUIFromData = Event() var SearchStarted = Event() @@ -301,14 +298,14 @@ class UpnpManager @Inject constructor( // region search - fun Search(onlyIfNotYetSearched: Boolean): Boolean { - if (onlyIfNotYetSearched && HasSearched) { + fun search(onlyIfNotYetSearched: Boolean): Boolean { + if (onlyIfNotYetSearched && hasSearched) { return false } SearchStarted.invoke(null) clearOldData() //NetworkInfoAtTimeOfSearch = OurNetworkInfo.GetNetworkInfo(PortForwardApplication.appContext, true) - HasSearched = true + hasSearched = true // can do urn:schemas-upnp-org:device:{deviceType}:{ver} // 0-1 second response time intentional delay from devices upnpClient.search(1) @@ -323,12 +320,12 @@ class UpnpManager @Inject constructor( } - fun FullRefresh() { - Initialize(PortForwardApplication.appContext, true) - Search(false) + fun fullRefresh() { + initialize(true) + search(false) } - fun Initialize(context: Context, force: Boolean): Boolean { + fun initialize(force: Boolean): Boolean { if (_initialized && !force) { return true } @@ -337,8 +334,8 @@ class UpnpManager @Inject constructor( // initialization failed. no point in trying as even if we later get service, we // do not re-intialize automatically - FailedToInitialize = !upnpClient.isInitialized() - if (FailedToInitialize) { + failedToInitialize = !upnpClient.isInitialized() + if (failedToInitialize) { return false } diff --git a/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt b/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt index d99678e..7696237 100644 --- a/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt +++ b/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt @@ -170,7 +170,7 @@ class ValidatorsTest { val client = MockUpnpClient(MockUpnpClientConfig(Speed.Fastest)) val repo = UpnpManager(client) GlobalScope.launch { - repo.Search(false) + repo.search(false) } delay(60000) repo.getAllRules() From 884fc931a2c49fb8d11013f07b6435147ff07b0f Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Sun, 31 Aug 2025 14:36:15 -0400 Subject: [PATCH 09/24] move logs out of upnpclient and into upnprepository. fix return type of getspecific/getgeneric. failure details class. --- .../portmapper/MainActivity.kt | 1 - .../portmapper/UPnPManager.kt | 92 +++++++++++++++---- .../portmapper/client/IUpnpClient.kt | 29 ++++-- .../portmapper/domain/PortMapping.kt | 6 +- .../portmapper/ui/PortScreen.kt | 1 - .../portmapper/client/MockUpnpClient.kt | 2 +- .../portmapper/client/UpnpClient.kt | 81 +++------------- 7 files changed, 116 insertions(+), 96 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt index 4082645..1510250 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt @@ -210,7 +210,6 @@ class MainActivity : ComponentActivity() { (duration == SnackbarDuration.Indefinite), duration ) - println("shown") when (snackbarResult) { SnackbarResult.Dismissed -> {} SnackbarResult.ActionPerformed -> onAction() diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index 6a9e006..a4060e2 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -6,6 +6,7 @@ import com.shinjiindustrial.portmapper.PortForwardApplication.Companion.OurLogge import com.shinjiindustrial.portmapper.client.IUpnpClient import com.shinjiindustrial.portmapper.client.UPnPCreateMappingResult import com.shinjiindustrial.portmapper.client.UPnPCreateMappingWrapperResult +import com.shinjiindustrial.portmapper.client.UPnPGetGenericMappingResult import com.shinjiindustrial.portmapper.client.UPnPGetSpecificMappingResult import com.shinjiindustrial.portmapper.client.UPnPResult import com.shinjiindustrial.portmapper.common.Event @@ -17,6 +18,7 @@ import com.shinjiindustrial.portmapper.domain.PortMappingKey import com.shinjiindustrial.portmapper.domain.PortMappingPref import com.shinjiindustrial.portmapper.domain.PortMappingUserInput import com.shinjiindustrial.portmapper.domain.PortMappingWithPref +import com.shinjiindustrial.portmapper.domain.formatShortName import com.shinjiindustrial.portmapper.domain.getPrefs import com.shinjiindustrial.portmapper.persistence.PortMappingDao import com.shinjiindustrial.portmapper.persistence.PortMappingEntity @@ -432,7 +434,23 @@ class UpnpManager @Inject constructor( val device: IIGDDevice = getIGDDevice(pm.DeviceIP) portMappingDao.deleteByKey(pm.DeviceIP, device.udn, pm.Protocol, pm.ExternalPort) val result = upnpClient.deletePortMapping(device, pm) - removeForDelete(result, portMappingWithPref) + if (result is UPnPResult.Success) + { + PortForwardApplication.Companion.OurLogger.log( + Level.INFO, + "Successfully deleted rule (${pm.shortName()})." + ) + removeMapping(portMappingWithPref) + } + else if (result is UPnPResult.Failure) + { + PortForwardApplication.Companion.OurLogger.log( + Level.SEVERE, + "Failed to delete rule (${pm.shortName()})." + ) + PortForwardApplication.Companion.OurLogger.log(Level.SEVERE, + result.details.toString()) + } return result } catch (exception: Exception) { OurLogger.log( @@ -459,7 +477,23 @@ class UpnpManager @Inject constructor( portMapping.ExternalPort ) val result = upnpClient.deletePortMapping(device, portMapping) - removeForDelete(result, portMappingWithPref) + if (result is UPnPResult.Success) + { + PortForwardApplication.Companion.OurLogger.log( + Level.INFO, + "Successfully deleted rule (${portMapping.shortName()})." + ) + removeMapping(portMappingWithPref) + } + else if (result is UPnPResult.Failure) + { + PortForwardApplication.Companion.OurLogger.log( + Level.SEVERE, + "Failed to delete rule (${portMapping.shortName()})." + ) + PortForwardApplication.Companion.OurLogger.log(Level.SEVERE, + result.details.toString()) + } result } catch (exception: Exception) { @@ -623,15 +657,37 @@ class UpnpManager @Inject constructor( portMappingRequest.externalPort, portMappingRequest.protocol, ) - return result + if (result is UPnPGetSpecificMappingResult.Success) { + PortForwardApplication.Companion.OurLogger.log( + Level.INFO, + "Successfully read back our new rule (${result.requestInfo.shortName()})" + ) + } + else if (result is UPnPGetSpecificMappingResult.Failure) { + val rule = formatShortName(portMappingRequest.protocol, + portMappingRequest.externalIp, + portMappingRequest.externalPort) + PortForwardApplication.Companion.OurLogger.log( + Level.SEVERE, + "Failed to read back our new rule ($rule). Remote Host: ${portMappingRequest.remoteHost}" + ) + PortForwardApplication.Companion.OurLogger.log(Level.SEVERE, result.details.toString()) + } + + return result.toCreatePortMappingResult() } } is UPnPCreateMappingResult.Failure -> { - val result = UPnPCreateMappingWrapperResult.Failure( - createMappingResult.reason, - createMappingResult.response + PortForwardApplication.Companion.OurLogger.log( + Level.SEVERE, + "Failed to $createContext rule (${portMappingRequest.realize().shortName()})." ) + PortForwardApplication.Companion.OurLogger.log(Level.SEVERE, + createMappingResult.details.toString()) + + val result = UPnPCreateMappingWrapperResult.Failure( + createMappingResult.details) return result } } @@ -683,7 +739,8 @@ class UpnpManager @Inject constructor( //future.get() // SYNCHRONOUS (note this can, and does, throw) val result = upnpClient.getGenericPortMappingRule(device, slotIndex) when (result) { - is UPnPGetSpecificMappingResult.Success -> { + is UPnPGetGenericMappingResult.Success -> { + PortForwardApplication.Companion.OurLogger.log(Level.INFO, "GetGenericPortMapping succeeded for entry $slotIndex") val portMapping = result.resultingMapping val entity = portMappingDao.getByPrimaryKey( device.getIpAddress(), @@ -711,10 +768,10 @@ class UpnpManager @Inject constructor( } - is UPnPGetSpecificMappingResult.Failure -> { + is UPnPGetGenericMappingResult.Failure -> { OurLogger.log( Level.INFO, - "GetGenericPortMapping failed for entry $slotIndex: ${result.reason}. NOTE: This is normal." + "GetGenericPortMapping failed for entry $slotIndex: ${result.details}. NOTE: This is normal." ) } } @@ -764,14 +821,6 @@ class UpnpManager @Inject constructor( _portMappings.update { mapOf() } } - private fun removeForDelete(result: UPnPResult, portMappingWithPref: PortMappingWithPref) { - - println("default adding rule callback") - if (result is UPnPResult.Success) { - removeMapping(portMappingWithPref) - } - } - private fun addDevice(igdDevice: IIGDDevice) { add(igdDevice) Log.i("portmapperUI", "IGD device added") @@ -798,6 +847,15 @@ class UpnpManager @Inject constructor( } +fun UPnPGetSpecificMappingResult.toCreatePortMappingResult() : UPnPCreateMappingWrapperResult = + when(this){ + is UPnPGetSpecificMappingResult.Failure -> { + UPnPCreateMappingWrapperResult.Failure(this.details) + } + is UPnPGetSpecificMappingResult.Success -> { + UPnPCreateMappingWrapperResult.Success(this.resultingMapping, this.resultingMapping, true) + } +} // this is the request we give the router. basically a port mapping minus time read and psuedo slot data class PortMappingRequest( diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt b/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt index 3f90e5a..2e8a39e 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt @@ -28,12 +28,12 @@ interface IUpnpClient { remoteHost: String, remotePort: String, protocol: String, - ): UPnPCreateMappingWrapperResult + ): UPnPGetSpecificMappingResult suspend fun getGenericPortMappingRule( device: IIGDDevice, slotIndex: Int, - ): UPnPGetSpecificMappingResult + ): UPnPGetGenericMappingResult fun search(maxSeconds: Int) @@ -55,24 +55,41 @@ sealed class UPnPCreateMappingWrapperResult { val wasReadBack: Boolean ) : UPnPCreateMappingWrapperResult() - data class Failure(val reason: String, val response: UpnpResponse) : + data class Failure(val details: FailureDetails) : UPnPCreateMappingWrapperResult() } sealed class UPnPCreateMappingResult { data class Success(val requestInfo: PortMapping) : UPnPCreateMappingResult() - data class Failure(val reason: String, val response: UpnpResponse) : UPnPCreateMappingResult() + data class Failure(val details: FailureDetails) : UPnPCreateMappingResult() +} + +sealed class UPnPGetGenericMappingResult { + data class Success(val requestInfo: PortMapping, val resultingMapping: PortMapping) : + UPnPGetGenericMappingResult() + + data class Failure(val details: FailureDetails) : + UPnPGetGenericMappingResult() } sealed class UPnPGetSpecificMappingResult { data class Success(val requestInfo: PortMapping, val resultingMapping: PortMapping) : UPnPGetSpecificMappingResult() - data class Failure(val reason: String, val response: UpnpResponse) : + data class Failure(val details: FailureDetails) : UPnPGetSpecificMappingResult() + + companion object } sealed class UPnPResult { data class Success(val requestInfo: PortMapping) : UPnPResult() - data class Failure(val reason: String, val response: UpnpResponse) : UPnPResult() + data class Failure(val details: FailureDetails) : UPnPResult() +} + +class FailureDetails(val reason: String, val response: UpnpResponse) +{ + override fun toString() : String{ + return "\t${reason}\t${response}" + } } \ No newline at end of file diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt index 8931eaf..2516dfb 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt @@ -68,10 +68,12 @@ data class PortMapping( //dropped (this corresponds to the address dependent filtering behaviour defined in [RFC 4787])." - // needs to be * for Verizon Router - CR1000B, but only for delete call + // needs to be * for Verizon Router - CR1000B, but only for delete call (create works fine) + // I tried other port mapping tools PortMapper.jar - fails deletion (since it uses "") + // TinyUPnP.cpp has "" hardcoded in the soap response. // else one will get InvalidArgs exception fun getRemoteHostNormalizedForDelete(): String = - this.RemoteHost.ifBlank { "*" } + this.RemoteHost.ifBlank { "" } fun getKey(): PortMappingKey { return PortMappingKey(this.DeviceIP, this.ExternalPort, this.Protocol) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt index e44579e..46b5686 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt @@ -82,7 +82,6 @@ fun PortMappingCard( additionalModifier: Modifier = Modifier.Companion ) { val portMapping = portMappingWithPref.portMapping - println("external ip test ${portMapping.DeviceIP}") Card( // onClick = { diff --git a/app/src/mock/java/com/shinjiindustrial/portmapper/client/MockUpnpClient.kt b/app/src/mock/java/com/shinjiindustrial/portmapper/client/MockUpnpClient.kt index d82ddfd..3c64780 100644 --- a/app/src/mock/java/com/shinjiindustrial/portmapper/client/MockUpnpClient.kt +++ b/app/src/mock/java/com/shinjiindustrial/portmapper/client/MockUpnpClient.kt @@ -205,7 +205,7 @@ class MockUpnpClient(val config : MockUpnpClientConfig) : IUpnpClient { } override suspend fun getGenericPortMappingRule(device : IIGDDevice, - slotIndex : Int) : UPnPGetSpecificMappingResult + slotIndex : Int) : UPnPGetGenericMappingResult { tick() return UPnPGetSpecificMappingResult.Success(store[device]!!.values.elementAt(slotIndex),store[device]!!.values.elementAt(slotIndex)) diff --git a/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt b/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt index 91d1833..712cf4a 100644 --- a/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt +++ b/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt @@ -17,7 +17,6 @@ import com.shinjiindustrial.portmapper.domain.AndroidUpnpServiceConfigurationImp import com.shinjiindustrial.portmapper.domain.IIGDDevice import com.shinjiindustrial.portmapper.domain.NetworkInterfaceInfo import com.shinjiindustrial.portmapper.domain.PortMapping -import com.shinjiindustrial.portmapper.domain.formatShortName import com.shinjiindustrial.portmapper.domain.getIGDDevice import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.suspendCancellableCoroutine @@ -32,7 +31,6 @@ import org.fourthline.cling.registry.Registry import org.fourthline.cling.registry.RegistryListener import java.util.logging.Level import javax.inject.Inject -import javax.inject.Singleton import kotlin.coroutines.resume class UpnpClient @Inject constructor(@ApplicationContext private val context: Context) : IUpnpClient { @@ -146,23 +144,9 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co return result } is ClingExecutionResult.Failure -> { - // Handle failure - println(result.defaultMsg) - - println(result.operation.statusMessage) - println(result.operation.responseDetails) - println(result.operation.statusCode) - println(result.operation.isFailed) - - PortForwardApplication.Companion.OurLogger.log( - Level.SEVERE, - "Failed to create rule (${portMappingRequest.realize().shortName()})." - ) - PortForwardApplication.Companion.OurLogger.log(Level.SEVERE, "\t$result.defaultMsg") - - return UPnPCreateMappingResult.Failure(result.defaultMsg, result.operation) + return UPnPCreateMappingResult.Failure(FailureDetails(result.defaultMsg, result.operation)) } - } + } } override suspend fun deletePortMapping( @@ -176,33 +160,13 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co actionInvocation.setInput("NewProtocol", portMapping.Protocol) val result = this.executeAction(actionInvocation) - when (result) { + return when (result) { is ClingExecutionResult.Success -> { - println("Successfully deleted") - - PortForwardApplication.Companion.OurLogger.log( - Level.INFO, - "Successfully deleted rule (${portMapping.shortName()})." - ) - - return UPnPResult.Success(portMapping) + UPnPResult.Success(portMapping) } - is ClingExecutionResult.Failure -> { - // Handle failure - println(result.defaultMsg) - println(result.operation.statusMessage) - println(result.operation.responseDetails) - println(result.operation.statusCode) - println(result.operation.isFailed) - - PortForwardApplication.Companion.OurLogger.log( - Level.SEVERE, - "Failed to delete rule (${portMapping.shortName()})." - ) - PortForwardApplication.Companion.OurLogger.log(Level.SEVERE, "\t$result.defaultMsg") - - return UPnPResult.Failure(result.defaultMsg, result.operation) + is ClingExecutionResult.Failure -> { + UPnPResult.Failure(FailureDetails(result.defaultMsg, result.operation)) } } } @@ -212,7 +176,7 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co remoteHost: String, remotePort: String, protocol: String, - ): UPnPCreateMappingWrapperResult { + ): UPnPGetSpecificMappingResult { val remoteIp = device.getIpAddress() val actionInvocation = device.getActionInvocation(ACTION_NAMES.GetSpecificPortMappingEntry) @@ -244,38 +208,19 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co GetPsuedoSlot() ) // best bet for DateTime.UtcNow - PortForwardApplication.Companion.OurLogger.log( - Level.INFO, - "Successfully read back our new rule (${pm.shortName()})" - ) - - val result = UPnPCreateMappingWrapperResult.Success(pm, pm, true) + val result = UPnPGetSpecificMappingResult.Success(pm, pm) return result } is ClingExecutionResult.Failure -> { - println(result.defaultMsg) - - println(result.operation.statusMessage) - println(result.operation.responseDetails) - println(result.operation.statusCode) - println(result.operation.isFailed) - - val rule = formatShortName(protocol, remoteIp, remotePort) - PortForwardApplication.Companion.OurLogger.log( - Level.SEVERE, - "Failed to read back our new rule ($rule)." - ) - PortForwardApplication.Companion.OurLogger.log(Level.SEVERE, "\t$result.defaultMsg") - - val result = UPnPCreateMappingWrapperResult.Failure(result.defaultMsg, result.operation) + val result = UPnPGetSpecificMappingResult.Failure(FailureDetails(result.defaultMsg, result.operation)) return result } } } override suspend fun getGenericPortMappingRule(device : IIGDDevice, - slotIndex : Int) : UPnPGetSpecificMappingResult + slotIndex : Int) : UPnPGetGenericMappingResult { val ipAddress = device.getIpAddress() val actionInvocation = device.getActionInvocation(ACTION_NAMES.GetGenericPortMappingEntry) @@ -285,7 +230,6 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co when (result) { is ClingExecutionResult.Success -> { val invocation = result.invocation - PortForwardApplication.Companion.OurLogger.log(Level.INFO, "GetGenericPortMapping succeeded for entry $slotIndex") val remoteHost = invocation.getOutput("NewRemoteHost") //string datatype // the .value is null (also empty if GetListOfPortMappings is used) val externalPort = invocation.getOutput("NewExternalPort") //unsigned 2 byte int @@ -308,11 +252,11 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co SystemClock.elapsedRealtime(), slotIndex ) - val result = UPnPGetSpecificMappingResult.Success(portMapping, portMapping) + val result = UPnPGetGenericMappingResult.Success(portMapping, portMapping) return result } is ClingExecutionResult.Failure -> { - val result = UPnPGetSpecificMappingResult.Failure(result.defaultMsg, result.operation) + val result = UPnPGetGenericMappingResult.Failure(FailureDetails(result.defaultMsg, result.operation)) return result } } @@ -348,6 +292,7 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co //data class DeviceFoundEvent(val remoteDevice: RemoteDevice) } +//TODO move @Preview(showBackground = true) @Composable fun CreateRuleContentsPreview() { From 93ef14835215e945ce72b611a3524ee609685b8e Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Sun, 31 Aug 2025 16:34:09 -0400 Subject: [PATCH 10/24] retry delete logic --- app/build.gradle | 14 +- .../1.json | 91 +++++++++++++ .../2.json | 124 ++++++++++++++++++ .../portmapper/PortViewModel.kt | 28 ++-- .../portmapper/UPnPManager.kt | 42 +++++- .../portmapper/domain/IGDDevice.kt | 6 - .../portmapper/domain/PortMapping.kt | 4 +- .../persistence/PortMapperPersistence.kt | 63 ++++++++- .../portmapper/client/UpnpClient.kt | 2 +- 9 files changed, 332 insertions(+), 42 deletions(-) create mode 100644 app/schemas/com.shinjiindustrial.portmapper.persistence.AppDatabase/1.json create mode 100644 app/schemas/com.shinjiindustrial.portmapper.persistence.AppDatabase/2.json diff --git a/app/build.gradle b/app/build.gradle index cfcff5a..046d7af 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -109,15 +109,10 @@ def compose_version = "2.8.2" dependencies { - implementation("androidx.room:room-runtime:$room_version") - implementation 'androidx.compose.material3:material3:1.3.2' - - // If this project uses any Kotlin source, use Kotlin Symbol Processing (KSP) - // See Add the KSP plugin to your project ksp("androidx.room:room-compiler:$room_version") - - // optional - Kotlin Extensions and Coroutines support for Room + implementation("androidx.room:room-runtime:$room_version") implementation("androidx.room:room-ktx:$room_version") + implementation 'androidx.compose.material3:material3:1.3.2' implementation 'androidx.core:core-ktx:1.8.0' implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') @@ -189,4 +184,9 @@ dependencies { androidTestImplementation 'androidx.compose.ui:ui-test-junit4' testImplementation "org.jetbrains.kotlin:kotlin-test:1.7.20" +} + +ksp { + arg("room.schemaLocation", "$projectDir/schemas") + arg("room.generateKotlin", "true") } \ No newline at end of file diff --git a/app/schemas/com.shinjiindustrial.portmapper.persistence.AppDatabase/1.json b/app/schemas/com.shinjiindustrial.portmapper.persistence.AppDatabase/1.json new file mode 100644 index 0000000..baf4db3 --- /dev/null +++ b/app/schemas/com.shinjiindustrial.portmapper.persistence.AppDatabase/1.json @@ -0,0 +1,91 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "bdcfcad7c7fe227ce0eb8afe905fba09", + "entities": [ + { + "tableName": "port_mappings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceIp` TEXT NOT NULL, `deviceSignature` TEXT NOT NULL, `externalPort` INTEGER NOT NULL, `protocol` TEXT NOT NULL, `description` TEXT NOT NULL, `internalIp` TEXT NOT NULL, `internalPort` INTEGER NOT NULL, `autorenew` INTEGER NOT NULL, `desiredLeaseDuration` INTEGER NOT NULL, `desiredEnabled` INTEGER NOT NULL, PRIMARY KEY(`deviceIp`, `deviceSignature`, `protocol`, `externalPort`))", + "fields": [ + { + "fieldPath": "deviceIp", + "columnName": "deviceIp", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceSignature", + "columnName": "deviceSignature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "externalPort", + "columnName": "externalPort", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "protocol", + "columnName": "protocol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "internalIp", + "columnName": "internalIp", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "internalPort", + "columnName": "internalPort", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoRenew", + "columnName": "autorenew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "desiredLeaseDuration", + "columnName": "desiredLeaseDuration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "desiredEnabled", + "columnName": "desiredEnabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceIp", + "deviceSignature", + "protocol", + "externalPort" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bdcfcad7c7fe227ce0eb8afe905fba09')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.shinjiindustrial.portmapper.persistence.AppDatabase/2.json b/app/schemas/com.shinjiindustrial.portmapper.persistence.AppDatabase/2.json new file mode 100644 index 0000000..63ac514 --- /dev/null +++ b/app/schemas/com.shinjiindustrial.portmapper.persistence.AppDatabase/2.json @@ -0,0 +1,124 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "437b91cca60dc16f3af517e0ca3388a3", + "entities": [ + { + "tableName": "port_mappings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceIp` TEXT NOT NULL, `deviceSignature` TEXT NOT NULL, `externalPort` INTEGER NOT NULL, `protocol` TEXT NOT NULL, `description` TEXT NOT NULL, `internalIp` TEXT NOT NULL, `internalPort` INTEGER NOT NULL, `autorenew` INTEGER NOT NULL, `desiredLeaseDuration` INTEGER NOT NULL, `desiredEnabled` INTEGER NOT NULL, PRIMARY KEY(`deviceIp`, `deviceSignature`, `protocol`, `externalPort`))", + "fields": [ + { + "fieldPath": "deviceIp", + "columnName": "deviceIp", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceSignature", + "columnName": "deviceSignature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "externalPort", + "columnName": "externalPort", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "protocol", + "columnName": "protocol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "internalIp", + "columnName": "internalIp", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "internalPort", + "columnName": "internalPort", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoRenew", + "columnName": "autorenew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "desiredLeaseDuration", + "columnName": "desiredLeaseDuration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "desiredEnabled", + "columnName": "desiredEnabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceIp", + "deviceSignature", + "protocol", + "externalPort" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "devices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceIp` TEXT NOT NULL, `deviceSignature` TEXT NOT NULL, `useWildcardForRemoteHostDelete` INTEGER NOT NULL, PRIMARY KEY(`deviceIp`, `deviceSignature`))", + "fields": [ + { + "fieldPath": "deviceIp", + "columnName": "deviceIp", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceSignature", + "columnName": "deviceSignature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "useWildcardForRemoteHostDelete", + "columnName": "useWildcardForRemoteHostDelete", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceIp", + "deviceSignature" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '437b91cca60dc16f3af517e0ca3388a3')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index 47b8e6a..0fa62b9 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -194,7 +194,7 @@ class PortViewModel @Inject constructor( if (res is UPnPCreateMappingWrapperResult.Success) { _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } else { - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).reason}")) + _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).details.reason}")) } } catch (e: Exception) { _events.emit(UiEvent.SnackBarViewLogEvent("Renew Port Mapping Failed")) @@ -214,8 +214,8 @@ class PortViewModel @Inject constructor( is UPnPCreateMappingWrapperResult.Failure -> { print("failure") - print(res.reason) - print(res.response) + print(res.details.reason) + print(res.details.response) } } } @@ -224,7 +224,7 @@ class PortViewModel @Inject constructor( if (anyFailed) { val res = result.first { it is UPnPCreateMappingWrapperResult.Failure } - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).reason}")) + _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).details.reason}")) } else { _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } @@ -243,7 +243,7 @@ class PortViewModel @Inject constructor( if (res is UPnPCreateMappingWrapperResult.Success) { _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } else { - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).reason}")) + _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).details.reason}")) } } catch (e: Exception) { val enableDisableString = if (enable) "Enable" else "Disable" @@ -281,8 +281,8 @@ class PortViewModel @Inject constructor( is UPnPCreateMappingWrapperResult.Failure -> { print("failure") - print(res.reason) - print(res.response) + print(res.details.reason) + print(res.details.response) } } } @@ -291,7 +291,7 @@ class PortViewModel @Inject constructor( if (anyFailed) { val res = result.first { it is UPnPCreateMappingWrapperResult.Failure } - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).reason}")) + _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).details.reason}")) } else { _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } @@ -389,8 +389,8 @@ class PortViewModel @Inject constructor( is UPnPResult.Failure -> { print("failure") - print(res.reason) - print(res.response) + print(res.details.reason) + print(res.details.response) } } } @@ -399,7 +399,7 @@ class PortViewModel @Inject constructor( if (anyFailed) { val res = result.first { it is UPnPResult.Failure } - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPResult.Failure).reason}")) + _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPResult.Failure).details.reason}")) } else { _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } @@ -414,7 +414,7 @@ class PortViewModel @Inject constructor( if (res is UPnPResult.Success) { _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } else { - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPResult.Failure).reason}")) + _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPResult.Failure).details.reason}")) } } catch (e: Exception) { _events.emit(UiEvent.SnackBarViewLogEvent("Delete Port Mapping Failed")) @@ -454,8 +454,8 @@ class PortViewModel @Inject constructor( is UPnPCreateMappingWrapperResult.Failure -> { print("failure") - print(res.reason) - print(res.response) + print(res.details.reason) + print(res.details.response) } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index a4060e2..adf8681 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -423,6 +423,38 @@ class UpnpManager @Inject constructor( } } + suspend fun deletePortMappingWithFallback(device: IIGDDevice, portMapping : PortMapping) : UPnPResult { + val result = upnpClient.deletePortMapping(device, portMapping) + if (result is UPnPResult.Failure) + { + if (result.details.response.statusCode == 500) + { + // add breadcrumb? no only on another failure + val portMappingFallback = portMapping.copy(RemoteHost = "*") + val fallbackResult = upnpClient.deletePortMapping(device, portMappingFallback) + if (fallbackResult is UPnPResult.Success) + { + OurLogger.log(Level.INFO, "Fallback Delete Success") + // TODO set new default + return fallbackResult + } + else + { + // this means we maybe should not have retried // log both original and fallback errors to firebase + OurLogger.log(Level.SEVERE, "Fallback Delete Failed") + // failed even with fallback + return fallbackResult + } + } + else + { + // we failed with non retriable error + return result + } + } + return result + } + // DeletePortMappingRange is only available in v2.0 (also it can only delete // a contiguous range) @@ -433,13 +465,9 @@ class UpnpManager @Inject constructor( println("Requesting Delete: ${pm.shortName()}") val device: IIGDDevice = getIGDDevice(pm.DeviceIP) portMappingDao.deleteByKey(pm.DeviceIP, device.udn, pm.Protocol, pm.ExternalPort) - val result = upnpClient.deletePortMapping(device, pm) + val result = deletePortMappingWithFallback(device, pm) if (result is UPnPResult.Success) { - PortForwardApplication.Companion.OurLogger.log( - Level.INFO, - "Successfully deleted rule (${pm.shortName()})." - ) removeMapping(portMappingWithPref) } else if (result is UPnPResult.Failure) @@ -476,7 +504,7 @@ class UpnpManager @Inject constructor( portMapping.Protocol, portMapping.ExternalPort ) - val result = upnpClient.deletePortMapping(device, portMapping) + val result = deletePortMappingWithFallback(device, portMapping) if (result is UPnPResult.Success) { PortForwardApplication.Companion.OurLogger.log( @@ -822,8 +850,8 @@ class UpnpManager @Inject constructor( } private fun addDevice(igdDevice: IIGDDevice) { + // get dao add(igdDevice) - Log.i("portmapperUI", "IGD device added") OurLogger.log( Level.INFO, "Added Device ${igdDevice.getDisplayName()} at ${igdDevice.getIpAddress()}." diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt index 2cbf6b1..e187629 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt @@ -11,7 +11,6 @@ interface IIGDDevice { fun getUpnpVersion(): Int fun supportsAction(actionName: String): Boolean fun getActionInvocation(actionName: String): ActionInvocation<*> - fun getDeviceSignature(): String val udn: String } @@ -57,11 +56,6 @@ class IGDDevice(private val rootDevice: RemoteDevice, private val wanIPService: return ActionInvocation(action) } - override fun getDeviceSignature(): String { - this.actionsMap[""] - return "" - } - init { for (action in wanIPService.actions) { if (ActionNames.contains(action.name)) { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt index 2516dfb..5786aa3 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt @@ -68,12 +68,10 @@ data class PortMapping( //dropped (this corresponds to the address dependent filtering behaviour defined in [RFC 4787])." - // needs to be * for Verizon Router - CR1000B, but only for delete call (create works fine) + // RemoteHost needs to be * for Verizon Router - CR1000B, but only for delete call (create works fine) // I tried other port mapping tools PortMapper.jar - fails deletion (since it uses "") // TinyUPnP.cpp has "" hardcoded in the soap response. // else one will get InvalidArgs exception - fun getRemoteHostNormalizedForDelete(): String = - this.RemoteHost.ifBlank { "" } fun getKey(): PortMappingKey { return PortMappingKey(this.DeviceIP, this.ExternalPort, this.Protocol) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/persistence/PortMapperPersistence.kt b/app/src/main/java/com/shinjiindustrial/portmapper/persistence/PortMapperPersistence.kt index 06ecf31..84ed7fb 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/persistence/PortMapperPersistence.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/persistence/PortMapperPersistence.kt @@ -1,6 +1,7 @@ package com.shinjiindustrial.portmapper.persistence import android.content.Context +import androidx.room.AutoMigration import androidx.room.ColumnInfo import androidx.room.Dao import androidx.room.Database @@ -34,14 +35,62 @@ data class PortMappingEntity( @ColumnInfo(name = "internalIp") val internalIp: String, @ColumnInfo(name = "internalPort") val internalPort: Int, - // these are the preferences that we cannot store on the device + // these are the preferences that we cannot store on the router @ColumnInfo(name = "autorenew") val autoRenew: Boolean, @ColumnInfo(name = "desiredLeaseDuration") val desiredLeaseDuration: Int, - // this is a preference we can sometimes store on the device + // this is a preference we can sometimes store on the router @ColumnInfo(name = "desiredEnabled") val desiredEnabled: Boolean, ) +@Entity( + tableName = "devices", + primaryKeys = ["deviceIp", "deviceSignature"] +) +data class DevicesEntity( + + // uniquely identifies a device + @ColumnInfo(name = "deviceIp") val deviceIp: String, + @ColumnInfo(name = "deviceSignature") val deviceSignature: String, + + // these are the preferences that we cannot store on the router + @ColumnInfo(name = "useWildcardForRemoteHostDelete") val useWildcardForRemoteHostDelete: Boolean, +) + +@Dao +interface DevicesDao { + @Query("SELECT * FROM devices") + suspend fun getAll(): List + + @Query( + """ + SELECT * FROM devices + WHERE deviceIp = :deviceIp + AND deviceSignature = :deviceSignature + LIMIT 2 + """ + ) + suspend fun getByPrimaryKey( + deviceIp: String, + deviceSignature: String, + ): DevicesEntity? + + @Upsert + suspend fun upsert(entity: DevicesEntity) + + @Query( + """ + DELETE FROM devices + WHERE deviceIp = :deviceIp + AND deviceSignature = :deviceSignature + """ + ) + suspend fun deleteByKey( + deviceIp: String, + deviceSignature: String, + ): Int +} + @Dao interface PortMappingDao { @Query("SELECT * FROM port_mappings") @@ -84,9 +133,13 @@ interface PortMappingDao { ): Int } -@Database(entities = [PortMappingEntity::class], version = 1, exportSchema = true) +@Database(entities = [PortMappingEntity::class, DevicesEntity::class], + version = 2, + exportSchema = true, + autoMigrations = [AutoMigration(from = 1, to = 2)]) abstract class AppDatabase : RoomDatabase() { abstract fun portMappingDao(): PortMappingDao + abstract fun devicesDao(): DevicesDao } @Module @@ -97,9 +150,11 @@ object DatabaseModule { @Singleton fun provideDatabase(@ApplicationContext context: Context): AppDatabase = Room.databaseBuilder(context, AppDatabase::class.java, "app.db") - // .addMigrations(MIGRATION_1_2, ...) .build() @Provides fun providePortMappingDao(db: AppDatabase): PortMappingDao = db.portMappingDao() + + @Provides + fun provideDevicesDao(db: AppDatabase): DevicesDao = db.devicesDao() } \ No newline at end of file diff --git a/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt b/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt index 712cf4a..b376e72 100644 --- a/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt +++ b/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt @@ -154,7 +154,7 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co portMapping: PortMapping, ): UPnPResult { val actionInvocation = device.getActionInvocation(ACTION_NAMES.DeletePortMapping) - actionInvocation.setInput("NewRemoteHost", portMapping.getRemoteHostNormalizedForDelete()) + actionInvocation.setInput("NewRemoteHost", portMapping.RemoteHost) // it does validate the args (to at least be in range of 2 unsigned bytes i.e. 65535) actionInvocation.setInput("NewExternalPort", "${portMapping.ExternalPort}") actionInvocation.setInput("NewProtocol", portMapping.Protocol) From 9fcb0b0649c99437b9c00a8bc8e4d5e7895e1bb8 Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Sun, 31 Aug 2025 17:34:19 -0400 Subject: [PATCH 11/24] with dao --- .../portmapper/UPnPManager.kt | 59 +++++++++++++++---- .../portmapper/client/IUpnpClient.kt | 3 +- .../portmapper/domain/ClingUtil.kt | 7 ++- .../portmapper/domain/IGDDevice.kt | 41 +++++++++++-- .../portmapper/domain/PortMapping.kt | 7 ++- .../portmapper/client/UpnpClient.kt | 3 +- 6 files changed, 100 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index adf8681..6f32289 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -11,6 +11,7 @@ import com.shinjiindustrial.portmapper.client.UPnPGetSpecificMappingResult import com.shinjiindustrial.portmapper.client.UPnPResult import com.shinjiindustrial.portmapper.common.Event import com.shinjiindustrial.portmapper.domain.ACTION_NAMES +import com.shinjiindustrial.portmapper.domain.DevicePreferences import com.shinjiindustrial.portmapper.domain.IGDDevice import com.shinjiindustrial.portmapper.domain.IIGDDevice import com.shinjiindustrial.portmapper.domain.PortMapping @@ -20,6 +21,8 @@ import com.shinjiindustrial.portmapper.domain.PortMappingUserInput import com.shinjiindustrial.portmapper.domain.PortMappingWithPref import com.shinjiindustrial.portmapper.domain.formatShortName import com.shinjiindustrial.portmapper.domain.getPrefs +import com.shinjiindustrial.portmapper.persistence.DevicesDao +import com.shinjiindustrial.portmapper.persistence.DevicesEntity import com.shinjiindustrial.portmapper.persistence.PortMappingDao import com.shinjiindustrial.portmapper.persistence.PortMappingEntity import kotlinx.coroutines.CoroutineScope @@ -48,6 +51,7 @@ import kotlin.system.measureTimeMillis class UpnpManager @Inject constructor( private val upnpClient: IUpnpClient, private val portMappingDao: PortMappingDao, + private val devicesDao: DevicesDao, @ApplicationScope private val applicationContext: CoroutineScope ) { private val renewMutex = Mutex() @@ -73,15 +77,28 @@ class UpnpManager @Inject constructor( init { upnpClient.deviceFoundEvent += { device -> // this is on the cling thread - if (_devices.value.any { it.getIpAddress() == device.getIpAddress() }) { + if (_devices.value.any { it.getIpAddress() == device.deviceDetails.ipAddress }) { OurLogger.log( Level.WARNING, - "device ${device.udn} came in again at ${device.getIpAddress()}. ignoring." + "device ${device.deviceDetails.udn} came in again at ${device.deviceDetails.ipAddress}. ignoring." ) } else { - addDevice(device)//TODO test with suspend + + val devicePreferencesNullable = runBlocking { + devicesDao.getByPrimaryKey(device.deviceDetails.ipAddress, device.deviceDetails.udn) + } + if (devicePreferencesNullable == null) { + OurLogger.log(Level.INFO, "Preferences not found for device") + } else { + OurLogger.log(Level.INFO, "Preferences found for device") + } + val igdDevice = + IGDDevice(device.deviceDetails, + devicePreferencesNullable?.getPrefs() ?: DevicePreferences(), + device.remoteService) + addDevice(igdDevice)//TODO test with suspend runBlocking { - enumeratePortMappings(device.getIpAddress()) + enumeratePortMappings(igdDevice.getIpAddress()) } } } @@ -424,25 +441,46 @@ class UpnpManager @Inject constructor( } suspend fun deletePortMappingWithFallback(device: IIGDDevice, portMapping : PortMapping) : UPnPResult { - val result = upnpClient.deletePortMapping(device, portMapping) + // if set to something special then proceed as normal + if(!device.devicePreferences.isBlankOrStar(portMapping.RemoteHost)) + { + return upnpClient.deletePortMapping(device, portMapping) + } + + val result = upnpClient.deletePortMapping(device, + portMapping.copy(RemoteHost = device.devicePreferences.getDefaultWildcard())) if (result is UPnPResult.Failure) { if (result.details.response.statusCode == 500) { // add breadcrumb? no only on another failure - val portMappingFallback = portMapping.copy(RemoteHost = "*") + val portMappingFallback = portMapping.copy(RemoteHost = device.devicePreferences.getBackupWildcard()) val fallbackResult = upnpClient.deletePortMapping(device, portMappingFallback) + //TODO when() if (fallbackResult is UPnPResult.Success) { OurLogger.log(Level.INFO, "Fallback Delete Success") - // TODO set new default + // set new default to be the opposite of the old + device.devicePreferences = device.devicePreferences.copy(useWildcardForRemoteHostDelete = !device.devicePreferences.useWildcardForRemoteHostDelete) + devicesDao.upsert( + DevicesEntity( + device.getIpAddress(), + device.udn, + device.devicePreferences.useWildcardForRemoteHostDelete + ) + ) return fallbackResult } - else + else if (fallbackResult is UPnPResult.Failure) { - // this means we maybe should not have retried // log both original and fallback errors to firebase + // this means we maybe should not have retried + // log both original and fallback errors to firebase OurLogger.log(Level.SEVERE, "Fallback Delete Failed") - // failed even with fallback + // log to firebase + OurLogger.log(Level.SEVERE, "Fallback Original and Delete Failed") + OurLogger.log(Level.SEVERE, result.details.toString()) + OurLogger.log(Level.SEVERE, fallbackResult.details.toString()) + // return fallbackResult } } @@ -851,6 +889,7 @@ class UpnpManager @Inject constructor( private fun addDevice(igdDevice: IIGDDevice) { // get dao + portMappingDao add(igdDevice) OurLogger.log( Level.INFO, diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt b/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt index 2e8a39e..c650f63 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt @@ -2,6 +2,7 @@ package com.shinjiindustrial.portmapper.client import com.shinjiindustrial.portmapper.PortMappingRequest import com.shinjiindustrial.portmapper.common.Event +import com.shinjiindustrial.portmapper.domain.ClingIGDDevice import com.shinjiindustrial.portmapper.domain.IIGDDevice import com.shinjiindustrial.portmapper.domain.NetworkInterfaceInfo import com.shinjiindustrial.portmapper.domain.PortMapping @@ -45,7 +46,7 @@ interface IUpnpClient { fun instantiateAndBindUpnpService() - val deviceFoundEvent: Event + val deviceFoundEvent: Event } sealed class UPnPCreateMappingWrapperResult { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt index 456d463..4d3b694 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt @@ -2,6 +2,7 @@ package com.shinjiindustrial.portmapper.domain import com.shinjiindustrial.portmapper.PortForwardApplication.Companion.OurLogger import org.fourthline.cling.model.meta.RemoteDevice +import org.fourthline.cling.model.meta.RemoteService import java.util.logging.Level object UPnPNames { @@ -44,7 +45,9 @@ val ActionNames: List = listOf( ACTION_NAMES.GetListOfPortMappings, ) -fun RemoteDevice.getIGDDevice(): IGDDevice? { +data class ClingIGDDevice(val deviceDetails: DeviceDetails, val remoteService: RemoteService) + +fun RemoteDevice.getIGDDevice(): ClingIGDDevice? { if (this.type.type.equals(UPnPNames.InternetGatewayDevice)) // version agnostic { @@ -67,7 +70,7 @@ fun RemoteDevice.getIGDDevice(): IGDDevice? { if (wanIPService != null) { //get relevant actions here... //TODO add relevant service (and cause event) - val igdDevice = IGDDevice(this, wanIPService) + val igdDevice = ClingIGDDevice(DeviceDetails.fromRemoteDevice(this), wanIPService) return igdDevice } else { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt index e187629..1021050 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt @@ -5,6 +5,22 @@ import org.fourthline.cling.model.meta.Action import org.fourthline.cling.model.meta.RemoteDevice import org.fourthline.cling.model.meta.RemoteService +data class DevicePreferences(val useWildcardForRemoteHostDelete: Boolean = false) { + fun isBlankOrStar(remoteHost : String) : Boolean { + return (remoteHost.isBlank() || remoteHost == "*") + } + + fun getDefaultWildcard() : String + { + return if (useWildcardForRemoteHostDelete) "*" else "" + } + + fun getBackupWildcard() : String + { + return if (useWildcardForRemoteHostDelete) "" else "*" + } +} + interface IIGDDevice { fun getDisplayName(): String fun getIpAddress(): String @@ -12,21 +28,36 @@ interface IIGDDevice { fun supportsAction(actionName: String): Boolean fun getActionInvocation(actionName: String): ActionInvocation<*> val udn: String + var devicePreferences: DevicePreferences +} + +data class DeviceDetails(val displayName: String, val ipAddress: String, val upnpVersion: Int, val udn : String) +{ + companion object { + fun fromRemoteDevice(remoteDevice: RemoteDevice): DeviceDetails { + return DeviceDetails( + remoteDevice.displayString, + remoteDevice.identity.descriptorURL.host, + remoteDevice.type.version, + remoteDevice.root.identity.udn.toString() + ) + } + } } // has state including rules. add update those rules. and do a upnp action. and sort. -class IGDDevice(private val rootDevice: RemoteDevice, private val wanIPService: RemoteService) : +class IGDDevice(val deviceDetails: DeviceDetails, override var devicePreferences : DevicePreferences, private val wanIPService: RemoteService) : IIGDDevice { // nullrefs warning: this is SSDP response, very possible for some fields to be null, DeviceDetails constructor allows it. // the best bet on ip is (rootDevice!!.identity.descriptorURL.host) imo. since that is what we use in RetreiveRemoteDescriptors class // which then calls remoteDeviceAdded (us). presentationURI can be and is null sometimes. - private val displayName: String = this.rootDevice.displayString //i.e. Nokia IGD Version 2.00 - private val ipAddress: String = rootDevice.identity.descriptorURL.host //i.e. 192.168.18.1 + private val displayName: String = deviceDetails.displayName//i.e. Nokia IGD Version 2.00 + private val ipAddress: String = deviceDetails.ipAddress //i.e. 192.168.18.1 //this.rootDevice!!.details.presentationURI.host - private val upnpTypeVersion: Int = this.rootDevice.type.version //i.e. 2 + private val upnpTypeVersion: Int = deviceDetails.upnpVersion//i.e. 2 private val actionsMap: MutableMap> = mutableMapOf() - override val udn = rootDevice.root.identity.udn.toString() + override val udn = deviceDetails.udn //private val upnpType : String = this.rootDevice.type.type //i.e. InternetGatewayDevice // can cause crashes if friendlyName is null diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt index 5786aa3..995b687 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/PortMapping.kt @@ -3,6 +3,8 @@ package com.shinjiindustrial.portmapper.domain import android.os.Parcelable import android.os.SystemClock import com.shinjiindustrial.portmapper.PortForwardApplication +import com.shinjiindustrial.portmapper.persistence.DevicesDao +import com.shinjiindustrial.portmapper.persistence.DevicesEntity import com.shinjiindustrial.portmapper.persistence.PortMappingEntity import kotlinx.parcelize.Parcelize @@ -257,4 +259,7 @@ fun formatShortName(protocol: String, externalIp: String, externalPort: String): } fun PortMappingEntity.getPrefs(): PortMappingPref = - PortMappingPref(this.autoRenew, this.desiredLeaseDuration) \ No newline at end of file + PortMappingPref(this.autoRenew, this.desiredLeaseDuration) + +fun DevicesEntity.getPrefs(): DevicePreferences = + DevicePreferences(this.useWildcardForRemoteHostDelete) diff --git a/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt b/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt index b376e72..f40bd7e 100644 --- a/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt +++ b/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt @@ -14,6 +14,7 @@ import com.shinjiindustrial.portmapper.PortMappingRequest import com.shinjiindustrial.portmapper.common.Event import com.shinjiindustrial.portmapper.domain.ACTION_NAMES import com.shinjiindustrial.portmapper.domain.AndroidUpnpServiceConfigurationImpl +import com.shinjiindustrial.portmapper.domain.ClingIGDDevice import com.shinjiindustrial.portmapper.domain.IIGDDevice import com.shinjiindustrial.portmapper.domain.NetworkInterfaceInfo import com.shinjiindustrial.portmapper.domain.PortMapping @@ -283,7 +284,7 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co return (upnpService.configuration as AndroidUpnpServiceConfigurationImpl).NetworkInterfacesUsedInfos } - override val deviceFoundEvent = Event() + override val deviceFoundEvent = Event() // private val _deviceFoundEvent = MutableSharedFlow( // replay = 0, extraBufferCapacity = 1 // ) From 3ebedfd396dab2186710e1506c71cde661bf30af Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Sun, 31 Aug 2025 21:04:30 -0400 Subject: [PATCH 12/24] logger injection with crashlytics support --- .../portmapper/LogViewActivity.kt | 12 +- .../shinjiindustrial/portmapper/Logging.kt | 94 ++++++++++++++ .../portmapper/MainActivity.kt | 49 ++----- .../portmapper/PortForwardApplication.kt | 10 +- .../portmapper/PortViewModel.kt | 5 +- .../portmapper/UPnPManager.kt | 120 +++++++++--------- .../portmapper/domain/ClingUtil.kt | 14 +- .../portmapper/ui/RuleCreationDialog.kt | 9 +- .../portmapper/client/UpnpClient.kt | 5 +- .../portmapper/LoggingModule.kt | 58 +++++++++ 10 files changed, 255 insertions(+), 121 deletions(-) create mode 100644 app/src/main/java/com/shinjiindustrial/portmapper/Logging.kt create mode 100644 app/src/release/java/com/shinjiindustrial/portmapper/LoggingModule.kt diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt index 451b13c..6377c0d 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt @@ -41,12 +41,16 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import com.shinjiindustrial.portmapper.ui.theme.MyApplicationTheme import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint class LogViewActivity : ComponentActivity() { val settingsViewModel: SettingsViewModel by viewModels() + @Inject + lateinit var logStoreRepository : LogStoreRepository + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { @@ -85,14 +89,14 @@ class LogViewActivity : ComponentActivity() { actions = { IconButton(onClick = { - PortForwardApplication.Logs.clear() + logStoreRepository.logs.clear() }) { Icon(Icons.Default.DeleteSweep, contentDescription = "Clear Logs") } IconButton(onClick = { CopyTextToClipboard( - PortForwardApplication.Logs.joinToString( + logStoreRepository.logs.joinToString( "\n" ) ) @@ -157,7 +161,7 @@ class LogViewActivity : ComponentActivity() { fun LogViewContent() { val scrollToBottom = this.intent.getBooleanExtra(PortForwardApplication.ScrollToBottom, false) - val logLines = PortForwardApplication.Logs + val logLines = logStoreRepository.logs val themeState by settingsViewModel.uiState.collectAsStateWithLifecycle() LogViewInner(themeState, logLines, scrollToBottom) } @@ -179,7 +183,7 @@ class LogViewActivity : ComponentActivity() { resultData?.data?.also { uri -> val contentResolver = applicationContext.contentResolver contentResolver.openOutputStream(uri)?.bufferedWriter().use { out -> - out?.write(PortForwardApplication.Logs.toString()) + out?.write(logStoreRepository.logs.toString()) } } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/Logging.kt b/app/src/main/java/com/shinjiindustrial/portmapper/Logging.kt new file mode 100644 index 0000000..9fc8f06 --- /dev/null +++ b/app/src/main/java/com/shinjiindustrial/portmapper/Logging.kt @@ -0,0 +1,94 @@ +package com.shinjiindustrial.portmapper + +import android.os.Handler +import android.os.Looper +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import javax.inject.Singleton + +enum class FirebaseRoute { NO_OPINION, SILENT, BREADCRUMB, NON_FATAL } + +data class LogOptions( + // logs everything "SEVERE" by default + val firebase: FirebaseRoute = FirebaseRoute.NO_OPINION, +) + +interface ILogger { + fun log( + level: Level, + event: String, + t: Throwable? = null, + opts: LogOptions = LogOptions() + ) +} + +interface ILogSink { + fun log(level: Level, msg: String, t: Throwable?, opts: LogOptions) +} + +class LogcatSink : ILogSink { + val logger: Logger = Logger.getLogger("PortMapper") + override fun log(level: Level, msg: String, t: Throwable?, opts: LogOptions) { + logger.log(level, msg) + } +} + +class LogStringBuilderSink( + private val logStoreRepository: LogStoreRepository, +) : ILogSink { + private val main = Handler(Looper.getMainLooper()) + override fun log(level: Level, msg: String, t: Throwable?, opts: LogOptions) { + main.post { + val prefix = when (level) { + Level.INFO -> "I: " + Level.WARNING -> "W: " + Level.SEVERE -> "E: " + else -> return@post // i.e. do not log + } + logStoreRepository.logs.add(prefix + msg) + } + } +} + +@Singleton +class LogStoreRepository @Inject constructor( +) { + var logs: SnapshotStateList = mutableStateListOf() +} + +class CompositeLogger( + private val sinks: Set +) : ILogger { + override fun log(level: Level, event: String, t: Throwable?, opts: LogOptions) { + sinks.forEach { it.log(level, event, t, opts) } + } +} + + +@Module +@InstallIn(SingletonComponent::class) +object BaseLoggingModule { + + @Provides + @IntoSet + @Singleton + fun provideLogcatLogger() : ILogSink = LogcatSink() + + @Provides + @IntoSet + @Singleton + fun provideInMemoryStringBuilderLogger(store : LogStoreRepository) : ILogSink = LogStringBuilderSink(store) + + @Provides @Singleton + fun provideLogger( + sinks: Set<@JvmSuppressWildcards ILogSink> + ): ILogger = CompositeLogger(sinks) +} diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt index 1510250..d653b23 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape @@ -126,32 +127,6 @@ fun GetPsuedoSlot(): Int { return PseudoSlotCounter } - -class StringBuilderHandler(private val stringBuilder: SnapshotStateList) : - java.util.logging.Handler() { - - override fun publish(record: LogRecord?) { - record?.let { - val prefix = when (it.level) { - Level.INFO -> "I: " - Level.WARNING -> "W: " - Level.SEVERE -> "E: " - else -> return // i.e. do not log - } - stringBuilder.add(prefix + it.message) - } - } - - // - override fun flush() { - // Nothing to do here since StringBuilder doesn't need to be flushed - } - - override fun close() { - // Nothing to do here since StringBuilder doesn't need to be closed - } -} - enum class Protocol(val protocol: String) { TCP("TCP"), UDP("UDP"), @@ -230,7 +205,6 @@ class MainActivity : ComponentActivity() { onBackPressedDispatcher.addCallback(this) { - println("My Back Pressed Callback") if (portViewModel.inMultiSelectMode.value) { portViewModel.clearSelection() } else { @@ -731,16 +705,21 @@ fun ColumnScope.CreateRuleContents( }, state = rememberTooltipState() ) { - //MaterialTheme.colorScheme.outline - Checkbox( - autoRenew.value, - onCheckedChange = { autoRenew.value = it }, - // uncheckedColor == border color - colors = CheckboxDefaults.colors(uncheckedColor = AdditionalColors.TextColorWeak) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.heightIn(min = 48.dp) + ) { + //MaterialTheme.colorScheme.outline + Checkbox( + autoRenew.value, + onCheckedChange = { autoRenew.value = it }, + // uncheckedColor == border color + colors = CheckboxDefaults.colors(uncheckedColor = AdditionalColors.TextColorWeak) - ) - Text("Auto Renew", color = AdditionalColors.TextColor) + ) + Text("Auto Renew", color = AdditionalColors.TextColor) + } } } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortForwardApplication.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortForwardApplication.kt index 54101aa..674fd74 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortForwardApplication.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortForwardApplication.kt @@ -5,9 +5,7 @@ import android.content.Context import android.widget.Toast import androidx.activity.ComponentActivity import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.unit.dp import com.shinjiindustrial.portmapper.domain.PortMappingWithPref import dagger.hilt.android.HiltAndroidApp @@ -37,9 +35,6 @@ class PortForwardApplication : Application() { instance = this appContext = applicationContext - val logger = Logger.getLogger("") - logger.addHandler(StringBuilderHandler(Logs)) - println("PortForwardApplication onCreate Finished") } @@ -54,8 +49,9 @@ class PortForwardApplication : Application() { lateinit var currentSingleSelectedObject: MutableState var showContextMenu: MutableState = mutableStateOf(false) var PaddingBetweenCreateNewRuleRows = 4.dp - var Logs: SnapshotStateList = mutableStateListOf() - var OurLogger: Logger = Logger.getLogger("PortMapper") + + // can create a singleton logs repo + val ScrollToBottom = "ScrollToBottom" var crashlyticsEnabled: Boolean = false diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index 0fa62b9..2bee96c 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -52,6 +52,7 @@ class PortViewModel @Inject constructor( private val upnpRepository: UpnpManager, private val preferencesRepository: PreferencesManager, private val savedStateHandle: SavedStateHandle, + private val ourLogger: ILogger, @ApplicationScope private val applicationScope: CoroutineScope, ) : ViewModel() { @@ -430,7 +431,7 @@ class PortViewModel @Inject constructor( return@launch } } catch (exception: Exception) { - PortForwardApplication.OurLogger.log( + ourLogger.log( Level.SEVERE, "Delete Original Port Mappings Failed: " + exception.message + exception.stackTraceToString() ) @@ -483,7 +484,7 @@ class PortViewModel @Inject constructor( _events.emit(UiEvent.SnackBarViewShortNoEvent("Success")) } } catch (exception: Exception) { - PortForwardApplication.OurLogger.log( + ourLogger.log( Level.SEVERE, "Delete Original Port Mappings Failed: " + exception.message + exception.stackTraceToString() ) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index 6f32289..c0d8041 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -1,8 +1,6 @@ package com.shinjiindustrial.portmapper import android.os.SystemClock -import android.util.Log -import com.shinjiindustrial.portmapper.PortForwardApplication.Companion.OurLogger import com.shinjiindustrial.portmapper.client.IUpnpClient import com.shinjiindustrial.portmapper.client.UPnPCreateMappingResult import com.shinjiindustrial.portmapper.client.UPnPCreateMappingWrapperResult @@ -52,6 +50,7 @@ class UpnpManager @Inject constructor( private val upnpClient: IUpnpClient, private val portMappingDao: PortMappingDao, private val devicesDao: DevicesDao, + private val ourLogger: ILogger, @ApplicationScope private val applicationContext: CoroutineScope ) { private val renewMutex = Mutex() @@ -78,7 +77,7 @@ class UpnpManager @Inject constructor( upnpClient.deviceFoundEvent += { device -> // this is on the cling thread if (_devices.value.any { it.getIpAddress() == device.deviceDetails.ipAddress }) { - OurLogger.log( + ourLogger.log( Level.WARNING, "device ${device.deviceDetails.udn} came in again at ${device.deviceDetails.ipAddress}. ignoring." ) @@ -88,9 +87,9 @@ class UpnpManager @Inject constructor( devicesDao.getByPrimaryKey(device.deviceDetails.ipAddress, device.deviceDetails.udn) } if (devicePreferencesNullable == null) { - OurLogger.log(Level.INFO, "Preferences not found for device") + ourLogger.log(Level.INFO, "Preferences not found for device") } else { - OurLogger.log(Level.INFO, "Preferences found for device") + ourLogger.log(Level.INFO, "Preferences found for device") } val igdDevice = IGDDevice(device.deviceDetails, @@ -387,7 +386,7 @@ class UpnpManager @Inject constructor( return res } catch (exception: Exception) { val enableDisableString = if (enable) "Enable" else "Disable" - OurLogger.log( + ourLogger.log( Level.SEVERE, "$enableDisableString Port Mapping Failed: " + exception.message + exception.stackTraceToString() ) @@ -409,7 +408,7 @@ class UpnpManager @Inject constructor( addOrUpdateForRenew(res, portMapping) res } catch (exception: Exception) { - OurLogger.log( + ourLogger.log( Level.SEVERE, "Renew Port Mappings Failed: " + exception.message + exception.stackTraceToString() ) @@ -432,7 +431,7 @@ class UpnpManager @Inject constructor( addOrUpdateForRenew(res, portMapping) return res } catch (exception: Exception) { - OurLogger.log( + ourLogger.log( Level.SEVERE, "Renew Port Mappings Failed: " + exception.message + exception.stackTraceToString() ) @@ -457,31 +456,28 @@ class UpnpManager @Inject constructor( val portMappingFallback = portMapping.copy(RemoteHost = device.devicePreferences.getBackupWildcard()) val fallbackResult = upnpClient.deletePortMapping(device, portMappingFallback) //TODO when() - if (fallbackResult is UPnPResult.Success) - { - OurLogger.log(Level.INFO, "Fallback Delete Success") - // set new default to be the opposite of the old - device.devicePreferences = device.devicePreferences.copy(useWildcardForRemoteHostDelete = !device.devicePreferences.useWildcardForRemoteHostDelete) - devicesDao.upsert( - DevicesEntity( - device.getIpAddress(), - device.udn, - device.devicePreferences.useWildcardForRemoteHostDelete + when(fallbackResult) { + is UPnPResult.Success -> { + ourLogger.log(Level.INFO, "Fallback Delete Success") + // set new default to be the opposite of the old + device.devicePreferences = device.devicePreferences.copy(useWildcardForRemoteHostDelete = !device.devicePreferences.useWildcardForRemoteHostDelete) + devicesDao.upsert( + DevicesEntity( + device.getIpAddress(), + device.udn, + device.devicePreferences.useWildcardForRemoteHostDelete + ) ) - ) - return fallbackResult - } - else if (fallbackResult is UPnPResult.Failure) - { - // this means we maybe should not have retried - // log both original and fallback errors to firebase - OurLogger.log(Level.SEVERE, "Fallback Delete Failed") - // log to firebase - OurLogger.log(Level.SEVERE, "Fallback Original and Delete Failed") - OurLogger.log(Level.SEVERE, result.details.toString()) - OurLogger.log(Level.SEVERE, fallbackResult.details.toString()) - // - return fallbackResult + return fallbackResult + } + is UPnPResult.Failure -> { + // this means we maybe should not have retried + // log both original and fallback errors to firebase + // log to firebase + ourLogger.log(Level.SEVERE, "Fallback Original and Delete Failed. Original: " + + result.details.toString() + "\n Fallback: " + fallbackResult.details.toString()) + return fallbackResult + } } } else @@ -506,20 +502,24 @@ class UpnpManager @Inject constructor( val result = deletePortMappingWithFallback(device, pm) if (result is UPnPResult.Success) { + ourLogger.log( + Level.INFO, + "Successfully deleted rule (${pm.shortName()})." + ) removeMapping(portMappingWithPref) } else if (result is UPnPResult.Failure) { - PortForwardApplication.Companion.OurLogger.log( + ourLogger.log( Level.SEVERE, "Failed to delete rule (${pm.shortName()})." ) - PortForwardApplication.Companion.OurLogger.log(Level.SEVERE, + ourLogger.log(Level.SEVERE, result.details.toString()) } return result } catch (exception: Exception) { - OurLogger.log( + ourLogger.log( Level.SEVERE, "Delete Port Mappings Failed: " + exception.message + exception.stackTraceToString() ) @@ -545,7 +545,7 @@ class UpnpManager @Inject constructor( val result = deletePortMappingWithFallback(device, portMapping) if (result is UPnPResult.Success) { - PortForwardApplication.Companion.OurLogger.log( + ourLogger.log( Level.INFO, "Successfully deleted rule (${portMapping.shortName()})." ) @@ -553,17 +553,17 @@ class UpnpManager @Inject constructor( } else if (result is UPnPResult.Failure) { - PortForwardApplication.Companion.OurLogger.log( + ourLogger.log( Level.SEVERE, "Failed to delete rule (${portMapping.shortName()})." ) - PortForwardApplication.Companion.OurLogger.log(Level.SEVERE, + ourLogger.log(Level.SEVERE, result.details.toString()) } result } catch (exception: Exception) { - OurLogger.log( + ourLogger.log( Level.SEVERE, "Delete Port Mappings Failed: " + exception.message + exception.stackTraceToString() ) @@ -620,7 +620,7 @@ class UpnpManager @Inject constructor( result } } catch (exception: Exception) { - OurLogger.log( + ourLogger.log( Level.SEVERE, "Create Rule Failed: " + exception.message + exception.stackTraceToString() ) @@ -647,7 +647,7 @@ class UpnpManager @Inject constructor( } catch (exception: Exception) { val enableDisableString = if (enable) "Enable" else "Disable" - OurLogger.log( + ourLogger.log( Level.SEVERE, "$enableDisableString Port Mappings Failed: " + exception.message + exception.stackTraceToString() ) @@ -702,7 +702,7 @@ class UpnpManager @Inject constructor( val createMappingResult = upnpClient.createPortMappingRule(device, portMappingRequest) when (createMappingResult) { is UPnPCreateMappingResult.Success -> { - OurLogger.log( + ourLogger.log( Level.INFO, "Successfully $createContext rule (${ portMappingRequest.realize().shortName() @@ -724,7 +724,7 @@ class UpnpManager @Inject constructor( portMappingRequest.protocol, ) if (result is UPnPGetSpecificMappingResult.Success) { - PortForwardApplication.Companion.OurLogger.log( + ourLogger.log( Level.INFO, "Successfully read back our new rule (${result.requestInfo.shortName()})" ) @@ -733,11 +733,11 @@ class UpnpManager @Inject constructor( val rule = formatShortName(portMappingRequest.protocol, portMappingRequest.externalIp, portMappingRequest.externalPort) - PortForwardApplication.Companion.OurLogger.log( + ourLogger.log( Level.SEVERE, "Failed to read back our new rule ($rule). Remote Host: ${portMappingRequest.remoteHost}" ) - PortForwardApplication.Companion.OurLogger.log(Level.SEVERE, result.details.toString()) + ourLogger.log(Level.SEVERE, result.details.toString()) } return result.toCreatePortMappingResult() @@ -745,11 +745,11 @@ class UpnpManager @Inject constructor( } is UPnPCreateMappingResult.Failure -> { - PortForwardApplication.Companion.OurLogger.log( + ourLogger.log( Level.SEVERE, "Failed to $createContext rule (${portMappingRequest.realize().shortName()})." ) - PortForwardApplication.Companion.OurLogger.log(Level.SEVERE, + ourLogger.log(Level.SEVERE, createMappingResult.details.toString()) val result = UPnPCreateMappingWrapperResult.Failure( @@ -765,17 +765,17 @@ class UpnpManager @Inject constructor( val device: IIGDDevice = getIGDDevice(externalIp) val timeTakenMillis = measureTimeMillis { if (device.supportsAction(ACTION_NAMES.GetGenericPortMappingEntry)) { - OurLogger.log( + ourLogger.log( Level.INFO, "Enumerating Port Listings using GetGenericPortMappingEntry" ) getAllPortMappingsUsingGenericPortMappingEntry(device) } else { //TODO firebase integration - OurLogger.log(Level.SEVERE, "device does not have GetGenericPortMappingEntry") + ourLogger.log(Level.SEVERE, "device does not have GetGenericPortMappingEntry") } } - OurLogger.log(Level.INFO, "Time to enumerate ports: $timeTakenMillis ms") + ourLogger.log(Level.INFO, "Time to enumerate ports: $timeTakenMillis ms") } private fun isRuleOurs( @@ -806,7 +806,7 @@ class UpnpManager @Inject constructor( val result = upnpClient.getGenericPortMappingRule(device, slotIndex) when (result) { is UPnPGetGenericMappingResult.Success -> { - PortForwardApplication.Companion.OurLogger.log(Level.INFO, "GetGenericPortMapping succeeded for entry $slotIndex") + ourLogger.log(Level.INFO, "GetGenericPortMapping succeeded for entry $slotIndex") val portMapping = result.resultingMapping val entity = portMappingDao.getByPrimaryKey( device.getIpAddress(), @@ -816,26 +816,26 @@ class UpnpManager @Inject constructor( ) var pref: PortMappingPref? = null if (isRuleOurs(entity, device, portMapping)) { - OurLogger.log( + ourLogger.log( Level.INFO, "The rule is ours: ${portMapping.shortName()}" ) pref = entity!!.getPrefs() } else { - OurLogger.log( + ourLogger.log( Level.INFO, "The rule is not ours: ${portMapping.shortName()}" ) } addOrUpdateMapping(PortMappingWithPref(portMapping, pref)) //!! success = true - OurLogger.log(Level.INFO, portMapping.toStringFull()) + ourLogger.log(Level.INFO, portMapping.toStringFull()) retryCount = 0 } is UPnPGetGenericMappingResult.Failure -> { - OurLogger.log( + ourLogger.log( Level.INFO, "GetGenericPortMapping failed for entry $slotIndex: ${result.details}. NOTE: This is normal." ) @@ -847,11 +847,11 @@ class UpnpManager @Inject constructor( shouldRetry = true // network issue.. retrying does work. retryCount++ } - OurLogger.log(Level.SEVERE, "GetGenericPortMapping threw ${e.message}") + ourLogger.log(Level.SEVERE, "GetGenericPortMapping threw ${e.message}") } if (shouldRetry) { - OurLogger.log(Level.INFO, "Retrying for entry $slotIndex") + ourLogger.log(Level.INFO, "Retrying for entry $slotIndex") continue } @@ -863,7 +863,7 @@ class UpnpManager @Inject constructor( slotIndex++ if (slotIndex > 65535) { - OurLogger.log(Level.SEVERE, "CRITICAL ERROR ENUMERATING PORTS, made it past 65535") + ourLogger.log(Level.SEVERE, "CRITICAL ERROR ENUMERATING PORTS, made it past 65535") } } } @@ -891,7 +891,7 @@ class UpnpManager @Inject constructor( // get dao portMappingDao add(igdDevice) - OurLogger.log( + ourLogger.log( Level.INFO, "Added Device ${igdDevice.getDisplayName()} at ${igdDevice.getIpAddress()}." ) @@ -905,7 +905,7 @@ class UpnpManager @Inject constructor( if (portMappings.value.containsKey(id)) { portMappingWithPrefList.add(portMappings.value[id]!!) } else { - OurLogger.log(Level.SEVERE, "Cannot find port mapping with key $id") + ourLogger.log(Level.SEVERE, "Cannot find port mapping with key $id") } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt index 4d3b694..4a79f30 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt @@ -1,6 +1,6 @@ package com.shinjiindustrial.portmapper.domain -import com.shinjiindustrial.portmapper.PortForwardApplication.Companion.OurLogger +import com.shinjiindustrial.portmapper.ILogger import org.fourthline.cling.model.meta.RemoteDevice import org.fourthline.cling.model.meta.RemoteService import java.util.logging.Level @@ -47,11 +47,11 @@ val ActionNames: List = listOf( data class ClingIGDDevice(val deviceDetails: DeviceDetails, val remoteService: RemoteService) -fun RemoteDevice.getIGDDevice(): ClingIGDDevice? { +fun RemoteDevice.getIGDDevice(ourLogger : ILogger): ClingIGDDevice? { if (this.type.type.equals(UPnPNames.InternetGatewayDevice)) // version agnostic { - OurLogger.log( + ourLogger.log( Level.INFO, "Device ${this.displayString} is of interest, type is ${this.type}" ) @@ -74,25 +74,25 @@ fun RemoteDevice.getIGDDevice(): ClingIGDDevice? { return igdDevice } else { - OurLogger.log( + ourLogger.log( Level.SEVERE, "WanConnectionDevice does not have WanIPConnection service" ) } } else { - OurLogger.log( + ourLogger.log( Level.SEVERE, "WanConnectionDevice not found under WanDevice" ) } } else { - OurLogger.log( + ourLogger.log( Level.SEVERE, "WanDevice not found under InternetGatewayDevice" ) } } else { - OurLogger.log( + ourLogger.log( Level.INFO, "Device ${this.displayString} is NOT of interest, type is ${this.type}" ) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt index 115f670..78d6c74 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt @@ -271,10 +271,11 @@ fun RuleCreationDialog( if (ruleToEdit != null) { if (ruleToEdit == portMappingRequestInput) { - PortForwardApplication.OurLogger.log( - Level.INFO, - "Rule has not changed. Nothing to do." - ) + // TODO add this back. composition local? +// ourLogger.log( +// Level.INFO, +// "Rule has not changed. Nothing to do." +// ) navController.popBackStack() } else { val oldRulesToDelete = ruleToEdit.splitIntoRules() diff --git a/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt b/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt index f40bd7e..4d68b58 100644 --- a/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt +++ b/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview import com.shinjiindustrial.portmapper.CreateRuleContents import com.shinjiindustrial.portmapper.GetPsuedoSlot +import com.shinjiindustrial.portmapper.ILogger import com.shinjiindustrial.portmapper.PortForwardApplication import com.shinjiindustrial.portmapper.PortMappingRequest import com.shinjiindustrial.portmapper.common.Event @@ -34,7 +35,7 @@ import java.util.logging.Level import javax.inject.Inject import kotlin.coroutines.resume -class UpnpClient @Inject constructor(@ApplicationContext private val context: Context) : IUpnpClient { +class UpnpClient @Inject constructor(@ApplicationContext private val context: Context, private val ourLogger : ILogger) : IUpnpClient { sealed class ClingExecutionResult { data class Success(val invocation: ActionInvocation<*>) : ClingExecutionResult() @@ -67,7 +68,7 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co // complete metadata override fun remoteDeviceAdded(registry: Registry, rootDevice: RemoteDevice) { - val igdDevice = rootDevice.getIGDDevice() + val igdDevice = rootDevice.getIGDDevice(ourLogger) if (igdDevice != null) { deviceFoundEvent.invoke(igdDevice) } diff --git a/app/src/release/java/com/shinjiindustrial/portmapper/LoggingModule.kt b/app/src/release/java/com/shinjiindustrial/portmapper/LoggingModule.kt new file mode 100644 index 0000000..8c873ed --- /dev/null +++ b/app/src/release/java/com/shinjiindustrial/portmapper/LoggingModule.kt @@ -0,0 +1,58 @@ +package com.shinjiindustrial.portmapper + +import com.google.firebase.crashlytics.FirebaseCrashlytics +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import java.util.logging.Level +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object LoggingModule { + + @Provides + @Singleton + fun provideFirebaseCrashlytics(): FirebaseCrashlytics = + FirebaseCrashlytics.getInstance() + + @Provides + @IntoSet + @Singleton + fun providesCrashlyticsSink(crashlytics: FirebaseCrashlytics): ILogSink = CrashlyticsSink(crashlytics) + + class CrashlyticsSink( + private val crashlytics: FirebaseCrashlytics, + ) : ILogSink { + override fun log(level: Level, msg: String, t: Throwable?, opts: LogOptions) { + var firebaseLevel = opts.firebase + if (firebaseLevel == FirebaseRoute.SILENT) return + if (level == Level.SEVERE && firebaseLevel == FirebaseRoute.NO_OPINION) + { + firebaseLevel = FirebaseRoute.NON_FATAL + } + + when (firebaseLevel) { + FirebaseRoute.BREADCRUMB -> { + // Breadcrumb-style line (Crashlytics keeps a rolling buffer) + crashlytics.log("${level.name}: $msg") + } + FirebaseRoute.NON_FATAL -> { + // Non-fatal crash report + if (t != null) + { + crashlytics.log("${level.name}: $msg") + crashlytics.recordException(t) + } + else + { + crashlytics.recordException(RuntimeException("NonFatal: $msg")) + } + } + else -> Unit + } + } + } +} \ No newline at end of file From af4f3c62a05da2a271f3a63ff2c0d4780e45291f Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Sun, 31 Aug 2025 21:13:24 -0400 Subject: [PATCH 13/24] update version --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 046d7af..38f8842 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,8 +24,8 @@ android { applicationId "com.shinjiindustrial.portmapper" minSdk 21 targetSdk 35 - versionCode 7 - versionName '1.2.0' + versionCode 8 + versionName '1.2.1' multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From ecea7cd4724a507e80f73db4f3cc21cdd6e42d87 Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Mon, 1 Sep 2025 14:41:18 -0400 Subject: [PATCH 14/24] fix nullref on timeout --- .../java/com/shinjiindustrial/portmapper/UPnPManager.kt | 2 +- .../com/shinjiindustrial/portmapper/client/IUpnpClient.kt | 6 +++++- .../com/shinjiindustrial/portmapper/client/UpnpClient.kt | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index c0d8041..0c8ad51 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -450,7 +450,7 @@ class UpnpManager @Inject constructor( portMapping.copy(RemoteHost = device.devicePreferences.getDefaultWildcard())) if (result is UPnPResult.Failure) { - if (result.details.response.statusCode == 500) + if (result.details.response != null && result.details.response.statusCode == 500) { // add breadcrumb? no only on another failure val portMappingFallback = portMapping.copy(RemoteHost = device.devicePreferences.getBackupWildcard()) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt b/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt index c650f63..645409f 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt @@ -88,9 +88,13 @@ sealed class UPnPResult { data class Failure(val details: FailureDetails) : UPnPResult() } -class FailureDetails(val reason: String, val response: UpnpResponse) +class FailureDetails(val reason: String, val response: UpnpResponse?) { override fun toString() : String{ + if (response == null) + { + return "\t${reason}. No response from router." + } return "\t${reason}\t${response}" } } \ No newline at end of file diff --git a/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt b/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt index 4d68b58..45e5cd3 100644 --- a/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt +++ b/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt @@ -39,7 +39,7 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co sealed class ClingExecutionResult { data class Success(val invocation: ActionInvocation<*>) : ClingExecutionResult() - data class Failure(val invocation: ActionInvocation<*>, val operation : UpnpResponse, val defaultMsg : String) : + data class Failure(val invocation: ActionInvocation<*>, val operation : UpnpResponse?, val defaultMsg : String) : ClingExecutionResult() } @@ -114,7 +114,7 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co override fun failure( invocation: ActionInvocation<*>?, - operation: UpnpResponse, + operation: UpnpResponse?, defaultMsg: String ) { val result = ClingExecutionResult.Failure(invocation!!, operation, defaultMsg) From d47b5302b780213b21463ad5e742f834185e52cd Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Mon, 1 Sep 2025 14:41:36 -0400 Subject: [PATCH 15/24] update timeout from 60s to 10s. --- .../domain/AndroidUpnpServiceConfiguration.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/AndroidUpnpServiceConfiguration.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/AndroidUpnpServiceConfiguration.kt index 3213dfc..707c855 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/AndroidUpnpServiceConfiguration.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/AndroidUpnpServiceConfiguration.kt @@ -1,12 +1,17 @@ package com.shinjiindustrial.portmapper.domain import android.content.Context +import android.os.Build import com.shinjiindustrial.portmapper.common.NetworkType import org.fourthline.cling.android.AndroidNetworkAddressFactory import org.fourthline.cling.android.AndroidUpnpServiceConfiguration import org.fourthline.cling.binding.xml.ServiceDescriptorBinder import org.fourthline.cling.binding.xml.UDA10ServiceDescriptorBinderImpl +import org.fourthline.cling.model.ServerClientTokens +import org.fourthline.cling.transport.impl.jetty.StreamClientConfigurationImpl +import org.fourthline.cling.transport.impl.jetty.StreamClientImpl import org.fourthline.cling.transport.spi.NetworkAddressFactory +import org.fourthline.cling.transport.spi.StreamClient class AndroidUpnpServiceConfigurationImpl(context: Context) : AndroidUpnpServiceConfiguration() { @@ -16,6 +21,22 @@ class AndroidUpnpServiceConfigurationImpl(context: Context) : AndroidUpnpService Context = context } + // we override this in order to set timeout + override fun createStreamClient(): StreamClient<*>? { + // Use Jetty + return StreamClientImpl( + object : StreamClientConfigurationImpl(getSyncProtocolExecutorService(), 10) { + override fun getUserAgentValue(majorVersion: Int, minorVersion: Int): String { + // UPNP VIOLATION: Some devices (e.g., Synology NAS) expect "Android" in User-Agent + val tokens = ServerClientTokens(majorVersion, minorVersion) + tokens.setOsName("Android") + tokens.setOsVersion(Build.VERSION.RELEASE) + return tokens.toString() + } + } + ) + } + override fun getServiceDescriptorBinderUDA10(): ServiceDescriptorBinder { return UDA10ServiceDescriptorBinderImpl() } From 8ce23c435b45fa8180fefd1ba1009a84a45ea3c2 Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Mon, 1 Sep 2025 16:18:21 -0400 Subject: [PATCH 16/24] snackbarmanager architecture --- .../portmapper/MainActivity.kt | 52 ------ .../portmapper/PortMapperMainScreen.kt | 38 +---- .../portmapper/PortMapperNavGraph.kt | 13 ++ .../portmapper/PortViewModel.kt | 69 ++++---- .../portmapper/SnackbarManager.kt | 156 ++++++++++++++++++ .../portmapper/ui/RuleCreationDialog.kt | 40 +---- 6 files changed, 213 insertions(+), 155 deletions(-) create mode 100644 app/src/main/java/com/shinjiindustrial/portmapper/SnackbarManager.kt diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt index d653b23..ace3c3a 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt @@ -144,58 +144,6 @@ class MainActivity : ComponentActivity() { private val portViewModel: PortViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels() - companion object { - - fun viewLogCallback() { - val intent = - Intent(PortForwardApplication.CurrentActivity, LogViewActivity::class.java) - intent.putExtra(PortForwardApplication.ScrollToBottom, true) - PortForwardApplication.CurrentActivity?.startActivity(intent) - } - - fun showSnackBarViewLog(message: String) { - showSnackBar(message, "View Log", SnackbarDuration.Long, ::viewLogCallback) - } - - fun showSnackBarShortNoAction(message: String) { - showSnackBar(message, null, SnackbarDuration.Short) - } - - fun showSnackBarLongNoAction(message: String) { - showSnackBar(message, null, SnackbarDuration.Long) - } - - fun showSnackBar( - message: String, - action: String?, - duration: SnackbarDuration, - onAction: () -> Unit = { } - ) { - if (OurSnackbarHostState == null) { - PortForwardApplication.ShowToast( - message, - if ((duration == SnackbarDuration.Long || duration == SnackbarDuration.Indefinite)) Toast.LENGTH_LONG else Toast.LENGTH_SHORT - ) - } else { - GlobalScope.launch(Dispatchers.Main) { - - val snackbarResult = OurSnackbarHostState!!.showSnackbar( - message, - action, - (duration == SnackbarDuration.Indefinite), - duration - ) - when (snackbarResult) { - SnackbarResult.Dismissed -> {} - SnackbarResult.ActionPerformed -> onAction() - } - } - } - } - - var OurSnackbarHostState: SnackbarHostState? = null - } - @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt index 4d35025..bfe43b9 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt @@ -5,7 +5,6 @@ import android.text.Html import android.text.Spanned import android.text.method.LinkMovementMethod import android.widget.TextView -import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth @@ -27,9 +26,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold -import androidx.compose.material3.Snackbar -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -39,7 +35,6 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -58,8 +53,6 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import com.example.myapplication.R -import com.shinjiindustrial.portmapper.MainActivity.Companion.OurSnackbarHostState -import com.shinjiindustrial.portmapper.MainActivity.Companion.showSnackBarViewLog import com.shinjiindustrial.portmapper.common.NetworkType import com.shinjiindustrial.portmapper.domain.PortMappingWithPref import com.shinjiindustrial.portmapper.ui.BottomSheetSortBy @@ -83,20 +76,6 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState val inMultiSelectMode by portViewModel.inMultiSelectMode.collectAsStateWithLifecycle() val selectedIds by portViewModel.selectedIds.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { - portViewModel.events.collect { ev -> - if (ev is PortViewModel.UiEvent.ToastEvent) { - Toast.makeText( - PortForwardApplication.appContext, - ev.msg, - ev.duration - ).show() - } else if (ev is PortViewModel.UiEvent.SnackBarViewLogEvent) { - showSnackBarViewLog(ev.msg) // emit these - } - } - } - PortForwardApplication.currentSingleSelectedObject = remember { mutableStateOf(null) } @@ -166,7 +145,6 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState } - OurSnackbarHostState = remember { SnackbarHostState() } rememberCoroutineScope() val coroutineScope: CoroutineScope = rememberCoroutineScope() @@ -199,21 +177,7 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState } Scaffold( - - snackbarHost = { - SnackbarHost(OurSnackbarHostState!!) { data -> - // custom snackbar with the custom colors - Snackbar( - data, - actionColor = AdditionalColors.PrimaryDarkerBlue - // according to https://m2.material.io/design/color/dark-theme.html - // light snackbar in darkmode is good. -// containerColor = AdditionalColors.CardContainerColor, -// contentColor = AdditionalColors.TextColor -// //contentColor = ..., - ) - } - }, + snackbarHost = { OurSnackbarHost(portViewModel.snackbarManager) }, floatingActionButton = { if (anyDevices) { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt index e9e6e0c..72f1541 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt @@ -1,5 +1,7 @@ package com.shinjiindustrial.portmapper +import android.content.Intent +import android.widget.Toast import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween @@ -7,7 +9,17 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -15,6 +27,7 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.shinjiindustrial.portmapper.domain.PortMappingUserInput import com.shinjiindustrial.portmapper.ui.RuleCreationDialog +import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors @OptIn(ExperimentalAnimationApi::class) @Composable diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index 2bee96c..31eac37 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -17,6 +17,7 @@ import com.shinjiindustrial.portmapper.domain.UpnpViewRow import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay @@ -53,17 +54,12 @@ class PortViewModel @Inject constructor( private val preferencesRepository: PreferencesManager, private val savedStateHandle: SavedStateHandle, private val ourLogger: ILogger, + val snackbarManager : SnackbarManager, @ApplicationScope private val applicationScope: CoroutineScope, ) : ViewModel() { - sealed interface UiEvent { - data class ToastEvent(val msg: String, val duration: Int = Toast.LENGTH_SHORT) : UiEvent - data class SnackBarViewLogEvent(val msg: String) : UiEvent - data class SnackBarViewShortNoEvent(val msg: String) : UiEvent - } - - private val _events = MutableSharedFlow() - val events: SharedFlow = _events + private val _events = MutableSharedFlow() + val events: SharedFlow = _events val searchStartedRecently: MutableStateFlow = MutableStateFlow(false) @@ -193,12 +189,12 @@ class PortViewModel @Inject constructor( try { val res = upnpRepository.renewRule(portMapping) if (res is UPnPCreateMappingWrapperResult.Success) { - _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) + snackbarManager.show(UiSnackToastEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } else { - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).details.reason}")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).details.reason}")) } } catch (e: Exception) { - _events.emit(UiEvent.SnackBarViewLogEvent("Renew Port Mapping Failed")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Renew Port Mapping Failed")) } } @@ -225,12 +221,12 @@ class PortViewModel @Inject constructor( if (anyFailed) { val res = result.first { it is UPnPCreateMappingWrapperResult.Failure } - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).details.reason}")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).details.reason}")) } else { - _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) + snackbarManager.show(UiSnackToastEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } } catch (e: Exception) { - _events.emit(UiEvent.SnackBarViewLogEvent("Renew Port Mapping Failed")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Renew Port Mapping Failed")) } } @@ -242,13 +238,13 @@ class PortViewModel @Inject constructor( try { val res = upnpRepository.disableEnablePortMappingEntry(portMapping, enable) if (res is UPnPCreateMappingWrapperResult.Success) { - _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) + snackbarManager.show(UiSnackToastEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } else { - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).details.reason}")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).details.reason}")) } } catch (e: Exception) { val enableDisableString = if (enable) "Enable" else "Disable" - _events.emit(UiEvent.SnackBarViewLogEvent("$enableDisableString Port Mapping Failed")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("$enableDisableString Port Mapping Failed")) } } @@ -292,13 +288,13 @@ class PortViewModel @Inject constructor( if (anyFailed) { val res = result.first { it is UPnPCreateMappingWrapperResult.Failure } - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).details.reason}")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPCreateMappingWrapperResult.Failure).details.reason}")) } else { - _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) + snackbarManager.show(UiSnackToastEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } } catch (e: Exception) { val enableDisableString = if (enable) "Enable" else "Disable" - _events.emit(UiEvent.SnackBarViewLogEvent("$enableDisableString Port Mappings Failed")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("$enableDisableString Port Mappings Failed")) } } @@ -337,6 +333,13 @@ class PortViewModel @Inject constructor( init { upnpRepository.SearchStarted += onSearchStarted + GlobalScope.launch { + while(true) + { + delay(10000) + snackbarManager.show(UiSnackToastEvent.SnackBarViewShortNoEvent("Initializing...")) + } + } combine(_selectedIds, upnpRepository.portMappings) { selectedIds, currentMappings -> val cur = currentMappings.keys val sel = selectedIds @@ -400,12 +403,12 @@ class PortViewModel @Inject constructor( if (anyFailed) { val res = result.first { it is UPnPResult.Failure } - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPResult.Failure).details.reason}")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPResult.Failure).details.reason}")) } else { - _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) + snackbarManager.show(UiSnackToastEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } } catch (e: Exception) { - _events.emit(UiEvent.SnackBarViewLogEvent("Delete Port Mappings Failed")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Delete Port Mappings Failed")) } } @@ -413,12 +416,12 @@ class PortViewModel @Inject constructor( try { val res = upnpRepository.deletePortMappingEntry(portMapping) if (res is UPnPResult.Success) { - _events.emit(UiEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) + snackbarManager.show(UiSnackToastEvent.ToastEvent("Success", Toast.LENGTH_SHORT)) } else { - _events.emit(UiEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPResult.Failure).details.reason}")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Failure - ${(res as UPnPResult.Failure).details.reason}")) } } catch (e: Exception) { - _events.emit(UiEvent.SnackBarViewLogEvent("Delete Port Mapping Failed")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Delete Port Mapping Failed")) } } @@ -427,7 +430,7 @@ class PortViewModel @Inject constructor( try { val res = upnpRepository.deletePortMappingEntry(oldRule) if (res is UPnPResult.Failure) { - _events.emit(UiEvent.SnackBarViewLogEvent("Failed to modify entry.")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Failed to modify entry.")) return@launch } } catch (exception: Exception) { @@ -435,7 +438,7 @@ class PortViewModel @Inject constructor( Level.SEVERE, "Delete Original Port Mappings Failed: " + exception.message + exception.stackTraceToString() ) - _events.emit(UiEvent.SnackBarViewLogEvent("Failed to modify entry.")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Failed to modify entry.")) return@launch } // delete was successful, create new rules @@ -473,22 +476,22 @@ class PortViewModel @Inject constructor( // all failed if (numFailed == result.size) { if (result.size == 1) { - _events.emit(UiEvent.SnackBarViewLogEvent("Failed to $verbString rule.")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Failed to $verbString rule.")) } else { - _events.emit(UiEvent.SnackBarViewLogEvent("Failed to $verbString rules.")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Failed to $verbString rules.")) } } else { - _events.emit(UiEvent.SnackBarViewLogEvent("Failed to $verbString some rules.")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Failed to $verbString some rules.")) } } else { - _events.emit(UiEvent.SnackBarViewShortNoEvent("Success")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewShortNoEvent("Success")) } } catch (exception: Exception) { ourLogger.log( Level.SEVERE, "Delete Original Port Mappings Failed: " + exception.message + exception.stackTraceToString() ) - _events.emit(UiEvent.SnackBarViewLogEvent("Failed to modify entry.")) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("Failed to modify entry.")) return@launch } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/SnackbarManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/SnackbarManager.kt new file mode 100644 index 0000000..5ec3c28 --- /dev/null +++ b/app/src/main/java/com/shinjiindustrial/portmapper/SnackbarManager.kt @@ -0,0 +1,156 @@ +package com.shinjiindustrial.portmapper + +import android.content.Intent +import android.widget.Toast +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import java.util.logging.Level +import javax.inject.Inject +import javax.inject.Singleton + + +suspend fun showSnackBar( + snackbarHostState : SnackbarHostState, + message: String, + action: String?, + duration: SnackbarDuration, + onAction: () -> Unit = { } +) { + val snackbarResult = snackbarHostState.showSnackbar( + message, + action, + (duration == SnackbarDuration.Indefinite), + duration + ) + when (snackbarResult) { + SnackbarResult.Dismissed -> {} + SnackbarResult.ActionPerformed -> onAction() + } +} + +fun viewLogCallback() { + val intent = + Intent(PortForwardApplication.CurrentActivity, LogViewActivity::class.java) + intent.putExtra(PortForwardApplication.ScrollToBottom, true) + PortForwardApplication.CurrentActivity?.startActivity(intent) +} + + +suspend fun showSnackBarShortNoAction(snackbarHostState : SnackbarHostState, message: String) { + showSnackBar(snackbarHostState, message, null, SnackbarDuration.Short) +} + +suspend fun showSnackBarLongNoAction(snackbarHostState : SnackbarHostState, message: String) { + showSnackBar(snackbarHostState, message, null, SnackbarDuration.Long) +} + +suspend fun showSnackBarViewLog(snackbarHostState : SnackbarHostState, message: String) { + showSnackBar(snackbarHostState, message, "View Log", SnackbarDuration.Long, ::viewLogCallback) +} + +suspend fun subscribeToSnackbarEvents(snackbarManager: SnackbarManager, snackbarHostState: SnackbarHostState) +{ + +} + +@Composable +fun SubscribeToSnackbarManager(hostState: SnackbarHostState, snackbarManager: SnackbarManager) { + val context = LocalContext.current.applicationContext + LaunchedEffect(Unit) { + snackbarManager.events.collect { e -> + when(e) { + is UiSnackToastEvent.ToastEvent -> { + Toast.makeText( + context, + e.msg, + e.duration + ).show() + } +// is UiSnackToastEvent.SnackbarEvent -> { +// showSnackBar(hostState, e.message, e.actionLabel, e.duration, e.onAction) +// } + is UiSnackToastEvent.SnackBarViewShortNoEvent -> { + showSnackBarShortNoAction(hostState, e.msg) + } + is UiSnackToastEvent.SnackBarLongNoAction -> { + showSnackBarLongNoAction(hostState, e.msg) + } + is UiSnackToastEvent.SnackBarViewLogEvent -> { + showSnackBarViewLog(hostState, e.msg) + } + } + } + } +} + +@Composable +fun OurSnackbarHost(snackbarManager : SnackbarManager) +{ + val hostState = remember { SnackbarHostState() } + SubscribeToSnackbarManager(hostState, snackbarManager) + SnackbarHost(hostState) { data -> + Snackbar( + data, + actionColor = AdditionalColors.PrimaryDarkerBlue + // according to https://m2.material.io/design/color/dark-theme.html + // light snackbar in darkmode is good. + ) + } +} + +@Composable +fun SnackbarScaffold( + snackbarManager: SnackbarManager, + content: @Composable (PaddingValues) -> Unit, +) { + + Scaffold( + snackbarHost = { OurSnackbarHost(snackbarManager) } + ) { innerPadding -> + content(innerPadding) + } +} + +@Singleton +class SnackbarManager @Inject constructor( + private val ourLogger: ILogger +) { + private val _events = MutableSharedFlow( + extraBufferCapacity = 5, onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val events: SharedFlow = _events + + fun show(event: UiSnackToastEvent) { + if(!_events.tryEmit(event)) + { + ourLogger.log(Level.SEVERE, "flow is DROP_OLDEST, but tryEmit returned false. This is unexpected.") + } + } +} + +sealed interface UiSnackToastEvent { + data class ToastEvent(val msg: String, val duration: Int = Toast.LENGTH_SHORT) : UiSnackToastEvent + data class SnackBarViewLogEvent(val msg: String) : UiSnackToastEvent + data class SnackBarViewShortNoEvent(val msg: String) : UiSnackToastEvent + data class SnackBarLongNoAction(val msg: String) : UiSnackToastEvent + data class SnackbarEvent( + val message: String, + val actionLabel: String? = null, + val withDismissAction: Boolean = false, + val duration: SnackbarDuration = SnackbarDuration.Short, + val onAction: (() -> Unit)? = null + ) +} diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt index 78d6c74..e030ca8 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt @@ -10,9 +10,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Snackbar -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar @@ -28,8 +25,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavHostController import com.shinjiindustrial.portmapper.CreateRuleContents -import com.shinjiindustrial.portmapper.MainActivity -import com.shinjiindustrial.portmapper.MainActivity.Companion.OurSnackbarHostState +import com.shinjiindustrial.portmapper.OurSnackbarHost import com.shinjiindustrial.portmapper.PortForwardApplication import com.shinjiindustrial.portmapper.Protocol import com.shinjiindustrial.portmapper.common.validateDescription @@ -44,7 +40,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import com.shinjiindustrial.portmapper.PortViewModel -import java.util.logging.Level +import com.shinjiindustrial.portmapper.UiSnackToastEvent @Composable @@ -68,10 +64,6 @@ fun RuleCreationDialog( portViewModel: PortViewModel, ruleToEdit: PortMappingUserInput? = null ) { - - - // TODO vm.collect() - val isPreview = false val hasSubmitted = remember { mutableStateOf(false) } @@ -143,27 +135,7 @@ fun RuleCreationDialog( // //END - // this can be null as its possible to start at this navhost - if (OurSnackbarHostState == null) { - OurSnackbarHostState = remember { SnackbarHostState() } - } - Scaffold( - - snackbarHost = { - SnackbarHost(OurSnackbarHostState!!) { data -> - // custom snackbar with the custom colors - Snackbar( - data, - actionColor = AdditionalColors.PrimaryDarkerBlue - // according to https://m2.material.io/design/color/dark-theme.html - // light snackbar in darkmode is good. -// containerColor = AdditionalColors.CardContainerColor, -// contentColor = AdditionalColors.TextColor -// //contentColor = ..., - ) - } - }, topBar = { TopAppBar( // modifier = Modifier.height(40.dp), @@ -231,7 +203,7 @@ fun RuleCreationDialog( val invalidFieldsStr = invalidFields.joinToString(", ") - MainActivity.showSnackBarLongNoAction("Invalid Fields: $invalidFieldsStr") + portViewModel.snackbarManager.show(UiSnackToastEvent.SnackBarLongNoAction("Invalid Fields: $invalidFieldsStr")) return@TextButton } @@ -254,18 +226,19 @@ fun RuleCreationDialog( var errorString = portMappingRequestInput.validateAutoRenew() if (errorString.isNotEmpty()) { - MainActivity.showSnackBarLongNoAction(errorString) + portViewModel.snackbarManager.show(UiSnackToastEvent.SnackBarLongNoAction(errorString)) return@TextButton } errorString = portMappingRequestInput.validateRange() if (errorString.isNotEmpty()) { - MainActivity.showSnackBarLongNoAction(errorString) + portViewModel.snackbarManager.show(UiSnackToastEvent.SnackBarLongNoAction(errorString)) return@TextButton } //Toast.makeText(PortForwardApplication.appContext, "Adding Rule", Toast.LENGTH_SHORT).show() + // TODO can be any scope with DispatchersMain. GlobalScope.launch(Dispatchers.Main) { @@ -304,6 +277,7 @@ fun RuleCreationDialog( } ) }, + snackbarHost = { OurSnackbarHost(portViewModel.snackbarManager) }, content = { it -> Column( From 3ae7f6d4d2958b34e7a2dcd2d9b6901cd3d1018b Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Mon, 1 Sep 2025 19:26:07 -0400 Subject: [PATCH 17/24] cleanup - no more static for context menu, context, current activity. --- .../portmapper/LogViewActivity.kt | 2 - .../portmapper/MainActivity.kt | 39 ++++++++----------- .../portmapper/PortForwardApplication.kt | 21 ---------- .../portmapper/PortMapperMainScreen.kt | 23 ++++++----- .../portmapper/PortViewModel.kt | 35 +++++++++++++---- .../portmapper/SettingsActivity.kt | 1 - .../portmapper/SnackbarManager.kt | 17 +++++--- .../portmapper/UPnPManager.kt | 2 +- .../portmapper/common/ConnectionReceiver.kt | 4 +- .../domain/AndroidUpnpServiceConfiguration.kt | 12 ++---- .../portmapper/domain/LocalNetworkInfo.kt | 3 +- .../portmapper/ui/PortScreen.kt | 13 ++++--- .../portmapper/ui/PreviewUtil.kt | 1 - .../portmapper/ui/RuleCreationDialog.kt | 4 +- .../portmapper/UPnPManagerTests.kt | 2 +- .../portmapper/common/ValidatorsTest.kt | 4 +- 16 files changed, 89 insertions(+), 94 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt index 6377c0d..7e96442 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt @@ -56,8 +56,6 @@ class LogViewActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - PortForwardApplication.CurrentActivity = this - setContent { LogViewContent() } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt index ace3c3a..7dc246c 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt @@ -10,7 +10,6 @@ import android.os.Handler import android.os.Looper import android.os.SystemClock import android.util.DisplayMetrics -import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.addCallback import androidx.activity.compose.setContent @@ -56,9 +55,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.PlainTooltip -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TooltipBox @@ -71,7 +67,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -81,6 +76,7 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType @@ -106,20 +102,16 @@ import com.shinjiindustrial.portmapper.common.validateInternalIp import com.shinjiindustrial.portmapper.common.validateStartPort import com.shinjiindustrial.portmapper.domain.ActionNames import com.shinjiindustrial.portmapper.domain.PortMapping +import com.shinjiindustrial.portmapper.domain.PortMappingKey import com.shinjiindustrial.portmapper.domain.PortMappingWithPref import com.shinjiindustrial.portmapper.ui.DurationPickerDialog import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import com.shinjiindustrial.portmapper.ui.theme.MyApplicationTheme import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import org.fourthline.cling.model.meta.RemoteDevice import java.net.InetAddress import java.nio.ByteBuffer import java.nio.ByteOrder -import java.util.logging.Level -import java.util.logging.LogRecord var PseudoSlotCounter: Int = MAX_PORT // as starting slot fun GetPsuedoSlot(): Int { @@ -149,8 +141,6 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - PortForwardApplication.CurrentActivity = this - onBackPressedDispatcher.addCallback(this) { if (portViewModel.inMultiSelectMode.value) { @@ -206,13 +196,15 @@ fun fallbackRecursiveSearch(rootDevice: RemoteDevice) { @OptIn(ExperimentalMaterial3Api::class) @Composable fun EnterContextMenu( - singleSelectedItem: MutableState, + selectedKey: PortMappingKey?, + getSelectedItem: (PortMappingKey) -> PortMappingWithPref, + closeContextMenu: () -> Unit, showMoreInfoDialog: MutableState, navController: NavHostController, portViewModel: PortViewModel, themeState: ThemeUiState ) { - if (singleSelectedItem.value == null) { + if (selectedKey == null) { return } @@ -225,7 +217,7 @@ fun EnterContextMenu( ) MyApplicationTheme(themeState) { Dialog( - onDismissRequest = { PortForwardApplication.showContextMenu.value = false }, + onDismissRequest = { closeContextMenu() }, properties = prop, ) { Surface( @@ -238,12 +230,12 @@ fun EnterContextMenu( ) { // it redraws starting at this inner context... - if (singleSelectedItem.value != null) { + if (selectedKey != null) { val menuItems: MutableList Unit>> = mutableListOf() - val portMappingWithPref = - singleSelectedItem.value as PortMappingWithPref // TODO remove cast + //TODO + val portMappingWithPref = getSelectedItem(selectedKey) val portMapping = portMappingWithPref.portMapping menuItems.add( Pair Unit>( @@ -320,7 +312,7 @@ fun EnterContextMenu( .fillMaxWidth() .clickable { menuItem.second() - PortForwardApplication.showContextMenu.value = false + closeContextMenu() } .padding(vertical = 14.dp) ) { @@ -1145,6 +1137,7 @@ fun OverflowMenu(showAboutDialogState: MutableState, portViewModel: Por items.add(R.string.about) } + val context = LocalContext.current items.forEach { label -> DropdownMenuItem(text = { Text(stringResource(label)) }, onClick = { // handle item click @@ -1190,19 +1183,19 @@ fun OverflowMenu(showAboutDialogState: MutableState, portViewModel: Por R.string.view_log_action -> { val intent = Intent( - PortForwardApplication.CurrentActivity, + context, LogViewActivity::class.java ) - PortForwardApplication.CurrentActivity?.startActivity(intent) + context.startActivity(intent) } R.string.settings -> { val intent = Intent( - PortForwardApplication.CurrentActivity, + context, SettingsActivity::class.java ) - PortForwardApplication.CurrentActivity?.startActivity(intent) + context.startActivity(intent) } R.string.about -> { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortForwardApplication.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortForwardApplication.kt index 674fd74..0a6d4d1 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortForwardApplication.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortForwardApplication.kt @@ -32,22 +32,13 @@ class PortForwardApplication : Application() { FirebaseConditional.Initialize(this) - instance = this - appContext = applicationContext - println("PortForwardApplication onCreate Finished") } companion object { - lateinit var appContext: Context - lateinit var instance: PortForwardApplication - var CurrentActivity: ComponentActivity? = null - // lateinit var showPopup : MutableState - lateinit var currentSingleSelectedObject: MutableState - var showContextMenu: MutableState = mutableStateOf(false) var PaddingBetweenCreateNewRuleRows = 4.dp // can create a singleton logs repo @@ -57,18 +48,6 @@ class PortForwardApplication : Application() { val RENEW_RULE_WITHIN_X_SECONDS_OF_EXPIRING = 30L val RENEW_BATCH_WITHIN_X_SECONDS = 5L - - fun ShowToast(msg: String, toastLength: Int) { - GlobalScope.launch(Dispatchers.Main) { - CurrentActivity?.runOnUiThread { - Toast.makeText( - appContext, - msg, - toastLength - ).show() - } - } - } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt index bfe43b9..87202b8 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -75,13 +76,14 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState val showAboutDialog by showAboutDialogState //mutable state binds to UI (in sense if value changes, redraw). remember says when redrawing dont discard us. val inMultiSelectMode by portViewModel.inMultiSelectMode.collectAsStateWithLifecycle() val selectedIds by portViewModel.selectedIds.collectAsStateWithLifecycle() + val contextMenuUiState by portViewModel.contextMenuUiState.collectAsStateWithLifecycle() + val context = LocalContext.current - PortForwardApplication.currentSingleSelectedObject = - remember { mutableStateOf(null) } - - if (PortForwardApplication.showContextMenu.value && PortForwardApplication.currentSingleSelectedObject.value != null) { + if (contextMenuUiState.isOpen()) { EnterContextMenu( - PortForwardApplication.currentSingleSelectedObject, // why is this not typed?? + contextMenuUiState.selectedId, + portViewModel::getSelectedItem, + portViewModel::closeContextMenu, showMoreInfoDialogState, navController, portViewModel, @@ -91,11 +93,13 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState if (showMoreInfoDialogState.value) { MoreInfoDialog( - portMappingWithPref = PortForwardApplication.currentSingleSelectedObject.value as PortMappingWithPref, + portMappingWithPref = portViewModel.getSelectedItem(contextMenuUiState.selectedId!!), showDialog = showMoreInfoDialogState ) } + + if (showAboutDialog) { AlertDialog( onDismissRequest = { showAboutDialogState.value = false }, @@ -105,10 +109,10 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState Column() { var aboutString: String = - PortForwardApplication.appContext.getString(R.string.about_body) + context.getString(R.string.about_body) val packageInfo = - PortForwardApplication.appContext.packageManager.getPackageInfo( - PortForwardApplication.appContext.packageName, + context.packageManager.getPackageInfo( + context.packageName, 0 ) @@ -397,6 +401,7 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState uiState, isInMultiSelectMode, portViewModel::toggle, + portViewModel::openContextMenu, selectedIds ) } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index 31eac37..3a05b47 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow @@ -48,9 +49,18 @@ data class PortUiState( val userMessage: Int? = null ) +data class ContextMenuUiState( + val selectedId: PortMappingKey? = null) +{ + fun isOpen() : Boolean + { + return selectedId != null + } +} + @HiltViewModel class PortViewModel @Inject constructor( - private val upnpRepository: UpnpManager, + private val upnpRepository: UpnpRepository, private val preferencesRepository: PreferencesManager, private val savedStateHandle: SavedStateHandle, private val ourLogger: ILogger, @@ -63,6 +73,17 @@ class PortViewModel @Inject constructor( val searchStartedRecently: MutableStateFlow = MutableStateFlow(false) + private val _contextMenuUiState = MutableStateFlow(ContextMenuUiState()) + val contextMenuUiState = _contextMenuUiState.asStateFlow() + + fun openContextMenu(id: PortMappingKey) { + _contextMenuUiState.update { cur -> ContextMenuUiState(id) } + } + + fun closeContextMenu() { + _contextMenuUiState.update { cur -> ContextMenuUiState(null) } + } + // we want to use key for selections. so if a rule renews while the user is in multi select // mode, don't deselect that rule. but if we lose a rule (i.e. it gets deleted) then // we still want to deselect. @@ -83,6 +104,11 @@ class PortViewModel @Inject constructor( return upnpRepository.portMappingsFromIds(selectedIds) } + fun getSelectedItem(selectedId: PortMappingKey): PortMappingWithPref { + val listOfMappings = upnpRepository.portMappingsFromIds(setOf(selectedId)) + return listOfMappings[0] + } + fun clearSelection() { _selectedIds.value = emptySet() } @@ -333,13 +359,6 @@ class PortViewModel @Inject constructor( init { upnpRepository.SearchStarted += onSearchStarted - GlobalScope.launch { - while(true) - { - delay(10000) - snackbarManager.show(UiSnackToastEvent.SnackBarViewShortNoEvent("Initializing...")) - } - } combine(_selectedIds, upnpRepository.portMappings) { selectedIds, currentMappings -> val cur = currentMappings.keys val sel = selectedIds diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt index e75f5ec..6ebb181 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/SettingsActivity.kt @@ -56,7 +56,6 @@ class SettingsActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - PortForwardApplication.CurrentActivity = this setContent { Settings() } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/SnackbarManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/SnackbarManager.kt index 5ec3c28..01e9781 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/SnackbarManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/SnackbarManager.kt @@ -1,6 +1,7 @@ package com.shinjiindustrial.portmapper import android.content.Intent +import android.content.Context import android.widget.Toast import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.Scaffold @@ -41,11 +42,11 @@ suspend fun showSnackBar( } } -fun viewLogCallback() { +fun viewLogCallback(context : Context) { val intent = - Intent(PortForwardApplication.CurrentActivity, LogViewActivity::class.java) + Intent(context, LogViewActivity::class.java) intent.putExtra(PortForwardApplication.ScrollToBottom, true) - PortForwardApplication.CurrentActivity?.startActivity(intent) + context.startActivity(intent) } @@ -57,8 +58,12 @@ suspend fun showSnackBarLongNoAction(snackbarHostState : SnackbarHostState, mess showSnackBar(snackbarHostState, message, null, SnackbarDuration.Long) } -suspend fun showSnackBarViewLog(snackbarHostState : SnackbarHostState, message: String) { - showSnackBar(snackbarHostState, message, "View Log", SnackbarDuration.Long, ::viewLogCallback) +suspend fun showSnackBarViewLog(snackbarHostState : SnackbarHostState, context : Context, message: String) { + showSnackBar(snackbarHostState, message, "View Log", SnackbarDuration.Long) { + viewLogCallback( + context + ) + } } suspend fun subscribeToSnackbarEvents(snackbarManager: SnackbarManager, snackbarHostState: SnackbarHostState) @@ -89,7 +94,7 @@ fun SubscribeToSnackbarManager(hostState: SnackbarHostState, snackbarManager: Sn showSnackBarLongNoAction(hostState, e.msg) } is UiSnackToastEvent.SnackBarViewLogEvent -> { - showSnackBarViewLog(hostState, e.msg) + showSnackBarViewLog(hostState, context, e.msg) } } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index 0c8ad51..b0776e7 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -46,7 +46,7 @@ import javax.inject.Singleton import kotlin.system.measureTimeMillis @Singleton -class UpnpManager @Inject constructor( +class UpnpRepository @Inject constructor( private val upnpClient: IUpnpClient, private val portMappingDao: PortMappingDao, private val devicesDao: DevicesDao, diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/common/ConnectionReceiver.kt b/app/src/main/java/com/shinjiindustrial/portmapper/common/ConnectionReceiver.kt index ea6a54f..afbc160 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/common/ConnectionReceiver.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/common/ConnectionReceiver.kt @@ -20,12 +20,12 @@ class ConnectionReceiver : BroadcastReceiver() { //TODO: multiple transports val isWifi = cm.activeNetworkInfo!!.type == ConnectivityManager.TYPE_WIFI if (isWifi) { - PortForwardApplication.ShowToast("Is Connected Wifi", Toast.LENGTH_LONG) + //PortForwardApplication.ShowToast("Is Connected Wifi", Toast.LENGTH_LONG) } val isData = cm.activeNetworkInfo!!.type == ConnectivityManager.TYPE_MOBILE if (isData) { - PortForwardApplication.ShowToast("Is Connected Data", Toast.LENGTH_LONG) + //PortForwardApplication.ShowToast("Is Connected Data", Toast.LENGTH_LONG) } } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/AndroidUpnpServiceConfiguration.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/AndroidUpnpServiceConfiguration.kt index 707c855..b9dc18b 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/AndroidUpnpServiceConfiguration.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/AndroidUpnpServiceConfiguration.kt @@ -13,13 +13,7 @@ import org.fourthline.cling.transport.impl.jetty.StreamClientImpl import org.fourthline.cling.transport.spi.NetworkAddressFactory import org.fourthline.cling.transport.spi.StreamClient -class AndroidUpnpServiceConfigurationImpl(context: Context) : AndroidUpnpServiceConfiguration() { - - var Context: Context - - init { - Context = context - } +class AndroidUpnpServiceConfigurationImpl(private val context: Context) : AndroidUpnpServiceConfiguration() { // we override this in order to set timeout override fun createStreamClient(): StreamClient<*>? { @@ -59,14 +53,14 @@ class AndroidUpnpServiceConfigurationImpl(context: Context) : AndroidUpnpService networkInterfaces.add(iterator.next()) } NetworkInterfacesUsed = networkInterfaces - NetworkMappings = OurNetworkInfo.GetNameTypeMappings(Context) + NetworkMappings = OurNetworkInfo.GetNameTypeMappings(context) NetworkInterfacesUsedInfos = mutableListOf() for (netInterface in networkInterfaces) { NetworkInterfacesUsedInfos.add( NetworkInterfaceInfo( netInterface, - OurNetworkInfo.GetTypeFromInterfaceName(NetworkMappings, netInterface.name) + OurNetworkInfo.GetTypeFromInterfaceName(context, NetworkMappings, netInterface.name) ) ) } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/LocalNetworkInfo.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/LocalNetworkInfo.kt index 0686ee6..735cc06 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/LocalNetworkInfo.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/LocalNetworkInfo.kt @@ -103,13 +103,14 @@ class OurNetworkInfo { } fun GetTypeFromInterfaceName( + context : Context, _mappings: MutableMap?, interfaceName: String ): NetworkType { var mappings = _mappings if (mappings == null) { mappings = - GetNameTypeMappings(PortForwardApplication.appContext) //TODO: dont call this expensive call everytime + GetNameTypeMappings(context) //TODO: dont call this expensive call everytime } mappings if (mappings.containsKey(interfaceName)) { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt index 46b5686..6f0abdc 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PortScreen.kt @@ -78,6 +78,7 @@ fun PortMappingCard( now: Long = -1, isInMultiSelectMode: Boolean = false, toggleSelection: (PortMappingKey) -> Unit = {}, + onClick: (PortMappingKey) -> Unit = {}, selectedIds: Set, additionalModifier: Modifier = Modifier.Companion ) { @@ -101,10 +102,7 @@ fun PortMappingCard( if (isInMultiSelectMode) { toggleSelection(portMappingWithPref.getKey()) } else { - // TODO cleanup - PortForwardApplication.Companion.showContextMenu.value = true - PortForwardApplication.Companion.currentSingleSelectedObject.value = - portMappingWithPref + onClick(portMappingWithPref.getKey()) } }, @@ -546,7 +544,7 @@ fun PreviewConversation() { for (i in 0..20) { msgs.add(upnpViewEl) } - PortMappingsListContent(msgs, false, {}, emptySet()) + PortMappingsListContent(msgs, false, {}, {},emptySet()) } } @@ -555,9 +553,10 @@ fun PortMappingContent( uiState: PortUiState, isInMultiSelectMode: Boolean, onToggle: (PortMappingKey) -> Unit, + onClick: (PortMappingKey) -> Unit, selectedIds: Set ) { - PortMappingsListContent(uiState.items, isInMultiSelectMode, onToggle, selectedIds) + PortMappingsListContent(uiState.items, isInMultiSelectMode, onToggle, onClick, selectedIds) } @Composable @@ -579,6 +578,7 @@ fun PortMappingsListContent( messages: List, isInMultiSelectMode: Boolean, onToggle: (PortMappingKey) -> Unit, + onClick: (PortMappingKey) -> Unit, selectedIds: Set ) { @@ -614,6 +614,7 @@ fun PortMappingsListContent( now, isInMultiSelectMode, onToggle, + onClick, selectedIds, Modifier.animateItem() ) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PreviewUtil.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PreviewUtil.kt index da2f674..2e94d5f 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/PreviewUtil.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/PreviewUtil.kt @@ -13,5 +13,4 @@ import com.shinjiindustrial.portmapper.SharedPrefValues fun SetupPreview() { SharedPrefValues.DayNightPref = DayNightMode.FORCE_NIGHT SharedPrefValues.MaterialYouTheme = false - PortForwardApplication.appContext = LocalContext.current } \ No newline at end of file diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt index e030ca8..734b8f9 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavHostController @@ -97,13 +98,14 @@ fun RuleCreationDialog( ).hasError ) } + val context = LocalContext.current val autoRenew = remember { mutableStateOf(ruleToEdit?.autoRenew ?: false) } val (ourIp, ourGatewayIp) = remember { if (isPreview) Pair( "192.168.0.1", "" ) else OurNetworkInfo.GetLocalAndGatewayIpAddrWifi( - PortForwardApplication.appContext, + context, false ) } diff --git a/app/src/testMock/java/com/shinjiindustrial/portmapper/UPnPManagerTests.kt b/app/src/testMock/java/com/shinjiindustrial/portmapper/UPnPManagerTests.kt index bf1ceed..a318b08 100644 --- a/app/src/testMock/java/com/shinjiindustrial/portmapper/UPnPManagerTests.kt +++ b/app/src/testMock/java/com/shinjiindustrial/portmapper/UPnPManagerTests.kt @@ -14,7 +14,7 @@ import kotlin.system.measureTimeMillis * * See [testing documentation](http://d.android.com/tools/testing). */ -class UPnPManagerTests { +class UpnpRepositoryTests { @Test fun addingAndReplaceRules() diff --git a/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt b/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt index 7696237..fde05f3 100644 --- a/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt +++ b/app/src/testMock/java/com/shinjiindustrial/portmapper/common/ValidatorsTest.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.launch import org.junit.Assert.assertEquals import android.util.Log -import com.shinjiindustrial.portmapper.UpnpManager +import com.shinjiindustrial.portmapper.UpnpRepository import com.shinjiindustrial.portmapper.client.MockUpnpClient import com.shinjiindustrial.portmapper.client.MockUpnpClientConfig import com.shinjiindustrial.portmapper.client.Speed @@ -168,7 +168,7 @@ class ValidatorsTest { every { Log.e(any(), any()) } returns 0 val client = MockUpnpClient(MockUpnpClientConfig(Speed.Fastest)) - val repo = UpnpManager(client) + val repo = UpnpRepository(client) GlobalScope.launch { repo.search(false) } From 602337fcb0185d1e752abf51714bff759e18115d Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Mon, 1 Sep 2025 21:36:48 -0400 Subject: [PATCH 18/24] update mock client. fix bug in snackbarmanager where applicationcontext was used when activity context was needed. update loading so "no port mappings found" no longer shows up for a split second. --- .../portmapper/PortViewModel.kt | 7 +- .../portmapper/SnackbarManager.kt | 2 +- .../portmapper/UPnPManager.kt | 38 ++++++---- .../portmapper/client/IUpnpClient.kt | 4 +- .../portmapper/domain/ClingUtil.kt | 16 +++- .../portmapper/domain/IGDDevice.kt | 38 +++++++--- .../portmapper/domain/UpnpViewRow.kt | 4 +- .../portmapper/client/MockUpnpClient.kt | 75 +++++++++++++------ 8 files changed, 122 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index 3a05b47..26ee06c 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -8,6 +8,7 @@ import com.shinjiindustrial.portmapper.client.UPnPCreateMappingWrapperResult import com.shinjiindustrial.portmapper.client.UPnPResult import com.shinjiindustrial.portmapper.common.SortBy import com.shinjiindustrial.portmapper.common.SortInfo +import com.shinjiindustrial.portmapper.domain.DeviceStatus import com.shinjiindustrial.portmapper.domain.IIGDDevice import com.shinjiindustrial.portmapper.domain.NetworkInterfaceInfo import com.shinjiindustrial.portmapper.domain.PortMappingKey @@ -156,7 +157,7 @@ class PortViewModel @Inject constructor( anyFound = true } } - if (!anyFound) + if (!anyFound && curDevice.status == DeviceStatus.FinishedEnumeratingMappings) { upnpElements.add(UpnpViewRow.DeviceEmptyViewRow(curDevice)) } @@ -207,10 +208,6 @@ class PortViewModel @Inject constructor( upnpRepository.fullRefresh() } - fun devices(): StateFlow> { - return upnpRepository.devices - } - fun renew(portMapping: PortMappingWithPref) = applicationScope.launch { try { val res = upnpRepository.renewRule(portMapping) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/SnackbarManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/SnackbarManager.kt index 01e9781..57ac334 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/SnackbarManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/SnackbarManager.kt @@ -73,7 +73,7 @@ suspend fun subscribeToSnackbarEvents(snackbarManager: SnackbarManager, snackbar @Composable fun SubscribeToSnackbarManager(hostState: SnackbarHostState, snackbarManager: SnackbarManager) { - val context = LocalContext.current.applicationContext + val context = LocalContext.current LaunchedEffect(Unit) { snackbarManager.events.collect { e -> when(e) { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index b0776e7..02b8b47 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -10,6 +10,7 @@ import com.shinjiindustrial.portmapper.client.UPnPResult import com.shinjiindustrial.portmapper.common.Event import com.shinjiindustrial.portmapper.domain.ACTION_NAMES import com.shinjiindustrial.portmapper.domain.DevicePreferences +import com.shinjiindustrial.portmapper.domain.DeviceStatus import com.shinjiindustrial.portmapper.domain.IGDDevice import com.shinjiindustrial.portmapper.domain.IIGDDevice import com.shinjiindustrial.portmapper.domain.PortMapping @@ -91,11 +92,9 @@ class UpnpRepository @Inject constructor( } else { ourLogger.log(Level.INFO, "Preferences found for device") } - val igdDevice = - IGDDevice(device.deviceDetails, - devicePreferencesNullable?.getPrefs() ?: DevicePreferences(), - device.remoteService) - addDevice(igdDevice)//TODO test with suspend + val igdDevice = device.createClingDevice( + devicePreferencesNullable?.getPrefs() ?: DevicePreferences()) + addDevice(igdDevice) runBlocking { enumeratePortMappings(igdDevice.getIpAddress()) } @@ -225,14 +224,8 @@ class UpnpRepository @Inject constructor( _devices.update { curList -> buildList { addAll(curList) - var index = 0 - for (i in 0..size - 1) { - if (curList[i].getIpAddress() > device.getIpAddress()) { - break - } - index++ - } - add(index, device) + add(device) + sortWith { o1, o2 -> o1.getIpAddress().compareTo(o2.getIpAddress()) } } } } @@ -776,6 +769,7 @@ class UpnpRepository @Inject constructor( } } ourLogger.log(Level.INFO, "Time to enumerate ports: $timeTakenMillis ms") + updateDeviceState(device, DeviceStatus.FinishedEnumeratingMappings) } private fun isRuleOurs( @@ -831,7 +825,6 @@ class UpnpRepository @Inject constructor( success = true ourLogger.log(Level.INFO, portMapping.toStringFull()) retryCount = 0 - } is UPnPGetGenericMappingResult.Failure -> { @@ -887,9 +880,22 @@ class UpnpRepository @Inject constructor( _portMappings.update { mapOf() } } + private fun updateDeviceState(deviceToUpdate: IIGDDevice, newStatus : DeviceStatus) { + _devices.update { list -> + list.map { device -> + if (device.getKey() == deviceToUpdate.getKey()) + { + device.withStatus(newStatus) + } + else + { + device + } + } + } + } + private fun addDevice(igdDevice: IIGDDevice) { - // get dao - portMappingDao add(igdDevice) ourLogger.log( Level.INFO, diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt b/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt index 645409f..1be8f03 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/client/IUpnpClient.kt @@ -2,7 +2,7 @@ package com.shinjiindustrial.portmapper.client import com.shinjiindustrial.portmapper.PortMappingRequest import com.shinjiindustrial.portmapper.common.Event -import com.shinjiindustrial.portmapper.domain.ClingIGDDevice +import com.shinjiindustrial.portmapper.domain.IClingIGDDevice import com.shinjiindustrial.portmapper.domain.IIGDDevice import com.shinjiindustrial.portmapper.domain.NetworkInterfaceInfo import com.shinjiindustrial.portmapper.domain.PortMapping @@ -46,7 +46,7 @@ interface IUpnpClient { fun instantiateAndBindUpnpService() - val deviceFoundEvent: Event + val deviceFoundEvent: Event } sealed class UPnPCreateMappingWrapperResult { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt index 4a79f30..e561086 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt @@ -45,9 +45,21 @@ val ActionNames: List = listOf( ACTION_NAMES.GetListOfPortMappings, ) -data class ClingIGDDevice(val deviceDetails: DeviceDetails, val remoteService: RemoteService) +abstract class IClingIGDDevice() +{ + abstract val deviceDetails : DeviceDetails + abstract fun createClingDevice(preferences : DevicePreferences) : IIGDDevice +} + +data class ClingIGDDevice(override val deviceDetails: DeviceDetails, val remoteService: RemoteService) : IClingIGDDevice() +{ + override fun createClingDevice(preferences : DevicePreferences) : IIGDDevice + { + return IGDDevice(deviceDetails, preferences, remoteService) + } +} -fun RemoteDevice.getIGDDevice(ourLogger : ILogger): ClingIGDDevice? { +fun RemoteDevice.getIGDDevice(ourLogger : ILogger): IClingIGDDevice? { if (this.type.type.equals(UPnPNames.InternetGatewayDevice)) // version agnostic { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt index 1021050..2d5dd25 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/IGDDevice.kt @@ -21,14 +21,25 @@ data class DevicePreferences(val useWildcardForRemoteHostDelete: Boolean = false } } -interface IIGDDevice { - fun getDisplayName(): String - fun getIpAddress(): String - fun getUpnpVersion(): Int - fun supportsAction(actionName: String): Boolean - fun getActionInvocation(actionName: String): ActionInvocation<*> - val udn: String - var devicePreferences: DevicePreferences +enum class DeviceStatus { + Discovered, + FinishedEnumeratingMappings +} + +abstract class IIGDDevice { + abstract fun getDisplayName(): String + abstract fun getIpAddress(): String + abstract fun getUpnpVersion(): Int + abstract fun supportsAction(actionName: String): Boolean + abstract fun getActionInvocation(actionName: String): ActionInvocation<*> + abstract fun withStatus(status: DeviceStatus): IIGDDevice + abstract val udn: String + abstract val status: DeviceStatus + abstract var devicePreferences: DevicePreferences + + fun getKey() : String { + return "${getIpAddress()}:${udn}" + } } data class DeviceDetails(val displayName: String, val ipAddress: String, val upnpVersion: Int, val udn : String) @@ -46,8 +57,11 @@ data class DeviceDetails(val displayName: String, val ipAddress: String, val upn } // has state including rules. add update those rules. and do a upnp action. and sort. -class IGDDevice(val deviceDetails: DeviceDetails, override var devicePreferences : DevicePreferences, private val wanIPService: RemoteService) : - IIGDDevice { +data class IGDDevice(val deviceDetails: DeviceDetails, + override var devicePreferences : DevicePreferences, + private val wanIPService: RemoteService, + override val status: DeviceStatus = DeviceStatus.Discovered) : + IIGDDevice() { // nullrefs warning: this is SSDP response, very possible for some fields to be null, DeviceDetails constructor allows it. // the best bet on ip is (rootDevice!!.identity.descriptorURL.host) imo. since that is what we use in RetreiveRemoteDescriptors class // which then calls remoteDeviceAdded (us). presentationURI can be and is null sometimes. @@ -87,6 +101,10 @@ class IGDDevice(val deviceDetails: DeviceDetails, override var devicePreferences return ActionInvocation(action) } + override fun withStatus(status: DeviceStatus): IIGDDevice { + return this.copy(status = status) + } + init { for (action in wanIPService.actions) { if (ActionNames.contains(action.name)) { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/UpnpViewRow.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/UpnpViewRow.kt index f6dad74..d3e0bd7 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/UpnpViewRow.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/UpnpViewRow.kt @@ -29,10 +29,10 @@ sealed class UpnpViewRow { } data class DeviceHeaderViewRow(val device: IIGDDevice) : UpnpViewRow() { - override val key = ViewKey.DeviceHeaderKey(device.getIpAddress()) + override val key = ViewKey.DeviceHeaderKey(device.getKey()) } data class DeviceEmptyViewRow(val device: IIGDDevice) : UpnpViewRow() { - override val key = ViewKey.DeviceEmptyKey(device.getIpAddress()) + override val key = ViewKey.DeviceEmptyKey(device.getKey()) } } diff --git a/app/src/mock/java/com/shinjiindustrial/portmapper/client/MockUpnpClient.kt b/app/src/mock/java/com/shinjiindustrial/portmapper/client/MockUpnpClient.kt index 3c64780..6271ab7 100644 --- a/app/src/mock/java/com/shinjiindustrial/portmapper/client/MockUpnpClient.kt +++ b/app/src/mock/java/com/shinjiindustrial/portmapper/client/MockUpnpClient.kt @@ -3,6 +3,10 @@ package com.shinjiindustrial.portmapper.client import android.os.SystemClock import com.shinjiindustrial.portmapper.PortMappingRequest import com.shinjiindustrial.portmapper.common.Event +import com.shinjiindustrial.portmapper.domain.IClingIGDDevice +import com.shinjiindustrial.portmapper.domain.DeviceDetails +import com.shinjiindustrial.portmapper.domain.DevicePreferences +import com.shinjiindustrial.portmapper.domain.DeviceStatus import com.shinjiindustrial.portmapper.domain.IIGDDevice import com.shinjiindustrial.portmapper.domain.NetworkInterfaceInfo import com.shinjiindustrial.portmapper.domain.PortMapping @@ -14,8 +18,16 @@ import org.fourthline.cling.model.action.ActionInvocation import org.fourthline.cling.model.message.UpnpResponse import org.fourthline.cling.model.meta.RemoteService -class MockIGDDevice(private val displayName : String, private val ipAddress : String, override val udn : String, private val upnpTypeVersion : Int = 2) : IIGDDevice +data class MockIGDDevice(val deviceDetails : DeviceDetails, + override val status: DeviceStatus, + override var devicePreferences: DevicePreferences +) : IIGDDevice() { + private val displayName : String = deviceDetails.displayName + override val udn : String = deviceDetails.udn + private val ipAddress : String = deviceDetails.ipAddress + private val upnpTypeVersion : Int = deviceDetails.upnpVersion + override fun getDisplayName(): String { return displayName } @@ -33,8 +45,8 @@ class MockIGDDevice(private val displayName : String, private val ipAddress : St return ActionInvocation(ActionException(0, "")) } - override fun getDeviceSignature(): String { - return udn + override fun withStatus(status: DeviceStatus): IIGDDevice { + return this.copy(status = status) } override fun getUpnpVersion() : Int { @@ -43,6 +55,14 @@ class MockIGDDevice(private val displayName : String, private val ipAddress : St } +data class MockClingIGDDevice(override val deviceDetails: DeviceDetails) : IClingIGDDevice() +{ + override fun createClingDevice(preferences : DevicePreferences) : IIGDDevice + { + return MockIGDDevice(deviceDetails, DeviceStatus.Discovered, preferences) + } +} + enum class Speed(val latency : Long) { Fastest(1), Fast(15), @@ -72,7 +92,8 @@ class MockUpnpClient(val config : MockUpnpClientConfig) : IUpnpClient { // remoteHost is literally remoteHost (since that's what get specific key mapping uses) data class Key(val remoteHost: String, val remotePort: String, val protocol: String) - private val store = mutableMapOf>() + private val store = mutableMapOf>() + private val deviceStore = mutableMapOf() private val interfacesUsed = false private var initialized = false @@ -117,10 +138,13 @@ class MockUpnpClient(val config : MockUpnpClientConfig) : IUpnpClient { ) init { - var igdDevice = MockIGDDevice("IGD Other", "192.168.1.255", "UUID-1") - store.put(igdDevice, linkedMapOf()) + var details = DeviceDetails("IGD Other", "192.168.1.255", 2, "UUID-1") + var igdDevice = MockIGDDevice(details, DeviceStatus.Discovered, DevicePreferences()) + store.put(igdDevice.getKey(), linkedMapOf()) + deviceStore.put(igdDevice.getKey(), igdDevice) - igdDevice = MockIGDDevice("IGD Main", "192.168.1.1", "UUID-2") + details = DeviceDetails("IGD Main", "192.168.1.1", 2, "UUID-2") + igdDevice = MockIGDDevice(details, DeviceStatus.Discovered, DevicePreferences()) var mappings: List = (1..200).map { i -> makePortMapping( @@ -136,9 +160,11 @@ class MockUpnpClient(val config : MockUpnpClientConfig) : IUpnpClient { ) } - store.put(igdDevice, linkedMapOf(*mappings.map { getKey(it) to it }.toTypedArray())) + store.put(igdDevice.getKey(), linkedMapOf(*mappings.map { getKey(it) to it }.toTypedArray())) + deviceStore.put(igdDevice.getKey(), igdDevice) - igdDevice = MockIGDDevice("IGD Other2", "192.168.1.244", "UUID-3") + details = DeviceDetails("IGD Other2", "192.168.1.244", 2, "UUID-3") + igdDevice = MockIGDDevice(details, DeviceStatus.Discovered, DevicePreferences()) mappings = (1..2).map { i -> makePortMapping( @@ -153,7 +179,8 @@ class MockUpnpClient(val config : MockUpnpClientConfig) : IUpnpClient { pseudoSlot = i ) } - store.put(igdDevice, linkedMapOf(*mappings.map { getKey(it) to it }.toTypedArray())) + store.put(igdDevice.getKey(), linkedMapOf(*mappings.map { getKey(it) to it }.toTypedArray())) + deviceStore.put(igdDevice.getKey(), igdDevice) } override suspend fun createPortMappingRule( @@ -164,11 +191,11 @@ class MockUpnpClient(val config : MockUpnpClientConfig) : IUpnpClient { return when{ shouldThrow(portMappingRequest.description, NoCreate) -> throw IllegalStateException("Action blocked by $NoDelete $Exception") shouldFail(portMappingRequest.description, NoCreate) -> UPnPCreateMappingResult.Failure( - "test", - UpnpResponse(UpnpResponse.Status.INTERNAL_SERVER_ERROR) + FailureDetails("test", + UpnpResponse(UpnpResponse.Status.INTERNAL_SERVER_ERROR)) ) else -> { - store[device]!!.put(getKey(portMappingRequest.realize()), portMappingRequest.realize()) + store[device.getKey()]!!.put(getKey(portMappingRequest.realize()), portMappingRequest.realize()) UPnPCreateMappingResult.Success(portMappingRequest.realize()) } } @@ -182,11 +209,11 @@ class MockUpnpClient(val config : MockUpnpClientConfig) : IUpnpClient { return when{ shouldThrow(portMapping.Description, NoDelete) -> throw IllegalStateException("Action blocked by $NoDelete $Exception") shouldFail(portMapping.Description, NoDelete) -> UPnPResult.Failure( - "test", - UpnpResponse(UpnpResponse.Status.INTERNAL_SERVER_ERROR) + FailureDetails("test", + UpnpResponse(UpnpResponse.Status.INTERNAL_SERVER_ERROR)) ) else -> { - store[device]!!.remove(getKey(portMapping)) + store[device.getKey()]!!.remove(getKey(portMapping)) UPnPResult.Success(portMapping) } } @@ -197,29 +224,29 @@ class MockUpnpClient(val config : MockUpnpClientConfig) : IUpnpClient { remoteHost: String, remotePort: String, protocol: String, - ): UPnPCreateMappingWrapperResult { + ): UPnPGetSpecificMappingResult { tick() - val pm = store[device]!!.get(Key(remoteHost, remotePort, protocol)) + val pm = store[device.getKey()]!!.get(Key(remoteHost, remotePort, protocol)) pm!! - return UPnPCreateMappingWrapperResult.Success(pm, pm, true) + return UPnPGetSpecificMappingResult.Success(pm, pm) } override suspend fun getGenericPortMappingRule(device : IIGDDevice, slotIndex : Int) : UPnPGetGenericMappingResult { tick() - return UPnPGetSpecificMappingResult.Success(store[device]!!.values.elementAt(slotIndex),store[device]!!.values.elementAt(slotIndex)) + return UPnPGetGenericMappingResult.Success(store[device.getKey()]!!.values.elementAt(slotIndex),store[device.getKey()]!!.values.elementAt(slotIndex)) } override fun search(maxSeconds : Int) { GlobalScope.launch { tick() - deviceFoundEvent.invoke(store.keys.elementAt(0)) + deviceFoundEvent.invoke(MockClingIGDDevice(deviceStore.values.elementAt(0).deviceDetails)) tick() - deviceFoundEvent.invoke(store.keys.elementAt(1)) + deviceFoundEvent.invoke(MockClingIGDDevice(deviceStore.values.elementAt(1).deviceDetails)) tick() - deviceFoundEvent.invoke(store.keys.elementAt(2)) + deviceFoundEvent.invoke(MockClingIGDDevice(deviceStore.values.elementAt(2).deviceDetails)) } } @@ -241,7 +268,7 @@ class MockUpnpClient(val config : MockUpnpClientConfig) : IUpnpClient { } - override val deviceFoundEvent = Event() + override val deviceFoundEvent: Event = Event() // private val _deviceFoundEvent = MutableSharedFlow( // replay = 0, extraBufferCapacity = 1 // ) From 0d71e253233c7caa35ebdaf0ae882726b5abea4b Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Mon, 1 Sep 2025 21:58:26 -0400 Subject: [PATCH 19/24] remove unnecessary globalscope --- .../portmapper/PortViewModel.kt | 2 +- .../portmapper/ui/RuleCreationDialog.kt | 45 ++++++++----------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index 26ee06c..3a00bdd 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -64,7 +64,7 @@ class PortViewModel @Inject constructor( private val upnpRepository: UpnpRepository, private val preferencesRepository: PreferencesManager, private val savedStateHandle: SavedStateHandle, - private val ourLogger: ILogger, + val ourLogger: ILogger, val snackbarManager : SnackbarManager, @ApplicationScope private val applicationScope: CoroutineScope, ) : ViewModel() { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt index 734b8f9..6a38300 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import com.shinjiindustrial.portmapper.PortViewModel import com.shinjiindustrial.portmapper.UiSnackToastEvent +import java.util.logging.Level @Composable @@ -238,35 +239,27 @@ fun RuleCreationDialog( return@TextButton } - //Toast.makeText(PortForwardApplication.appContext, "Adding Rule", Toast.LENGTH_SHORT).show() - - // TODO can be any scope with DispatchersMain. - GlobalScope.launch(Dispatchers.Main) { - - - if (ruleToEdit != null) { - if (ruleToEdit == portMappingRequestInput) { - // TODO add this back. composition local? -// ourLogger.log( -// Level.INFO, -// "Rule has not changed. Nothing to do." -// ) - navController.popBackStack() - } else { - val oldRulesToDelete = ruleToEdit.splitIntoRules() - portViewModel.editRule( - PortMappingWithPref( - oldRulesToDelete[0].realize(), - null - ), - portMappingRequestInput - ) - navController.popBackStack() - } + if (ruleToEdit != null) { + if (ruleToEdit == portMappingRequestInput) { + portViewModel.ourLogger.log( + Level.INFO, + "Rule has not changed. Nothing to do." + ) + navController.popBackStack() } else { - portViewModel.createRules(portMappingRequestInput) + val oldRulesToDelete = ruleToEdit.splitIntoRules() + portViewModel.editRule( + PortMappingWithPref( + oldRulesToDelete[0].realize(), + null + ), + portMappingRequestInput + ) navController.popBackStack() } + } else { + portViewModel.createRules(portMappingRequestInput) + navController.popBackStack() } } ) { From 02e708c8ffe60777e866b7c2c22cb683231d4c2d Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Tue, 2 Sep 2025 23:21:26 -0400 Subject: [PATCH 20/24] use global shared root scaffold so that toasts will not immediately dissappear on leaving screen. --- .../portmapper/MainActivity.kt | 34 +++++++++++++++++-- .../portmapper/PortMapperMainScreen.kt | 24 ++++++------- .../portmapper/PortMapperNavGraph.kt | 5 +-- .../portmapper/PortViewModel.kt | 7 ++++ .../portmapper/domain/LocalNetworkInfo.kt | 1 + .../portmapper/ui/RuleCreationDialog.kt | 19 +++++------ 6 files changed, 63 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt index 7dc246c..c71310e 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/MainActivity.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer @@ -55,18 +56,22 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -92,6 +97,7 @@ import androidx.compose.ui.window.SecureFlagPolicy import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import com.example.myapplication.R +import com.shinjiindustrial.portmapper.MainActivity.ScaffoldController import com.shinjiindustrial.portmapper.common.MAX_PORT import com.shinjiindustrial.portmapper.common.ValidationError import com.shinjiindustrial.portmapper.common.capLeaseDur @@ -167,14 +173,38 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - PortMapperNavGraph(portViewModel, themeState) + RootScaffold() + { + it -> PortMapperNavGraph(portViewModel, themeState, Modifier.padding(it)) + } } } } + @Stable + class ScaffoldController { + var fab: (@Composable () -> Unit)? by mutableStateOf(null) + var topBar: (@Composable () -> Unit)? by mutableStateOf(null) + var bottomBar: (@Composable () -> Unit)? by mutableStateOf(null) + } - + @Composable + fun RootScaffold(content: @Composable (PaddingValues) -> Unit) + { + val ctrl = remember { ScaffoldController() } + CompositionLocalProvider(LocalScaffoldController provides ctrl) { + Scaffold( + snackbarHost = { OurSnackbarHost(portViewModel.snackbarManager) }, + topBar = { ctrl.topBar?.invoke() }, + bottomBar = { ctrl.bottomBar?.invoke() }, + floatingActionButton = { ctrl.fab?.invoke() }) + { innerPadding -> + content(innerPadding) + } + } + } } +val LocalScaffoldController = staticCompositionLocalOf { ScaffoldController() } fun fallbackRecursiveSearch(rootDevice: RemoteDevice) { // recursively look through devices diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt index 87202b8..44ceb94 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperMainScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -180,9 +181,9 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState // } } - Scaffold( - snackbarHost = { OurSnackbarHost(portViewModel.snackbarManager) }, - floatingActionButton = { + val ctrl = LocalScaffoldController.current + DisposableEffect(Unit) { + ctrl.fab = { if (anyDevices) { FloatingActionButton( @@ -202,8 +203,8 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState ) } } - }, - topBar = { + } + ctrl.topBar = { TopAppBar( navigationIcon = { if (inMultiSelectMode) { @@ -257,11 +258,11 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState OverflowMenu( showAboutDialogState, portViewModel ) - } - ) - }, - content = { it -> + }) + } + onDispose {} + } // TODO can revisit this - if we already know we have some kind of device // then we can forget the main loading circle and show this until we // finish enumerating our first device (or have a flow which is like @@ -281,7 +282,6 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState PullToRefreshBox( isRefreshing = isRefreshing.value, onRefresh = ::refresh, - modifier = Modifier.padding(it), state = state, indicator = { // dont show after initially pulling down @@ -409,6 +409,4 @@ fun PortMapperMainScreen(portViewModel : PortViewModel, themeState: ThemeUiState } - } - ) -} + } \ No newline at end of file diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt index 72f1541..779332d 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortMapperNavGraph.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -31,11 +32,11 @@ import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors @OptIn(ExperimentalAnimationApi::class) @Composable -fun PortMapperNavGraph(portViewModel : PortViewModel, themeState : ThemeUiState) { +fun PortMapperNavGraph(portViewModel : PortViewModel, themeState : ThemeUiState, modifier : Modifier = Modifier) { val navController = rememberNavController() - NavHost(navController = navController, startDestination = "main_screen") { + NavHost(navController = navController, startDestination = "main_screen", modifier = modifier) { composable( "main_screen", exitTransition = { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index 3a00bdd..85423f2 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -355,6 +355,13 @@ class PortViewModel @Inject constructor( private val onSearchStarted: (Any?) -> Unit = { o -> searchStarted(o) } init { + GlobalScope.launch { + while(true) + { + delay(7000) + snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("test")) + } + } upnpRepository.SearchStarted += onSearchStarted combine(_selectedIds, upnpRepository.portMappings) { selectedIds, currentMappings -> val cur = currentMappings.keys diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/LocalNetworkInfo.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/LocalNetworkInfo.kt index 735cc06..93db15b 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/LocalNetworkInfo.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/LocalNetworkInfo.kt @@ -25,6 +25,7 @@ class OurNetworkInfo { var gatewayIp: String? = null var networkType: NetworkType? = null + // TODO remove these fun GetNetworkInfo(context: Context, forceRefresh: Boolean): OurNetworkInfoBundle { GetConnectionType(context, forceRefresh) if (networkType == NetworkType.WIFI) { diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt index 6a38300..b464613 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/ui/RuleCreationDialog.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -26,6 +27,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavHostController import com.shinjiindustrial.portmapper.CreateRuleContents +import com.shinjiindustrial.portmapper.LocalScaffoldController import com.shinjiindustrial.portmapper.OurSnackbarHost import com.shinjiindustrial.portmapper.PortForwardApplication import com.shinjiindustrial.portmapper.Protocol @@ -137,9 +139,10 @@ fun RuleCreationDialog( // //END - - Scaffold( - topBar = { + val ctrl = LocalScaffoldController.current + DisposableEffect(Unit) { + ctrl.fab = { } + ctrl.topBar = { TopAppBar( // modifier = Modifier.height(40.dp), colors = TopAppBarDefaults.centerAlignedTopAppBarColors( @@ -271,13 +274,11 @@ fun RuleCreationDialog( } } ) - }, - snackbarHost = { OurSnackbarHost(portViewModel.snackbarManager) }, - content = { it -> - + } + onDispose { } + } Column( modifier = Modifier - .padding(it) .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -305,6 +306,4 @@ fun RuleCreationDialog( autoRenew ) } - - }) } From 9b42a6b7c8757c6f346f5373458fb79c10d19b11 Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Fri, 5 Sep 2025 22:11:41 -0400 Subject: [PATCH 21/24] fix crash where android recreates process at log activity with scroll to bottom = true. remove deprecated methods. fix toString when saving to disk. --- .../portmapper/LogViewActivity.kt | 70 ++++++++++++------- .../shinjiindustrial/portmapper/Logging.kt | 11 ++- .../portmapper/PortViewModel.kt | 7 -- 3 files changed, 52 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt b/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt index 7e96442..0ee24c5 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/LogViewActivity.kt @@ -1,12 +1,15 @@ package com.shinjiindustrial.portmapper +import kotlinx.coroutines.launch import android.annotation.SuppressLint +import android.app.Application import android.content.ClipData import android.content.ClipboardManager import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -18,6 +21,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.DeleteSweep @@ -41,7 +45,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.shinjiindustrial.portmapper.ui.theme.AdditionalColors import com.shinjiindustrial.portmapper.ui.theme.MyApplicationTheme import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import java.util.logging.Level import javax.inject.Inject +import kotlin.math.max @AndroidEntryPoint class LogViewActivity : ComponentActivity() { @@ -51,6 +58,16 @@ class LogViewActivity : ComponentActivity() { @Inject lateinit var logStoreRepository : LogStoreRepository + @Inject + lateinit var ourLogger : ILogger + + @Inject + lateinit var snackbarManager : SnackbarManager + + @Inject + @ApplicationScope + lateinit var applicationScope: CoroutineScope + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { @@ -66,13 +83,14 @@ class LogViewActivity : ComponentActivity() { fun LogViewInner(themeUiState: ThemeUiState, logLines: List, scrollToBottom: Boolean) { MyApplicationTheme(themeUiState) { Scaffold( + snackbarHost = { OurSnackbarHost(snackbarManager) }, topBar = { TopAppBar( navigationIcon = { IconButton(onClick = { this.finish() }) { - Icon(Icons.Filled.ArrowBack, contentDescription = "Back") + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } }, // modifier = Modifier.height(40.dp), @@ -93,11 +111,7 @@ class LogViewActivity : ComponentActivity() { } IconButton(onClick = { - CopyTextToClipboard( - logStoreRepository.logs.joinToString( - "\n" - ) - ) + copyTextToClipboard(logStoreRepository.getLogsAsText()) }) { Icon( Icons.Default.ContentCopy, @@ -120,12 +134,16 @@ class LogViewActivity : ComponentActivity() { //scrollToBottom = true // not necessary? // launched effect - action to take when composable is first launched / relaunched // passing in Unit ensures its only done the first time. + if (logLines.isEmpty()) + { + // I think this can happen on process getting recreated. + ourLogger.log(Level.SEVERE, "scrollToBottom is true but logLines is empty") + } LaunchedEffect(Unit) { - listState.scrollToItem(logLines.size - 1) + listState.scrollToItem(max(logLines.size - 1, 0)) } } - LazyColumn( state = listState, //modifier = Modifier.background(MaterialTheme.colorScheme.background), @@ -164,7 +182,20 @@ class LogViewActivity : ComponentActivity() { LogViewInner(themeState, logLines, scrollToBottom) } - private val CREATE_LOG_FILE = 1 + private val logSaveAsLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == RESULT_OK) { + applicationScope.launch { + result.data?.data?.also { uri -> + val contentResolver = applicationContext.contentResolver + contentResolver.openOutputStream(uri)?.bufferedWriter().use { out -> + out?.write(logStoreRepository.getLogsAsText()) + } + } + } + } + } private fun logsSaveAs() { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { @@ -172,25 +203,12 @@ class LogViewActivity : ComponentActivity() { type = "text/plain" putExtra(Intent.EXTRA_TITLE, "portMapperLog.txt") } - startActivityForResult(intent, CREATE_LOG_FILE) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { - super.onActivityResult(requestCode, resultCode, resultData) - if (requestCode == CREATE_LOG_FILE && resultCode == RESULT_OK) { - resultData?.data?.also { uri -> - val contentResolver = applicationContext.contentResolver - contentResolver.openOutputStream(uri)?.bufferedWriter().use { out -> - out?.write(logStoreRepository.logs.toString()) - } - } - } + logSaveAsLauncher.launch(intent) } - - fun CopyTextToClipboard(txt: String) { - var clipboardManager = this.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - var clipData = ClipData.newPlainText("simple text", txt) + fun copyTextToClipboard(txt: String) { + val clipboardManager = this.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText("PortMapper Logs", txt) clipboardManager.setPrimaryClip(clipData) } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/Logging.kt b/app/src/main/java/com/shinjiindustrial/portmapper/Logging.kt index 9fc8f06..1627673 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/Logging.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/Logging.kt @@ -24,7 +24,7 @@ data class LogOptions( interface ILogger { fun log( level: Level, - event: String, + msg: String, t: Throwable? = null, opts: LogOptions = LogOptions() ) @@ -62,13 +62,18 @@ class LogStringBuilderSink( class LogStoreRepository @Inject constructor( ) { var logs: SnapshotStateList = mutableStateListOf() + + fun getLogsAsText(): String + { + return logs.joinToString("\n") + } } class CompositeLogger( private val sinks: Set ) : ILogger { - override fun log(level: Level, event: String, t: Throwable?, opts: LogOptions) { - sinks.forEach { it.log(level, event, t, opts) } + override fun log(level: Level, msg: String, t: Throwable?, opts: LogOptions) { + sinks.forEach { it.log(level, msg, t, opts) } } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index 85423f2..3a00bdd 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -355,13 +355,6 @@ class PortViewModel @Inject constructor( private val onSearchStarted: (Any?) -> Unit = { o -> searchStarted(o) } init { - GlobalScope.launch { - while(true) - { - delay(7000) - snackbarManager.show(UiSnackToastEvent.SnackBarViewLogEvent("test")) - } - } upnpRepository.SearchStarted += onSearchStarted combine(_selectedIds, upnpRepository.portMappings) { selectedIds, currentMappings -> val cur = currentMappings.keys From ed00acd392ebba21b51e2f5a2c9783cc512077c7 Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Sat, 6 Sep 2025 09:12:17 -0400 Subject: [PATCH 22/24] set cannot be saved to savedStateHandle. change to list. --- .../java/com/shinjiindustrial/portmapper/PortViewModel.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt index 3a00bdd..43b9b2e 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/PortViewModel.kt @@ -88,9 +88,7 @@ class PortViewModel @Inject constructor( // we want to use key for selections. so if a rule renews while the user is in multi select // mode, don't deselect that rule. but if we lose a rule (i.e. it gets deleted) then // we still want to deselect. - private val _selectedIds = MutableStateFlow>( - savedStateHandle["selected_ids"] ?: emptySet() - ) + private val _selectedIds = MutableStateFlow>(savedStateHandle.get>("selected_ids")?.toSet() ?: emptySet()) val selectedIds: StateFlow> = _selectedIds val inMultiSelectMode: StateFlow = @@ -368,7 +366,7 @@ class PortViewModel @Inject constructor( .onEach { filtered -> println("filtered changed ...") if (filtered != _selectedIds.value) _selectedIds.value = filtered - savedStateHandle["selected_ids"] = filtered + savedStateHandle["selected_ids"] = filtered.toList() } .launchIn(viewModelScope) } From dbb51589910266493f3b6597e4bdbea87d3898fd Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Sat, 6 Sep 2025 12:23:33 -0400 Subject: [PATCH 23/24] better logging with breadcrumbs --- .../shinjiindustrial/portmapper/Logging.kt | 29 ++++++++--- .../portmapper/UPnPManager.kt | 52 +++++++++++++------ .../portmapper/LoggingModule.kt | 6 ++- 3 files changed, 63 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/Logging.kt b/app/src/main/java/com/shinjiindustrial/portmapper/Logging.kt index 1627673..1d6f407 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/Logging.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/Logging.kt @@ -14,10 +14,19 @@ import java.util.logging.Logger import javax.inject.Inject import javax.inject.Singleton -enum class FirebaseRoute { NO_OPINION, SILENT, BREADCRUMB, NON_FATAL } +enum class FirebaseRoute +{ + // all loggers should log as normally, firebase should log SEVERE as exception + NO_OPINION, + // all loggers should log as normally, but firebase should not log + SILENT, + // all loggers should log as normally, but firebase should log breadcrumb + BREADCRUMB, + // all loggers should log as normally, firebase should log as exception + NON_FATAL +} data class LogOptions( - // logs everything "SEVERE" by default val firebase: FirebaseRoute = FirebaseRoute.NO_OPINION, ) @@ -28,13 +37,17 @@ interface ILogger { t: Throwable? = null, opts: LogOptions = LogOptions() ) + fun logBreadcrumb(obj : Any) } -interface ILogSink { - fun log(level: Level, msg: String, t: Throwable?, opts: LogOptions) +abstract class ILogSink { + abstract fun log(level: Level, msg: String, t: Throwable?, opts: LogOptions) + open fun isFirebaseLogger() : Boolean { + return false + } } -class LogcatSink : ILogSink { +class LogcatSink : ILogSink() { val logger: Logger = Logger.getLogger("PortMapper") override fun log(level: Level, msg: String, t: Throwable?, opts: LogOptions) { logger.log(level, msg) @@ -43,7 +56,7 @@ class LogcatSink : ILogSink { class LogStringBuilderSink( private val logStoreRepository: LogStoreRepository, -) : ILogSink { +) : ILogSink() { private val main = Handler(Looper.getMainLooper()) override fun log(level: Level, msg: String, t: Throwable?, opts: LogOptions) { main.post { @@ -75,6 +88,10 @@ class CompositeLogger( override fun log(level: Level, msg: String, t: Throwable?, opts: LogOptions) { sinks.forEach { it.log(level, msg, t, opts) } } + + override fun logBreadcrumb(obj: Any) { + sinks.filter { it.isFirebaseLogger() }.forEach { it.log(Level.INFO, obj.toString(), null, LogOptions(FirebaseRoute.BREADCRUMB)) } + } } diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index 02b8b47..d326202 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -379,6 +379,7 @@ class UpnpRepository @Inject constructor( return res } catch (exception: Exception) { val enableDisableString = if (enable) "Enable" else "Disable" + ourLogger.logBreadcrumb(portMapping) ourLogger.log( Level.SEVERE, "$enableDisableString Port Mapping Failed: " + exception.message + exception.stackTraceToString() @@ -391,8 +392,8 @@ class UpnpRepository @Inject constructor( : List { return portMappings.map { portMapping -> + val portMappingRequest = PortMappingRequest.from(portMapping) try { - val portMappingRequest = PortMappingRequest.from(portMapping) val res = createPortMappingRuleWrapper( portMappingRequest, false, @@ -401,6 +402,7 @@ class UpnpRepository @Inject constructor( addOrUpdateForRenew(res, portMapping) res } catch (exception: Exception) { + ourLogger.logBreadcrumb(portMapping) ourLogger.log( Level.SEVERE, "Renew Port Mappings Failed: " + exception.message + exception.stackTraceToString() @@ -424,6 +426,7 @@ class UpnpRepository @Inject constructor( addOrUpdateForRenew(res, portMapping) return res } catch (exception: Exception) { + ourLogger.logBreadcrumb(portMapping) ourLogger.log( Level.SEVERE, "Renew Port Mappings Failed: " + exception.message + exception.stackTraceToString() @@ -443,6 +446,7 @@ class UpnpRepository @Inject constructor( portMapping.copy(RemoteHost = device.devicePreferences.getDefaultWildcard())) if (result is UPnPResult.Failure) { + // 500 can also occur with generic "500 Internal Server Error" it doesnt help to retry these fwiw if (result.details.response != null && result.details.response.statusCode == 500) { // add breadcrumb? no only on another failure @@ -467,6 +471,7 @@ class UpnpRepository @Inject constructor( // this means we maybe should not have retried // log both original and fallback errors to firebase // log to firebase + ourLogger.logBreadcrumb(portMapping) ourLogger.log(Level.SEVERE, "Fallback Original and Delete Failed. Original: " + result.details.toString() + "\n Fallback: " + fallbackResult.details.toString()) return fallbackResult @@ -503,15 +508,19 @@ class UpnpRepository @Inject constructor( } else if (result is UPnPResult.Failure) { - ourLogger.log( - Level.SEVERE, - "Failed to delete rule (${pm.shortName()})." + ourLogger.logBreadcrumb(portMappingWithPref) + ourLogger.log( + Level.SEVERE, + "Failed to delete rule (${pm.shortName()}).", + null, + LogOptions(FirebaseRoute.BREADCRUMB) ) - ourLogger.log(Level.SEVERE, + ourLogger.log(Level.SEVERE, result.details.toString()) } return result } catch (exception: Exception) { + ourLogger.logBreadcrumb(portMappingWithPref) ourLogger.log( Level.SEVERE, "Delete Port Mappings Failed: " + exception.message + exception.stackTraceToString() @@ -546,9 +555,12 @@ class UpnpRepository @Inject constructor( } else if (result is UPnPResult.Failure) { + ourLogger.logBreadcrumb(portMapping) ourLogger.log( Level.SEVERE, - "Failed to delete rule (${portMapping.shortName()})." + "Failed to delete rule (${portMapping.shortName()}).", + null, + LogOptions(firebase = FirebaseRoute.BREADCRUMB) ) ourLogger.log(Level.SEVERE, result.details.toString()) @@ -556,6 +568,7 @@ class UpnpRepository @Inject constructor( result } catch (exception: Exception) { + ourLogger.logBreadcrumb(portMappingWithPref) ourLogger.log( Level.SEVERE, "Delete Port Mappings Failed: " + exception.message + exception.stackTraceToString() @@ -613,6 +626,7 @@ class UpnpRepository @Inject constructor( result } } catch (exception: Exception) { + ourLogger.logBreadcrumb(portMappingUserInput) ourLogger.log( Level.SEVERE, "Create Rule Failed: " + exception.message + exception.stackTraceToString() @@ -628,8 +642,8 @@ class UpnpRepository @Inject constructor( return portMappings.map { portMapping -> + val portMappingRequest = PortMappingRequest.from(portMapping).copy(enabled = enable) try { - val portMappingRequest = PortMappingRequest.from(portMapping).copy(enabled = enable) val res = createPortMappingRuleWrapper( portMappingRequest, false, @@ -640,6 +654,7 @@ class UpnpRepository @Inject constructor( } catch (exception: Exception) { val enableDisableString = if (enable) "Enable" else "Disable" + ourLogger.logBreadcrumb(portMappingRequest) ourLogger.log( Level.SEVERE, "$enableDisableString Port Mappings Failed: " + exception.message + exception.stackTraceToString() @@ -723,12 +738,11 @@ class UpnpRepository @Inject constructor( ) } else if (result is UPnPGetSpecificMappingResult.Failure) { - val rule = formatShortName(portMappingRequest.protocol, - portMappingRequest.externalIp, - portMappingRequest.externalPort) - ourLogger.log( + val rule = portMappingRequest.realize() + ourLogger.logBreadcrumb(rule) + ourLogger.log( Level.SEVERE, - "Failed to read back our new rule ($rule). Remote Host: ${portMappingRequest.remoteHost}" + "Failed to read back our new rule (${rule.shortName()}). Remote Host: ${portMappingRequest.remoteHost}" ) ourLogger.log(Level.SEVERE, result.details.toString()) } @@ -738,11 +752,16 @@ class UpnpRepository @Inject constructor( } is UPnPCreateMappingResult.Failure -> { - ourLogger.log( + // TODO past tense + val rule = portMappingRequest.realize() + ourLogger.logBreadcrumb(rule) + ourLogger.log( Level.SEVERE, - "Failed to $createContext rule (${portMappingRequest.realize().shortName()})." + "Failed to $createContext rule (${rule.shortName()}).", + null, + LogOptions(FirebaseRoute.BREADCRUMB) ) - ourLogger.log(Level.SEVERE, + ourLogger.log(Level.SEVERE, createMappingResult.details.toString()) val result = UPnPCreateMappingWrapperResult.Failure( @@ -764,7 +783,6 @@ class UpnpRepository @Inject constructor( ) getAllPortMappingsUsingGenericPortMappingEntry(device) } else { - //TODO firebase integration ourLogger.log(Level.SEVERE, "device does not have GetGenericPortMappingEntry") } } @@ -856,7 +874,7 @@ class UpnpRepository @Inject constructor( slotIndex++ if (slotIndex > 65535) { - ourLogger.log(Level.SEVERE, "CRITICAL ERROR ENUMERATING PORTS, made it past 65535") + ourLogger.log(Level.SEVERE, "Critical Error Enumerating Ports, made it past 65535") } } } diff --git a/app/src/release/java/com/shinjiindustrial/portmapper/LoggingModule.kt b/app/src/release/java/com/shinjiindustrial/portmapper/LoggingModule.kt index 8c873ed..b5e23c3 100644 --- a/app/src/release/java/com/shinjiindustrial/portmapper/LoggingModule.kt +++ b/app/src/release/java/com/shinjiindustrial/portmapper/LoggingModule.kt @@ -25,7 +25,11 @@ object LoggingModule { class CrashlyticsSink( private val crashlytics: FirebaseCrashlytics, - ) : ILogSink { + ) : ILogSink() { + override fun isFirebaseLogger() : Boolean { + return true + } + override fun log(level: Level, msg: String, t: Throwable?, opts: LogOptions) { var firebaseLevel = opts.firebase if (firebaseLevel == FirebaseRoute.SILENT) return From 9362104c3e82f1bc4bb270bd01ecdf393919cfb5 Mon Sep 17 00:00:00 2001 From: jackBonadies Date: Sat, 6 Sep 2025 13:03:08 -0400 Subject: [PATCH 24/24] better logging with breadcrumbs for device not found bug --- .../shinjiindustrial/portmapper/UPnPManager.kt | 16 +++++++++++++++- .../portmapper/domain/ClingUtil.kt | 2 +- .../portmapper/client/UpnpClient.kt | 3 ++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt index d326202..bf1a072 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/UPnPManager.kt @@ -247,8 +247,19 @@ class UpnpRepository @Inject constructor( // region datagetters + inline fun Iterable.firstOrThrow( + predicate: (T) -> Boolean, + onMissing: () -> E + ): T = firstOrNull(predicate) ?: throw onMissing() + fun getIGDDevice(ipAddr: String): IIGDDevice { - return devices.value.first { it.getIpAddress() == ipAddr } + return devices.value.firstOrThrow( + { it.getIpAddress() == ipAddr }, + { + ourLogger.logBreadcrumb("devices: ${devices.value.joinToString("\n") }") + ourLogger.logBreadcrumb("ip address: $ipAddr") + RuntimeException("No Device Found at ip address $ipAddr") + }) } // returns anyEnabled, anyDisabled @@ -1011,6 +1022,9 @@ data class PortMappingRequest( } } + +class DeviceNotFound(message: String) : RuntimeException(message); + //CompletableFuture is api >=24 //basic futures do not implement continuewith... diff --git a/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt b/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt index e561086..c999edc 100644 --- a/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt +++ b/app/src/main/java/com/shinjiindustrial/portmapper/domain/ClingUtil.kt @@ -59,7 +59,7 @@ data class ClingIGDDevice(override val deviceDetails: DeviceDetails, val remoteS } } -fun RemoteDevice.getIGDDevice(ourLogger : ILogger): IClingIGDDevice? { +fun RemoteDevice.getIGDDevice(ourLogger : ILogger): ClingIGDDevice? { if (this.type.type.equals(UPnPNames.InternetGatewayDevice)) // version agnostic { diff --git a/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt b/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt index 45e5cd3..86c2f2f 100644 --- a/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt +++ b/app/src/prod/java/com/shinjiindustrial/portmapper/client/UpnpClient.kt @@ -16,6 +16,7 @@ import com.shinjiindustrial.portmapper.common.Event import com.shinjiindustrial.portmapper.domain.ACTION_NAMES import com.shinjiindustrial.portmapper.domain.AndroidUpnpServiceConfigurationImpl import com.shinjiindustrial.portmapper.domain.ClingIGDDevice +import com.shinjiindustrial.portmapper.domain.IClingIGDDevice import com.shinjiindustrial.portmapper.domain.IIGDDevice import com.shinjiindustrial.portmapper.domain.NetworkInterfaceInfo import com.shinjiindustrial.portmapper.domain.PortMapping @@ -285,7 +286,7 @@ class UpnpClient @Inject constructor(@ApplicationContext private val context: Co return (upnpService.configuration as AndroidUpnpServiceConfigurationImpl).NetworkInterfacesUsedInfos } - override val deviceFoundEvent = Event() + override val deviceFoundEvent = Event() // private val _deviceFoundEvent = MutableSharedFlow( // replay = 0, extraBufferCapacity = 1 // )