diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..6030a3d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..8e78a60 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..a07bb9d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,44 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..fdf8d99 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..ad5b6e6 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b71762f..351ae6e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) +// id "kotlin-serialization' // Make sure this is included + } @@ -46,6 +48,8 @@ android { packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" + excludes += "META-INF/INDEX.LIST" + excludes += "META-INF/io.netty.versions.properties" } } } @@ -61,6 +65,7 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.compose.material) + implementation(libs.play.services.dtdi) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -84,19 +89,30 @@ dependencies { implementation ("androidx.work:work-runtime-ktx:2.7.1") - implementation ("androidx.navigation:navigation-compose:2.8.3") - - - -// implementation(libs.charts) + implementation ("androidx.navigation:navigation-compose:2.8.3") implementation (libs.kotlinx.serialization.json) // implementation ("com.github.bumptech.glide:glide:4.15.1") implementation(libs.timber) // implementation (libs.mpandroidchart) + //proxy + // Netty dependencies +// implementation ("io.netty:netty-all:4.1.95.Final") +// implementation ("io.netty:netty-all:4.1.68.Final") + // Netty dependencies + implementation ("io.netty:netty-handler:4.1.68.Final") + implementation ("io.netty:netty-codec-socks:4.1.68.Final") + implementation ("io.netty:netty-transport:4.1.68.Final") + implementation ("io.netty:netty-transport-native-epoll:4.1.68.Final") + + + +// implementation(libs.charts) + + diff --git a/app/src/main/java/com/example/wifip2photspot/ConnectedDevicesSection.kt b/app/src/main/java/com/example/wifip2photspot/ConnectedDevicesSection.kt index 9f38982..15dd8c5 100644 --- a/app/src/main/java/com/example/wifip2photspot/ConnectedDevicesSection.kt +++ b/app/src/main/java/com/example/wifip2photspot/ConnectedDevicesSection.kt @@ -33,92 +33,6 @@ import java.util.Locale // ConnectedDevicesSection.kt - -@Composable -fun ConnectedDevicesSection( - devices: List, - onDeviceAliasChange: (String, String) -> Unit, - onBlockUnblock: (String) -> Unit, - onDisconnect: (String) -> Unit -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) { - Text( - text = "Connected Devices (${devices.size}):", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .padding(vertical = 8.dp) - .fillMaxWidth() - .semantics { contentDescription = "Connected Devices Header: ${devices.size} devices connected" } - ) - - if (devices.isEmpty()) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = "No devices connected.", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.semantics { contentDescription = "No devices connected" } - ) - } - } else { - LazyColumn( - modifier = Modifier.fillMaxWidth() - ) { - items(devices) { deviceInfo -> - DeviceInfoCard( - deviceInfo = deviceInfo, - onAliasChange = { alias -> - onDeviceAliasChange(deviceInfo.device.deviceAddress, alias) - }, - onBlockUnblock = onBlockUnblock, - onDisconnect = onDisconnect - ) - Divider() - } - } - } - } -} - - - -// Text( -// text = "Connected Devices (${devices.size}):", -// style = MaterialTheme.typography.titleMedium, -// modifier = Modifier -// .padding(vertical = 8.dp) -// .fillMaxWidth() -// .semantics { contentDescription = "Connected Devices Header: ${devices.size} devices connected" } -// ) -// -// if (devices.isEmpty()) { -// Box( -// modifier = Modifier -// .fillMaxWidth() -// .padding(32.dp), -// contentAlignment = Alignment.Center -// ) { -// Text( -// text = "No devices connected.", -// style = MaterialTheme.typography.bodyLarge, -// color = MaterialTheme.colorScheme.onSurfaceVariant, -// modifier = Modifier.semantics { contentDescription = "No devices connected" } -// ) -// } -// } else { -// devices.forEach { device -> -// DeviceItem(device = device, onClick = onDeviceClick) -// } -// } @Composable fun DeviceInfoCard( deviceInfo: DeviceInfo, @@ -267,147 +181,3 @@ fun LazyListScope.connectedDevicesSection( } } -// -//@Composable -//fun DeviceItem( -// device: WifiP2pDevice, -// onClick: (WifiP2pDevice) -> Unit = {} -//) { -// Card( -// elevation = CardDefaults.cardElevation(2.dp), -// shape = MaterialTheme.shapes.medium, -// modifier = Modifier -// .fillMaxWidth() -// .padding(vertical = 4.dp) -// .clickable { onClick(device) } -// .semantics { contentDescription = "Device: ${device.deviceName.ifBlank { "Unknown Device" }}, Address: ${device.deviceAddress}" } -// ) { -// Row( -// verticalAlignment = Alignment.CenterVertically, -// modifier = Modifier.padding(12.dp) -// ) { -// Icon( -// imageVector = Icons.Filled.Smartphone, -// contentDescription = "Device Icon", -// tint = MaterialTheme.colorScheme.primary, -// modifier = Modifier.size(40.dp) -// ) -// Spacer(modifier = Modifier.width(16.dp)) -// Column( -// verticalArrangement = Arrangement.Center, -// modifier = Modifier.weight(1f) -// ) { -// Text( -// text = device.deviceName.ifBlank { "Unknown Device" }, -// style = MaterialTheme.typography.titleMedium, -// color = MaterialTheme.colorScheme.onSurface, -// modifier = Modifier.semantics { contentDescription = "Device Name: ${device.deviceName.ifBlank { "Unknown Device" }}" } -// ) -// Text( -// text = "Address: ${device.deviceAddress}", -// style = MaterialTheme.typography.bodySmall, -// color = MaterialTheme.colorScheme.onSurfaceVariant, -// modifier = Modifier.semantics { contentDescription = "Device Address: ${device.deviceAddress}" } -// ) -// Text( -// text = "Status: ${getDeviceStatus(device.status)}", -// style = MaterialTheme.typography.bodySmall, -// color = MaterialTheme.colorScheme.onSurfaceVariant, -// modifier = Modifier.semantics { contentDescription = "Device Status: ${getDeviceStatus(device.status)}" } -// ) -// } -// IconButton( -// onClick = { -// // Handle device action (e.g., view details, disconnect) -// }, -// modifier = Modifier.semantics { contentDescription = "View details for ${device.deviceName.ifBlank { "Unknown Device" }}" } -// ) { -// Icon( -// imageVector = Icons.Filled.Info, -// contentDescription = "Device Info Icon", -// tint = MaterialTheme.colorScheme.primary -// ) -// } -// } -// } -//} -// -//fun getDeviceStatus(status: Int): String { -// return when (status) { -// WifiP2pDevice.AVAILABLE -> "Available" -// WifiP2pDevice.INVITED -> "Invited" -// WifiP2pDevice.CONNECTED -> "Connected" -// WifiP2pDevice.FAILED -> "Failed" -// WifiP2pDevice.UNAVAILABLE -> "Unavailable" -// else -> "Unknown" -// } -//} - -@Composable -fun HotspotScheduler( - onScheduleStart: (Long) -> Unit, - onScheduleStop: (Long) -> Unit -) { - val context = LocalContext.current - var startTime by remember { mutableStateOf(null) } - var stopTime by remember { mutableStateOf(null) } - - Column(modifier = Modifier.padding(16.dp)) { - Text("Hotspot Scheduler", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - - Button(onClick = { - showTimePicker(context) { timeInMillis -> - startTime = timeInMillis - } - }) { - Text("Set Start Time") - } - startTime?.let { - Text("Start Time: ${formatTime(it)}") - } - - Spacer(modifier = Modifier.height(8.dp)) - - Button(onClick = { - showTimePicker(context) { timeInMillis -> - stopTime = timeInMillis - } - }) { - Text("Set Stop Time") - } - stopTime?.let { - Text("Stop Time: ${formatTimes(it)}") - } - - Spacer(modifier = Modifier.height(8.dp)) - - Button(onClick = { - startTime?.let { onScheduleStart(it) } - stopTime?.let { onScheduleStop(it) } - }) { - Text("Schedule Hotspot") - } - } -} - -// Helper functions -fun showTimePicker(context: Context, onTimeSelected: (Long) -> Unit) { - val calendar = Calendar.getInstance() - TimePickerDialog( - context, - { _, hourOfDay, minute -> - calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) - calendar.set(Calendar.MINUTE, minute) - onTimeSelected(calendar.timeInMillis) - }, - calendar.get(Calendar.HOUR_OF_DAY), - calendar.get(Calendar.MINUTE), - true - ).show() -} - -fun formatTimes(timeInMillis: Long): String { - val sdf = SimpleDateFormat("HH:mm", Locale.getDefault()) - return sdf.format(Date(timeInMillis)) -} \ No newline at end of file diff --git a/app/src/main/java/com/example/wifip2photspot/DeviceInfo.kt b/app/src/main/java/com/example/wifip2photspot/DeviceInfo.kt index c6fd6e5..99ed3e2 100644 --- a/app/src/main/java/com/example/wifip2photspot/DeviceInfo.kt +++ b/app/src/main/java/com/example/wifip2photspot/DeviceInfo.kt @@ -9,7 +9,11 @@ data class DeviceInfo( val alias: String? = null, val connectionTime: Long = System.currentTimeMillis(), val ipAddress: String? = null, - val isBlocked: Boolean = false + val isBlocked: Boolean = false, +// val connectionTime: Long, + var disconnectionTime: Long = 0L, + var dataSent: Long = 0L, + var dataReceived: Long = 0L ) diff --git a/app/src/main/java/com/example/wifip2photspot/HotspotControlSection.kt b/app/src/main/java/com/example/wifip2photspot/HotspotControlSection.kt index 3f9cb6f..0e690f4 100644 --- a/app/src/main/java/com/example/wifip2photspot/HotspotControlSection.kt +++ b/app/src/main/java/com/example/wifip2photspot/HotspotControlSection.kt @@ -25,8 +25,9 @@ fun HotspotControlSection( ssidInput: String, passwordInput: String, selectedBand: String, + socksPortInput: String, onStartTapped: () -> Unit, - onStopTapped: () -> Unit, + onStopTapped: () -> Unit ) { val context = LocalContext.current @@ -45,11 +46,11 @@ fun HotspotControlSection( if (isProcessing) { CircularProgressIndicator( modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary ) } else { - Text("Start Hotspot") + Text("Start Tethering") } } @@ -78,11 +79,11 @@ fun HotspotControlSection( if (isProcessing) { CircularProgressIndicator( modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimary, - strokeWidth = 2.dp + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary ) } else { - Text("Stop Hotspot") + Text("Stop Tethering") } } } diff --git a/app/src/main/java/com/example/wifip2photspot/HotspotViewModel.kt b/app/src/main/java/com/example/wifip2photspot/HotspotViewModel.kt index ce5e85f..8eb5e54 100644 --- a/app/src/main/java/com/example/wifip2photspot/HotspotViewModel.kt +++ b/app/src/main/java/com/example/wifip2photspot/HotspotViewModel.kt @@ -15,6 +15,7 @@ import android.net.Uri import android.net.wifi.WifiManager import android.net.wifi.p2p.WifiP2pConfig import android.net.wifi.p2p.WifiP2pDevice +import android.net.wifi.p2p.WifiP2pInfo import android.net.wifi.p2p.WifiP2pManager import android.os.BatteryManager import android.os.Build @@ -31,20 +32,18 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager +import com.example.wifip2photspot.Proxy.SocksProxyServer import com.github.mikephil.charting.data.Entry import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.io.IOException -import java.time.LocalDate import java.util.concurrent.TimeUnit -import kotlin.time.Duration.Companion.seconds +import com.example.wifip2photspot.ClientInfo @RequiresApi(Build.VERSION_CODES.Q) class HotspotViewModel(application: Application, private val dataStore: DataStore) : @@ -72,6 +71,7 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor // Other preference keys... } + // Enum for themes enum class AppTheme { DEFAULT, DARK, AMOLED, CUSTOM @@ -121,12 +121,11 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor val isDarkTheme: StateFlow = _isDarkTheme.asStateFlow() - // ----- Log Entries ----- private val _logEntries = MutableStateFlow>(emptyList()) val logEntries: StateFlow> = _logEntries.asStateFlow() -// // ----- UI Events ----- + // // ----- UI Events ----- private val _eventFlow = MutableSharedFlow() val eventFlow: SharedFlow = _eventFlow.asSharedFlow() @@ -135,6 +134,7 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor val connectedDevices: StateFlow> = _connectedDevices.asStateFlow() private val _connectedDeviceInfos = MutableStateFlow>(emptyList()) val connectedDeviceInfos: StateFlow> = _connectedDeviceInfos.asStateFlow() + //visible password private var passwordVisible by mutableStateOf(false) @@ -154,7 +154,6 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor val proxyPort: StateFlow = _proxyPort.asStateFlow() - // StateFlows to hold the lists private val _allowedMacAddresses = MutableStateFlow>(emptySet()) @@ -196,7 +195,8 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor val notificationSoundEnabled: StateFlow = _notificationSoundEnabled.asStateFlow() private val _notificationVibrationEnabled = MutableStateFlow(true) - val notificationVibrationEnabled: StateFlow = _notificationVibrationEnabled.asStateFlow() + val notificationVibrationEnabled: StateFlow = + _notificationVibrationEnabled.asStateFlow() //battery level private val _batteryLevel = MutableStateFlow(100) @@ -238,61 +238,29 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor // Wi-Fi Lock Variables private var wifiLock: WifiManager.WifiLock? = null + // Wi-Fi Lock Enabled StateFlow private val _wifiLockEnabled = MutableStateFlow(false) val wifiLockEnabled: StateFlow = _wifiLockEnabled.asStateFlow() + private val _socksPort = MutableStateFlow("1080") + val socksPort: StateFlow = _socksPort + private var socksProxyServer: SocksProxyServer? = null -// private var proxyServer: proxyServer? = null -// -// // Start Proxy Server -// fun startProxyServer() { -// if (_isProxyRunning.value) { -// updateLog("Proxy server is already running.") -// return -// } -// -// proxyServer = proxyServer(_proxyPort.value) -// try { -// proxyServer?.start() -// _isProxyRunning.value = true -// updateLog("Proxy server started on port ${_proxyPort.value}") -// } catch (e: IOException) { -// updateLog("Failed to start proxy server: ${e.message}") -// } -// } - - // Stop Proxy Server -// fun stopProxyServer() { -// if (!_isProxyRunning.value) { -// updateLog("Proxy server is not running.") -// return -// } -// -// proxyServer?.stop() -// proxyServer = null -// _isProxyRunning.value = false -// updateLog("Proxy server stopped.") -// } - - // Update Proxy Port - fun updateProxyPort(port: Int) { - _proxyPort.value = port + fun updateSocksPort(newPort: String) { + _socksPort.value = newPort } -// fun startVpn(context: Context) { -// val intent = Intent(context, MyVpnService::class.java) -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { -// context.startForegroundService(intent) -// } else { -// context.startService(intent) -// } -// } + // Create a StateFlow to hold the IP address + private val _serverIpAddress = MutableStateFlow("") + val serverIpAddress: StateFlow = _serverIpAddress + private val _connectedClients = MutableStateFlow>(emptyList()) + val connectedClients: StateFlow> = _connectedClients + /////****************************************************************************************//////////////////////////////// init { - // ----- Load SSID and Password from DataStore ----- viewModelScope.launch { dataStore.data @@ -317,7 +285,6 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor } } // ----- Start Monitoring Network Speeds ----- -// In the coroutine where you update uploadSpeed and downloadSpeed viewModelScope.launch { var time = 0f while (true) { @@ -366,21 +333,14 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor _deviceAliases.value = Json.decodeFromString(aliasesJson) } } -// Load historical data usage from DataStore -// viewModelScope.launch { -// dataStore.data.collect { preferences -> -// val json = preferences[DATA_USAGE_KEY] ?: "[]" -// _historicalDataUsage.value = Json.decodeFromString(json) -// } -// } + // Periodically update clients' data usage + viewModelScope.launch { + while (true) { + delay(1000) // Update every second + _connectedClients.value = _connectedClients.value.toList() + } + } monitorNetworkSpeeds() -// -// viewModelScope.launch { -// dataStore.data.collect { preferences -> -// _dataUsageThreshold.value = preferences[DATA_USAGE_THRESHOLD_KEY] ?: 0L -// } -// } - } @Suppress("EXPERIMENTAL_API_USAGE") @@ -467,6 +427,19 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor // ----- Function to Set Wi-Fi P2P Enabled State ----- + fun updateConnectionInfo(wifiP2pInfo: WifiP2pInfo) { + if (wifiP2pInfo.groupFormed && wifiP2pInfo.isGroupOwner) { + _serverIpAddress.value = wifiP2pInfo.groupOwnerAddress.hostAddress + } + } + + // Add this function to retrieve the IP address + private fun getGroupOwnerIpAddress(): String { + // The Group Owner's IP is typically 192.168.49.1 + // Alternatively, retrieve it from the WifiP2pInfo + return "192.168.49.1" + } + fun setWifiP2pEnabled(enabled: Boolean) { _isWifiP2pEnabled.value = enabled updateLog("Wi-Fi P2P Enabled: $enabled") @@ -505,7 +478,8 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor fun onButtonStartTapped( ssidInput: String, passwordInput: String, - selectedBand: String + selectedBand: String, + socksPortInput: String ) { onButtonStopTapped() viewModelScope.launch { @@ -547,6 +521,13 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor else -> WifiP2pConfig.GROUP_OWNER_BAND_AUTO } + // Validate SOCKS Proxy Port + val port = socksPortInput.toIntOrNull() + if (port == null || port !in 1024..65535) { + _eventFlow.emit(UiEvent.ShowToast("Invalid SOCKS Proxy Port")) + return@launch + } + val config = WifiP2pConfig.Builder() .setNetworkName(ssid) .setPassphrase(passwordTrimmed) @@ -573,6 +554,24 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor _isProcessing.value = false _eventFlow.emit(UiEvent.ShowToast("Hotspot started successfully.")) // _eventFlow.emit(UiEvent.StartProxyService) + // Start the SOCKS Proxy Server with callbacks + val port = socksPortInput.toIntOrNull() ?: 1080 + socksProxyServer = SocksProxyServer(port, + onClientConnected = { clientInfo -> + viewModelScope.launch { + _connectedClients.value += clientInfo + updateLog("Client connected: ${clientInfo.ipAddress}") + } + }, + onClientDisconnected = { clientInfo -> + viewModelScope.launch { + _connectedClients.value -= clientInfo + updateLog("Client disconnected: ${clientInfo.ipAddress}") + } + }) + socksProxyServer?.start() + updateLog("SOCKS Proxy Server started on port $port") + acquireWifiLock() showHotspotStatusNotification() @@ -584,6 +583,7 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor } } + override fun onFailure(reason: Int) { val reasonStr = when (reason) { WifiP2pManager.ERROR -> "General error" @@ -605,6 +605,8 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor _isHotspotEnabled.value = false _isProcessing.value = false _eventFlow.emit(UiEvent.ShowToast("Exception occurred: ${e.message}")) + updateLog("Failed to start SOCKS Proxy Server: ${e.message}") + _eventFlow.emit(UiEvent.ShowToast("Failed to start SOCKS Proxy Server")) } } } @@ -632,12 +634,18 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor _isProcessing.value = false _eventFlow.emit(UiEvent.ShowToast("Hotspot stopped successfully.")) // _eventFlow.emit(UiEvent.StopProxyService) - releaseWifiLock() + + // Stop the SOCKS Proxy Server + socksProxyServer?.stop() + socksProxyServer = null + updateLog("SOCKS Proxy Server stopped") + + // Clear the server IP address + _serverIpAddress.value = "" removeHotspotStatusNotification() - // Stop monitoring - // Cancel idle monitoring coroutine if necessary _remainingIdleTime.value = 0L + } // saveSessionDataUsage() } @@ -675,10 +683,14 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor //*****************************************************Fine**************************************** fun startNetworkMonitoring() { - val connectivityManager = getApplication().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val connectivityManager = + getApplication().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val networkCallback = object : ConnectivityManager.NetworkCallback() { - override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { val linkDownstreamBandwidthKbps = networkCapabilities.linkDownstreamBandwidthKbps val linkUpstreamBandwidthKbps = networkCapabilities.linkUpstreamBandwidthKbps @@ -700,7 +712,8 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor } fun startBatteryMonitoring() { - val batteryManager = getApplication().getSystemService(Context.BATTERY_SERVICE) as BatteryManager + val batteryManager = + getApplication().getSystemService(Context.BATTERY_SERVICE) as BatteryManager val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) val batteryStatus = getApplication().registerReceiver(null, intentFilter) batteryStatus?.let { @@ -757,8 +770,12 @@ class HotspotViewModel(application: Application, private val dataStore: DataStor fun acquireWifiLock() { if (_wifiLockEnabled.value) { - val wifiManager = getApplication().getSystemService(Context.WIFI_SERVICE) as WifiManager - wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "WiFiP2PHotspotLock") + val wifiManager = + getApplication().getSystemService(Context.WIFI_SERVICE) as WifiManager + wifiLock = wifiManager.createWifiLock( + WifiManager.WIFI_MODE_FULL_HIGH_PERF, + "WiFiP2PHotspotLock" + ) wifiLock?.acquire() } } diff --git a/app/src/main/java/com/example/wifip2photspot/ImprovedHeader.kt b/app/src/main/java/com/example/wifip2photspot/ImprovedHeader.kt index fcf0e0f..a9db878 100644 --- a/app/src/main/java/com/example/wifip2photspot/ImprovedHeader.kt +++ b/app/src/main/java/com/example/wifip2photspot/ImprovedHeader.kt @@ -1,8 +1,8 @@ package com.example.wifip2photspot - import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.material3.* @@ -17,6 +17,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.filled.HelpOutline //@OptIn(ExperimentalMaterial3Api::class) //@Composable @@ -27,7 +29,9 @@ import androidx.compose.foundation.layout.padding fun ImprovedHeader( isHotspotEnabled: Boolean, viewModel: HotspotViewModel, - onSettingsClick: () -> Unit + onSettingsClick: () -> Unit, + onHelpClick: () -> Unit + ) { var showMenu by remember { mutableStateOf(false) } @@ -47,8 +51,16 @@ fun ImprovedHeader( } DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { DropdownMenuItem( - text = { Text("Help") }, - onClick = { /* Navigate to help */ } + text = { + Row { + Icon(Icons.Default.HelpOutline, contentDescription = "Help") + Spacer(Modifier.width(8.dp)) + Text("Help") + } + }, + onClick = { + onHelpClick() + } ) DropdownMenuItem( text = { Text("About") }, diff --git a/app/src/main/java/com/example/wifip2photspot/InputFieldsSection.kt b/app/src/main/java/com/example/wifip2photspot/InputFieldsSection.kt index d535e18..c50c35f 100644 --- a/app/src/main/java/com/example/wifip2photspot/InputFieldsSection.kt +++ b/app/src/main/java/com/example/wifip2photspot/InputFieldsSection.kt @@ -26,6 +26,8 @@ fun InputFieldsSection( passwordInput: TextFieldValue, onPasswordChange: (TextFieldValue) -> Unit, isHotspotEnabled: Boolean, + socksPortInput: String, + onSocksPortChange: (String) -> Unit, // proxyPort: Int, // onProxyPortChange: (Int) -> Unit, // selectedBand: String, @@ -34,6 +36,11 @@ fun InputFieldsSection( ) { // State for password visibility var passwordVisible by rememberSaveable { mutableStateOf(false) } + // Compute error states +// val ssidErrorState = ssidInput.isEmpty() +// val passwordErrorState = passwordInput.length !in 8..63 + val socksPortErrorState = socksPortInput.isEmpty() || socksPortInput.toIntOrNull() == null + // Compute error states val ssidErrorState = ssidInput.text.isEmpty() @@ -131,28 +138,41 @@ fun InputFieldsSection( } ) - // Proxy Port Input Field - -// // Proxy Port Field -// if (!isHotspotEnabled) { -// Spacer(modifier = Modifier.height(8.dp)) -// Row(verticalAlignment = Alignment.CenterVertically) { -// Text("Proxy Port:", style = MaterialTheme.typography.bodyMedium) -// Spacer(modifier = Modifier.width(8.dp)) -// OutlinedTextField( -// value = proxyPort.toString(), -// onValueChange = { newPort -> -// newPort.toIntOrNull()?.let { -// onProxyPortChange(it) -// } -// }, -// label = { Text("Port") }, -// singleLine = true, -// modifier = Modifier.width(100.dp) -// ) -// } + // SOCKS Proxy Port Input Field + OutlinedTextField( + value = socksPortInput, + onValueChange = onSocksPortChange, + label = { Text("SOCKS Proxy Port") }, + placeholder = { Text("1080") }, + leadingIcon = { + Icon( + Icons.Default.Dns, + contentDescription = "Port Icon" + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .alpha(if (!isHotspotEnabled) 1f else ContentAlpha.disabled), + enabled = !isHotspotEnabled, + singleLine = true, + isError = socksPortErrorState, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number + ), + supportingText = { + if (socksPortErrorState) { + Text( + text = "Invalid port number", + color = MaterialTheme.colorScheme.error + ) + } + } + ) } } + } +} // // Band Selection Section // BandSelection( @@ -160,9 +180,8 @@ fun InputFieldsSection( // onBandSelected = onBandSelected, // bands = bands, // isHotspotEnabled = isHotspotEnabled -// ) - } -} +// ) } + // //@Composable diff --git a/app/src/main/java/com/example/wifip2photspot/WiFiP2PHotspotApp.kt b/app/src/main/java/com/example/wifip2photspot/WiFiP2PHotspotApp.kt index 17328ec..e45af07 100644 --- a/app/src/main/java/com/example/wifip2photspot/WiFiP2PHotspotApp.kt +++ b/app/src/main/java/com/example/wifip2photspot/WiFiP2PHotspotApp.kt @@ -44,20 +44,23 @@ import com.example.wifip2photspot.ui.screen.MainScreen @Composable fun WiFiP2PHotspotApp(viewModel: HotspotViewModel) { val navController = rememberNavController() - WiFiP2PHotspotNavHost(navController = navController, viewModel = viewModel) + + } @Composable fun WiFiP2PHotspotNavHost(navController: NavHostController, viewModel: HotspotViewModel) { NavHost(navController = navController, startDestination = "main_screen") { composable("main_screen") { - MainScreen(navController = navController, viewModel = viewModel) + MainScreen(navController = navController, viewModel = viewModel, onHelpClick = { navController.navigate("help") }) } composable("settings_screen") { SettingsScreen(navController = navController, viewModel = viewModel) } - // Add other destinations if needed + composable("help") { + HelpScreen(onBack = { navController.popBackStack() }) + } } } // diff --git a/app/src/main/java/com/example/wifip2photspot/ui/screen/MainScreen.kt b/app/src/main/java/com/example/wifip2photspot/ui/screen/MainScreen.kt index feb8396..15710ee 100644 --- a/app/src/main/java/com/example/wifip2photspot/ui/screen/MainScreen.kt +++ b/app/src/main/java/com/example/wifip2photspot/ui/screen/MainScreen.kt @@ -36,6 +36,8 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.example.wifip2photspot.BatteryStatusSection +import com.example.wifip2photspot.ClientMonitoringSection +import com.example.wifip2photspot.ConnectionInfoSection import com.example.wifip2photspot.ConnectionStatusBar import com.example.wifip2photspot.HotspotControlSection import com.example.wifip2photspot.HotspotViewModel @@ -53,11 +55,15 @@ import com.example.wifip2photspot.ui.SettingsScreen @SuppressLint("StateFlowValueCalledInComposition") @Composable -fun MainScreen(navController: NavHostController, viewModel: HotspotViewModel) { +fun MainScreen(navController: NavHostController, viewModel: HotspotViewModel, onHelpClick: () -> Unit) { val context = LocalContext.current val ssid by viewModel.ssid.collectAsState() val password by viewModel.password.collectAsState() val selectedBand by viewModel.selectedBand.collectAsState() +// val socksPort by viewModel.socksPort.collectAsState() +// +// val socksPortInput by viewModel.socksPortInput.collectAsState() +// val onSocksPortChange by viewModel.onSocksPortChange.collectAsState() val isHotspotEnabled by viewModel.isHotspotEnabled.collectAsState() val isProcessing by viewModel.isProcessing.collectAsState() val uploadSpeed by viewModel.uploadSpeed.collectAsState() @@ -88,8 +94,14 @@ fun MainScreen(navController: NavHostController, viewModel: HotspotViewModel) { val downloadSpeedEntries by viewModel.downloadSpeedEntries.collectAsState() val batteryLevel by viewModel.batteryLevel.collectAsState() - // Proxy server state - val proxyPort by viewModel.proxyPort.collectAsState() + // Collect the server IP address and socks port + val serverIpAddress by viewModel.serverIpAddress.collectAsState() + val socksPort by viewModel.socksPort.collectAsState() + + val connectedClientsInfo by viewModel.connectedClients.collectAsState() +// val scaffoldState = rememberScaffoldState() + + // Update ViewModel when text changes LaunchedEffect(ssidFieldState.text) { @@ -128,11 +140,14 @@ fun MainScreen(navController: NavHostController, viewModel: HotspotViewModel) { // Scaffold for overall layout Scaffold( +// scaffoldState = scaffoldState, topBar = { ImprovedHeader( isHotspotEnabled = isHotspotEnabled, viewModel = viewModel, - onSettingsClick = { navController.navigate("settings_screen") } + onSettingsClick = { navController.navigate("settings_screen") }, + onHelpClick = onHelpClick + ) }, content = { paddingValues -> @@ -152,16 +167,31 @@ fun MainScreen(navController: NavHostController, viewModel: HotspotViewModel) { item { Spacer(modifier = Modifier.height(16.dp)) } - - if (connectedDeviceInfos.isNotEmpty()) { + // Inside your LazyColumn or Column + if (isHotspotEnabled) { item { - SpeedGraphSection( - uploadSpeeds = uploadSpeedEntries, - downloadSpeeds = downloadSpeedEntries + ConnectionInfoSection( + serverIpAddress = serverIpAddress, + socksPort = socksPort ) } } +// if (connectedDeviceInfos.isNotEmpty()) { +// item { +// SpeedGraphSection( +// uploadSpeeds = uploadSpeedEntries, +// downloadSpeeds = downloadSpeedEntries +// ) +// } +// } + // Inside your LazyColumn or Column + if (connectedClientsInfo.isNotEmpty()) { + item { + ClientMonitoringSection(clients = connectedClientsInfo) + } + } + // Input Fields and Band Selection if (connectedDeviceInfos.isEmpty()) { item { @@ -174,8 +204,12 @@ fun MainScreen(navController: NavHostController, viewModel: HotspotViewModel) { onPasswordChange = { newValue -> passwordFieldState = newValue }, - isHotspotEnabled = isHotspotEnabled - ) + socksPortInput = socksPort, + onSocksPortChange = { viewModel.updateSocksPort(it) }, + isHotspotEnabled = isHotspotEnabled, + + + ) } } else { item { @@ -203,12 +237,16 @@ fun MainScreen(navController: NavHostController, viewModel: HotspotViewModel) { ssidInput = ssidFieldState.text, passwordInput = passwordFieldState.text, selectedBand = selectedBand, + socksPortInput = socksPort, onStartTapped = { viewModel.onButtonStartTapped( ssidInput = ssidFieldState.text.ifBlank { "TetherGuard" }, passwordInput = passwordFieldState.text.ifBlank { "00000000" }, + socksPortInput = if (socksPort.isBlank()) "1080" else socksPort, selectedBand = selectedBand, - ) + + + ) }, onStopTapped = { viewModel.onButtonStopTapped() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b3f839..e66cdec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ composeBom = "2024.04.01" composeMaterial = "1.4.0" mpandroidchart = "v3.1.0" timber = "5.0.1" +playServicesDtdi = "16.0.0-beta02" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -34,6 +35,7 @@ androidx-compose-material = { group = "androidx.wear.compose", name = "compose-m kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } mpandroidchart = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "mpandroidchart" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } +play-services-dtdi = { group = "com.google.android.gms", name = "play-services-dtdi", version.ref = "playServicesDtdi" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }